Overview
컴퓨터에 연결된 장치를 제어하는 일은 운영체제의 주요 관심사.
마우스, 하드디스크, 플래시 드라이브, 테이프 로봇 같은 입출력 장치는 기능과 속도 면에서 매우 다양한 특성.
각 장치의 특성에 맞는 제어 방법이 필요하며, 이러한 다양한 제어 방법들이 커널의 입출력 서브시스템을 형성하는 구조.
입출력 서브시스템은 커널의 다른 부분이 입출력 장치를 관리하는 복잡한 일에 직접 신경 쓰지 않도록 해주는 계층.
입출력 장치 기술은 표준화와 다양화라는 두 가지 상충하는 방향으로 발전.
표준화는 새 장치가 기존 컴퓨터와 운영체제에 쉽게 결합할 수 있도록 만드는 방향.
다양화는 기존 장치와 전혀 다른 새 장치가 계속 등장하면서 운영체제 통합을 어렵게 만드는 방향.
이 문제는 하드웨어와 소프트웨어 기술의 조합으로 해결되는 구조.
다양성을 해결하는 기본 바탕은 포트, 버스, 장치 컨트롤러 같은 표준적인 하드웨어 요소.
운영체제 커널은 장치 드라이버 모듈을 사용하여 각 장치의 차이를 숨기는 구조.
장치 드라이버는 모든 하드웨어를 일관된 인터페이스로 표현하고,
이 인터페이스를 상위 계층인 커널 입출력 서브시스템에 제공하는 역할.
이는 시스템 콜이 응용 프로그램과 운영체제 사이에 표준 인터페이스를 제공하는 것과 비슷한 구조.
입출력 하드웨어
컴퓨터에서 작동하는 장치는 저장 장치, 전송 장치, 사용자 인터페이스 장치, 특수 목적 장치 등으로 구분 가능.
저장 장치에는 디스크와 테이프,
전송 장치에는 네트워크 연결과 블루투스, 사
용자 인터페이스 장치에는 스크린, 키보드, 마우스, 오디오 입출력 등이 포함.
제트기 조종 장치처럼 특수한 장치도 존재하며, 사
용자는 조이스틱이나 페달을 통해 입력을 보내고 컴퓨터는 비행기 날개나 모터에 출력 명령을 전달하는 구조.
입출력 장치는 매우 다양하지만, 장치가 어떻게 부착되고 제어되는지 이해하기 위해 필요한 핵심 개념은 비교적 제한적.
하드웨어 장치는 케이블이나 무선 신호를 통해 컴퓨터 시스템과 통신.
장치는 포트라고 부르는 연결점을 통해 컴퓨터와 접속되며, 직렬 포트가 대표적인 예.
PHY라는 용어도 포트와 관련하여 사용되며,
이는 OSI 모델의 물리 계층을 뜻하는 표현이지만 데이터 센터 명명법에서 더 일반적인 용어.
하나 이상의 장치가 여러 선을 공동으로 사용하면 그 선들의 집합은 버스.
버스는 단순한 전선 묶음이 아니라, 회선 집합과 그 회선으로 메시지를 주고받는 프로토콜을 함께 포함하는 개념.
전자적으로 메시지는 미리 정해진 시간에 맞추어 전선에 보내지는 전압 패턴에 의해 전달되는 방식.
장치 A가 장치 B에 연결되고, B가 C에 연결되고, C가 컴퓨터 포트에 연결되는 구조는 데이지 체인.
데이지 체인은 보통 하나의 버스처럼 동작하는 연결 방식.

PCI 버스는 프로세서-메모리 서브시스템을 고속 장치와 확장 버스에 연결하는 일반적인 PC 시스템 버스.
확장 버스는 키보드, 직렬 포트, USB 포트처럼 상대적으로 느린 장치를 연결하는 버스.
그림의 왼쪽 하단에는 네 개의 디스크가 SAS 컨트롤러에 접속된 직렬 연결 SCSI 버스에 연결된 구조.
SAS는 Serial Attached SCSI의 약자이며, 디스크 같은 저장장치를 연결하기 위한 직렬 버스 방식.
PCIe는 하나 이상의 레인을 통해 데이터를 전송하는 유연한 버스.
PCIe 레인은 두 개의 신호 쌍으로 구성되며, 한 쌍은 데이터 수신용이고 다른 한 쌍은 데이터 전송용.
따라서 각 레인은 네 개의 선으로 구성되고,
양방향으로 동시에 8비트 바이트 형식의 데이터 패킷을 전송하는 전이중 바이트 스트림 역할.
물리적 PCIe 링크에는 x 접두사로 표시되는 1, 2, 4, 8, 12, 16, 32개의 레인이 포함 가능.
예를 들어 8개의 레인을 사용하는 PCIe 카드나 커넥터는 x8로 표현.
PCIe는 여러 세대에 걸쳐 발전했으며, PCIe gen3 x8은 3세대 PCIe와 8개 레인을 사용하는 장치라는 의미.
PCIe gen3 x8 장치의 최대 처리량은 초당 8GB 수준.
컨트롤러는 포트, 버스, 장치를 작동할 수 있는 전자 장치 집합체.
직렬 포트 컨트롤러는 비교적 단순한 장치 컨트롤러이며, 직렬 포트의 전선에 나타나는 전기 신호를 제어하는 역할.
반대로 광섬유 채널 버스 컨트롤러는 단순하지 않은 구조.
광섬유 채널은 Fibre Channel, 즉 FC로 표현되며, 복잡한 프로토콜이고 주로 PC가 아니라 데이터 센터에서 사용되는 방식.
FC 버스 컨트롤러는 종종 컴퓨터 버스에 연결되는 별도 회로 보드 또는 호스트 버스 어댑터로 구현.
호스트 버스 어댑터는 HBA라고 부르며,
FC 프로토콜 메시지를 처리할 수 있는 프로세서, 마이크로코드, 전용 메모리를 포함하는 경우가 많음.
일부 장치는 자체 컨트롤러를 내장하며, 디스크 드라이브 한쪽의 회로 기판이 그 예.
디스크 컨트롤러는 SAS나 SATA처럼 컴퓨터와 디스크를 연결하는 프로토콜의 디스크 측 부분을 구현.
또한 불량 섹터 매핑, 선반입, 버퍼링, 캐싱 같은 작업을 수행하는 마이크로코드와 프로세서를 포함하는 구조.
메모리 맵 입출력
입출력을 수행하려면 처리기가 명령어와 데이터를 컨트롤러에 전달해야 하는 필요.
모든 컨트롤러는 제어용 또는 데이터용 레지스터를 가지며,
본체 프로세서는 이 레지스터에 비트 패턴을 쓰거나 읽어서 입출력을 수행.
컨트롤러와 통신하는 한 방법은 특별한 입출력 명령어 사용.
입출력 명령어는 한 바이트나 워드를 특정 입출력 포트 주소로 전달하도록 지정하는 명령.
이 명령은 해당 장치에 맞는 버스 회선을 선택하여 장치 레지스터로 비트를 보내거나 읽어 오도록 촉발하는 방식.
다른 방법은 특수 입출력 명령어 대신 장치 제어 레지스터를 프로세서의 주소 공간에 사상하는 방식.
이 방식이 메모리 맵 입출력.
메모리 맵 입출력에서는 각 주변 장치 레지스터가 메모리 주소와 일대일 대응.
CPU는 물리 메모리에 사상된 장치 제어 레지스터를 표준 데이터 전송 명령으로 읽고 쓰면서 입출력 요청을 수행.

과거 PC는 일부 장치를 제어하기 위해 입출력 명령을 사용하고,
다른 장치를 제어하기 위해 메모리 맵 입출력을 사용하는 혼합 방식.
그래픽 컨트롤러는 기본 제어 연산에는 입출력 포트를 가지지만,
스크린에 출력할 내용을 저장하기 위해 큰 메모리 맵 영역도 가질 수 있음.
메모리 맵 방식은 그래픽 같은 대량 데이터 장치에서 빠른 접근을 가능하게 하는 장점.
장치 레지스터를 메모리 주소처럼 다루면 일반 메모리 명령으로 장치를 제어할 수 있어 프로그래밍이 단순해지는 구조.
일반적인 장치 인터페이스는 상태 레지스터, 제어 레지스터, 입력 데이터 레지스터, 출력 데이터 레지스터로 구성.
상태 레지스터는 호스트가 읽어 장치 상태를 확인하는 레지스터.
상태 레지스터에는 명령 완료 여부, 입력 데이터 레지스터에 자료가 있는지 여부, 장치 오류 여부 같은 비트 포함 가능.
제어 레지스터는 호스트가 장치에 명령을 내리거나 장치 동작 모드를 변경하기 위해 쓰는 레지스터.
입력 데이터 레지스터는 호스트가 읽어 장치에서 들어온 자료를 얻는 레지스터.
출력 데이터 레지스터는 호스트가 써서 장치로 나갈 자료를 전달하는 레지스터.
입력 데이터 레지스터와 출력 데이터 레지스터의 크기는 보통 1바이트에서 4바이트 정도.
일부 컨트롤러는 FIFO 칩을 사용하여 여러 바이트의 입력 또는 출력 자료를 보관할 수 있음.
FIFO는 선입선출 방식의 버퍼로, 컨트롤러와 호스트 사이의 속도 차이를 어느 정도 완충하는 구조.
폴링
호스트와 컨트롤러 사이의 통신은 손 흔들기, 즉 핸드셰이킹으로 설명 가능.
폴링은 CPU가 장치 상태 레지스터를 반복적으로 읽어 장치가 준비되었는지 확인하는 방식.
호스트는 상태 레지스터의 busy 비트가 0이 될 때까지 반복적으로 검사.
busy 비트가 0이면 장치가 새 명령을 받을 준비가 된 상태.
호스트는 명령 레지스터의 write 비트를 설정하고, 출력할 바이트를 data-out 레지스터에 기록.
그 뒤 command-ready 비트를 설정하여 컨트롤러에게 명령이 준비되었음을 알림.
컨트롤러는 command-ready 비트를 감지하면 busy 비트를 설정하여 자신이 작업 중임을 표시.
컨트롤러는 명령 레지스터를 읽어 write 명령임을 확인하고, data-out 레지스터의 데이터를 읽어 장치로 전송.
전송이 완료되면 컨트롤러는 command-ready 비트를 지우고, 오류가 있으면 error 비트를 설정.
마지막으로 컨트롤러는 busy 비트를 지워 다음 명령을 받을 수 있음을 표시.
이 과정에서 호스트가 상태 레지스터를 반복적으로 검사하는 동작이 폴링.
폴링은 단순하고 예측 가능한 방식이지만, CPU가 장치를 기다리며 계속 상태를 확인하면 바쁜 대기 발생.
장치가 빠르거나 대기 시간이 짧으면 폴링이 효율적일 수 있음.
그러나 장치가 느리거나 요청이 오래 걸리면 CPU 시간이 낭비되므로 인터럽트 방식이 더 적합.
컴퓨터 하드웨어에는 보통 입출력 장치가 CPU에게 스스로 서비스를 요청할 수 있게 하는 인터럽트 기능 존재.
인터럽트
인터럽트는 장치가 CPU에게 주의가 필요하다는 신호를 보내는 방식.
CPU 하드웨어에는 인터럽트 요청 라인이라는 신호선이 존재.
CPU는 각 명령어 실행 후 인터럽트 요청 라인을 검사하고, 신호가 감지되면 인터럽트 처리 루틴으로 이동.
장치 컨트롤러는 인터럽트 요청 라인에 신호를 보내 인터럽트를 발생시킴.
인터럽트 핸들러는 인터럽트 발생 원인을 조사하고 필요한 작업을 수행한 뒤, 인터럽트 이전의 실행 상태로 복귀시키는 역할.

