Skip to content

필사 모드: [운영체제] 12. I/O 시스템

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

I/O 시스템

운영체제의 핵심 역할 중 하나는 다양한 I/O 장치를 관리하고,

애플리케이션에 일관된 인터페이스를 제공하는 것입니다.

이 글에서는 I/O 하드웨어 구조부터 커널 I/O 서브시스템까지 살펴봅니다.

1. I/O 하드웨어

포트, 버스, 컨트롤러

┌─────────────────────────────────────────────┐

│ CPU │

│ ┌──────────┐ │

│ │ 메모리 │ │

│ └────┬─────┘ │

│ │ 시스템 버스 │

│ ┌─────────┼─────────┐ │

│ │ │ │ │

│ ┌────┴───┐ ┌───┴────┐ ┌─┴──────┐ │

│ │ SATA │ │ USB │ │ PCIe │ ← 컨트롤러│

│ │Controller│ │Controller│ │Controller│ │

│ └────┬───┘ └───┬────┘ └─┬──────┘ │

│ │ │ │ │

│ ┌───┴──┐ ┌───┴──┐ ┌──┴───┐ │

│ │ HDD │ │ 마우스│ │ GPU │ ← 장치 │

│ └──────┘ └──────┘ └──────┘ │

└─────────────────────────────────────────────┘

- **포트(Port)**: 장치와의 연결 지점 (예: USB 포트, SATA 포트)

- **버스(Bus)**: 신호를 전달하는 공유 통신 경로 (예: PCIe 버스)

- **컨트롤러(Controller)**: 포트, 버스, 장치를 관리하는 전자 회로

장치 컨트롤러의 레지스터

각 장치 컨트롤러는 일반적으로 다음 레지스터를 가집니다.

| 레지스터 | 역할 |

| -------- | ------------------------------ |

| data-in | 호스트가 읽을 데이터 |

| data-out | 호스트가 쓸 데이터 |

| status | 장치 상태 (busy, ready, error) |

| command | 호스트가 내리는 명령 |

2. 폴링 (Polling)

CPU가 장치의 상태 레지스터를 반복적으로 확인하여 I/O 완료를 감지하는 방식입니다.

// 폴링 기반 I/O 예시 (의사 코드)

void polling_write(char data) {

// 1. 장치가 준비될 때까지 busy-wait

while (read_status_register() & BUSY_BIT)

; // 바쁜 대기

// 2. 데이터 레지스터에 데이터 쓰기

write_data_register(data);

// 3. 명령 레지스터에 쓰기 명령 설정

write_command_register(WRITE_COMMAND);

// 4. 완료될 때까지 다시 대기

while (read_status_register() & BUSY_BIT)

;

}

**장점**: 구현이 단순하고, 짧은 I/O에는 효율적

**단점**: CPU 사이클 낭비 (바쁜 대기), 긴 I/O에 비효율적

3. 인터럽트 (Interrupt)

장치가 I/O 완료 시 CPU에 신호를 보내는 방식입니다. CPU는 다른 작업을 수행하다가 인터럽트를 받으면 처리합니다.

인터럽트 처리 흐름

┌──────┐ 1. I/O 요청 ┌──────────┐

│ CPU │ ──────────────────────→ │ 장치 │

│ │ │ 컨트롤러 │

│ │ 다른 작업 수행 중 │ │

│ │ │ I/O 수행 │

│ │ │ │

│ │ ←── 2. 인터럽트 신호 ── │ 완료! │

│ │ └──────────┘

│ │ 3. 현재 상태 저장

│ │ 4. 인터럽트 핸들러 실행

│ │ 5. 상태 복원, 작업 재개

└──────┘

인터럽트 벡터 테이블

// 인터럽트 벡터 테이블 개념 (의사 코드)

typedef void (*interrupt_handler_t)(void);

interrupt_handler_t interrupt_vector[256];

// 초기화 시 핸들러 등록

void init_interrupts() {

interrupt_vector[0] = divide_error_handler;

interrupt_vector[1] = debug_handler;

interrupt_vector[14] = page_fault_handler;

interrupt_vector[32] = timer_handler;

interrupt_vector[33] = keyboard_handler;

interrupt_vector[46] = disk_handler;

// ...

}

// 인터럽트 발생 시 디스패치

void dispatch_interrupt(int vector_num) {

interrupt_vector[vector_num]();

}

인터럽트 우선순위

운영체제는 인터럽트에 우선순위를 부여하여 중요한 인터럽트가 먼저 처리되도록 합니다.

