[OSTEP] 스터디 14주차 - 영속성 1 Part.1

운영체제의 세 번째 핵심 주제인 영속성(Persistence)은 시스템 전원이 꺼지거나 충돌이 발생해도 데이터가 영구적으로 보존되도록 보장하는 개념이다. 이 영속성은 하드웨어(I/O 장치, 디스크)와 소프트웨어(파일 시스템, 디렉터리)의 유기적인 상호작용으로 완성된다.


1. 시스템 아키텍처 (System Architecture)

I/O 장치를 이해하려면 먼저 이들이 시스템 전체에서 어디에 위치하는지 봐야 한다. 컴퓨터 시스템은 성능 비용에 따라 계층적인 버스(Bus) 구조를 가진다.

1.1. 계층적 버스 구조

왜 계층적인 구조가 필요할까? 이는 물리학적 제약비용 때문이다.

  • 버스는 고속일수록 길이가 짧아져야 한다. (신호 무결성 문제)
  • 고속 버스는 제작 비용이 비싸기 때문에 모든 장치를 고속 버스에 연결할 수 없다.

일반적인 시스템은 다음과 같은 계층 구조를 가진다.

  • 메모리 버스 (Memory Bus): CPU와 메인 메모리를 연결하는 버스다. 가장 빠르고(대역폭이 높고) 지연 시간이 짧지만, 가격이 비싸고 길이를 늘리기 어렵다. 주로 그래픽 카드 같은 고성능 장치들이 이곳에 가깝게 연결된다.
  • 범용 I/O 버스 (General I/O Bus): PCI(Peripheral Component Interconnect) 등이 여기에 해당한다. 메모리 버스보다는 느리지만, 여전히 꽤 빠른 속도를 제공한다.
  • 주변 장치 버스 (Peripheral Bus): SCSI, SATA, USB 등이 여기에 속한다. 가장 느리지만, 길이를 길게 늘릴 수 있고 많은 장치를 연결할 수 있다. 하드 디스크, 마우스, 키보드 등이 이곳에 연결된다.
“PCI”는 “Peripheral Component Interconnect”의 약자로, 1990년대 중반부터 2000년대 초반까지 컴퓨터에서 주로 사용되던 IO Bus 표준 중 하나다. PCI에 대한 몇 가지 주요 특징은 다음과 같다.

플러그 앤 플레이: 장치를 컴퓨터에 연결하면 운영 체제가 자동으로 장치를 감지하고 설정한다.
32비트와 64비트 버전: 초기 PCI 버스는 32비트 데이터 경로를 사용했지만, 나중에 64비트 버전도 출시되었다.
높은 데이터 전송 속도: PCI 버스는 그 당시 다른 버스 표준에 비해 높은 데이터 전송 속도를 제공했다.
범용성: 여러 종류의 장치 (그래픽 카드, 네트워크 카드, 사운드 카드 등)와 호환된다.

그러나 PCI는 오래된 기술이기 때문에 현대의 PC나 서버에서는 더 빠른 성능과 확장성을 제공하는 PCI Express (PCIe)와 같은 새로운 표준에 의해 대체되었다. PCIe는 PCI의 후속 표준으로, 데이터 전송 속도와 확장성, 그리고 세밀한 전력 관리 기능 등 여러 가지 향상된 기능을 제공한다.

2. 표준 장치 (A Canonical Device)와 동작 방식

운영체제가 실제 장치를 제어하는 방법을 이해하기 위해, 표준 장치(Canonical Device)라는 추상적인 모델을 제시한다.

2.1. 장치의 내부 구조

모든 장치는 크게 두 부분으로 나뉜다.

  1. 하드웨어 인터페이스 (Interface): OS가 장치를 제어하기 위해 들여다보는 '창문'이다. 주로 3개의 레지스터(저장소)로 구성된다.
    • 상태(Status) 레지스터: 장치가 바쁜지(BUSY), 준비됐는지(READY), 에러가 났는지 알려준다.
    • 명령(Command) 레지스터: OS가 "데이터를 읽어라/써라" 같은 명령을 내리는 곳이다.
    • 데이터(Data) 레지스터: 데이터를 주고받는 통로다.
  2. 내부 구조 (Internals): 실제 장치의 기능을 수행하는 부분이다. (예: 디스크의 모터와 헤드, 펌웨어 칩 등)

2.2. 장치와 소통하는 기본 방법: 폴링(Polling)과 PIO