인터럽트 기반 입출력은 장치가 작업을 수행하는 동안 CPU가 다른 일을 할 수 있게 해주는 구조.
이 방식은 CPU가 입출력 완료를 계속 기다리는 폴링보다 일반적으로 효율적.
그러나 현대 시스템에서는 단순히 인터럽트를 받는 것만으로 충분하지 않음.
운영체제는 인터럽트 발생을 필요에 따라 지연시키고,
어떤 장치가 인터럽트를 발생시켰는지 빠르게 식별하며, 여러 인터럽트 사이의 우선순위를 처리할 수 있어야 하는 필요.
또한 페이지 폴트, 0으로 나누기 오류, 잘못된 메모리 접근 같은 예외도
인터럽트와 유사한 방식으로 처리해야 하는 구조.

이 그림은 사용자가 특별한 작업을 하지 않는 짧은 시간에도 수많은 인터럽트가 발생할 수 있음을 보여주는 자료.
따라서 현대 운영체제는 인터럽트를 단순히 처리하는 수준을 넘어
지연 처리, 빠른 원인 식별, 우선순위 처리, 예외 처리까지 지원해야 하는 구조.
대부분의 CPU는 마스크 가능 인터럽트 요청 라인과 마스크 불가능 인터럽트 요청 라인을 가짐.
마스크 가능 인터럽트는 필요한 경우 CPU가 잠시 무시하거나 지연시킬 수 있는 일반 장치 인터럽트.
마스크 불가능 인터럽트는 복구하기 어려운 메모리 오류처럼 반드시 즉시 처리해야 하는 긴급 이벤트에 사용.
인터럽트 벡터는 인터럽트 번호와 인터럽트 핸들러 주소를 연결하는 테이블.
CPU는 인터럽트 벡터를 사용하여 인터럽트 원인에 맞는 핸들러로 빠르게 이동.

Intel Pentium의 벡터 0~31은
나누기 오류, 디버그 예외, null 인터럽트, 중단 지점,
오버플로우, 경계 범위 예외, 불법 명령, 페이지 폴트 같은 내부 예외에 사용.
벡터 32~255는 마스크 가능한 인터럽트에 사용되는 범위.
인터럽트 벡터의 항목 수는 제한되어 있으므로, 모든 장치에 독립된 벡터를 제공하기 어려운 경우 존재.
이 문제를 해결하기 위해 인터럽트 사슬화 사용.
인터럽트 사슬화에서는 인터럽트 벡터의 각 원소가 여러 핸들러의 리스트를 가리키고,
해당 리스트를 차례로 검사하여 실제 인터럽트를 처리할 핸들러를 찾는 구조.
이 방식은 큰 인터럽트 테이블을 만들지 않으면서 여러 장치를 처리할 수 있지만,
핸들러를 찾는 시간은 증가할 수 있는 절충.
인터럽트 우선순위 수준은 긴급한 인터럽트가 덜 중요한 인터럽트보다 먼저 처리될 수 있도록 하는 기법.
높은 우선순위 인터럽트는 낮은 우선순위 인터럽트의 실행을 선점할 수 있는 구조.
운영체제는 부팅 시 하드웨어 버스를 조사하여 어떤 장치가 있는지 파악하고, 인터럽트 벡터에 필요한 장치 핸들러 등록.
입출력 중에는 장치 컨트롤러가 인터럽트를 발생시키고,
운영체제는 해당 핸들러를 통해 장치의 상태를 확인하고 입출력 완료나 오류를 처리.
인터럽트는 커널 안에서도 여러 방식으로 사용.
페이지 폴트는 예외 형태의 인터럽트로 발생하며,
운영체제는 페이지가 메모리에 없으면 프로세스를 대기 큐로 보내고 필요한 페이지 입력을 스케줄.
시스템 콜도 소프트웨어 인터럽트 또는 트랩을 통해 사용자 모드에서 커널 모드로 전환하는 방식으로 구현 가능.
시스템 콜 인터럽트는 보통 장치 인터럽트보다 낮은 우선순위를 가지며,
네트워크나 디스크 장치 인터럽트 처리를 방해하지 않아야 하는 구조.
인터럽트는 커널 내부 제어 흐름 관리에도 사용 가능.
예를 들어 디스크 읽기에서는 높은 우선순위 인터럽트가 입출력 상태를 기록하고 다음 대기 입출력을 시작한 뒤,
낮은 우선순위 인터럽트가 커널 버퍼의 데이터를 응용 프로그램 주소 공간으로 복사하는 구조.
이처럼 하나의 입출력 작업도 우선순위가 다른 여러 인터럽트 단계로 나뉠 수 있음.
대부분의 경우 인터럽트 처리는 시간과 자원이 제한되어 복잡하게 구현하기 어려우므로,
시스템은 인터럽트 관리를 1차 인터럽트 처리기와 2차 인터럽트 처리기로 나누는 구조.
1차 인터럽트 처리기는 FLIH라고 부르며, First-Level Interrupt Handler의 의미.
FLIH는 문맥 교환, 상태 저장, 처리 작업을 큐에 삽입하는 빠른 작업을 수행.
2차 인터럽트 처리기는 SLIH라고 부르며, Second-Level Interrupt Handler의 의미.
SLIH는 별도로 스케줄되어 요청된 실제 작업 처리를 수행.
스레드를 구현한 커널은 여러 단계의 우선순위를 가진 인터럽트 처리를 구현하거나,
커널 내부 백그라운드 처리와 응용 루틴보다 인터럽트 핸들링에 우선권을 부여하기에 적합.
Solaris에서는 각 인터럽트 핸들러가 하나의 커널 스레드에 대응하고,
이 스레드들은 높은 스케줄링 우선순위를 부여받는 구조.
서로 다른 인터럽트 핸들러 스레드에 상이한 우선순위를 주면
낮은 우선순위 핸들러는 높은 우선순위 핸들러에게 CPU를 양보 가능.
다중 처리기에서는 여러 인터럽트 핸들러가 병렬로 수행될 수 있음.
인터럽트는 모든 현대 시스템에서 비동기적으로 일어나는 이벤트를 처리하고,
커널 내 슈퍼바이저 루틴으로 달려가기 위한 방법.
인터럽트 구동 입출력은 현대 시스템에서 폴링보다 훨씬 일반적이며,
폴링은 주로 빠른 장치나 입출력 발생률이 매우 높아 폴링이 더 효율적인 경우에 사용되는 방식.
일부 장치 드라이버는 입출력 발생률이 낮을 때 인터럽트를 사용하고, 발생률이 높아지면 폴링으로 전환하는 구조.
직접 메모리 접근
디스크처럼 많은 데이터를 입출력하는 장치에서 CPU가 매번 바이트 전송을 제어하는 방식은 낭비.
CPU가 상태 비트를 반복적으로 검사하면서 1바이트씩 옮기는 방식은 PIO.
PIO는 Programmed I/O의 약자.
컴퓨터는 CPU의 PIO 작업 일부를 DMA 컨트롤러라는 특수 프로세서에 위임하여 CPU 부담을 줄이는 구조.
DMA는 Direct Memory Access의 약자.
DMA 전송을 시작하기 위해 호스트는 메모리에 DMA 명령 블록을 기록.
DMA 명령 블록에는 전송할 데이터의 위치를 나타내는 포인터, 전송 목적지 포인터, 전송 바이트 수 같은 정보 포함.
봉쇄되지 않은 명령이 모두 완료되면 인터럽트를 일으키라는 명령도 DMA 명령 블록에 포함 가능.
산포-수집 방식은 하나의 DMA 명령으로 여러 개의 전송을 실행할 수 있게 하는 기법.
산포-수집은 scatter-gather의 의미.
CPU는 DMA 명령 블록의 주소를 DMA 컨트롤러에 알려주고 다른 일을 수행.
DMA 컨트롤러는 CPU 도움 없이 버스를 통해 DMA 명령 블록에 접근하고, 장치와 메모리 사이의 데이터 전송을 수행.
대상 주소가 커널 주소 공간에 있는 경우가 가장 단순한 구조.
대상 주소가 사용자 공간에 있으면 사용자가 전송 중 해당 공간의 내용을 수정할 수 있어
일부 데이터 손실이나 불일치 가능.
DMA로 전송된 데이터를 스레드가 접근할 수 있게 하려면
커널 메모리에서 사용자 메모리로 두 번째 복사가 필요할 수 있음.
이러한 이중 버퍼링은 비효율적.
시간이 지나며 운영체제는 장치와 사용자 주소 공간 사이의 직접 입출력 전송을 위해
메모리 매핑을 사용하는 방향으로 발전.