높은 우선순위

│ ┌────────────────────────┐

│ │ NMI (Non-Maskable) │ ← 하드웨어 오류

│ │ 타이머 인터럽트 │ ← 스케줄링

│ │ 디스크 인터럽트 │ ← I/O 완료

│ │ 네트워크 인터럽트 │ ← 패킷 도착

│ │ 키보드/마우스 인터럽트 │ ← 사용자 입력

▼ └────────────────────────┘

낮은 우선순위

4. DMA (Direct Memory Access)

대량 데이터 전송 시 CPU 개입 없이 장치와 메모리 간 직접 데이터를 전송하는 방식입니다.

DMA 동작 과정

┌──────┐ ┌──────────┐

│ CPU │ 1. DMA 요청 설정 │ DMA │

│ │ ─────────────────→ │ Controller│

│ │ │ │

│ │ 자유롭게 다른 │ 2. 버스 제어권│

│ │ 작업 수행 │ 획득 │

│ │ │ │

│ │ │ 3. 장치 ↔ 메모리│

│ │ │ 직접 전송│

│ │ │ │

│ │ ←── 4. 완료 │ 전송 완료 │

│ │ 인터럽트 │ │

└──────┘ └──────────┘

// DMA 전송 설정 (의사 코드)

void setup_dma_transfer(

void *buffer, // 메모리 버퍼 주소

int device_id, // 대상 장치

int byte_count, // 전송 바이트 수

int direction // READ 또는 WRITE

) {

dma_controller.address = buffer;

dma_controller.count = byte_count;

dma_controller.device = device_id;

dma_controller.command = direction;

// DMA 전송 시작 - CPU는 다른 작업 수행 가능

dma_controller.start = 1;

}

5. 애플리케이션 I/O 인터페이스

운영체제는 다양한 장치를 몇 가지 유형으로 추상화하여 일관된 인터페이스를 제공합니다.

장치 유형별 인터페이스

┌───────────────────────────────────────┐

│ 애플리케이션 │

│ read() write() ioctl() │

├───────────────────────────────────────┤

│ 커널 I/O 서브시스템 │

├──────┬──────┬──────┬──────┬──────────┤

│ 블록 │ 문자 │네트워크│클럭 │ 기타 │

│ 장치 │ 장치 │소켓 │타이머│ │

├──────┼──────┼──────┼──────┼──────────┤

│ 디스크│키보드│ NIC │ RTC │ │

│ SSD │마우스│ │ PIT │ │

└──────┴──────┴──────┴──────┴──────────┘

| 장치 유형 | 특성 | 주요 연산 | 예시 |

| ------------- | ------------------------ | ------------------- | ----------------- |

| 블록 장치 | 고정 크기 블록 단위 접근 | read, write, seek | 디스크, SSD |

| 문자 장치 | 바이트 스트림 | get, put | 키보드, 직렬 포트 |

| 네트워크 장치 | 소켓 인터페이스 | send, receive | NIC |

| 클럭/타이머 | 시간 측정, 알림 | get_time, set_timer | RTC, HPET |

블로킹 vs 논블로킹 I/O

// 블로킹 I/O - 완료될 때까지 프로세스 대기

ssize_t bytes = read(fd, buffer, size);

// 이 줄은 read가 완료된 후에 실행됨

// 논블로킹 I/O - 즉시 반환

fcntl(fd, F_SETFL, O_NONBLOCK);

ssize_t bytes = read(fd, buffer, size);

if (bytes == -1 && errno == EAGAIN) {

// 데이터가 아직 준비되지 않음

}

// 비동기 I/O - 요청 후 완료 시 통지

struct aiocb cb;

cb.aio_fildes = fd;

cb.aio_buf = buffer;

cb.aio_nbytes = size;

aio_read(&cb); // 즉시 반환

// 나중에 완료 확인

while (aio_error(&cb) == EINPROGRESS)

do_other_work();

6. 커널 I/O 서브시스템

커널은 I/O를 효율적으로 관리하기 위한 여러 서비스를 제공합니다.

I/O 스케줄링

여러 I/O 요청의 실행 순서를 최적화합니다.

요청 큐: [디스크 읽기 A] [디스크 쓰기 B] [디스크 읽기 C]

↓ I/O 스케줄러 (재배치)

실행 순서: [디스크 읽기 A] [디스크 읽기 C] [디스크 쓰기 B]

→ 디스크 헤드 이동 최소화

버퍼링 (Buffering)

