
I/O 시스템은 세 가지 계층으로 구성된다.
| 계층 | 영문 | 역할 |
|---|---|---|
| I/O 장치 | I/O Devices | 키보드, 디스크, 프린터 등 실제 하드웨어 |
| 디바이스 드라이버 | Device Drivers | 각 장치를 제어하는 소프트웨어 모듈 |
| I/O 서브시스템 | I/O Subsystem | 커널 내부에서 장치 독립적 인터페이스를 제공하는 계층 |
I/O 서브시스템이 존재하는 핵심 이유는 추상화(Abstraction)이다.
open(), close(), read(), write(), ioctl() 같은 동일한 시스템 콜로 사용할 수 있게 한다.계층 구조 (위에서 아래로 갈수록 장치에 특화됨):
+----------------------------------+ <-- Generic (장치 독립적)
| File System, VM System, etc. |
+----------------------------------+
| I/O Subsystem |
+----------------------------------+
| Device Drivers |
+----------------------------------+
| Interrupt Handlers |
+----------------------------------+
| I/O Device Hardware | <-- Specific Details (장치 종속적)
+----------------------------------+
핵심 포인트: 위로 갈수록 "어떤 장치인지 몰라도 되는" 범용(Generic) 코드이고, 아래로 갈수록 "특정 장치에 맞춘" 전용(Specific) 코드이다. 이 계층 분리 덕분에 새 장치를 추가할 때 드라이버만 작성하면 되고, 커널이나 앱을 수정할 필요가 없다.

Linux에서 I/O 서브시스템의 추상화가 어떻게 실현되는지를 보여주는 핵심 구조체가 file_operations이다.
동작 원리:
틀(껍데기) 정의: I/O 서브시스템이 file_operations라는 구조체를 정의한다. 이 구조체에는 .open, .read, .write, .ioctl 등의 함수 포인터 슬롯이 있지만, 내용(실제 함수 주소)은 비어 있다.
드라이버가 슬롯을 채움: 키보드 드라이버(atkbd.ko)가 부팅 시 로드되면, 자신의 함수 주소를 각 슬롯에 등록한다.
.open -> atkbd_open() (장치 초기화, IRQ 등록).read -> atkbd_read() (키 이벤트, 버퍼 반환).write -> atkbd_write() (LED 켜기, CapsLock 등).ioctl -> atkbd_ioctl() (키 반복 속도, 레이아웃 설정)시스템 콜 경로: 사용자가 read(fd, buf, n)을 호출하면:
file_operations의 .read 슬롯에 적힌 주소로 점프atkbd_read()가 실행됨App: read(fd, buf, n) 호출
|
VFS: fd -> inode -> fops 포인터 조회 -> .read 호출
|
I/O 서브시스템: file_operations 구조체
+--------+--------+---------+--------+
| .open | .read | .write | .ioctl | <-- 함수 포인터 슬롯
+---+----+---+----+----+----+---+----+
| | | |
atkbd_open atkbd_read atkbd_write atkbd_ioctl <-- 드라이버 함수
|
키보드 하드웨어 (i8042): IRQ 발생, 포트 0x60 데이터
왜 이런 구조인가?: 이 "함수 포인터 테이블" 패턴은 다형성(Polymorphism)을 C 언어에서 구현한 것이다. C++의 가상 함수 테이블(vtable)과 동일한 원리다. 커널은 "어떤 장치의 read인지" 몰라도, 그냥 .read 슬롯에 적힌 주소를 호출하면 된다.