DMA 컨트롤러와 장치 컨트롤러 간의 핸드셰이킹은 DMA-request와 DMA-acknowledge라는 두 선을 통해 수행.
장치 컨트롤러가 전송할 데이터가 준비되면 DMA-request 선에 신호를 보냄.
DMA 컨트롤러는 메모리 버스를 얻고 원하는 주소를 메모리 주소선에 올려놓는 과정.
그 뒤 DMA-acknowledge 선에 신호를 보내 장치 컨트롤러에게 전송 준비가 되었음을 알림.
장치 컨트롤러는 DMA-acknowledge 신호를 받으면 한 워드를 메모리로 전송하고 DMA-request 신호를 제거.
전송이 완전히 끝나면 DMA 컨트롤러가 CPU에 인터럽트를 발생시키는 구조.
DMA가 메모리 버스를 점유하는 동안 CPU는 캐시에 있는 데이터에는 접근할 수 있지만,
주 메모리 접근은 일시적으로 지연될 수 있음.
이러한 현상은 사이클 훔치기(cycle stealing).
사이클 훔치기는 CPU 실행을 다소 느리게 만들지만,
입출력 작업을 DMA로 넘기는 것이 전체 시스템 성능을 향상시키는 경우가 많음.
일부 컴퓨터는 DMA에서 물리 주소를 사용하고, 다른 컴퓨터는 직접 가상 메모리 접근을 사용.
직접 가상 메모리 접근은 DVMA라고 하며, Direct Virtual Memory Access의 약자.
DVMA를 사용하면 CPU와 메모리의 개입 없이도
가상 주소로 두 개의 메모리 맵 장치 사이에서 데이터를 전송할 수 있는 구조.
보호 모드 커널을 가진 운영체제는 일반 프로세스가 입출력 명령을 직접 내리지 못하게 함.
이는 접근 제어뿐 아니라 시스템 고장을 유발할 수 있는 실수로부터 시스템을 보호하기 위한 목적.
대신 운영체제는 충분한 권한을 가진 프로세스가 하드웨어에 대해 낮은 수준의 연산을 수행할 수 있는 함수를 제공.
메모리 보호 장치가 없는 커널에서는 일반 프로세스가 직접 장치 컨트롤러에 접근 가능.
이 방식은 커널 통신, 문맥 교환, 커널 소프트웨어 계층을 거치지 않으므로 높은 성능을 낼 수 있지만,
시스템 보안과 안정성에 큰 문제를 야기.
일반적인 범용 운영체제는 메모리와 장치를 보호하여
잘못되거나 악의적인 응용 프로그램으로부터 시스템을 보호하는 구조.
입출력 하드웨어 요약
입출력 하드웨어를 전자 회로 수준까지 내려가 보면 복잡하지만, 운영체제 관점에서 중요한 개념은 몇 가지로 정리 가능.
주요 개념은 버스, 컨트롤러, 입출력 포트와 레지스터, 호스트와 장치 컨트롤러 간의 핸드셰이킹,
폴링이나 인터럽트를 통한 핸드셰이킹 수행, 큰 데이터 전송을 DMA 컨트롤러로 넘기는 방식.
앞 절의 예는 호스트와 장치 컨트롤러 간의 간단한 핸드셰이킹 사례.
그러나 실제 시스템에는 매우 다양한 입출력 장치가 존재하고, 매일 새로운 장치가 등장하는 구조.
새로운 장치를 운영체제를 크게 고치지 않고 추가하려면 운영체제는 장치 다양성을 수용할 수 있게 설계되어야 함.
장치들은 제어 비트 정의, 호스트와 상호 작용하는 프로토콜, 제공해야 할 기능이 모두 다를 수 있음.
운영체제는 이러한 차이를 장치 드라이버와 표준 인터페이스로 감추고,
응용 프로그램에는 편리하고 일관된 입출력 인터페이스를 제공해야 하는 역할.
응용 입출력 인터페이스
운영체제는 모든 입출력 장치가 일관된 방법으로 다루어질 수 있도록 인터페이스를 구성.
응용 프로그램은 디스크 종류를 알 필요 없이 파일을 열 수 있어야 하고,
새로운 디스크가 추가되어도 기존 운영체제에 큰 혼란 없이 사용할 수 있어야 하는 구조.
이 문제는 추상화, 캡슐화, 소프트웨어 계층화로 해결.
운영체제는 대표적인 장치 종류를 정의하고, 각 종류의 장치에 표준 함수 집합인 인터페이스를 제공.
장치 드라이버는 각 입출력 장치의 구체적 기능을 제공하고,
상위에서 정의한 표준 인터페이스의 내부 구현을 담당하는 커널 모듈.

장치 드라이버 계층의 목적은 여러 입출력 하드웨어 간의 차이를 숨기고,
상위 커널 입출력 서브시스템에는 간단한 표준 인터페이스로 보이게 포장하는 것.
입출력 서브시스템은 하드웨어와 독립적으로 동작할 수 있어 운영체제 개발자의 작업을 단순화.
하드웨어 제조업자는 새 장치를 기존 컨트롤러 인터페이스와 같게 만들거나,
새 장치 드라이버를 제공함으로써 운영체제 전체 수정 없이 장치를 추가 가능.
문제는 운영체제마다 장치 드라이버 인터페이스 규격이 다르다는 점.
따라서 새 장치는 Windows, Linux, AIX, macOS용 드라이버처럼
여러 운영체제별 장치 드라이버와 함께 제공되어야 할 수 있음.