데이터 전송 시 임시 저장 공간을 사용하여 속도 차이를 완화합니다.

생산자(장치) 소비자(프로세스)

│ │

│ ┌──────────┐ │

├→ │ Buffer 1 │ (채우는 중) │

│ └──────────┘ │

│ ┌──────────┐ │

│ │ Buffer 2 │ ────────────────→ ├→ 처리

│ └──────────┘ (비우는 중) │

│ │

│ 이중 버퍼링: 채우기와 비우기 동시 │

캐싱 (Caching)

자주 접근하는 데이터의 복사본을 빠른 저장장치에 보관합니다.

애플리케이션 → 캐시 확인 → Hit? → 캐시에서 반환

└→ Miss → 디스크에서 읽기 → 캐시에 저장 → 반환

스풀링 (Spooling)

동시에 하나의 작업만 처리할 수 있는 장치(예: 프린터)를 위한 출력 큐잉 메커니즘입니다.

프로세스 A ─→ ┌────────────┐

프로세스 B ─→ │ Spool 큐 │ ─→ 프린터 (한 번에 하나씩)

프로세스 C ─→ │ (디스크) │

└────────────┘

7. I/O 성능

I/O는 시스템 전체 성능의 주요 병목 지점입니다.

성능 개선 원칙

┌────────────────────────────────────────┐

│ I/O 성능 최적화 전략 │

│ │

│ 1. 컨텍스트 스위치 횟수 줄이기 │

│ 2. 데이터 복사 횟수 줄이기 │

│ (Zero-copy 기법) │

│ 3. 인터럽트 빈도 줄이기 │

│ (인터럽트 병합) │

│ 4. DMA 활용하여 CPU 부담 줄이기 │

│ 5. 폴링과 인터럽트의 적절한 조합 │

│ 6. 기능을 하드웨어로 이전 │

│ (Hardware Offloading) │

└────────────────────────────────────────┘

Zero-copy 전송 예시

// 전통적 방식: 4번의 데이터 복사

// 디스크 → 커널 버퍼 → 사용자 버퍼 → 소켓 버퍼 → NIC

// sendfile()을 이용한 Zero-copy (Linux)

#include <sys/sendfile.h>

// 파일에서 소켓으로 직접 전송 (커널 내에서만 복사)

ssize_t sent = sendfile(socket_fd, file_fd, &offset, count);

// 디스크 → 커널 버퍼 → NIC (사용자 공간 복사 없음)

8. 정리

- **폴링**: 단순하지만 CPU 낭비. 짧은 I/O에 적합

- **인터럽트**: CPU 효율적이지만 오버헤드 존재. 대부분의 I/O에 사용

- **DMA**: 대량 데이터 전송에 필수. CPU 부담 최소화

- **커널 I/O 서브시스템**: 스케줄링, 버퍼링, 캐싱, 스풀링으로 성능과 호환성 확보

- **성능 최적화**: Zero-copy, 인터럽트 병합, 하드웨어 오프로딩 등 다양한 기법 활용

**Q1.** 폴링과 인터럽트 방식의 차이점은 무엇이며, 각각 어떤 상황에 적합한가요?

**A1.** 폴링은 CPU가 장치 상태를 반복 확인하는 방식으로, 매우 짧은 I/O에서는 인터럽트 오버헤드보다 효율적입니다.

인터럽트는 장치가 완료 시 CPU에 알리는 방식으로, CPU가 대기 중 다른 작업을 할 수 있어

대부분의 I/O에 더 적합합니다.

**Q2.** DMA가 CPU 성능에 어떤 이점을 주나요?

**A2.** DMA 컨트롤러가 메모리와 장치 간 데이터 전송을 직접 처리하므로,

CPU는 전송 완료를 기다리지 않고 다른 연산을 수행할 수 있습니다.

특히 대용량 디스크 I/O나 네트워크 전송에서 CPU 사용률을 크게 줄여줍니다.

**Q3.** 버퍼링과 캐싱의 차이점은 무엇인가요?

**A3.** 버퍼링은 생산자와 소비자 간 속도 차이를 완화하기 위한 임시 저장이며,

데이터가 한 번 사용되면 버퍼에서 제거됩니다.

캐싱은 자주 접근하는 데이터의 복사본을 빠른 저장소에 유지하여 재사용하는 것입니다.

현재 단락 (1/214)

운영체제의 핵심 역할 중 하나는 다양한 I/O 장치를 관리하고,

작성 글자: 0원문 글자: 5,347작성 단락: 0/214