예외(Exception)와 인터럽트(Interrupt)는 프로세서의 정상적인 명령어 실행 흐름을 방해(disturb)하여, 특수한 명령어(서비스 루틴)를 실행하도록 프로세서의 주의를 전환시키는 이벤트이다.
예외는 프로세서 명령어의 실행에 의해 발생하는 내부 이벤트이다. 즉, CPU가 현재 실행 중인 명령어와 직접적인 인과 관계가 있다.
| 예외 종류 | 설명 |
|---|---|
| Page Fault Exception | 접근하려는 페이지가 물리 메모리에 없을 때 |
| Arithmetic Exception | 0으로 나누기(Division by Zero) 등 |
| TRAP | 의도적인 예외 (시스템 콜 진입점 등) |
"내부 이벤트"라는 점이 핵심: page fault는 MOV 명령어가 특정 주소를 접근하다가 발생한 것이고, division by zero는 DIV 명령어가 실행되다가 발생한 것이다. 모두 CPU가 실행한 명령어가 원인이다.
인터럽트는 프로세서 명령어의 실행과 관련 없는 외부 이벤트이다. 외부 이벤트의 소스는 일반적으로 외부 하드웨어 장치이다.
| 인터럽트 예시 | 설명 |
|---|---|
| Reset Button | 임베디드 보드의 리셋 버튼 |
| Device Signal | 장치에서 생성된 전기적 신호 (키보드 키 입력, 디스크 I/O 완료 등) |
| 구분 | Exception | Interrupt |
|---|---|---|
| 발생 원인 | CPU가 실행 중인 명령어 (내부) | 외부 하드웨어 장치 (외부) |
| 동기/비동기 | 동기적 (Synchronous) — 명령어 실행과 동시에 | 비동기적 (Asynchronous) — 언제든 발생 가능 |
| 예시 | Page fault, Division by zero, TRAP | 키보드 입력, 디스크 완료, 타이머 |
| 재현성 | 같은 조건이면 반복 재현 가능 | 외부 이벤트이므로 타이밍이 비결정적 |

외부 하드웨어 장치들은 CPU와 병렬로(in parallel) 장치 고유의 작업을 수행할 수 있다. 병행성의 핵심은 장치가 작업을 완료했는지 알아내는 것이다.
예시: 애플리케이션이 프로세서에서 실행 중인데, 디스크 드라이브가 데이터 읽기를 완료하면 외부 인터럽트를 발생시켜 완료를 알린다.
만약 인터럽트가 없다면? CPU가 계속 "끝났어? 끝났어?" 하고 확인해야 한다(Polling). 인터럽트 덕분에 CPU는 다른 일을 하다가 장치가 "끝났어!"라고 알려주면 그때 처리하면 된다.
외부 하드웨어 장치가 CPU에게 서비스를 요청하는 통신 메커니즘으로도 인터럽트가 사용된다.
예시: 키보드를 누르면 키보드 컨트롤러가 인터럽트를 발생시켜 "사용자가 키를 눌렀으니 처리해달라"고 CPU에 요청한다.

인터럽트 요청을 발생시키는 각 장치는 IRQ 라인이라는 단일 출력선을 갖는다. 이 선을 통해 "나 인터럽트 요청할게!"라는 전기 신호를 보낸다.
모든 장치의 IRQ 라인은 PIC라는 하드웨어 칩의 입력 핀에 연결된다.
PIC의 역할:

PIC는 다음 단계를 순서대로 수행한다:
벡터(Vector)란?: 인터럽트 번호이다. 예를 들어 키보드는 IRQ 1이고, 이 번호가 벡터 테이블의 인덱스가 되어 해당 ISR(인터럽트 서비스 루틴)의 주소를 찾는다.
x86 아키텍처에서 사용된 고전적인 PIC이다. 하나의 8259는 8개의 IRQ 입력을 가지며, 두 개를 캐스케이드(cascade)로 연결하여 총 16개(IRQ 0~15)를 지원했다.
IRQ 0 : Timer --+
IRQ 1 : Keyboard | Master
IRQ 2 : Slave 연결 | 8259
... |
IRQ 7 --+--INT--CPU
^
IRQ 8~15 --+ Slave 8259
현대에는 APIC (Advanced PIC)로 대체되었다. APIC는 멀티프로세서 환경에서 인터럽트를 특정 CPU 코어로 라우팅할 수 있다.