입출력 장치는 여러 차원의 특성을 가짐.
문자 스트림 장치는 바이트를 하나씩 전송하고, 블록 장치는 블록 단위로 전송하는 방식.
순차 접근 장치는 정해진 순서로만 자료를 전송하고, 임의 접근 장치는 임의 위치의 자료도 입출력 가능.
동기식 장치는 시스템의 다른 측면과 조율되어 일정한 응답 시간을 보이고,
비동기식 장치는 다른 이벤트와 조율 없이 불규칙하거나 예측 불가능한 응답 시간을 보이는 특성.
공유 가능 장치는 여러 프로세스나 스레드가 동시에 사용할 수 있고,
전용 장치는 한 번에 하나의 사용자만 사용해야 하는 구조.
장치 속도는 초당 몇 바이트부터 초당 수 기가바이트까지 다양.
장치는 읽기·쓰기 가능, 읽기 전용, 한 번 쓰기 후 읽기 전용 등으로 구분 가능.
운영체제는 장치의 차이를 숨기고 접근 목적에 따라 장치를 몇 개의 범주로 분류.
대표적인 장치 범주는 블록 입출력, 문자 스트림 입출력, 메모리 맵 파일 접근, 네트워크 소켓.
운영체제는 현재 시각을 알리거나 타이머를 설정하는 클록과 타이머 같은 특수 장치에 대해서도 별도 시스템 콜 제공.
일부 운영체제는 그래픽 디스플레이, 비디오, 오디오 장치를 위한 특수 시스템 콜 집합도 제공.
대부분의 운영체제는 응용 프로그램이 입출력 장치로 임의의 명령을 전달할 수 있는
escape 또는 back-door 시스템 콜을 가짐.
UNIX에서는 ioctl()이 이러한 시스템 콜.
ioctl()은 새로운 시스템 콜을 만들 필요 없이
응용 프로그램이 특정 장치 드라이버가 제공하는 임의 기능을 사용할 수 있게 하는 방식.
ioctl()은 장치 식별자, 장치가 이해하는 명령 정수, 임의 자료 구조를 가리키는 포인터라는 세 가지 인자를 사용.
UNIX와 Linux의 장치 식별자는 메이저 번호와 마이너 번호의 튜플.
메이저 번호는 장치 유형을 나타내고, 마이너 번호는 해당 유형 안의 장치 인스턴스를 나타내는 값.
예를 들어 /dev/sda, /dev/sda1, /dev/sda2, /dev/sda3은 같은 메이저 번호를 공유하고,
마이너 번호로 디스크 또는 파티션 인스턴스를 구분.
운영체제는 이 번호를 사용하여 입출력 요청을 적절한 장치 드라이버와 정확한 장치 인스턴스로 라우팅.
블록 장치와 문자 장치
블록 장치 인터페이스는 디스크나 유사한 블록 지향 장치를 사용하기 위해 필요한 모든 기능을 제공.
일반적으로 읽기, 쓰기, 다음 전송 위치를 지정하는 탐색 기능을 포함.
응용 프로그램은 보통 파일 시스템 인터페이스를 통해 블록 장치에 접근.
read(), write(), seek() 같은 명령은
블록 저장장치의 핵심 동작을 대표하므로 응용은 장치의 저수준 차이에 신경 쓰지 않아도 되는 구조.
운영체제나 데이터베이스는 블록 장치를 선형 배열처럼 이해하고 직접 사용하기를 원할 수 있음.
이러한 접근 방식은 비가공 입출력.
비가공 입출력은 raw I/O의 의미.
응용이 자체 버퍼링을 수행한다면 파일 시스템은 불필요하고 중복된 버퍼링을 하게 될 수 있음.
마찬가지로 응용이 파일 블록이나 일부에 대한 자체 잠금 기능을 제공한다면
운영체제의 잠금 기능은 최소한 중복 기능이고 최악의 경우 모순 발생 가능.
이러한 충돌을 피하려고 raw 장치 접근은 장치 제어권을 직접 응용에 넘기고 운영체제는 한 발 물러나는 구조.
그러나 raw 장치에 대해서는 어떤 운영체제 서비스도 수행되지 말아야 하는 한계.
이에 대한 보편적인 절충안은 운영체제가 버퍼링과 잠금을 하지 않는 모드로 파일 입출력을 수행하는 것.
UNIX 시스템에서는 이러한 방식을 직접 입출력이라고 부름.
메모리 맵 파일 접근은 블록 장치 위의 계층으로 구현 가능.
메모리 맵 파일 접근은 실제 장치를 읽고 쓰는 명령 대신
메모리의 특정 번지를 읽거나 쓰는 명령으로 파일 입출력을 대신하는 방식.
파일을 메모리 맵 방식으로 사용하려면 파일 이름을 가지고 지정된 시스템 콜을 수행하고,
운영체제는 해당 파일을 가상 메모리로 사상한 뒤 가상 메모리 주소를 반환.
파일 데이터는 대응되는 메모리가 참조될 때만 실제로 메모리에 올라오는 요구 페이징 방식.
이 방식은 파일 입출력 인터페이스를 직접 쓰지 않고 요구 페이징을 사용하므로 더 효율적일 수 있음.
프로그래머 입장에서는 메모리 맵 파일 접근이 일반 메모리 읽기와 쓰기만큼 단순한 장점.
가상 메모리를 제공하는 운영체제는 커널 서비스를 위해 매핑 인터페이스를 이용.
예를 들어 운영체제가 새로운 사용자 프로그램 코드를 실행할 때 실행 가능 파일을 메모리에 사상하고,
제어를 실행 파일의 진입 주소로 이동시키는 방식.
커널이 디스크에 있는 스왑 공간을 접근할 때도 이 방식 사용.
키보드는 문자 스트림 인터페이스를 통해 접근되는 장치의 예.
문자 스트림 인터페이스는 응용 프로그램에 한 글자씩 보내고 받아 오는 put과 get 명령 제공.
이 위에 라이브러리를 만들면 한 줄 단위 입력, 버퍼링, 편집 기능을 제공 가능.
예를 들어 사용자가 백스페이스 키를 누르면 이전 문자는 스트림에서 제거되고 응용 프로그램에 전달되지 않는 방식.
문자 스트림 인터페이스는 키보드, 마우스, 모뎀 같은 장치에 적합.
이 장치들은 공유로 언제 어떤 데이터가 입력될지 응용 프로그램으로서는 사전에 예측하기 어려운 특성.
프린터나 audio-board처럼 바이트를 선형 스트림으로 처리하는 출력 장치에도 문자 스트림 인터페이스가 적합.
네트워크 장치
네트워크 입출력은 디스크 입출력과 상당히 다르므로,
대부분의 운영체제는 디스크의 read(), write(), seek() 인터페이스와 다른 인터페이스를 네트워크에 제공.
UNIX와 Windows NT를 포함한 많은 운영체제는 네트워크 소켓 인터페이스 사용.
소켓은 네트워크 통신의 끝점을 나타내는 추상화.
전기 소켓에 여러 가전제품을 꽂을 수 있듯,
네트워크 소켓은 응용 프로그램이 네트워크 연결을 만들고 사용할 수 있게 하는 인터페이스.
소켓 인터페이스는 응용 프로그램이 소켓을 생성하고,
로컬 소켓을 원격 주소와 연결하고, 연결 완료 여부를 확인하고, 패킷을 주고받는 기능 제공.
네트워크 서버를 지원하기 위해 소켓 인터페이스는 select() 함수 제공.
select()는 어느 소켓이 수신 대기 중인 패킷을 가지고 있는지,
어느 소켓이 전송할 패킷을 더 받아들일 여유 공간을 가졌는지 알려주는 기능.
select()를 사용하면 응용 프로그램이 네트워크 상태를 폴링하거나 바쁜 대기를 할 필요가 줄어드는 구조.
이러한 기능은 네트워크의 자세한 내부 사항을 캡슐화하여,
분산 응용이 임의의 네트워크 하드웨어나 프로토콜 스택을 사용할 수 있게 하는 역할.
프로세스 간 통신과 네트워크 통신을 위해 다양한 방법이 구현되어 왔음.
Windows는 네트워크 인터페이스 카드와 네트워크 프로토콜을 위해 서로 다른 인터페이스를 제공.
UNIX는 FIFO, full-duplex STREAMS, 메시지 큐, 소켓 등 다양한 IPC 방법을 제공하는 구조.
클록과 타이머
대부분의 컴퓨터는 하드웨어 클록과 타이머를 가지고
현재 시각 제공, 지난 시간 제공, 특정 시각이 되면 지정된 작업 실행이라는 세 가지 기본 기능 제공.
이 기능은 운영체제뿐 아니라 시간에 의존하는 응용들도 많이 사용하는 기능.
그러나 이러한 기능을 구현하는 시스템 콜은 운영체제 사이에서 표준화되어 있지 않은 편.
지나간 시간을 재고 특정 작업을 실행시키는 하드웨어는 프로그램 가능 인터벌 타이머.
프로그램 가능 인터벌 타이머는 programmable interval timer의 의미.
타이머는 일정 시간이 지나면 인터럽트를 발생시키도록 설정 가능하며,
한 번만 발생하거나 주기적으로 반복되도록 설정 가능.
이 기법은 스케줄러가 타임 슬라이스 종료 시 현재 프로세스로부터 CPU를 빼앗기 위해 사용.
또한 디스크 입출력 서브시스템이 변경된 캐시 버퍼를 주기적으로 디스크에 플러시하거나,
네트워크 서브시스템이 혼잡 또는 오류로 인해 작업을 취소할 때 사용.
운영체제는 일반 사용자 프로세스들이 타이머를 사용할 수 있도록 인터페이스 제공.
운영체제가 가상 클록을 흉내 내면 타이머 하드웨어 채널 수보다 더 많은 타이머 요청 지원 가능.
이를 위해 커널 또는 타이머 장치 드라이버는 시간 관련 인터럽트 요청을 마감 시간 순으로 정렬하고,
가장 가까운 마감 시간에 타이머를 설정.
타이머 인터럽트가 발생하면 요청자에게 신호를 보내고, 다음으로 빠른 마감 시간으로 타이머를 다시 설정하는 구조.
최신 PC에는 고성능 이벤트 타이머, 즉 HPET가 포함될 수 있음.
HPET는 10MHz 범위의 속도로 동작하며,
저장된 값과 HPET 값이 일치할 때 한 번 또는 반복적으로 트리거되도록 설정 가능한 비교기를 제공.
트리거가 발생하면 인터럽트가 생기고, 운영체제의 클록 관리 루틴은 타이머의 목적과 수행할 조치를 결정.
시간 간격이 매우 정밀하지 않은 이유는 타이머 하드웨어 자체의 한계뿐 아니라,
매우 정밀한 가상 클록을 유지하는 오버헤드가 크기 때문.
타이머 틱이 time-of-day 클록에 사용되면 시스템 시각이 부정확해질 수 있음.
이 부정확성은 NTP 같은 네트워크 시간 프로토콜을 사용하여 보정 가능.
NTP는 정교한 지연 시간 계산을 사용하여 컴퓨터 시계를 거의 원자시계 수준으로 정확하게 유지하는 프로토콜.
대부분의 컴퓨터에서 하드웨어 클록은 카운터로 구현.
일부 컴퓨터는 카운터 값을 장치 레지스터에서 읽을 수 있게 하며, 이러한 카운터는 정밀도가 높은 시계 역할.
하지만 높은 정밀도를 가진 클록은 보통 인터럽트 생성보다 시간 간격의 정확한 측정에 사용되는 구조.
봉쇄형과 비봉쇄형 입출력
입출력 시스템 콜 인터페이스에서는 봉쇄형과 비봉쇄형 또는 비동기식 방법의 선택 문제가 중요.
응용 프로그램이 봉쇄형 시스템 콜을 호출하면 호출 스레드는 봉쇄 상태가 되고,
운영체제는 그 스레드를 실행 큐에서 대기 큐로 이동.
입출력이 끝나면 스레드는 다시 실행 큐로 돌아오고,
이후 스케줄러에 의해 실행이 재개되며 시스템 콜의 반환 값을 받는 구조.
입출력 장치 작업은 일반적으로 비동기적으로 수행되고 수행 시간이 다양하며 예측하기 어려운 특성.
그럼에도 운영체제는 봉쇄형 코드가 작성하기 쉽기 때문에 응용 인터페이스에 봉쇄형 시스템 콜을 제공.
일부 사용자 프로그램에는 비봉쇄형 입출력이 필요.
대표적인 예는 스크린에 자료를 표시하거나 연산하는 동안 키보드와 마우스 입력을 받아들이는 경우.
또 다른 예는 디스크에서 비디오 파일을 읽으면서 동시에 압축 해제와 화면 출력을 수행하는 비디오 응용 프로그램.
응용 프로그램 작성자는 연산과 입출력의 중첩을 최대화하기 위해 다중 스레드 방식으로 프로그램을 작성할 수 있음.
일부 스레드는 봉쇄형 시스템 콜로 봉쇄되고, 나머지 스레드는 연산을 계속 수행하는 구조.
비봉쇄형 입출력 시스템 콜은 스레드를 오래 멈추지 않고 즉시 반환하며,
몇 바이트가 전송되었는지를 반환 값으로 알려주는 방식.
비동기식 시스템 콜도 즉시 반환하지만,
입출력이 완료되면 운영체제가 변수 설정, 신호, 소프트웨어 인터럽트, 별도 콜백 루틴 실행 등으로
완료 사실을 알려주는 방식.
비봉쇄형 읽기는 현재 가져올 수 있는 데이터를 전체 또는 일부만 가지고 즉시 반환.
비동기식 읽기는 입력이 완전히 끝난 뒤 완전한 데이터를 전송해 달라고 요청하는 방식.

