컴퓨터 구조 09 - I/O와 DMA
CPU가 외부 장치와 데이터를 주고받는 방식과 DMA를 통한 효율적 데이터 전송의 원리
CPU와 외부 세계의 접점
컴퓨터는 CPU와 메모리만으로 구성되지 않는다. 디스크에서 데이터를 읽고, 네트워크로 패킷을 전송하며, 화면에 이미지를 렌더링해야 한다. 이러한 외부 장치와 CPU 사이의 데이터 교환이 I/O(Input/Output)이며, I/O를 얼마나 효율적으로 처리하느냐가 시스템 전체의 성능을 좌우한다.
I/O가 정말 그렇게 큰 병목일까? 현대 CPU는 초당 수십억 개의 명령어를 처리할 수 있지만, 디스크 접근은 밀리초 단위이고 네트워크 왕복은 수백 마이크로초에서 수십 밀리초가 걸린다. CPU 사이클로 환산하면 디스크 I/O 하나를 기다리는 동안 수백만 개의 명령어를 실행할 수 있다는 뜻이다. 이 격차를 어떻게 다루느냐가 시스템 설계의 핵심 과제다.
프로그램 I/O
가장 단순한 I/O 방식은 CPU가 직접 장치 상태를 반복해서 확인하는 것이다. 이를 프로그램 I/O, 혹은 폴링(polling)이라고 부른다. CPU는 장치 레지스터를 읽어 데이터가 준비됐는지 보고, 아직 아니라면 같은 확인을 계속 반복한다.
; 프로그램 I/O (폴링) 의사코드
loop:
status = read(device_status_register)
if status != READY:
goto loop
data = read(device_data_register)
이 방식의 문제는 CPU가 데이터를 기다리는 동안 다른 유용한 작업을 수행할 수 없다는 것이다. 빠른 장치에서 소량의 데이터를 읽는 경우에는 폴링이 오히려 효율적일 수 있지만, 느린 장치를 대상으로 하면 CPU 자원의 심각한 낭비가 발생한다.
인터럽트 기반 I/O
폴링의 비효율을 해결하는 방법이 인터럽트 기반 I/O이다. CPU가 장치에 I/O 요청을 보낸 후 다른 작업을 수행하다가, 장치가 데이터 전송을 완료하면 인터럽트를 발생시켜 CPU에 알린다. CPU는 현재 작업을 중단하고 인터럽트 핸들러를 실행하여 데이터를 처리한다.
이 방식은 CPU가 I/O를 기다리는 동안 유휴 상태에 빠지지 않게 해 준다. 그러나 여전히 한 가지 문제가 남아 있다. 장치에서 메모리로(또는 메모리에서 장치로) 데이터를 옮기는 작업 자체는 CPU가 바이트 단위 또는 워드 단위로 직접 수행해야 한다는 것이다. 대용량 데이터를 전송할 때 CPU가 데이터 복사에 매여 있으면 그만큼 계산 능력이 소모된다.
DMA: CPU를 데이터 전송에서 해방시키다
DMA(Direct Memory Access)는 CPU의 개입 없이 장치가 메모리와 직접 데이터를 주고받을 수 있게 하는 메커니즘이다. CPU는 DMA 컨트롤러에게 전송할 데이터의 메모리 주소, 크기, 방향(읽기/쓰기)을 알려주기만 하면 된다. 이후 실제 데이터 전송은 DMA 컨트롤러가 독립적으로 수행하며, 전송이 완료되면 인터럽트로 CPU에 통지한다.
CPU DMA 컨트롤러 장치 메모리
│ │ │ │
│ ── 전송 설정 ──────────▶ │ │ │
│ (주소, 크기, 방향) │ │ │
│ │ │ │
│ (다른 작업 수행) │ ◀── 데이터 ────────│ │
│ │ ─── 데이터 ─────────────────────▶│
│ │ ◀── 데이터 ────────│ │
│ │ ─── 데이터 ─────────────────────▶│
│ │ │ │
│ ◀── 완료 인터럽트 ────── │ │ │
DMA의 도입으로 CPU는 대용량 데이터 전송 중에도 자유롭게 다른 연산을 수행할 수 있게 되었다. 디스크에서 메가바이트 단위의 파일을 읽는 동안에도 CPU는 다른 프로세스의 명령어를 계속 실행할 수 있다. 다만 DMA 전송과 CPU가 동시에 메모리 버스를 사용하므로, 대량의 DMA 전송이 진행될 때는 CPU의 메모리 접근 속도가 다소 저하될 수 있다. 이를 사이클 스틸링(cycle stealing)이라 한다.
메모리 맵드 I/O와 포트 I/O
CPU가 장치와 통신하는 방식에는 두 가지가 있다. 포트 I/O(port-mapped I/O)는 별도의 I/O 주소 공간과 전용 명령어(x86의 IN/OUT)를 사용하여 장치에 접근한다. 메모리 맵드 I/O(memory-mapped I/O)는 장치의 레지스터를 메모리 주소 공간의 일부에 매핑하여, 일반적인 메모리 읽기/쓰기 명령어로 장치에 접근한다.
현대 시스템에서는 메모리 맵드 I/O가 지배적이다. 별도의 I/O 명령어가 필요 없으므로 프로그래밍이 단순해지고, 기존의 메모리 보호 메커니즘을 장치 접근 제어에도 활용할 수 있기 때문이다. PCIe 장치의 설정 공간이나 GPU의 프레임 버퍼 접근이 모두 메모리 맵드 I/O를 통해 이루어진다.
버스 아키텍처의 진화
초기 PC의 ISA 버스는 8MHz에서 16비트 폭으로 동작하며 최대 8MB/s의 대역폭을 제공했다. 그래픽 카드, 네트워크 카드, 디스크 컨트롤러 등 점점 더 빠른 장치들이 등장하면서 이 대역폭은 곧 부족해졌다.
PCI 버스는 33MHz에서 32비트 또는 64비트 폭으로 동작하며 최대 533MB/s까지의 대역폭을 제공했다. 그러나 PCI는 공유 버스 방식이어서 여러 장치가 동일한 버스 대역폭을 나눠 써야 했다.
PCIe(PCI Express)는 이 한계를 근본적으로 바꾸었다. 공유 버스 대신 포인트-투-포인트 직렬 링크를 채택하여, 각 장치가 전용 대역폭을 확보할 수 있게 되었다. PCIe의 대역폭은 레인(lane) 수와 세대(generation)에 따라 결정된다.
| 세대 | 레인당 대역폭 | x16 대역폭 |
|---|---|---|
| PCIe 3.0 | ~1 GB/s | ~16 GB/s |
| PCIe 4.0 | ~2 GB/s | ~32 GB/s |
| PCIe 5.0 | ~4 GB/s | ~64 GB/s |
| PCIe 6.0 | ~8 GB/s | ~128 GB/s |
고성능 GPU가 PCIe x16 슬롯을 사용하는 이유가 여기에 있다. 대규모 데이터 전송이 필요한 장치일수록 더 많은 레인을 확보하여 대역폭을 늘리는 것이다.
I/O 스케줄링
여러 프로세스가 동시에 디스크 I/O를 요청하면, 운영체제의 I/O 스케줄러가 이 요청들의 처리 순서를 결정한다. 전통적인 회전식 하드 디스크에서는 디스크 헤드의 이동을 최소화하는 것이 중요했으므로, 엘리베이터 알고리즘과 같은 스케줄링 기법이 효과적이었다.
그러나 SSD의 보급으로 상황이 변했다. SSD에는 물리적으로 이동하는 헤드가 없으므로 탐색 시간(seek time)이라는 개념 자체가 사라진다. 따라서 요청 순서를 최적화하는 복잡한 스케줄러보다 단순한 FIFO나 간단한 우선순위 기반 스케줄러가 더 적합해진 것이다. Linux에서 기본 I/O 스케줄러가 CFQ에서 mq-deadline, 그리고 none으로 변화해 온 이유가 바로 이러한 하드웨어 변화를 반영한 결과이다.
IOMMU와 장치 격리
DMA는 장치가 메모리에 직접 접근할 수 있게 해 주지만, 이는 보안 위험을 수반한다. 악의적이거나 오동작하는 장치가 임의의 메모리 영역을 읽거나 덮어쓸 수 있기 때문이다.
IOMMU(I/O Memory Management Unit)는 이 문제를 해결한다. CPU의 MMU가 프로세스의 메모리 접근을 제어하는 것처럼, IOMMU는 장치의 메모리 접근을 제어한다. 각 장치에 대해 접근 가능한 메모리 영역을 제한하여, 장치가 허가되지 않은 메모리에 접근하는 것을 방지한다. 가상화 환경에서 게스트 OS에 물리 장치를 직접 할당(passthrough)할 때 IOMMU가 필수적인 이유도 이것이다.
현대 저장 장치 인터페이스: NVMe
SATA 인터페이스는 회전식 하드 디스크를 위해 설계된 것으로, 명령 큐의 깊이가 32개에 불과하고 단일 큐만 지원한다. SSD의 잠재적 성능을 완전히 활용하기에는 이 인터페이스가 병목이 된다.
NVMe(Non-Volatile Memory Express)는 SSD를 위해 처음부터 설계된 프로토콜이다. PCIe 버스 위에서 직접 동작하며, 최대 65,535개의 큐를 지원하고 각 큐의 깊이는 65,536개까지 가능하다. 이 설계 덕분에 멀티코어 환경에서 각 코어가 자신만의 I/O 큐를 가질 수 있어, 큐 접근 시 잠금 경합(lock contention)을 회피할 수 있다. NVMe SSD가 SATA SSD에 비해 압도적인 IOPS(초당 입출력 연산 수)를 달성할 수 있는 이유가 여기에 있다.
I/O 가상화: SR-IOV
가상화 환경에서 여러 가상 머신이 하나의 물리 네트워크 카드를 공유하려면, 하이퍼바이저가 I/O 요청을 중재해야 한다. 이 소프트웨어 중재 계층은 지연 시간과 CPU 오버헤드를 증가시킨다.
SR-IOV(Single Root I/O Virtualization)는 하나의 물리 장치를 여러 개의 가상 기능(Virtual Function)으로 분할하여, 각 가상 머신이 마치 전용 물리 장치를 가진 것처럼 직접 장치에 접근할 수 있게 한다. 하이퍼바이저의 중재 없이 직접 장치에 접근하므로 지연 시간이 크게 줄어들고, CPU 오버헤드도 최소화된다. 고성능 네트워킹이 요구되는 클라우드 환경에서 SR-IOV가 널리 채택되고 있는 것은 이러한 이점 때문이다.
다음 글에서는 멀티코어와 현대 프로세서를 다룬다.