운영체제 아주 쉬운 세가지 이야기 13주차(I/O 장치)

2025. 12. 2. 18:02개념 공부

[OS] I/O Devices 정리 – 인터럽트, DMA, 디바이스 드라이버까지 한 번에

 

0. 왜 I/O가 중요한가?

  • 입력이 없으면 프로그램은 매번 똑같이 동작합니다.
  • 출력이 없으면 프로그램이 뭘 했는지 알 수 없습니다.
  • 그래서 운영체제 입장에서는 “CPU + 메모리 + I/O 장치”를 함께 묶어서 관리하는 게 핵심입니다.

핵심 질문(CRUX)

  • 어떻게 I/O 장치를 전체 시스템에 효율적으로 통합할 것인가?
  • 어떻게 CPU 시간을 덜 쓰면서도 느린 장치들을 잘 활용할 수 있을까?

1. 시스템 아키텍처 – 버스 계층 구조

1-1. 고전적인 구조 (Figure 36.1)

책에서는 CPU–메모리–I/O가 계층형 버스 구조로 연결된 그림을 보여줍니다. (메모리 버스 > 일반 I/O 버스(PCI) > 주변기기 버스(SCSI, SATA, USB))

  • Memory Bus : CPU와 DRAM이 붙어 있는 가장 빠른 버스
  • General I/O Bus (PCI 등) : 그래픽 카드 같은 고성능 I/O 장치
  • Peripheral Bus (SCSI, SATA, USB) : 디스크, 키보드, 마우스 등 느린 장치들

왜 계층형인가?

  • 버스가 빠를수록 물리적으로 짧아야 하고, 설계·제작 비용이 비쌉니다.
  • 그래서 고성능 장치는 CPU 가까이, 저성능 장치는 먼 쪽 버스에 둡니다.

1-2. 현대 시스템 예시 – Intel Z270 (Figure 36.2)

현대 시스템에서는 CPU와 메모리, 그래픽, I/O 칩이 DMI, PCIe 등으로 연결됩니다.

  • CPU ↔ 메모리 : 가장 빠른 경로
  • CPU ↔ GPU : 고대역폭 연결 (게임, 그래픽 처리)
  • CPU ↔ I/O Chip (DMI) : 나머지 디바이스들은 I/O 칩 밑에 매달림
  • I/O Chip 아래 :
    • eSATA : 디스크
    • USB : 키보드/마우스 같은 저속 장치
    • PCIe : 네트워크 카드, 고성능 NVMe SSD 등

기억 포인트

  • CPU에 가까울수록 빠르고 비싸고 개수가 적다.
  • 멀수록 느리고 싸고 많이 붙는다.

2. Canonical Device – 디바이스의 정석 구조

2-1. 디바이스가 제공하는 두 가지

Figure 36.3에서는 “정석적인 장치”를 두 부분으로 나눠 설명합니다.

  1. 인터페이스 (Interface)
    • OS가 만지는 부분 – 레지스터들
    • 예: Status, Command, Data 레지스터
  2. 내부 구조 (Internals)
    • 마이크로컨트롤러(CPU), 메모리(DRAM/SRAM), 칩들
    • 장치가 약속한 추상화를 실제로 구현하는 부분
    • 복잡한 RAID 컨트롤러 같은 경우 내부에 수십만 줄 펌웨어가 돌아갈 수 있음

2-2. Canonical Protocol – OS가 장치랑 이야기하는 패턴

// 1. 장치가 바쁘지 않을 때까지 대기 (polling)
while (STATUS == BUSY)
    ;

// 2. 데이터 전송
write(DATA_REGISTER, data);

// 3. 커맨드 전송 (작업 시작)
write(COMMAND_REGISTER, command);

// 4. 완료될 때까지 다시 polling
while (STATUS == BUSY)
    ;
  
  • 이 방식은 Programmed I/O (PIO)라고 부릅니다.
  • CPU가 직접 레지스터에 데이터를 계속 써주기 때문에, CPU가 너무 바쁩니다.
  • 또한 상태 레지스터를 계속 읽어보는 polling 때문에 CPU 시간이 낭비됩니다.

이슈 정리

  • CPU가 장치의 “알바”처럼 데이터 전송 & 상태 확인을 다 떠맡음
  • 느린 장치일수록 CPU는 더 오래 기다리게 됨

3. Polling vs Interrupt – CPU 시간을 아끼는 방법

3-1. Polling의 문제점

  • OS가 상태 레지스터를 계속 읽어보며 “다 했어?”만 물어봄
  • 디스크처럼 느린 장치일수록 CPU는 놀면서 busy-wait
  • 다른 프로세스를 돌릴 수 있는 CPU 시간을 낭비

3-2. 인터럽트로 해결하기