비동기적 행동은 현대 운영체제 전반에 걸쳐 존재하며, 사용자나 응용에 직접 노출되지 않는 경우도 많음.
보조저장장치와 네트워크 입출력이 대표적인 예.
응용이 네트워크 송신 요청이나 보조저장장치 쓰기 요청을 하면,
운영체제는 요청을 버퍼에 넣고 응용으로 되돌아간 뒤 여유가 생길 때 요청을 완료하는 방식.
시스템 고장이 발생하면 아직 기록되지 않은 요청이 손실될 수 있으므로, 운영체제는 버퍼링 시간에 제한을 둠.
일부 UNIX 시스템은 30초마다 보조저장장치 버퍼를 디스크에 기록하거나,
각 요청을 시작 후 30초 안에 플러시하는 방식.
운영체제는 응용이 버퍼 플러시를 요청하여 플러시 간격을 기다리지 않고
데이터를 보조저장장치로 강제 전송할 수 있는 방법 제공.
커널은 아직 기록되지 않은 데이터가 읽기 요청을 한 응용에 전달되도록 보장하여 응용의 데이터 일관성 유지.
그러나 같은 파일에 입출력을 수행하는 여러 스레드는 커널 구현 방식에 따라
일관되지 않은 데이터를 볼 수 있으므로 잠금 프로토콜 필요 가능.
일부 입출력 요청은 즉시 수행되어야 하므로,
입출력 시스템 콜은 특정 요청이나 장치에 대한 동기적 수행 필요성을 나타낼 방법을 제공.
비봉쇄형의 좋은 예는 네트워크 소켓의 select() 시스템 콜.
select()는 최대 대기 시간을 인자로 지정하고, 이를 0으로 지정하면 봉쇄 없이 네트워크 폴링만 수행.
하지만 select()는 입출력이 가능한지만 검사하고,
실제 데이터 전송에는 read()나 write()가 필요하므로 추가 오버헤드 발생.
Mach의 봉쇄형 다중 읽기 호출은 여러 입출력 장치를 검사하고,
그중 어느 하나라도 데이터가 있으면 데이터와 함께 즉시 반환하는 방식.
벡터형 입출력
벡터형 입출력은 하나의 시스템 콜로 여러 위치에 대한 입출력 연산을 수행할 수 있게 하는 방식.
UNIX readv 시스템 콜은 여러 버퍼로 이루어진 벡터를 인자로 받아 벡터와 목적지 사이의 읽기 또는 쓰기 작업 수행.
동일한 작업을 여러 번의 시스템 콜로도 수행할 수 있지만, 분산-수집 방식은 여러 이유로 유용.
여러 개의 개별 버퍼 내용을 한 번의 시스템 콜로 입출력할 수 있으므로 문맥 교환과 시스템 콜 오버헤드 감소.
벡터형 입출력이 없으면 데이터는 먼저 큰 버퍼에 올바른 순서로 모인 뒤 장치로 전송되어야 하므로 비효율 발생.
일부 scatter-gather 입출력은 원자성을 제공하여 전체 입출력이 방해받지 않도록 보장.
이 보장은 다른 스레드가 같은 버퍼에 입출력하는 경우 데이터 오염을 피하는 데 도움.
가능하다면 프로그래머는 scatter-gather 입출력을 사용하여 처리량을 증가시키고
시스템 오버헤드를 줄이는 것이 유리.
커널 입출력 서브시스템
커널은 입출력과 관련된 많은 서비스를 제공.
대표적인 서비스는 입출력 스케줄링, 버퍼링, 캐싱, 스풀링, 장치 예약, 오류 처리.
이 서비스들은 하드웨어와 장치 드라이버 구조를 바탕으로 제공되는 기능.
입출력 서브시스템은 오류가 있는 프로세스나 악의적인 사용자로부터 자신을 보호할 책임도 가짐.
I/O 스케줄링
입출력 요청을 스케줄한다는 것은 요청을 실행할 순서를 결정한다는 의미.
응용 프로그램이 요청한 순서대로만 처리하면 최상의 성능을 얻기 어려운 경우 존재.
스케줄링은 전체 시스템 성능을 향상시키고 프로세스 사이의 공정성을 보장하며,
입출력 완료까지의 평균 대기 시간을 줄이는 목적.
예를 들어 디스크 암이 시작 부분에 있고 세 응용이 각각 끝, 시작, 중간 블록을 요청했다면,
도착 순서보다 시작-중간-끝 순서로 처리하는 것이 더 효율적인 구조.
운영체제 개발자는 각 장치마다 대기 큐를 유지하여 스케줄링을 구현.

응용 프로그램이 봉쇄형 입출력 시스템 콜을 호출하면 해당 입출력 요청은 장치 큐에 들어감.
입출력 스케줄러는 시스템 성능과 각 응용의 평균 응답 시간을 향상시키기 위해 큐 안의 순서를 재배치.
운영체제는 보통 공평한 처리를 목표로 하지만, 시간에 민감한 작업은 더 빨리 처리 가능.
가상 메모리의 페이징 요청은 일반 응용 입출력 요청보다 높은 우선순위로 처리될 수 있음.
커널이 비동기적 입출력을 제공한다면 동시에 많은 입출력 요청을 추적해야 하는 필요.
이를 위해 운영체제는 각 장치 상태 테이블에 대기 큐를 연결.
장치 상태 테이블의 각 항목은 장치 종류, 주소, 상태, 대기 요청을 나타냄.
장치 상태는 작동하지 않음, 유휴, 동작 중 같은 값으로 표현 가능.
장치가 동작 중이면 같은 장치에 대한 추가 요청은 해당 장치의 대기 큐에 저장되는 구조.
입출력 스케줄링은 입출력 서브시스템이 컴퓨터 효율성을 높이는 방법 중 하나.
다른 방법은 버퍼링, 캐싱, 스풀링을 통해 메인 메모리나 저장장치 계층의 다른 저장 공간을 활용하는 것.
버퍼링
버퍼는 두 장치 사이 또는 장치와 응용 프로그램 사이에서 데이터가 전송되는 동안
전송 데이터를 임시로 저장하는 메모리 영역.
버퍼링이 필요한 첫 번째 이유는 데이터 생산자와 소비자 사이의 속도 차이를 흡수하기 위해서.
예를 들어 인터넷을 통해 파일을 수신하여 SSD에 저장할 때,
네트워크와 드라이브의 속도 차이를 메모리 버퍼가 완충.
네트워크로부터 수신되는 바이트를 임시 저장하기 위해 메모리에 버퍼를 만들고,
전체 데이터가 도착하면 한꺼번에 드라이브에 기록 가능.
드라이브 쓰기가 즉각적이지 않고 네트워크 인터페이스가 계속 데이터를 수신한다면 두 개의 버퍼가 필요.
네트워크가 첫 번째 버퍼를 채우면 드라이브 쓰기가 요청되고, 그동안 네트워크는 두 번째 버퍼를 채우는 방식.
이중 버퍼링은 생산자와 소비자 사이의 타이밍 요구를 완화하는 구조.

버퍼링이 필요한 두 번째 이유는 데이터 전송 크기가 다른 장치 사이의 완충.
네트워크에서는 송신 측의 큰 메시지가 작은 패킷으로 나뉘고, 수신 측에서는 패킷을 버퍼에서 결합하여 원래 자료 복원.
버퍼링이 필요한 세 번째 이유는 응용 프로그램의 입출력 복제 시맨틱 지원.
복제 시맨틱은 write() 호출 시점의 버퍼 내용이 디스크에 기록되어야 하며,
시스템 콜 이후 응용이 버퍼를 변경해도 이미 요청된 쓰기 결과에는 영향을 주지 않아야 한다는 의미.
운영체제가 복제 시맨틱을 보장하는 가장 단순한 방법은
write()가 반환되기 전에 응용 버퍼의 데이터를 커널 버퍼로 복사하는 것.
그 이후 디스크 쓰기는 커널 버퍼와 디스크 사이에서 수행되므로, 응용 버퍼의 이후 변경은 디스크 기록에 영향 없음.
이 방식은 추가 데이터 복사 부담이 있지만, 복제 시맨틱이 단순하다는 이유로 대부분의 운영체제에서 채택.
가상 메모리 매핑과 copy-on-write 페이지 보호를 활용하면 비슷한 효과를 더 효율적으로 얻을 수 있음.
캐싱
캐시는 자주 사용될 자료의 복사본을 저장하는 빠른 메모리 영역.
캐시된 복사본을 사용하면 원래 자료를 접근하는 것보다 효율적.
예를 들어 현재 수행 중인 프로세스의 명령은 디스크에 저장되어 있고,
물리 메모리에 캐시되며, 다시 CPU의 2차 캐시와 주 캐시에 복사되는 구조.
버퍼와 캐시의 차이는 데이터의 유일성 여부.
버퍼는 그 데이터를 가지고 있는 유일한 장소일 수 있지만,
캐시는 다른 곳에 이미 저장된 데이터의 복사본을 빠른 저장 장소에 추가로 저장하는 구조.
캐싱과 버퍼링은 서로 다른 기능이지만 같은 메모리 영역을 두 용도로 모두 사용할 수 있음.
예를 들어 운영체제는 복사 시맨틱을 유지하고 디스크 입출력의 효율적 스케줄링을 가능하게 하려고
메인 메모리 버퍼에 디스크 데이터를 유지.
이 버퍼는 캐시로도 사용되어, 공유되거나 기록 후 곧바로 다시 읽히는 파일의 입출력 효율을 향상.
커널이 파일 입출력 요청을 받으면 먼저 버퍼 캐시를 확인하여 해당 영역이 메인 메모리에 있는지 검사.
존재한다면 실제 디스크 입출력을 피하거나 연기 가능.
디스크 쓰기는 몇 초 동안 버퍼 캐시에 누적되어 대량 전송 가능하므로 효율적인 쓰기 스케줄링에 도움.
스풀링과 장치 예약
스풀은 인터리브 방식으로 동작할 수 없는 프린터 같은 장치를 위해 출력 데이터를 보관하는 버퍼.
프린터는 한 번에 하나의 작업을 끝까지 처리해야 하며, 여러 응용의 출력을 섞어 번갈아 출력할 수 없는 장치.
운영체제는 응용 프로그램에서 프린터로 가는 모든 출력을 가로채고, 각 응용의 출력을 별도 보조저장장치 파일에 스풀.
응용이 출력 데이터 생성을 끝내면 스풀링 시스템은 모아 둔 출력 데이터를 프린터 출력 대기열에 삽입.
스풀링 시스템은 대기 중인 스풀 파일을 한 번에 하나씩 프린터로 전송하는 구조.
일부 운영체제에서는 스풀링이 시스템 데몬 프로세스에 의해 관리되고, 다른 운영체제에서는 커널 스레드에 의해 처리.
운영체제는 프린터 대기 작업을 열거, 취소, 중단할 수 있는 제어 인터페이스 제공.
테이프나 프린터 같은 일부 장치는 여러 응용 프로그램의 입출력 요구를 멀티플렉스할 수 없음.
스풀링은 이런 장치를 공유하기 위한 한 방법.
다른 방법은 한 프로그램만 장치를 독점적으로 사용하도록 허용하는 장치 예약.
장치 예약 시스템에서는 프로세스가 유휴 장치를 명시적으로 할당받고, 사용이 끝나면 명시적으로 반납하는 구조.
일부 운영체제는 특정 장치에 대해 열 수 있는 파일 핸들 수를 하나로 제한.
많은 운영체제는 프로세스들이 협력적으로 배타적 접근을 해결할 수 있는 기능 제공.
Windows는 장치 객체가 사용 가능해질 때까지 기다리는 시스템 콜을 제공.
NT는 open 시스템 콜에서 다른 스레드가 해당 객체에 어떤 접근 권한을 가질 수 있는지도 선언 가능.
이런 시스템에서 교착 상태를 해결하는 것은 응용 프로그램의 책임.
오류 처리
보호되는 메모리를 사용하는 운영체제는 다양한 하드웨어 및 응용 프로그램 오류에 대처 가능.
그러한 오류가 발생해도 시스템 전체 마비로 확대되지 않게 하는 것이 운영체제의 역할.
입출력 장치나 네트워크 전송은 일시적 원인이나 영구적 원인으로 실패 가능.
일시적 오류의 예는 네트워크에 너무 많은 요청이 몰려 생기는 전송 실패.
영구적 오류의 예는 디스크 하드웨어 고장.
운영체제는 디스크 읽기 실패나 네트워크 전송 오류처럼 일시적인 고장에 대해 재시도를 통해 복구 가능.
그러나 중요한 구성 요소가 영구적인 고장을 일으키면 운영체제가 완전히 복구하기 어려운 경우 존재.
일반적으로 입출력 시스템 콜은 성공 또는 실패를 나타내는 정보를 반환.
UNIX는 반환 값 외에도 errno 변수를 사용하여 오류 원인을 구분.
errno는 잘못된 호출 인자, 잘못된 포인터 값, 파일 open 실패 등 많은 종류의 오류를 표현.
하드웨어는 운영체제보다 훨씬 자세한 오류 정보를 제공하는 경우가 있지만,
이러한 세부 정보가 항상 응용 프로그램까지 전달되지는 않음.
SCSI 장치의 문제는 sense key 형태로 보고되며, 이 값은 하드웨어 고장이나 불법 요청 같은 오류 유형을 나타냄.
SCSI는 추가 sense code를 통해 명령어 인자 문제나 하드웨어 자가 진단 실패 같은 더 자세한 정보를 제공.
sense-code qualifier는 명령어의 몇 번째 인자가 문제인지,
하드웨어 어느 부분이 자가 진단에 실패했는지 같은 세부 정보 제공.
SCSI는 호스트가 요청할 경우 error-log 기록도 제공할 수 있지만, 실제로 요청하는 경우는 흔하지 않은 편.
입출력 보호
오류는 보호 문제와 밀접한 관계.
사용자 프로세스는 고의적이든 아니든 불법적인 입출력을 시도하여 정상적인 시스템 동작을 방해할 수 있음.
이를 막기 위해 모든 입출력 명령은 특권 명령으로 정의.
사용자는 입출력 명령을 직접 수행할 수 없고, 대신 운영체제가 입출력을 대신 수행하도록 시스템 콜 사용.