소프트웨어에 의해 차단(block)하거나 허용(enable)할 수 있는 인터럽트이다.
소프트웨어로 차단할 수 없는 인터럽트이다. 프로세서가 즉시 확인하고 처리한다.
왜 Non-maskable인가?: 메모리 패리티 오류 같은 건 무시하면 시스템 전체가 망가질 수 있기 때문에, 어떤 상황에서도 반드시 즉시 처리해야 한다.

모든 프로세서는 예외와 인터럽트를 정해진 우선순위에 따라 처리한다. 모든 예외와 인터럽트는 OS 스레드보다 높은 처리 우선순위를 갖는다.
^ 높은 우선순위 (Absolute System-Wide Priority)
|
| +-------------------------------+
| | Non-maskable Interrupt | <-- 최고 우선순위
| +-------------------------------+
| | Exceptions |
| +-------------------------------+
| | Maskable Interrupts |
| +-------------------------------+ --- 하드웨어 제어 경계
| | Thread (높은 우선순위) | <-- 커널 제어 경계
| | Thread |
| | ... | Thread Priority
| | Thread (낮은 우선순위) | (Programmable)
| +-------------------------------+
v 낮은 우선순위
핵심 구분:


시스템은 예외/인터럽트 벡터 테이블을 알고 있다. 이 테이블에는 각 벡터 번호에 대응하는 서비스 루틴의 시작 주소가 저장되어 있다.
[인터럽트 경로]
장치 -> PIC Checks -> Interrupt enabled?
| Yes
CPU Calculates Vector -> Vector Table에서 ISR 주소 찾기
|
Memory에서 ISR 실행
|
원래 실행 지점으로 복귀
[예외 경로]
CPU 내부 이벤트 -> CPU Calculates Vector -> Vector Table에서 ESR 주소 찾기
|
Memory에서 ESR 실행
|
원래 실행 지점으로 복귀
인터럽트와 예외의 차이: 인터럽트는 외부에서 전달해서 PIC를 거치고, 예외는 CPU 내부에서 발생하므로 CPU가 직접 벡터를 계산한다.
| 상태 | 영문 | 설명 |
|---|---|---|
| 비활성 | Disable (Masked) | 마스킹되어 차단된 인터럽트 |
| 대기 중 | Pending | 아직 확인(acknowledge)되지 않은 인터럽트. 프로세서가 현재 더 높은 우선순위 인터럽트를 처리 중일 때 발생 |
| 활성 | Active | 프로세서가 현재 확인하고 처리 중인 인터럽트 |

| 벡터 번호 | 설명 |
|---|---|
| 0 | Divide Error (0으로 나누기) |
| 1 | Debug Exception |
| 2 | Null Interrupt (NMI) |
| 3 | Breakpoint |
| 6 | Invalid Opcode |
| 8 | Double Fault |
| 13 | General Protection Fault |
| 14 | Page Fault |
| 16 | Floating-Point Error |
| 18 | Machine Check |
| 19-31 | Intel 예약 (사용 금지) |
| 32-255 | Maskable Interrupts (외부 장치용) |
포인트: 벡터 0~31은 Intel이 예외(Exception)용으로 예약했고, 32번부터 255번까지가 외부 장치의 마스크 가능 인터럽트에 할당된다.

하나의 인터럽트를 서비스하는 동안 다른 인터럽트가 발생할 수 있다. 따라서 서로 다른 종류의 인터럽트에 대한 우선순위 제어가 필수적이다.

ISR이 인터럽트를 비활성화(disable)한 상태에서 너무 오래 실행하면, 그 동안 발생한 다른 인터럽트를 프로세서가 기록하지 못하는 상황이 Interrupt Miss이다.

