- Authors

- Name
- Youngju Kim
- @fjvbn20031
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, 인터럽트 병합, 하드웨어 오프로딩 등 다양한 기법 활용
퀴즈: I/O 시스템
Q1. 폴링과 인터럽트 방식의 차이점은 무엇이며, 각각 어떤 상황에 적합한가요?
A1. 폴링은 CPU가 장치 상태를 반복 확인하는 방식으로, 매우 짧은 I/O에서는 인터럽트 오버헤드보다 효율적입니다. 인터럽트는 장치가 완료 시 CPU에 알리는 방식으로, CPU가 대기 중 다른 작업을 할 수 있어 대부분의 I/O에 더 적합합니다.
Q2. DMA가 CPU 성능에 어떤 이점을 주나요?
A2. DMA 컨트롤러가 메모리와 장치 간 데이터 전송을 직접 처리하므로, CPU는 전송 완료를 기다리지 않고 다른 연산을 수행할 수 있습니다. 특히 대용량 디스크 I/O나 네트워크 전송에서 CPU 사용률을 크게 줄여줍니다.
Q3. 버퍼링과 캐싱의 차이점은 무엇인가요?
A3. 버퍼링은 생산자와 소비자 간 속도 차이를 완화하기 위한 임시 저장이며, 데이터가 한 번 사용되면 버퍼에서 제거됩니다. 캐싱은 자주 접근하는 데이터의 복사본을 빠른 저장소에 유지하여 재사용하는 것입니다.