모니터 모드에서 실행 중인 운영체제는 요청이 유효한지 검사하고, 유효하면 해당 입출력을 수행한 뒤 사용자에게 복귀.
메모리 맵 입출력 메모리나 입출력 포트 메모리의 위치도 메모리 보호 시스템에 의해 사용자로부터 보호되어야 함.
하지만 커널이 무조건 모든 사용자 접근을 막아서는 안 되는 경우도 존재.
대부분의 그래픽 게임, 비디오 편집기, 재생 소프트웨어는
그래픽 성능을 높이기 위해 메모리 맵 그래픽 컨트롤러 메모리에 직접 접근할 필요.
이 경우 커널은 그래픽 메모리의 일부가 한 번에 한 프로세스에 할당되도록 잠금 기법 제공 필요.
입출력 보호의 핵심은 장치의 안전한 제어와 필요한 고성능 직접 접근 사이의 균형.
커널 자료구조
커널은 입출력 구성 요소에 대한 상태 정보를 유지해야 하는 필요.
대표적인 구조는 open 파일 테이블.
커널은 네트워크 연결, 문자 장치 통신, 기타 입출력 활동을 관리하기 위해 여러 비슷한 자료구조 사용.
UNIX 파일 시스템 인터페이스는
사용자 파일, raw 장치, 프로세스 주소 공간 같은 다양한 객체를 파일 시스템처럼 접근할 수 있게 해주는 구조.
이 모든 객체가 read()를 지원하더라도 객체마다 read()의 의미는 다름.
사용자 파일을 읽는 경우 커널은 디스크 입출력을 수행하기 전에 버퍼 캐시를 먼저 조사.
raw 장치를 읽는 경우 커널은 요청 크기가 디스크 섹터 크기의 정수 배인지,
섹터 경계에 걸쳐 있지 않은지 검사해야 하는 구조.
프로세스 이미지 읽기는 단순히 해당 부분을 메모리에서 읽어 오는 작업.
UNIX는 이러한 다양성을 객체 지향 기법으로 하나의 구조에 묶음.