| 기호 | 영문 | 의미 |
|---|---|---|
| T_A | Time between interrupts | 인터럽트 간 시간 간격. 장치의 인터럽트 빈도에 의존 |
| T_B | Interrupt Latency | 인터럽트가 발생한 시점부터 ISR 실행이 시작되기까지의 지연 시간 |
| T_C | Processing Time | ISR의 실제 처리 시간 |
| T_D | Interrupt Response Time | 전체 응답 시간. T_D = T_B + T_C |

높은 우선순위 인터럽트의 처리 시간(T_C)이 길면, 낮은 우선순위 인터럽트의 지연(T_B)이 늘어나고, 낮은 우선순위 인터럽트의 누락(miss) 가능성이 증가한다.
이것이 바로 다음 절(Bottom Halves)의 동기이다.

ISR이 오래 실행되면 인터럽트 누락이 발생한다. 그렇다면 ISR을 어떻게 짧게 만들 수 있을까?
| 구분 | Top Half (상반부) | Bottom Half (하반부) |
|---|---|---|
| 실행 컨텍스트 | ISR 컨텍스트 (인터럽트 핸들러) | 스레드 컨텍스트 (커널 데몬 스레드) |
| 인터럽트 상태 | 일부 또는 전체 인터럽트 비활성화 | 모든 인터럽트 활성화 |
| 실행 시점 | 인터럽트 발생 즉시 | 나중에 (deferred) |
| 특징 | 빠르고 간단 | 시간 소모적 작업 처리 |
| 하는 일 | 최소한의 긴급 작업만 수행 | 데이터 처리, 프로토콜 처리 등 |
Bottom half의 핵심 목표는 인터럽트 핸들러의 실행 시간을 최소화하여 인터럽트 누락 가능성을 줄이는 것이다.
키보드 타이핑
|
Interrupt Controller -> Processor
|
ISR (Top Half)
- 스캔코드 읽기
- 이벤트 큐에 삽입
- 즉시 반환
|
Queue: [Req 3][Req 2][Req 1]
| |
Thread 0 (Daemon) Thread 1 (Daemon)
- 큐에서 요청 꺼내서 처리 (Bottom Half)
비유: 응급실 접수 데스크(Top Half)에서는 환자 이름만 빠르게 적고, 실제 치료(Bottom Half)는 의사가 나중에 한다. 접수 데스크가 한 환자를 오래 붙잡고 있으면 뒤에 온 응급 환자를 놓칠 수 있다.

CPU에는 두 종류의 주소 공간이 동시에 존재한다:
| 주소 공간 | 영문 | 범위 | 접근 명령어 | 용도 |
|---|---|---|---|---|
| 메모리 주소 공간 | Memory Address Space | 가상: 0x0000~0xFFFFFFFF | MOV, LOAD, STORE | RAM, MMIO |
| I/O 포트 주소 공간 | I/O Port Address Space | 0x0000~0xFFFF (64KB 고정) | IN, OUT (x86 전용) | 장치 레지스터 |
핵심 차이:
IN/OUT 명령어로만 접근 가능하다.
포트 주소는 디바이스 컨트롤러의 내부 레지스터 번지이다. 가상 addr 필요 없음
inb(0x60) 실행 시:
1. CPU가 I/O 버스에 주소 0x60을 송출
2. 키보드 컨트롤러(i8042) 내 주소 디코더가 0x60을 받아 자신의 데이터 레지스터를 선택
3. 해당 레지스터에 저장된 스캔코드 값을 버스로 출력
4. CPU가 이 값을 읽어감
i8042 키보드 컨트롤러 내부:
+----------------------------------------------+
| 주소 디코더 |
| 버스에서 받은 주소(0x60)로 |
| 어느 레지스터인지 선택 |
+--------------+--------------+----------------+
| 데이터 레지스터 | 상태 레지스터 | 명령 레지스터 |
| 포트 0x60 | 포트 0x64 | 포트 0x64 |
| 스캔코드 저장 | 버퍼 상태 | LED 제어 등 |
| *inb(0x60) | inb(0x64) | outb(0x64) |
+--------------+--------------+----------------+
[하드웨어]
키보드에서 'A' 키 누름
|
i8042 컨트롤러: 스캔코드 생성 -> IRQ 1 발생
|
[Top Half -- ISR]
CPU: 작업 중단, 레지스터 저장 -> ISR 호출
|
ISR: 포트 0x60에서 스캔코드 읽기 (0x1E)
|
ISR: 이벤트 큐에 KEY_A 삽입
|
ISR: CPU 작업 복귀 (수 마이크로초)
|
[Bottom Half]
키코드 변환: 0x1E -> KEY_A
|
tty 버퍼 기록: line discipline 처리
|
wake_up(): 대기 프로세스 깨움
|
[App]
read()에서 대기 중이던 프로세스 -> 버퍼에서 'a' 읽음 -> read() 반환