가장 원시적인 통신 방법은 폴링(Polling)PIO(Programmed I/O)다.

  • 상황: OS가 장치에게 데이터를 쓰라고 명령하고 싶다.
  • 단계 1 (폴링): OS는 Status 레지스터를 무한 반복문으로 계속 읽는다. ("지금 바빠? 바빠? 안 바빠?") 장치가 READY 상태가 될 때까지 CPU는 다른 일을 못 하고 기다린다.
  • 단계 2 (데이터 전송 - PIO): 준비가 되면 CPU가 메모리에서 데이터를 가져와 Data 레지스터에 직접 쓴다. CPU가 데이터 이동 노동을 직접 하므로 이를 PIO라고 한다.
  • 단계 3 (명령 전달): Command 레지스터에 '쓰기' 명령을 기록한다. 장치는 이제 작업을 시작하고 상태를 BUSY로 바꾼다.
  • 단계 4 (완료 대기): OS는 다시 Status를 계속 확인하며 작업이 끝날 때까지 기다린다.

문제점: 장치가 느리다면(예: 디스크), CPU는 그 긴 시간 동안 아무것도 못 하고 Status 레지스터만 쳐다보고 있어야 한다. 이는 엄청난 CPU 자원 낭비다.


3. 효율성을 위한 핵심 기술: 인터럽트와 DMA

위의 비효율을 해결하기 위해 운영체제는 두 가지 핵심 기술을 도입했다.

3.1. 인터럽트 (Interrupt): "다 되면 알려줘"

OS가 장치를 계속 감시(폴링)하는 대신, 작업을 시켜놓고 CPU를 다른 프로세스에 넘겨버리는 방식이다.

  1. OS는 장치에 명령을 내린 후, 해당 프로세스를 '대기(Blocked)' 상태로 만들고 다른 프로세스를 실행한다(Context Switch).
  2. 장치가 작업을 마치면 CPU에 전기 신호(하드웨어 인터럽트)를 보낸다.
  3. CPU는 하던 일을 잠시 멈추고, OS의 인터럽트 핸들러(Interrupt Handler)를 실행하여 "아, 디스크 작업 끝났구나"라고 인지하고, 대기하던 프로세스를 다시 깨운다.
    • 장점: I/O 작업 중에도 CPU가 놀지 않고 연산(Overlap)을 할 수 있어 효율이 극대화된다.
    • 주의: 아주 빠른 장치라면 인터럽트 처리 비용(문맥 교환 비용)이 폴링보다 더 비쌀 수 있으므로, 처음엔 잠깐 폴링하다가 안 끝나면 인터럽트를 쓰는 하이브리드 방식을 쓰기도 한다.

3.2. DMA (Direct Memory Access): "짐 옮기는 건 비서가 해"

인터럽트를 써도 여전히 데이터 복사(PIO)는 CPU가 직접 해야 했다. (예: 디스크에서 온 데이터를 메모리로 1바이트씩 옮기는 일). 이를 해결하는 것이 DMA다.

  1. 동작: OS는 CPU 대신 DMA 엔진이라는 특수 하드웨어에게 명령한다. "디스크(장치)에 있는 데이터 4KB를 메모리 주소 X로 옮겨줘."
  2. 해방: CPU는 데이터 복사 노동에서 해방되어 그 시간에 다른 프로세스를 처리한다.
  3. 완료: DMA 엔진이 데이터 복사를 다 마치면, 그때 CPU에게 인터럽트를 걸어 보고한다.
    • 결과: CPU 점유율을 획기적으로 낮출 수 있다.

4. 운영체제에 연결하기 : 디바이스 드라이버

최종적으로 다룰 문제는 서로 다른 인터페이스를 가지는 수많은 장치들과 운영체제를 연결시키는 일반화된 방법을 찾는 것이다.

SCSI 디스크, IDE 디스크, USB 등과 같은 기기 위에서 동작하는 파일 시스템을 만들고자 하는데, 각 장치들의 구체적인 입출력 명령어 형식에 종속되게 만들고 싶지 않다.

추상화(abstraction)라는 고전적 방법을 사용하여 이 문제를 해결할 수 있다.

파일 시스템은 어떤 디스크 종류를 사용하는지 전혀 알지 못한다. 파일 시스템은 범용 블록 계층에 블럭 read, write를 요청할 뿐이다. 범용 블럭 계층은 적절한 디바이스 드라이버로 받은 요청을 전달하며, 디바이스 드라이버는 특정 요청을 장치에 내리기 위해 필요한 일들을 처리한다. 이 그림을 통해 장치에 대한 구체적인 동작이 어떻게 운영체제의 대부분에게 숨겨지는지 알 수 있다.

이런 캡슐화는 단점도 있다. 커널이 범용적인 인터페이스만을 제공하는 경우, 특수 기능을 많이 가진 장치는 사용하기 힘들 것이다.

어떤 장치를 시스템에 연결하든 디바이스 드라이버가 필요하기 때문에, 시간이 지나면서 디바이스 드라이버 코드가 커널 코드의 대부분을 차지하게 되었다. 드라이버들이 대부분 전업 커널 개발자가 아닌 개발자들에 의해 만들어지기 때문에 상당한 버그를 포함하고 있고, 커널 크래시의 주범이 되고 있다.