UNIX에서 프로세스가 표준 입력을 읽을 때는 파일 디스크립터 0을 사용하며,
커널은 이를 프로세스별 열린 파일 테이블에서 찾고
시스템 전체 열린 파일 테이블과 장치 또는 파일 객체의 연산 테이블로 연결하는 구조.
open-file record는 파일 유형에 따라 적절한 함수들을 가리키는 포인터를 가진 dispatch table을 포함.
이 포인터들은 read, write, select, ioctl, close 같은 연산을 객체 유형별 구현으로 연결.
일부 운영체제는 객체 지향 기법을 더 확장.
Windows NT는 입출력 서비스를 커널이 직접 처리하지 않고 커널 밖의 입출력 manager 프로세스에 넘기는 구조.
커널에 입출력 요청이 오면 메시지로 바뀌어 입출력 manager에 전달되고, 다시 장치 드라이버에게 전달되는 방식.
출력 메시지는 출력 데이터를 담고, 입력 메시지는 입력 버퍼를 가리키는 구조.
입출력 서비스를 커널 밖의 독립 프로그램에서 처리하면 메시지 전달 횟수 증가로 오버헤드가 생기는 단점.
대신 입출력 시스템 구조와 설계가 단순해지고, 운영체제 커널 크기가 작아지며, 융통성이 커지는 장점.
전원 관리
데이터 센터의 전력 사용은 비용 증가와 온실가스 배출 문제로 중요한 관리 대상.
전기 사용은 열을 발생시키고, 고온은 컴퓨터 구성 요소 고장을 유발할 수 있으므로 냉각도 중요한 문제.
최신 데이터 센터의 냉각은 장비에 전력을 공급하는 것보다 더 많은 전기를 사용할 수도 있음.
데이터 센터 전력 최적화에는 외부 공기 사용, 호수나 태양광 패널 같은 자연 자원 활용 등 다양한 방법 존재.
운영체제는 전력 사용, 열 발생, 냉각 요구 감소에 중요한 역할.
클라우드 컴퓨팅 환경에서는 관리 도구가 시스템 부하를 조정하고,
모든 사용자 프로세스를 다른 시스템으로 옮긴 뒤 유휴 시스템 전원을 끌 수 있음.
운영체제는 시스템 활동을 분석하여 부하가 충분히 낮고 하드웨어 기능이 지원되면
CPU 외부 입출력 장치 같은 구성 요소의 전원을 끌 수 있음.
시스템 부하가 필요로 하지 않는 경우 CPU 코어도 일시 중단 가능.
부하가 증가하고 실행할 스레드 큐가 더 많은 코어를 필요로 하면 일시 중단된 코어가 다시 재개되는 구조.
코어의 상태는 일시 중단 시 저장되고 재개 시 복원되어야 함.
서버가 가득한 데이터 센터에서는 불필요한 코어를 비활성화하여 전기와 냉각 필요를 줄일 수 있음.
모바일 컴퓨팅에서 전력 관리는 운영체제의 우선순위가 높은 측면.
전력 사용을 최소화하고 배터리 수명을 최대화하면 장치 사용성이 향상되고 경쟁력 확보 가능.
오늘날 모바일 기기는 과거 고급 데스크톱 수준의 기능을 제공하지만, 배터리로 동작하고 작은 크기를 유지해야 하는 구조.
만족스러운 배터리 수명을 제공하기 위해 최신 모바일 운영체제는 전원 관리를 핵심 기능으로 설계.
Android 모바일 시스템은 배터리 수명 최대화를 위해
전원 축소, 구성 요소 수준 전원 관리, wakelock이라는 세 가지 주요 기능 사용.
전원 축소는 장치를 매우 깊은 슬립 상태로 만드는 기능.
장치는 완전히 꺼진 경우보다 약간 더 많은 전력을 사용하지만,
버튼 입력 같은 외부 자극에 반응하고 빠르게 다시 켜질 수 있음.
전원 축소는 화면, 스피커, 입출력 서브시스템 같은 여러 구성 요소의 전원을 꺼서 이루어지는 방식.
그 후 CPU를 가장 낮은 슬립 상태로 두어 전력 소비를 최소화.
최신 ARM CPU는 일반 부하에서는 코어당 수백 밀리와트를 소비하지만,
가장 낮은 슬립 상태에서는 몇 밀리와트만 소비 가능.
이 상태에서 CPU는 유휴 상태이지만 인터럽트를 받고 깨어난 뒤 이전 활동을 빠르게 재개 가능.
Android 휴대전화가 주머니 속에서 거의 전력을 쓰지 않다가 전화 수신 시 빠르게 정상 상태로 돌아오는 이유.
구성 요소 수준 전원 관리는 각 구성 요소 간 관계와 사용 여부를 이해하여 개별 장치를 끄거나 켜는 인프라.
Android는 물리적 기기 토폴로지를 나타내는 기기 트리를 구축.
예를 들어 플래시와 USB 저장장치는 입출력 서브시스템의 서브 노드이고,
입출력 서브시스템은 시스템 버스의 서브 노드이며, 시스템 버스는 CPU에 연결되는 구조.
각 구성 요소는 장치 드라이버와 연결되고, 드라이버는 해당 구성 요소가 사용 중인지 아닌지 추적.
플래시 장치에 대기 중인 입출력이 있거나 오디오 서브시스템을 응용 프로그램이 참조 중이면
해당 구성 요소는 사용 중인 상태.
구성 요소를 사용하지 않으면 전원이 꺼지고, 시스템 버스의 모든 구성 요소가 사용되지 않으면 시스템 버스도 꺼짐.
전체 장치 트리의 모든 구성 요소가 사용되지 않으면 시스템은 전원 축소 모드로 들어가는 구조.
wakelock은 응용 프로그램이 시스템의 전원 축소 진입을 일시적으로 막을 수 있게 하는 기능.
사용자가 입력하거나 비디오를 보거나 웹 페이지가 열리기를 기다리는 경우
응용 프로그램은 장치가 깨어 있도록 유지할 필요.
응용 프로그램은 필요에 따라 wakelock을 획득하고 해제.
응용이 wakelock을 보유하는 동안 시스템은 전원 축소 모드로 진입하지 않음.
예를 들어 Android Market이 응용 프로그램을 업데이트하는 동안 wakelock을 유지하여
업데이트 완료 전 시스템이 슬립 모드로 전환되지 않도록 하는 방식.
업데이트가 완료되면 wakelock을 해제하여 시스템이 전원 축소 모드로 들어갈 수 있게 함.
일반적인 전원 관리는 장치 관리에 기반하므로 매우 복잡한 작업.
부팅 시 펌웨어는 시스템 하드웨어를 분석하고 RAM에 장치 트리를 만듦.
커널은 이 장치 트리를 사용하여 장치 드라이버를 적재하고 장치를 관리.
그러나 장치 추가와 제거, 장치 상태 이해와 변경, 전원 관리는 시스템 실행 중 계속 관리되어야 하는 기능.
최신 범용 컴퓨터는 ACPI를 사용하여 이러한 하드웨어 측면 관리.
ACPI는 Advanced Configuration and Power Interface의 약자.
ACPI는 장치 상태 검색과 관리, 장치 오류 관리, 전원 관리를 위해 커널에서 호출할 수 있는 루틴 형태의 코드 제공.
ACPI는 운영체제가 하드웨어 구성과 전원 상태를
일관되게 파악하고 제어할 수 있도록 돕는 운영체제 독립적 인터페이스 역할.
예를 들어 커널이 장치를 정지시켜야 하면 장치 드라이버를 호출하고,
드라이버는 ACPI 루틴을 호출하며, 해당 루틴이 장치에 지시를 내리는 흐름.
커널 입출력 서브시스템 요약
입출력 서브시스템은 응용 프로그램과 커널의 다른 부분이 사용할 수 있는 광범위한 서비스를 조정하는 계층.
관리 대상에는
파일과 장치의 이름 관리, 파일과 장치에 대한 접근 제어, 작업 제어, 파일 시스템 공간 할당,
장치 할당, 버퍼링, 캐싱, 스풀링, 입출력 스케줄링, 장치 상태 모니터링,
오류 처리, 고장 복구, 장치 드라이버 구성과 초기화, 입출력 장치의 전원 관리가 포함.
입출력 서브시스템의 상위 부분은 장치 드라이버가 제공하는 공통 인터페이스를 통해 장치에 접근.
이 구조는 다양한 장치의 차이를 숨기고,
응용 프로그램과 커널의 나머지 부분이 일관된 입출력 서비스를 사용할 수 있게 하는 방식.
입출력 요구를 하드웨어 연산으로 변환
앞에서는 장치 컨트롤러와 장치 드라이버 사이의 핸드셰이킹을 설명했지만,
응용 프로그램의 요청이 네트워크 회선이나 특정 디스크 섹터로 연결되는 과정도 필요.
파일을 디스크에서 읽는 경우 응용 프로그램은 파일명으로 데이터를 참조하고,
운영체제는 파일 시스템 디렉터리를 통해 이 파일이 디스크 내 어디에 위치하는지 확인.
MS-DOS의 FAT에서는 이름이 파일 액세스 테이블 항목으로 변환되고,
해당 항목은 파일에 할당된 디스크 블록을 가리킴.
UNIX에서는 파일 이름이 inode 번호로 매핑되고, inode가 공간 할당 정보를 포함하는 구조.
MS-DOS의 FAT 방식에서는 파일명에서 콜론 앞부분이 특정 하드웨어 장치를 나타냄.
예를 들어 C:는 주 하드 디스크에 있는 파일 이름의 첫 부분.
MS-DOS에서 콜론은 장치 이름과 파일 이름을 구분하며,
이 구분은 운영체제가 각 장치에 추가 기능을 쉽게 연결하게 함.
예를 들어 프린터에 쓰이는 파일에 대해 스풀링을 호출하기 쉬운 구조.
UNIX처럼 장치 이름 공간이 정규 파일 시스템 이름 공간과 결합되어 있으면,
일반 파일 시스템 이름 서비스가 자동으로 제공.
파일 시스템이 모든 파일 이름에 대해 소유와 접근 제어를 제공하면 장치도 같은 방식으로 소유와 접근 제어를 가짐.
파일이 장치에 저장되므로 이 인터페이스는 장치 자체 접근과 장치에 저장된 파일 접근이라는 두 수준의 접근 제공.
UNIX는 장치 이름을 일반 파일 이름처럼 표시.
MS-DOS 파일명과 달리 UNIX 경로명은 경로 상의 어떤 부분도 표면적으로 장치 이름을 구분하지 않음.
UNIX는 경로 접두어를 특정 장치 이름과 연결하는 마운트 테이블을 가짐.
경로 이름을 해석할 때 UNIX는 마운트 테이블의 이름 중 경로명과 일치하는 가장 긴 접두어를 찾음.
그 항목이 장치 이름을 제공하며, 이 장치 이름 역시 파일 이름 형태.
UNIX가 이 이름을 파일 시스템 디렉터리 구조에서 찾으면 inode 번호 대신 메이저와 마이너 장치 번호를 얻음.
메이저 장치 번호는 이 장치에 대한 입출력을 처리하기 위해 호출할 장치 드라이버를 나타냄.
마이너 장치 번호는 장치 드라이버에게 전달되고, 보통 장치 테이블에 대한 인덱스로 사용.
해당 장치 테이블 항목은 장치 컨트롤러의 포트 주소나 메모리 맵 주소를 제공.
최신 운영체제는 입출력 요청과 장치 컨트롤러 사이에 여러 단계의 lookup table을 두어 유연성 제공.
이름 변환 흐름은
파일명, 파일 시스템 구조, 장치 이름 또는 inode, 메이저·마이너 번호,
장치 드라이버, 장치 테이블, 컨트롤러 포트 또는 메모리 맵 주소로 이어지는 다단계 매핑.
응용과 드라이버 사이에 요구를 전달하는 기법은 일반화되어 있으므로,
커널을 다시 컴파일하지 않고 새 장치와 드라이버를 추가할 수 있음.
운영체제는 부팅 시 버스를 조사하여 어떤 하드웨어가 있는지 확인하고, 필요한 드라이버를 함께 적재.
드라이버 적재는 부팅 시 수행할 수도 있고, 실제로 필요해질 때 지연하여 수행할 수도 있음.
부팅 후 추가된 장치는 관련 인터럽트 핸들러가 없는 인터럽트 같은 오류로 감지될 수 있음.
이 오류가 발생하면 커널은 장치 세부 정보를 검사하고
적절한 장치 드라이버를 동적으로 적재하게 만드는 메시지를 표시 가능.
동적 로딩과 언로딩은 정적 로딩보다 복잡하므로 더 복잡한 커널 알고리즘, 장치 구조 잠금, 오류 처리가 필요.

봉쇄형 read 요청의 전체 흐름은
하나의 입출력 작업이 많은 CPU 사이클을 사용하고 여러 단계를 거친다는 점을 보여주는 예시.
첫째, 프로세스가 이미 열린 파일의 파일 디스크립터에 대해 봉쇄형 read 시스템 콜 요청.
둘째, 커널의 시스템 콜 코드는 인자를 검사하고, 데이터가 버퍼 캐시에 있으면 바로 반환하여 입출력 요청 종료.
셋째,
데이터가 캐시에 없으면 실제 read가 필요하므로 프로세스는 실행 큐에서 대기 큐로 이동하고 입출력 요구가 스케줄.
넷째,
장치 드라이버는 입력될 자료를 위한 커널 버퍼를 확보하고 입출력을 스케줄한 뒤,
장치 컨트롤러의 제어 레지스터에 명령 기록.
다섯째, 장치 컨트롤러가 장치를 작동하여 데이터를 입력.
여섯째,
장치 드라이버는 상태 확인이나 데이터 획득을 위해 폴링할 수도 있고 DMA 방식으로 작업을 처리할 수도 있음.
일곱째,
DMA 방식이라면 DMA 컨트롤러가 전송 완료 시 인터럽트 생성.
여덟번째,
인터럽트 벡터 테이블을 통해 해당 인터럽트 핸들러가 인터럽트를 받고
장치 드라이버에 신호를 보낸 뒤 인터럽트에서 복귀.
아홉번째,
신호를 받은 장치 드라이버는 어떤 입출력이 완료되었는지 확인하고
커널 입출력 서브시스템에 요청 완료 알림.
열번째,
커널은 자신의 버퍼에서 요청한 프로세스의 버퍼로 데이터를 옮기고,
해당 프로세스를 봉쇄 상태에서 준비 완료 큐로 이동.
마지막으로 스케줄러가 언젠가 이 작업에 CPU를 할당하면 프로세스는 시스템 콜에서 복귀하는 흐름.
STREAMS
UNIX System V와 많은 후속 UNIX 릴리스는 STREAMS라는 기법 제공.
STREAMS는 응용 프로그램이 드라이버 코드의 파이프라인을 동적으로 조립할 수 있게 하는 구조.
스트림은 디바이스 드라이버와 사용자 레벨 프로세스 사이의 완전 양방향 연결.
스트림은 사용자 프로세스와 상호 작용하는 스트림 헤드,
디바이스를 제어하는 드라이버 엔드,
그 사이의 0개 이상의 스트림 모듈로 구성.
각 구성 요소는 읽기 큐와 쓰기 큐를 한 쌍으로 가지며, 큐 사이의 데이터 전송에는 메시지 전달 사용.