문제의 핵심 질문:

어떻게 장치 상태를 자주 polling하지 않고도, I/O 완료를 알 수 있을까?

인터럽트 기반 흐름

  1. 프로세스가 I/O 요청 → OS가 장치에 요청을 보냄
  2. 요청한 프로세스를 sleep 시킴
  3. OS는 다른 프로세스에게 CPU를 넘김 (context switch)
  4. 장치 작업 완료 시, 하드웨어 인터럽트 발생
  5. CPU는 OS의 ISR(Interrupt Service Routine)으로 점프
  6. ISR이 결과/에러코드 처리 후, 잠자던 프로세스를 깨움

장점

  • CPU와 디스크가 동시에 일을 할 수 있어 자원 활용도 향상
  • 느린 장치일수록 효과가 큼

3-3. 인터럽트가 항상 좋은 건 아니다

책에서 강조하는 중요한 포인트:

  • 장치가 너무 빠를 때 : 그냥 한두 번 polling 하면 끝나는데,
    굳이 context switch + 인터럽트 처리 오버헤드를 감당할 필요가 없음.
  • 네트워크 폭주 상황 : 패킷마다 인터럽트가 들어오면 OS가 인터럽트 처리만 하다가 user 프로세스는 못 도는 livelock 위험.
  • 해결책
    • polling과 interrupt를 섞어서(hybrid) 사용
    • interrupt coalescing – 여러 완료를 한 번에 모아서 인터럽트 한 번으로 처리

기억 포인트

  • 느린 장치 → 인터럽트 유리
  • 빠른 장치나 폭주 상황 → polling 혹은 하이브리드 전략이 더 효율적일 수 있음

4. DMA – 데이터 복사를 CPU 대신해주는 친구

4-1. PIO의 또 다른 문제

  • 대량의 데이터를 디스크로 보내려면 CPU가 데이터를 한 워드씩 직접 복사
  • 이 동안 CPU는 “복사 머신”이 되어버림

4-2. DMA(Direct Memory Access)의 아이디어

질문 : 데이터 복사 같은 단순 작업을, CPU 말고 다른 애가 대신해 줄 수 없을까?

DMA 엔진의 역할

  1. OS가 DMA 컨트롤러에게 다음 정보를 설정
    • 메모리에서 어디서부터 읽을지 (주소)
    • 얼마나 복사할지 (크기)
    • 어떤 장치로 보낼지 (목적지)
  2. 설정 후, OS는 다른 작업 수행 (CPU는 자유)
  3. DMA 엔진이 메모리 ↔ 장치 사이의 데이터 복사를 직접 수행
  4. 복사가 끝나면 DMA 컨트롤러가 인터럽트 발생
  5. OS는 “복사 완료”를 확인하고 다음 단계 진행

효과

  • CPU는 복사 작업에서 해방 → 다른 프로세스 실행
  • 대량 I/O에서 특히 이득이 큼

5. 장치와 통신하는 두 가지 방법 – I/O 명령 vs 메모리 매핑

5-1. I/O 전용 명령어 (Port I/O)

  • x86의 in, out 명령처럼, 별도의 I/O 명령어로 장치 레지스터에 접근
  • out port, value 형태로 특정 포트(주소)에 값 쓰기
  • 대부분 privileged instruction이어서 OS만 직접 사용 가능

5-2. Memory-mapped I/O

  • 장치 레지스터가 일부 메모리 주소 공간에 “매핑”됩니다.
  • OS 입장에서는 그냥 load/store 명령으로 메모리 읽고 쓰듯이 접근
  • 하드웨어가 해당 주소를 캐치해서 메모리가 아니라 장치로 라우팅

정리

  • 둘 다 여전히 사용 중이며, 절대적인 우열은 없음
  • memory-mapped I/O는 새 명령어가 필요 없다는 점이 장점

6. Device Driver – 장치 특성을 OS에 숨기는 레이어

6-1. 문제 정의

파일 시스템 같은 상위 레이어는 “SCSI냐, SATA냐, USB냐”를 신경 쓰고 싶지 않다.

그래서 OS는 추상화를 사용합니다.

  • Device Driver : 특정 장치와 실제로 통신하는 코드
    • 레지스터 주소, 프로토콜, 에러 처리 등 “지저분한 상세”를 캡슐화
  • 위쪽의 파일 시스템 / 앱
    • open / read / write / close 같은 POSIX API만 사용
    • 그 아래의 Generic Block Layer는 “블록 읽기/쓰기”만 신경

책의 Figure 36.4는 다음과 같은 스택을 보여줍니다.

  • Application
  • File System / Raw Interface
  • Generic Block Layer
  • Device Driver (SCSI, ATA, …)