장치가 컴퓨터 시스템과 통신하는 연결 지점이다. 장치는 케이블이나 무선을 통해 전기 신호를 보내며, 포트는 이 신호가 오가는 물리적/논리적 접속점이다.
여러 장치가 공통의 와이어(전선 묶음)를 사용하여 통신하면, 이 연결을 버스라고 한다.
| 버스 종류 | 영문 | 역할 |
|---|---|---|
| 메모리 버스 | Memory Bus | 프로세서와 메모리(또는 메모리 컨트롤러)를 연결 |
| PCI 버스 | PCI (Peripheral Component Interconnect) Bus | 프로세서-메모리를 빠른 장치들(그래픽, SCSI 등)에 연결 |
| 확장 버스 | Expansion Bus | 느린 장치들(키보드, 시리얼 포트 등)을 연결 |

컴퓨터와 주변 장치 사이의 데이터 전송을 제어하는 칩이다.

CPU가 상태 레지스터를 계속 확인하는 동안, 다른 유용한 처리가 진행되지 못한다. CPU 시간이 낭비된다.
하드웨어 컨트롤러가 장치의 서비스 준비가 완료되면 CPU에게 인터럽트를 발생시켜 알린다. CPU는 다른 일을 하다가 인터럽트가 오면 그때 처리하면 된다.
| 비교 항목 | Polling | Interrupt |
|---|---|---|
| CPU 활용 | 낮음 (busy-wait으로 낭비) | 높음 (다른 작업 가능) |
| 응답 속도 | 폴링 주기에 의존 | 즉각적 |
| 구현 복잡도 | 단순 | 복잡 (벡터 테이블, ISR 등 필요) |
| 적합한 상황 | 매우 빈번한 I/O, 짧은 대기 | 대부분의 I/O 장치 |

CPU가 직접 상태 비트를 폴링하여 I/O 요청 완료를 확인하고, 1바이트씩 데이터를 컨트롤러 레지스터에 공급하는 방식이다.
대량 데이터 전송을 하는 장치에서, 프로세서가 반복적으로 데이터를 레지스터에 넣는 것은 CPU 자원의 낭비이다. CPU는 연산에 쓰여야 하는데, 단순 데이터 복사에 묶여 있게 된다.

핵심 아이디어: 폴링과 데이터 전송 같은 작업을 DMA 컨트롤러라는 전용 프로세서에게 떠넘겨서 CPU의 부담을 줄인다.