모듈은 스트림 처리 기능을 제공하며 ioctl() 시스템 콜로 스트림에 push 가능.
예를 들어 프로세스는 USB 키보드 같은 장치를 스트림으로 열고, 입력 처리를 담당하는 모듈을 삽입 가능.
인접한 모듈의 큐 사이에서 메시지가 교환되므로 한 모듈의 큐가 인접 모듈을 오버플로시킬 수 있음.
이를 막기 위해 큐는 흐름 제어 지원 가능.
흐름 제어가 없으면 큐는 모든 메시지를 받아 즉시 인접 모듈 큐로 전달.
흐름 제어를 지원하는 큐는 메시지를 버퍼링하고, 충분한 버퍼 용량이 없으면 메시지를 받아들이지 않음.
흐름 제어는 인접 모듈 큐 사이의 제어 메시지 교환으로 지원.
사용자 프로세스는 write() 또는 putmsg() 시스템 콜을 사용하여 디바이스에 데이터 쓰기 가능.
write()는 미가공 데이터를 스트림에 쓰는 방식.
putmsg()는 사용자 프로세스가 메시지를 지정할 수 있게 하는 방식.
시스템 콜 종류와 관계없이 스트림 헤드는 데이터를 메시지로 복사하고 다음 모듈의 큐에 전달.
이 메시지 복사는 드라이버 엔드, 즉 디바이스에 도달할 때까지 계속.
사용자 프로세스는 read() 또는 getmsg() 시스템 콜을 사용하여 스트림 헤드에서 데이터 읽기 가능.
read()는 인접 큐에서 메시지를 얻고 사용자 프로세스에 구조화되지 않은 바이트 스트림으로 반환.
getmsg()는 하나의 메시지를 사용자 프로세스에 반환.
STREAMS는 사용자 프로세스가 스트림 헤드와 통신할 때를 제외하면 비동기적이고 비봉쇄적인 구조.
스트림에 쓰기 작업을 할 때 다음 큐가 흐름 제어를 사용한다면,
사용자 프로세스는 메시지를 저장할 공간이 있을 때까지 봉쇄될 수 있음.
스트림에서 읽기 작업을 할 때도 데이터가 이용 가능할 때까지 봉쇄될 수 있음.
드라이버 엔드는 스트림 헤드나 모듈처럼 읽기 큐와 쓰기 큐를 가지지만, 인터럽트에 반드시 응답해야 한다는 차이.
예를 들어 네트워크에서 프레임이 읽기 준비되면 드라이버 엔드는 즉시 처리해야 하는 구조.
스트림 헤드는 다음 큐에 메시지를 복사할 수 없으면 봉쇄될 수 있지만,
드라이버 엔드는 들어오는 모든 데이터를 처리해야 함.
디바이스의 버퍼가 가득 차면 디바이스는 일반적으로 수신 메시지를 버리는 방식.
입력 버퍼가 가득 찬 네트워크 카드는 들어오는 메시지를 저장할 공간이 생길 때까지 단순히 버릴 수 있음.
STREAMS의 장점은
디바이스 드라이버와 네트워크 프로토콜을 작성할 때 모듈식이고 점진적인 프레임워크를 제공한다는 점.
모듈은 다른 스트림과 디바이스에서도 재사용 가능.
예를 들어 네트워킹 모듈은 이더넷 네트워크 카드와 802.11 무선 네트워크 카드 모두에 의해 사용 가능.
STREAMS는 문자 장치 입출력을 단순한 구조화되지 않은 바이트 스트림으로만 취급하지 않고,
메시지 경계와 모듈 간 제어 정보를 지원.
대부분의 UNIX 변종은 STREAMS를 지원하며, 프로토콜과 디바이스 드라이버 작성에 선호되는 방법.
System V UNIX와 Solaris는 STREAMS를 사용하여 소켓 기법을 구현한 사례.
성능
입출력은 시스템 성능에 매우 중요한 요소.
입출력은 장치 드라이버 코드를 실행하고,
프로세스가 봉쇄되고 해제될 때 공정하고 효율적으로 스케줄하기 위해 CPU에 큰 부담을 줌.
문맥 교환은 CPU 하드웨어 캐시에 부담을 주고, 인터럽트 처리의 비효율성도 입출력 성능에 영향.
컨트롤러와 메모리 사이, 커널 버퍼와 응용 프로그램 데이터 공간 사이에서 데이터를 복사하는 일은
메모리 버스에 부하를 주는 작업.
현대 컴퓨터는 초당 수천 번의 인터럽트를 처리할 수 있지만, 인터럽트 처리는 여전히 부담이 큰 작업.
인터럽트가 발생할 때마다
시스템은 상태를 바꾸고, 인터럽트 핸들러를 실행하고, 다시 상태를 복원해야 하는 비용 발생.
바쁜 대기에서 낭비되는 사이클이 적다면 programmed I/O가 인터럽트보다 효율적일 수도 있음.
입출력 작업 완료 시에는 보통 봉쇄되었던 프로세스가 해제되며, 이때 다시 문맥 교환 오버헤드 발생.
네트워크 트래픽도 많은 문맥 교환을 유발 가능.
원격 로그인에서 로컬 기계에 문자가 입력되면 키보드 인터럽트가 발생하고,
문자는 인터럽트 핸들러, 장치 드라이버, 커널, 사용자 프로세스로 전달.
사용자 프로세스는 네트워크 입출력 시스템 콜을 통해 원격 기계로 문자를 전송.
문자는 다시 로컬 커널, 네트워크 계층, 네트워크 장치 드라이버,
네트워크 컨트롤러를 거쳐 전송되고, 완료 인터럽트가 발생.
원격 시스템에서도 네트워크 하드웨어가 패킷을 받고 인터럽트를 발생시키며,
문자는 네트워크 프로토콜과 네트워크 데몬을 거쳐 해당 로그인 세션으로 전달.
이 과정에서 문맥 교환과 상태 전환이 계속 발생.

보통 수신자는 받은 문자를 다시 송신자에게 되돌려 보내므로, 이러한 작업은 거의 두 배로 증가.
일부 시스템은 터미널 입출력을 담당하는 프론트엔드 처리기를 사용하여 CPU의 인터럽트 부담을 줄임.
터미널 집중기는 수백 개 원격 터미널에서 들어오는 정보를 멀티플렉스 가능.
입출력 채널은 메인프레임과 대형 시스템에서 CPU의 입출력 부담을 덜기 위한 전용 특수 목적 CPU.
입출력 채널은 데이터 흐름을 일정하게 유지하고, 메인 CPU가 세부 데이터를 직접 처리하지 않게 하는 역할.
작은 컴퓨터에서 사용하는 장치 컨트롤러나 DMA보다 입출력 채널은 더 일반적이고 복잡한 입출력 프로그램 수행 가능.
입출력 채널은 특정 작업에 맞게 조정 가능.
입출력 효율을 높이기 위한 방법에는
문맥 교환 빈도 감소, 장치와 응용 프로그램 사이의 데이터 복사 횟수 감소, 인터럽트 빈도 감소,
DMA 사용, 원시 처리 연산의 하드웨어 구현, CPU·메모리·버스·입출력 부하의 균형 유지가 포함.
인터럽트 빈도를 줄이는 방법에는
한 번에 많은 데이터를 전송하거나,
지능적인 컨트롤러를 사용하거나,
바쁜 대기를 최소화할 수 있을 때 폴링을 지혜롭게 사용하는 방법 존재.
DMA 채널을 사용하면 CPU의 입출력 부담을 줄이고,
입출력과 주 연산이 더 많이 중첩되도록 도울 수 있음.
원시 처리 연산을 장치 컨트롤러 하드웨어로 구현하면 컨트롤러 작업과 CPU 및 버스 작업이 병렬로 진행 가능.
장치 복잡도는 매우 다양.
마우스는 움직임이나 클릭이 수치로 변환되어 장치 드라이버에 전달되는 비교적 단순한 장치.
반면 Windows NT 디스크 장치 드라이버는 각 디스크뿐 아니라 RAID 배열도 관리해야 하므로 훨씬 복잡한 구조.
디스크 입출력 요청은 서로 연관된 여러 디스크 입출력 연산으로 변환될 수 있으며,
오류 처리와 데이터 복구 알고리즘도 함께 수행 필요.
디스크 성능이 시스템 전체 성능에 큰 영향을 주므로, 디스크 입출력은 여러 단계에서 최적화되는 구조.

입출력 기능을 장치 하드웨어, 장치 드라이버, 응용 프로그램 소프트웨어 중 어디에 구현할 것인지는 중요한 설계 문제.
처음에는 응용 프로그램 수준에서 실험적인 입출력 알고리즘을 구현하는 것이 유리.
응용 프로그램은 가장 신축적이고, 응용 프로그램의 버그가 시스템 전체 고장으로 이어질 가능성이 작기 때문.
응용 수준 구현은 장치 드라이버를 재부팅하거나 재적재하지 않고 실험할 수 있는 장점.
그러나 문맥 교환 오버헤드가 있고, 효율적인 커널 내 메시징, 스레딩, 락킹 같은 커널 기능을 활용하기 어렵다는 단점.
FUSE 시스템 인터페이스는 파일 시스템을 사용자 모드에서 작성하고 실행할 수 있게 하는 예.
응용 수준 알고리즘이 가치 있다고 판단되면 커널에서 재구현하여 성능을 크게 향상 가능.
하지만 커널 구현은 운영체제를 크고 복잡하게 만들며,
시스템 고장과 기존 자료 파괴 가능성이 있으므로 신중한 디버깅 필요.
가장 높은 성능을 얻으려면 입출력 작업을 장치 자체나 컨트롤러 하드웨어로 구현해야 함.
그러나 하드웨어 구현은 오류 수정이 어렵고 개선이 어렵고 개발 시간이 오래 걸리며 융통성이 적은 단점.
예를 들어 하드웨어 RAID 컨트롤러는 커널이 입출력 성능 개선을 위한 특별한 부하 정보를 가지고 있어도
각 블록 읽기와 쓰기의 위치나 순서에 영향을 줄 방법을 제공하지 않을 수 있음.
시간이 지나며 입출력 장치 속도는 계속 향상.
비휘발성 메모리 장치는 점점 인기를 얻고 있으며, 일부 차세대 장치는 DRAM 속도에 가까운 읽기·쓰기 속도 제공.
이러한 속도 향상은 현재 가능한 읽기·쓰기 속도를 활용하도록
입출력 서브시스템과 운영체제 알고리즘 모두에 압력을 주는 요인.

위의 그림은 입출력 작업의 용량과 지연 시간이라는 두 차원에서 CPU와 저장장치의 위치를 보여주는 도표.
네트워크가 입출력에 추가하는 성능 비용을 보여주기 위해 네트워크 지연 시간도 함께 표시된 구조.