6-2. Raw Interface

  • 파일 시스템을 거치지 않고, 블록을 직접 읽고 쓰는 인터페이스
  • fsck, 디스크 조각 모음 같은 툴이 사용

6-3. Device Driver의 그림자

  • Linux 기준, 커널 코드의 70% 이상이 드라이버 코드라는 연구 결과
  • 많은 드라이버가 “전업 커널 개발자”가 아닌 사람이 작성 → 버그 많음
  • 실제로 커널 크래시의 주요 원인 중 하나가 디바이스 드라이버

7. IDE Disk Driver 예시 – 진짜 프로토콜 맛보기

7-1. IDE 디스크 레지스터

책에서는 IDE 디스크의 레지스터 맵을 보여줍니다. 

  • Control Register (0x3F6) : 리셋, 인터럽트 활성화 등
  • Command Block Registers (0x1F0 ~ 0x1F7)
    • 0x1F0 : 데이터 포트
    • 0x1F1 : 에러 레지스터
    • 0x1F2 : 섹터 수
    • 0x1F3 ~ 0x1F5 : LBA 주소
    • 0x1F6 : 드라이브 선택, 상위 LBA
    • 0x1F7 : 커맨드 / 상태
  • Status Register (0x1F7) : BUSY, READY, ERROR 비트 등
  • Error Register (0x1F1) : 어떤 에러인지 상세 코드

7-2. 기본 I/O 프로토콜 요약

  1. 상태 레지스터에서 READY & not BUSY 될 때까지 기다림
  2. 섹터 수, LBA, 드라이브 번호 등 파라미터를 각 레지스터에 기록
  3. READ / WRITE 명령어를 상태/커맨드 레지스터(0x1F7)에 기록
  4. 쓰기의 경우 : READY & DRQ(데이터 요청) 비트를 확인 후 데이터 포트에 데이터 전송
  5. 인터럽트 처리 :
    • 섹터 단위 또는 여러 섹터 완료 후 한 번에 처리
  6. 에러 처리 :
    • STATUS의 ERROR 비트가 켜져 있으면 ERROR 레지스터 읽기

7-3. xv6 IDE 드라이버 코드 구조

Figure 36.6에는 xv6의 매우 단순화된 IDE 드라이버 코드가 나옵니다.

  • ide_wait_ready() : 상태 레지스터를 polling하여 READY & not BUSY가 될 때까지 대기
  • ide_start_request(b) :
    • 레지스터 세팅 (섹터 수, LBA, 드라이브 등)
    • WRITE인 경우 데이터도 함께 전송
  • ide_rw(b) :
    • 락을 잡고 큐에 요청 추가
    • 큐가 비어 있다면 바로 ide_start_request() 호출
    • 요청이 완료될 때까지 sleep
  • ide_intr() :
    • 인터럽트가 들어오면, READ의 경우 데이터 읽기
    • 버퍼 플래그 갱신 후, 잠자던 프로세스 깨우기
    • 다음 요청이 있다면 다시 ide_start_request()로 다음 I/O 시작

요약 : 디바이스 드라이버는 “요청 큐 관리 + 레지스터 프로토콜 구현 + 인터럽트 처리”를 담당합니다.


8. 최종 요약 – 이 장에서 꼭 기억할 것

8-1. 한 장 요약

  • 계층형 버스 구조 : CPU에 가까울수록 빠르고, 멀수록 느리고 많이 붙는다.
  • Canonical Device :
    • 인터페이스(레지스터) + 내부 구조(컨트롤러, 메모리)
    • PIO + polling은 단순하지만 비효율적
  • 인터럽트 :
    • CPU와 I/O를 겹쳐서 쓸 수 있게 해줌
    • 하지만 너무 빠른 장치나 폭주 상황에서는 오히려 문제 → hybrid, coalescing
  • DMA :
    • 대량 데이터 복사를 장치 전용 엔진이 수행
    • CPU는 다른 프로세스를 실행할 수 있음
  • 장치와 통신하는 방법 :
    • I/O 명령어 (in/out)
    • Memory-mapped I/O (load/store)
  • Device Driver :
    • 장치별 상세 프로토콜을 캡슐화
    • 위쪽(file system, app)은 “블록 읽기/쓰기” 같은 추상 인터페이스만 사용
    • 커널 코드의 상당 부분을 차지하고, 버그의 주요 원인

8-2. 시험/면접용 한 줄 정리

  • 인터럽트 : “느린 I/O 동안 CPU를 놀리지 않기 위한 메커니즘”
  • DMA : “대량 데이터 복사를 CPU 대신해주는 전용 컨트롤러”
  • Device Driver : “장치별 프로토콜을 숨기고, OS 나머지를 장치-독립적으로 만들어주는 모듈”