12. I/O Devices
- Haram Lee
- 2026-05-25
- studies / 26-1 / operating-systems
I/O Devices
Reading
이 단원부터는 운영체제의 세 번째 큰 주제인 persistence, 즉 영속성을 다룬다.
앞에서는 주로 다음 내용을 배웠다.
CPU virtualization
- 여러 process가 CPU를 나누어 쓰는 것처럼 보이게 함.
Memory virtualization
- 각 process가 자기만의 address space를 가진 것처럼 보이게 함.
Concurrency
- 여러 thread가 동시에 실행될 때 shared data를 안전하게 다루는 법을 봄.
이제부터는 데이터를 일시적으로 메모리에 두는 것이 아니라, 전원이 꺼져도 유지되는 저장 장치와 파일 시스템을 다룬다.
Persistence 파트의 첫 관문은 I/O device다.
I/O가 중요한 이유
- input이 없으면 프로그램은 매번 같은 결과만 만들 수 있다.
- output이 없으면 프로그램을 실행한 의미가 사라진다.
- 따라서 컴퓨터 시스템이 유용하려면 input과 output이 모두 필요하다.
이 단원의 핵심 질문은 다음과 같다.
- OS는 I/O device를 시스템에 어떻게 통합하는가?
- OS는 device와 어떤 방식으로 통신하는가?
- device가 느릴 때 CPU 낭비를 어떻게 줄이는가?
- 많은 종류의 device를 OS 안에서 어떻게 일반적으로 다루는가?
Big Picture
I/O device는 CPU나 memory와 달리 종류가 매우 다양하다.
- disk
- keyboard
- mouse
- network card
- graphics card
- USB device
- NVMe storage
각 device는 속도, 동작 방식, 데이터 전송 방식, 오류 처리 방식이 다르다.
OS 입장에서는 모든 device의 세부사항을 직접 다 알고 싶지 않다.
그래서 OS는 다음 두 가지를 동시에 해결해야 한다.
- device마다 다른 세부 동작은 감추기
- application과 file system에는 일반적인 interface 제공하기
이 단원의 핵심 문장
- OS는 device의 복잡한 세부사항을 device driver에 숨기고, interrupt와 DMA를 이용해 I/O를 효율적으로 처리한다.
System Architecture
컴퓨터 시스템은 보통 계층적인 bus 구조를 가진다.
대표적인 구조
- CPU
- Memory
- Memory bus
- General I/O bus
- Peripheral I/O bus
memory bus
- CPU와 main memory를 연결한다.
- 가장 빠른 쪽에 속한다.
- 빠른 대신 길게 만들기 어렵고, 많은 장치를 붙이기 어렵다.
general I/O bus
- 상대적으로 고성능 I/O device를 연결한다.
- 예를 들어 PCI, PCIe 계열이 여기에 해당한다.
- graphics card, network card, 고성능 storage 등이 연결될 수 있다.
peripheral I/O bus
- 더 느린 장치들을 연결한다.
- 예를 들어 SATA, USB, SCSI 등이 있다.
- disk, keyboard, mouse 같은 장치가 연결된다.
왜 계층 구조를 쓰는가?
- 빠른 bus는 물리적으로 짧아야 한다.
- 빠른 bus는 설계 비용이 크다.
- 모든 device를 CPU 가까이에 붙이면 비용과 구조가 너무 복잡해진다.
- 고성능 device는 CPU 가까이에 두고, 느린 device는 더 아래쪽 bus에 둔다.
핵심
- 빠른 장치는 CPU 가까이, 느린 장치는 더 멀리 둔다.
- 성능, 비용, 물리적 제약 때문에 bus는 계층적으로 구성된다.
Modern System Architecture
현대 시스템은 고전적인 단일 bus 구조보다 더 복잡하다.
CPU는 memory와 직접 빠르게 연결된다.
CPU는 graphics card 같은 고성능 장치와도 빠른 연결을 가질 수 있다.
CPU는 I/O chip과 연결되고, I/O chip 아래에 여러 device가 붙는다.
예시
- disk는 SATA/eSATA 같은 storage interface로 연결될 수 있다.
- keyboard, mouse는 USB로 연결될 수 있다.
- network interface나 NVMe storage는 PCIe로 연결될 수 있다.
핵심
- 현대 시스템도 기본 아이디어는 같다.
- 성능이 중요한 장치는 더 빠른 interconnect에 연결된다.
- 느린 장치는 더 저렴하고 범용적인 bus에 연결된다.
A Canonical Device
- OSTEP에서는 실제 특정 장치가 아니라, 이해를 돕기 위한 canonical device를 사용한다.
- 모든 device는 크게 두 부분으로 볼 수 있다.
1. Interface
device가 외부에 제공하는 하드웨어 API다.
OS는 이 interface를 통해 device를 제어한다.
대표적으로 다음 register들이 있다.
- Status register
- Command register
- Data register
2. Internals
device 내부 구현이다.
device마다 다르다.
단순한 device는 몇 개의 chip만 가질 수 있다.
복잡한 device는 다음을 가질 수 있다.
- micro-controller
- internal memory
- device-specific chip
- firmware
예를 들어 RAID controller 같은 장치는 내부적으로 상당히 복잡한 firmware를 가질 수 있다.
Device Registers
Status register
- device의 현재 상태를 알려준다.
- 예를 들어 busy인지, ready인지, error가 있는지 확인할 수 있다.
Command register
- OS가 device에게 어떤 일을 하라고 명령하는 데 사용한다.
- 예를 들어 read, write 같은 명령을 전달한다.
Data register
- OS와 device 사이에서 데이터를 주고받는 데 사용한다.
- 작은 device에서는 직접 data register로 데이터를 주고받을 수 있다.
- 큰 데이터 전송에서는 DMA 같은 방식이 더 효율적이다.
핵심
- OS는 device register를 읽고 쓰면서 device를 제어한다.
The Canonical Protocol
- OS가 device와 상호작용하는 가장 단순한 protocol은 다음과 같다.
while (STATUS == BUSY)
; // device가 준비될 때까지 기다림
write data to DATA register
write command to COMMAND register
while (STATUS == BUSY)
; // 작업이 끝날 때까지 기다림단계별 의미
1단계
- status register를 계속 확인한다.
- device가 busy가 아니고 준비될 때까지 기다린다.
2단계
- data register에 데이터를 쓴다.
- 예를 들어 disk write라면 쓸 데이터를 device 쪽으로 보낸다.
3단계
- command register에 명령을 쓴다.
- 이때 device는 실제 작업을 시작한다.
4단계
- 다시 status register를 계속 확인한다.
- device 작업이 끝날 때까지 기다린다.
이 방식의 장점
- 단순하다.
- 이해하기 쉽다.
- 작은 device나 간단한 상황에서는 작동한다.
이 방식의 문제점
- CPU가 계속 device 상태를 확인해야 한다.
- device가 느리면 CPU 시간이 낭비된다.
- OS가 다른 process를 실행할 수 있는 시간까지 polling에 써버릴 수 있다.
Polling
Polling은 OS가 device 상태를 반복적으로 확인하는 방식이다.
예시
- “device 끝났어?”
- “아직 busy야?”
- “이제 ready야?”
- 이런 식으로 계속 묻는 방식이다.
장점
- 구현이 단순하다.
- device가 매우 빠르면 오히려 괜찮을 수 있다.
- 작업이 거의 바로 끝나는 장치라면 interrupt보다 polling이 더 싸게 먹힐 수 있다.
단점
- device가 느리면 CPU를 낭비한다.
- CPU가 다른 일을 하지 못하고 기다리는 데 시간을 쓴다.
- 특히 disk 같은 느린 장치에서는 비효율적이다.
핵심
- polling은 단순하지만, 느린 device에서는 CPU utilization을 떨어뜨린다.
Interrupt
polling의 CPU 낭비를 줄이기 위해 사용하는 방식이 interrupt다.
interrupt 방식
- OS가 device에게 I/O 요청을 보낸다.
- 요청한 process는 sleep 상태로 들어간다.
- OS는 CPU를 다른 runnable process에게 넘긴다.
- device가 작업을 끝내면 hardware interrupt를 발생시킨다.
- CPU는 OS의 interrupt handler 또는 ISR로 진입한다.
- interrupt handler는 I/O 완료 처리를 한다.
- 기다리던 process를 깨운다.
장점
- CPU와 I/O를 overlap할 수 있다.
- device가 작업하는 동안 CPU는 다른 process를 실행할 수 있다.
- 느린 device에서 CPU utilization이 좋아진다.
예시
- Process 1이 disk read 요청을 보냄.
- disk가 데이터를 읽는 동안 OS는 Process 2를 실행함.
- disk 작업이 끝나면 interrupt 발생.
- OS가 Process 1을 깨움.
핵심
- interrupt는 CPU가 device를 계속 기다리지 않게 해준다.
Interrupt가 항상 좋은 것은 아님
interrupt는 polling보다 항상 좋은 방식이 아니다.
device가 매우 빠른 경우
polling 한 번이면 작업 완료를 확인할 수 있다.
그런데 interrupt를 쓰면 다음 비용이 생긴다.
- context switch
- interrupt handling
- 다시 원래 process로 돌아가는 비용
이런 경우 interrupt가 오히려 느릴 수 있다.
device가 느린 경우
- interrupt가 유리하다.
- CPU가 기다리지 않고 다른 일을 할 수 있기 때문이다.
device 속도가 애매하거나 상황에 따라 달라지는 경우
- hybrid 방식이 좋을 수 있다.
- 처음에는 잠깐 polling한다.
- 그래도 안 끝나면 interrupt 방식으로 전환한다.
network처럼 interrupt가 너무 많이 발생하는 경우
- interrupt flood가 생길 수 있다.
- OS가 user process를 실행하지 못하고 interrupt 처리만 하게 될 수 있다.
- 이런 상태를 livelock이라고 볼 수 있다.
interrupt coalescing
- device가 interrupt를 바로 보내지 않고 잠깐 기다린다.
- 여러 I/O 완료 interrupt를 하나로 묶어서 CPU에게 보낸다.
- interrupt 처리 overhead를 줄일 수 있다.
- 대신 너무 오래 기다리면 latency가 늘어난다.
핵심
- 느린 device에는 interrupt가 좋고, 빠른 device에는 polling이 더 나을 수도 있다.
- 실제 시스템은 polling과 interrupt를 섞어 쓰기도 한다.
Programmed I/O, PIO
canonical protocol에서는 CPU가 직접 data register에 데이터를 넣거나 꺼낸다.
이렇게 CPU가 직접 데이터를 옮기는 방식을 Programmed I/O, 줄여서 PIO라고 한다.
PIO의 문제
- 큰 데이터를 옮길 때 CPU가 너무 많은 시간을 쓴다.
- CPU는 단순히 memory에서 device로 데이터를 복사하는 일을 반복한다.
- 이 시간 동안 다른 process를 실행하지 못한다.
예시
- disk에 4KB block을 쓰려고 한다.
- CPU가 word 단위로 data register에 계속 데이터를 써야 한다.
- device 자체 작업뿐 아니라 데이터 이동에도 CPU 시간이 많이 소모된다.
핵심
- PIO는 단순하지만 큰 데이터 전송에서는 CPU를 낭비한다.
DMA
PIO의 비효율을 해결하는 방식이 DMA, 즉 Direct Memory Access다.
DMA는 CPU 대신 memory와 device 사이의 데이터 이동을 담당하는 장치 또는 기능이다.
DMA 동작 방식
OS가 DMA engine에게 필요한 정보를 준다.
- 데이터가 memory 어디에 있는지
- 얼마나 많은 데이터를 옮길지
- 어느 device로 보낼지 또는 어느 device에서 받을지
DMA engine이 직접 memory와 device 사이에서 데이터를 전송한다.
CPU는 그동안 다른 일을 할 수 있다.
DMA 전송이 끝나면 DMA controller가 interrupt를 발생시킨다.
OS는 interrupt를 받고 I/O 완료를 처리한다.
장점
- CPU가 직접 데이터를 복사하지 않아도 된다.
- CPU utilization이 좋아진다.
- 큰 데이터 전송에 특히 유리하다.
핵심
- interrupt는 기다리는 비용을 줄이고, DMA는 데이터 이동 비용을 줄인다.
- 둘 다 I/O 효율을 높이기 위한 중요한 기법이다.
Methods of Device Interaction
- OS가 device register와 통신하는 방식은 크게 두 가지다.
1. Explicit I/O Instructions
CPU가 device와 통신하기 위한 별도 instruction을 제공하는 방식이다.
예를 들어 x86에는
in,outinstruction이 있다.OS는 이 instruction을 사용해 특정 port나 device register에 접근한다.
보통 privileged instruction이다.
이유
- user program이 마음대로 disk나 device를 제어하면 시스템이 망가질 수 있다.
- 따라서 device 제어는 OS만 할 수 있어야 한다.
2. Memory-Mapped I/O
device register를 memory address처럼 보이게 만드는 방식이다.
OS는 일반적인 load/store instruction으로 device register에 접근한다.
hardware는 해당 address에 대한 load/store를 main memory가 아니라 device로 전달한다.
장점
- 새로운 I/O instruction이 필요 없다.
- memory 접근 방식과 비슷하게 device를 다룰 수 있다.
둘 중 하나가 절대적으로 우월한 것은 아니다.
실제 시스템에서는 두 방식이 모두 사용된다.
핵심
- explicit I/O instruction은 전용 명령으로 device에 접근한다.
- memory-mapped I/O는 device register를 memory처럼 접근한다.
Device Driver
device는 종류마다 너무 다르다.
OS 전체가 모든 device의 세부 동작을 직접 알면 구조가 너무 복잡해진다.
이를 해결하기 위해 사용하는 것이 device driver다.
device driver
- 특정 device의 세부 동작을 아는 OS 내부 소프트웨어다.
- device-specific protocol을 처리한다.
- 상위 OS layer에는 일반적인 interface를 제공한다.
예시
- file system은 SCSI disk인지, IDE disk인지, USB storage인지 자세히 몰라도 된다.
- file system은 generic block layer에 block read/write 요청을 보낸다.
- generic block layer는 해당 요청을 적절한 device driver로 전달한다.
- device driver가 실제 device-specific 명령을 수행한다.
Linux file system stack의 대략적인 흐름
Application
POSIX API
openreadwriteclose
File System
Generic Block Layer
Device Driver
Actual Device
핵심
- device driver는 device의 구체적인 차이를 숨기는 abstraction layer다.
- 덕분에 OS의 나머지 부분은 device-neutral하게 설계될 수 있다.
Device Driver의 장점과 단점
장점
- OS 상위 계층을 단순하게 유지할 수 있다.
- file system이나 application은 device 종류를 몰라도 된다.
- 새로운 device가 추가되어도 driver만 추가하면 된다.
단점
- generic interface 때문에 device의 특별한 기능을 제대로 활용하지 못할 수 있다.
- 예를 들어 어떤 device는 rich error reporting을 제공해도, 상위 계층에는 단순한 generic error만 전달될 수 있다.
- device driver가 kernel code의 큰 비중을 차지한다.
- driver는 버그가 많을 수 있고, kernel crash의 원인이 될 수 있다.
핵심
- driver는 abstraction을 제공하지만, abstraction 때문에 정보가 손실되거나 성능/기능이 제한될 수 있다.
Raw Interface
대부분의 application은 file abstraction을 통해 storage를 사용한다.
하지만 일부 프로그램은 file system을 거치지 않고 block device에 직접 접근해야 할 수 있다.
예시
- file-system checker
- disk defragmentation tool
- low-level storage management tool
이런 경우 raw interface가 사용된다.
raw interface
- file abstraction을 우회한다.
- device의 block을 직접 read/write할 수 있게 한다.
핵심
- 일반 application은 file system을 통해 device를 사용한다.
- 하지만 특수한 system tool은 raw block access가 필요할 수 있다.
Case Study: IDE Disk Driver
OSTEP은 실제 device 예시로 간단한 IDE disk driver를 다룬다.
IDE disk driver는 여러 register와 protocol을 사용한다.
주요 register 예시
- control register
- data port
- error register
- sector count
- LBA 관련 register
- command/status register
status register는 다음 같은 상태를 알려준다.
- BUSY
- READY
- FAULT
- DRQ
- ERROR
IDE disk 요청 처리 흐름
- drive가 준비될 때까지 기다린다.
- sector count를 설정한다.
- 접근할 LBA를 설정한다.
- drive number를 설정한다.
- command register에 READ 또는 WRITE 명령을 쓴다.
- write의 경우 data port에 데이터를 전달한다.
- interrupt를 처리한다.
- 작업 후 status register를 확인한다.
- ERROR bit가 켜져 있으면 error register를 읽는다.
핵심
- 실제 device driver는 단순히
read()한 줄로 끝나지 않는다. - 여러 register를 조작하고, 준비 상태를 확인하고, interrupt와 error를 처리해야 한다.
- 실제 device driver는 단순히
xv6 IDE Driver 흐름
OSTEP은 xv6의 IDE driver를 간단히 설명한다.
주요 함수 흐름
ide_rw()- 요청을 queue에 넣는다.
- disk가 비어 있으면 바로 요청을 시작한다.
- 요청이 완료될 때까지 calling process를 sleep시킨다.
ide_start_request()- 실제 disk에 요청을 보낸다.
- read/write command를 register에 쓴다.
- write라면 data도 같이 보낸다.
ide_wait_ready()- disk가 ready 상태가 될 때까지 기다린다.
ide_intr()- disk interrupt가 발생하면 실행된다.
- read 요청이면 data를 device에서 가져온다.
- 기다리던 process를 깨운다.
- queue에 다음 요청이 있으면 다음 I/O를 시작한다.
핵심
- device driver는 요청 queue, sleep/wakeup, interrupt handling을 함께 사용한다.
- 느린 I/O를 기다리는 동안 process를 잠재우고, 완료되면 interrupt로 다시 깨운다.
이 단원에서 중요한 비교
Polling vs Interrupt
Polling
- CPU가 device 상태를 계속 확인한다.
- 단순하다.
- 빠른 device에는 괜찮을 수 있다.
- 느린 device에서는 CPU 낭비가 크다.
Interrupt
- device가 끝났을 때 CPU에게 알린다.
- CPU와 I/O를 overlap할 수 있다.
- 느린 device에 좋다.
- 너무 자주 발생하면 interrupt overhead나 livelock 문제가 생길 수 있다.
PIO vs DMA
PIO
- CPU가 직접 데이터를 옮긴다.
- 단순하지만 큰 전송에서는 비효율적이다.
DMA
- DMA controller가 memory와 device 사이의 데이터를 옮긴다.
- CPU는 다른 일을 할 수 있다.
- 큰 data transfer에 유리하다.
Explicit I/O vs Memory-Mapped I/O
Explicit I/O
- device 접근용 특별한 instruction을 사용한다.
- 예: x86
in,out.
Memory-Mapped I/O
- device register를 memory address처럼 다룬다.
- load/store로 device에 접근한다.
Generic OS Layer vs Device Driver
Generic OS layer
- file system, generic block layer처럼 일반적인 interface를 다룬다.
Device driver
- 특정 device의 세부 protocol을 안다.
- 실제 register 조작, command 전달, interrupt 처리 등을 담당한다.
Summary
- 12단원은 persistence 파트의 시작으로, OS가 I/O device와 어떻게 상호작용하는지를 다룬다.
- I/O device는 input과 output을 가능하게 하므로 컴퓨터 시스템에서 필수적이다.
- 시스템은 보통 CPU와 memory를 빠른 bus로 연결하고, 더 느린 device는 general I/O bus나 peripheral bus 아래에 둔다.
- device는 외부에 interface를 제공하고, 내부에는 device-specific 구현을 가진다.
- 가장 단순한 device interface는 status, command, data register로 생각할 수 있다.
- OS는 status register로 device 상태를 확인하고, data register로 데이터를 전달하며, command register로 작업을 시작한다.
- polling은 device 상태를 반복적으로 확인하는 방식이다.
- polling은 단순하지만 느린 device에서는 CPU 시간을 낭비한다.
- interrupt는 device가 작업 완료를 CPU에게 알려주는 방식이다.
- interrupt를 쓰면 CPU가 I/O를 기다리는 동안 다른 process를 실행할 수 있다.
- 하지만 interrupt가 항상 좋은 것은 아니다.
- 빠른 device에서는 polling이 더 나을 수 있고, 너무 많은 interrupt는 livelock을 만들 수 있다.
- PIO는 CPU가 직접 데이터를 옮기는 방식이다.
- DMA는 CPU 대신 DMA controller가 memory와 device 사이의 데이터를 전송하는 방식이다.
- device와 통신하는 방식에는 explicit I/O instruction과 memory-mapped I/O가 있다.
- device driver는 device-specific detail을 감추고 OS 상위 계층이 일반적인 interface로 device를 사용할 수 있게 해준다.
- 실제 disk driver는 queue 관리, register 조작, interrupt handling, error handling을 모두 처리해야 한다.
- 결국 이 단원의 핵심은 이것이다.
OS는 다양한 I/O device를 효율적으로 사용하기 위해 polling, interrupt, DMA, memory-mapped I/O, device driver 같은 기법을 사용한다.
시험/복습 포인트
- Persistence는 데이터를 전원이 꺼져도 유지하는 문제를 다룬다.
- I/O device는 input과 output을 담당하는 장치다.
- bus는 보통 계층적으로 구성된다.
- 빠른 bus는 짧고 비싸며, 느린 bus는 많은 장치를 연결하기 쉽다.
- canonical device는 interface와 internals로 나눌 수 있다.
- interface에는 status, command, data register가 있다.
- polling은 status register를 계속 확인하는 방식이다.
- polling은 단순하지만 CPU를 낭비할 수 있다.
- interrupt는 device가 작업 완료를 CPU에게 알리는 방식이다.
- interrupt는 CPU와 I/O overlap을 가능하게 한다.
- 빠른 device에서는 interrupt보다 polling이 나을 수 있다.
- interrupt가 너무 많으면 livelock이 생길 수 있다.
- interrupt coalescing은 여러 interrupt를 묶어 overhead를 줄이는 방식이다.
- PIO는 CPU가 직접 데이터를 옮기는 방식이다.
- DMA는 CPU 대신 DMA controller가 데이터를 옮기는 방식이다.
- explicit I/O instruction은 device 접근용 특별한 명령을 사용한다.
- memory-mapped I/O는 device register를 memory address처럼 다룬다.
- device driver는 device-specific detail을 숨기는 OS 내부 소프트웨어다.
- generic block layer는 file system과 device driver 사이에서 일반적인 block read/write interface를 제공한다.
- raw interface는 file abstraction을 우회해 block device에 직접 접근할 수 있게 한다.
- 실제 device driver는 request queue, sleep/wakeup, interrupt, error handling을 함께 다룬다.