[Step 1] Device driver가 "drive 2의 데이터를 메모리 주소 x의 버퍼로 전송하라"는
명령을 받음
[Step 2] Device driver가 drive controller에게 "c바이트를 주소 x의 버퍼로 전송하라"고 지시
(소스 주소, 목적지 주소, 전송 길이를 DMA 컨트롤러에 설정)
[Step 3] Drive controller가 DMA 전송을 개시
[Step 4] DMA controller가 바이트를 메모리 주소 x의 버퍼로 전송
- 메모리 주소를 증가시키고 (x -> x+1 -> x+2 -> ...)
- 카운터 c를 감소시킴 (c -> c-1 -> ... -> 0)
- 이 동안 CPU는 다른 작업을 자유롭게 수행
[Step 5] c = 0이 되면, DMA controller가 CPU에 인터럽트를 발생시켜 전송 완료를 알림
+---------+ +--------------+
| CPU | | DMA Controller|
+----+----+ +------+-------+
| |
============+=========================+=========== (시스템 버스)
| Polling |
v v
+----------+ +----------+
| Main | | I/O |
| Memory | | Device |
+----------+ +----------+
DMA Controller가 장치<->메모리 간 데이터를 직접 전송
CPU는 전송 완료 인터럽트만 받으면 됨
| 비교 항목 | PIO (Programmed I/O) | DMA I/O |
|---|---|---|
| 데이터 전송 주체 | CPU | DMA Controller |
| CPU 부담 | 매우 높음 (1바이트마다 CPU 개입) | 낮음 (초기 설정 + 완료 인터럽트만) |
| 적합한 상황 | 소량 데이터 전송 | 대량 데이터 전송 (디스크, 네트워크 등) |
| 전송 중 CPU 상태 | 전송에 묶여 있음 | 다른 작업 수행 가능 |
사용자 앱: read(fd, buf, n)
| 시스템 콜
VFS -> file_operations -> .read 슬롯
| 드라이버 함수 호출
Device Driver -> 장치 레지스터에 명령 기록
|
장치가 작업 수행 (CPU와 병렬)
|
+- Polling: CPU가 상태 레지스터 반복 확인 (비효율)
+- Interrupt: 장치가 완료 시 IRQ 발생 -> PIC -> CPU -> ISR
+- DMA: DMA Controller가 데이터 전송 -> 완료 시 인터럽트
|
ISR (Top Half): 최소 작업만 수행, 요청을 큐에 넣음
|
Bottom Half: 스레드 컨텍스트에서 나머지 처리
|
프로세스 깨움 -> read() 반환
| 키워드 | 한 줄 정의 |
|---|---|
| file_operations | I/O 서브시스템이 정의한 함수 포인터 테이블 — 드라이버가 구현을 채움 |
| Exception | CPU 명령어 실행에 의해 발생하는 동기적 내부 이벤트 |
| Interrupt | 외부 장치에 의해 발생하는 비동기적 외부 이벤트 |
| PIC | 다수의 IRQ를 우선순위에 따라 관리하는 하드웨어 |
| Maskable Interrupt | 소프트웨어로 차단/허용 가능한 인터럽트 |
| Non-maskable Interrupt | 차단 불가능, 즉시 처리 필수인 인터럽트 |
| Vector Table | 벡터 번호 -> ISR/ESR 주소 매핑 테이블 |
| Interrupt Latency (T_B) | 인터럽트 발생 -> ISR 시작까지의 지연 시간 |
| Interrupt Miss | ISR이 너무 오래 실행되어 다른 인터럽트를 놓치는 현상 |
| Top Half | ISR에서 최소한의 긴급 작업만 수행하는 부분 |
| Bottom Half | 스레드 컨텍스트에서 나머지 작업을 처리하는 부분 |
| Polling | CPU가 상태 레지스터를 반복 확인하는 방식 |
| DMA | CPU 대신 데이터 전송을 수행하는 전용 컨트롤러 |
| I/O Port Address Space | 메모리 주소 공간과 별개인, 장치 레지스터용 주소 공간 (x86) |
| Device Controller | 컴퓨터와 주변 장치 간 데이터 전송을 제어하는 칩 |
| IRQ Line | 장치가 인터럽트를 요청하는 단일 출력선 |
| IPL | Interrupt Priority Level, 인터럽트 유형별 우선순위 |