인터럽트(Interrupt)와 DMA(Direct Memory Access)
1. 개요
인터럽트(Interrupt)란 CPU가 프로그램을 순차 실행하는 도중, 입출력 완료·예외·외부 신호 등 즉시 처리해야 할 사건이 발생했을 때 현재 수행 흐름을 일시 중단하고 사건 처리 루틴(ISR)으로 제어를 넘긴 뒤 원래 위치로 복귀하는 비동기 제어 전환 메커니즘이다. DMA(Direct Memory Access)란 대량의 입출력 데이터를 CPU를 거치지 않고 I/O 장치와 주기억장치 사이에서 전용 컨트롤러가 직접 전송하도록 하여, CPU를 데이터 복사라는 저부가가치 작업에서 해방시키는 입출력 기법이다.
인터럽트와 DMA가 등장한 배경은 CPU와 입출력 장치 사이의 속도 불균형(speed gap)에 있다. CPU는 나노초 단위로 명령을 처리하지만 디스크·네트워크·키보드 같은 주변장치는 밀리초에서 마이크로초 단위로 동작하므로, 둘 사이에는 수천 배에서 수백만 배의 속도 차가 존재한다. 초기 컴퓨터가 사용한 프로그램 제어 입출력(Programmed I/O)은 CPU가 장치 상태 레지스터를 반복해서 읽으며 완료를 기다리는 폴링(polling) 방식이었는데, 이 경우 CPU는 느린 장치가 준비될 때까지 아무 생산적 일도 하지 못하고 바쁜 대기(busy waiting)에 수억 사이클을 낭비한다. 디스크 한 블록을 읽는 수 밀리초 동안 CPU가 수백만 개의 다른 명령을 처리할 수 있음에도 이를 모두 버리는 것이다.
인터럽트는 이 낭비를 "장치가 준비되면 CPU에게 알려달라"는 사건 주도(event-driven) 방식으로 해결한다. CPU는 입출력을 요청한 뒤 즉시 다른 작업으로 전환하고, 장치가 완료되면 인터럽트 신호를 보내 CPU의 주의를 끈다. 이로써 CPU 사이클 낭비가 제거되고 멀티태스킹·실시간 응답의 토대가 마련된다. 그러나 인터럽트만으로는 부족한데, 데이터 전송 자체(메모리↔장치 버퍼의 바이트 이동)는 여전히 CPU가 수행해야 하기 때문이다. 1KB를 전송하려면 바이트(또는 워드)마다 인터럽트가 발생하거나 CPU가 루프를 돌아야 하므로, 고속·대용량 전송에서는 인터럽트 처리 오버헤드 자체가 병목이 된다.
여기서 흔한 오해 하나를 바로잡을 필요가 있다. 인터럽트는 데이터 전송량을 줄이는 기술이 아니라 CPU가 기다리는 시간을 없애는 기술이고, DMA는 CPU가 전송에 쓰는 노동을 없애는 기술이다. 인터럽트만 쓰면 CPU는 기다리지는 않지만 매 전송 단위마다 깨어나 바이트를 직접 나른다. DMA만 쓰고 완료를 폴링으로 확인하면 전송 노동은 없지만 완료를 기다리느라 CPU가 다시 묶인다. 두 기법이 결합되어야 비로소 CPU가 "기다리지도, 나르지도" 않는 이상적 입출력이 완성된다. 이 상보성을 전제로 아래 각 메커니즘을 본다.
DMA는 바로 이 지점을 보완한다. "전송 시작"과 "전송 완료"만 CPU가 관여하고, 그 사이의 실제 바이트 이동은 DMA 컨트롤러(DMAC)가 전담한다. 결과적으로 인터럽트는 "CPU를 기다림에서 해방"하고, DMA는 "CPU를 데이터 복사에서 해방"한다. 두 기법은 경쟁 관계가 아니라 상호 보완 관계이며, 현대의 모든 고성능 입출력 경로(NVMe SSD, 100GbE NIC, GPU)는 "DMA로 블록을 통째로 전송하고, 전송이 끝나면 인터럽트 한 번으로 CPU에 알린다"는 조합으로 동작한다. 아래에서 각 메커니즘의 내부 동작과 상호작용을 심화 수준으로 다룬다.
2. 인터럽트의 처리 메커니즘
인터럽트가 발생하면 CPU는 현재 명령의 실행을 마친 직후(명령어 경계)에 인터럽트 사이클로 진입한다. 핵심은 "중단된 지점으로 정확히 되돌아갈 수 있어야 한다"는 투명성(transparency) 요구이며, 이를 위해 현재 문맥(context)을 저장했다가 복원하는 과정이 반드시 수반된다. 이 과정이 어긋나면 인터럽트는 프로그램을 오염시키는 버그가 되므로, 하드웨어와 운영체제가 정교하게 협력한다.
이 투명성을 보장하는 책임은 하드웨어와 소프트웨어로 나뉜다. 하드웨어(CPU)는 명령어 경계 식별, PC·PSW의 자동 저장, 벡터 테이블을 통한 분기까지를 담당하고, 소프트웨어(ISR)는 자신이 훼손할 범용 레지스터의 저장·복원과 실제 사건 처리를 담당한다. 이 분담이 모호하면 ISR이 일반 코드가 쓰던 레지스터를 덮어써 복귀 후 프로그램이 오동작한다. 그래서 컴파일러·운영체제는 ISR에 대해 호출 규약(calling convention)과 다른 특수 규칙(모든 사용 레지스터 저장, 재진입 가능하도록 지역 상태만 사용)을 적용하며, C 언어에서도 ISR 함수는 interrupt 속성이나 별도 프롤로그/에필로그로 구분해 컴파일한다.
전형적인 인터럽트 처리 절차는 다음과 같다. 첫째, 장치가 인터럽트 요청(IRQ) 신호를 인터럽트 컨트롤러(예: x86의 APIC)로 보낸다. 둘째, 컨트롤러가 우선순위를 판정해 CPU의 INTR 핀을 활성화한다. 셋째, CPU는 현재 명령을 끝낸 뒤 인터럽트를 수용할지(마스크 여부)를 점검한다. 넷째, 수용하면 현재 프로그램 카운터(PC)와 상태 레지스터(PSW)를 스택에 저장하고, 추가로 범용 레지스터를 저장한다(일부는 ISR이 저장). 다섯째, 인터럽트 벡터 테이블에서 해당 인터럽트 번호에 대응하는 ISR 주소를 읽어 PC에 적재한다. 여섯째, ISR이 실행되어 장치 상태를 확인하고 필요한 처리를 수행한 뒤, 컨트롤러에 완료를 통지(EOI, End Of Interrupt)한다. 일곱째, 저장했던 문맥을 복원하고 IRET 명령으로 중단 지점으로 복귀한다.
flowchart TB
START["프로그램 정상 실행"] --> CHK{"명령어 경계에서 인터럽트 확인"}
CHK -->|"요청 없음"| START
CHK -->|"요청 있음"| MASK{"인터럽트 활성화 상태?"}
MASK -->|"마스킹됨"| START
MASK -->|"허용"| SAVE["문맥 저장(PC·PSW·레지스터)"]
SAVE --> VEC["벡터 테이블에서 ISR 주소 획득"]
VEC --> ISR["ISR 실행(장치 처리·EOI 통지)"]
ISR --> REST["문맥 복원"]
REST --> IRET["IRET로 중단 지점 복귀"]
IRET --> START
여기서 벡터형 인터럽트(vectored interrupt)의 역할이 중요하다. 초기 방식은 하나의 ISR이 모든 장치를 순회 조회(polling)해 원인을 찾았지만, 이는 장치 수에 비례해 지연이 커진다. 벡터형은 장치가 인터럽트와 함께 자신의 식별 번호(벡터)를 제공하여, CPU가 벡터 테이블에서 곧바로 해당 ISR로 분기하도록 한다. 예컨대 x86은 256개의 벡터(0255)를 가지며 031은 CPU 예외, 32 이상은 외부 장치에 할당된다. 이 구조 덕분에 수백 개의 장치가 있어도 인터럽트 응답 시간(interrupt latency)이 일정하게 유지된다.
실시간 시스템을 설계할 때는 인터럽트 지연을 구성 요소로 분해해 관리해야 한다. 지연은 ① 인터럽트가 비활성화된 구간이 끝나기를 기다리는 인터럽트 비활성 지연, ② 하드웨어가 신호를 전달하고 CPU가 명령 경계에 도달하는 하드웨어 지연, ③ 문맥 저장과 벡터 분기에 드는 디스패치 지연, ④ ISR이 실제 처리를 끝내기까지의 처리 지연으로 나뉜다. 자동차·항공처럼 최악 지연(worst-case)이 안전과 직결되는 영역에서는 ①을 짧게 유지하려 임계 영역에서의 인터럽트 비활성 구간을 수십 사이클 이내로 제한하고, ISR은 꼭 필요한 처리만 수행한 뒤 나머지를 하위 반(bottom-half)으로 넘기도록 설계한다. 요컨대 "인터럽트가 빠르다"는 말은 평균이 아니라 분해된 각 구성요소의 상한을 보장한다는 의미로 읽어야 한다.
또한 중첩 인터럽트(nested interrupt)와 우선순위가 실시간성을 좌우한다. ISR 실행 중에 더 높은 우선순위의 인터럽트가 들어오면 현재 ISR을 다시 중단하고 상위 ISR을 먼저 처리해야 하는데, 이를 위해 ISR 진입 시 상위 인터럽트만 허용하도록 마스크 레벨을 조정한다. 예를 들어 산업용 모터 제어기에서 "정전 감지"는 최우선 비마스커블 인터럽트(NMI)로 두어 어떤 상황에서도 즉시 안전 정지가 실행되도록 설계한다. 반대로 긴 처리가 필요한 인터럽트는 ISR을 최소화하고 나머지를 지연 처리(리눅스의 softirq·tasklet·workqueue, 이른바 top-half/bottom-half 분리)로 넘겨, 인터럽트 비활성 구간을 짧게 유지해 다른 장치의 응답성을 보장한다.
3. 인터럽트의 유형
인터럽트는 발생 원인에 따라 여러 축으로 분류되며, 각 유형은 처리 방식과 우선순위가 다르다. 유형을 정확히 구분해야 예외(exception)와 외부 인터럽트를 혼동하지 않고, 운영체제의 트랩 처리와 장치 처리를 올바르게 설계할 수 있다. 특히 "동기/비동기"와 "내부/외부"의 구분은 디버깅과 실시간 설계에서 실무적으로 중요하다.
내부에서 CPU 실행과 동기적으로 발생하는 것이 예외(trap/exception)다. 0으로 나누기, 페이지 폴트, 권한 위반, 시스템 콜(소프트웨어 인터럽트) 등이 여기 속하며, 특정 명령 실행이 원인이므로 재현 가능하고 해당 명령 지점에서 결정적으로 발생한다. 반면 하드웨어 인터럽트는 CPU 실행과 무관하게 외부 장치에서 비동기로 들어오며, 디스크 완료·네트워크 패킷 도착·타이머 틱이 대표적이다. 비동기이므로 언제 들어올지 예측할 수 없어, 공유 자료구조 접근 시 동시성 문제(인터럽트와 일반 코드 간 경쟁)가 발생하며 이를 막기 위해 짧은 구간에서 인터럽트를 비활성화하거나 스핀락을 사용한다.
예외 중에서도 페이지 폴트는 운영체제 설계의 정교함을 보여주는 사례다. 가상 메모리에서 접근한 페이지가 물리 메모리에 없으면 CPU가 페이지 폴트 예외를 발생시키고, 운영체제가 디스크에서 해당 페이지를 적재한 뒤 "폴트를 일으킨 바로 그 명령"을 재실행한다. 즉 예외는 오류 처리만이 아니라 요구 페이징(demand paging)·쓰기 시 복사(copy-on-write) 같은 정상 기능의 구현 수단으로도 쓰인다. 한편 시스템 콜은 응용 프로그램이 의도적으로 발생시키는 소프트웨어 인터럽트(트랩)로, 사용자 모드에서 커널 모드로 안전하게 진입하는 유일한 관문이다. 이처럼 "예외/트랩"은 단순한 사고가 아니라 보호와 가상화를 떠받치는 능동적 제어 전환임을 이해해야 운영체제의 동작을 올바르게 설명할 수 있다.
| 분류 축 | 유형 | 예시 | 특징 |
|---|---|---|---|
| 발생 위치 | 외부(하드웨어) | 디스크·NIC·타이머 | 비동기, I/O 완료 알림 |
| 내부(예외/트랩) | 0분할·페이지폴트·시스템콜 | 동기, 명령 실행이 원인 | |
| 마스킹 | 마스커블(IRQ) | 일반 장치 인터럽트 | 소프트웨어가 일시 차단 가능 |
| 비마스커블(NMI) | 전원 이상·하드웨어 고장 | 차단 불가, 최우선 처리 | |
| 신호 방식 | 레벨 트리거 | 다수 장치 공유 라인 | 신호 유지 동안 지속 |
| 에지 트리거 | 단발성 사건 | 상태 변화 순간만 감지 |
표의 구분 중 마스커블과 비마스커블의 차이는 안전 설계에서 결정적이다. 일반 장치 인터럽트는 임계 영역 보호를 위해 잠시 막을 수 있지만, 전원 이상이나 메모리 패리티 오류 같은 치명적 사건은 막아서는 안 되므로 NMI로 분류해 어떤 경우에도 즉시 처리되게 한다. 또한 에지 트리거와 레벨 트리거의 선택은 인터럽트 공유와 손실 방지에 영향을 준다. 레벨 트리거는 여러 장치가 한 라인을 공유할 때 신호가 유지되어 놓치지 않는 장점이 있지만 원인 장치를 식별하는 폴링이 필요하고, 에지 트리거는 변화 순간만 감지해 빠르지만 ISR이 신호를 제때 비우지 못하면 후속 인터럽트를 잃을 수 있다. 실제 리눅스 커널에서 공유 PCI 인터럽트가 "lost interrupt"로 장치가 멈추는 장애가 이 메커니즘 차이에서 비롯된 대표적 사례다.
4. DMA의 동작 원리와 전송 모드
DMA의 핵심 통찰은 "데이터 이동은 반드시 CPU의 레지스터를 경유할 필요가 없다"는 것이다. 메모리와 장치 버퍼는 모두 시스템 버스에 연결되어 있으므로, 버스의 주인(bus master) 역할만 할 수 있다면 어떤 장치든 메모리에 직접 읽고 쓸 수 있다. DMA 컨트롤러(DMAC)는 바로 이 버스 마스터 기능을 수행하는 전용 하드웨어로, CPU로부터 "어디서(장치) 어디로(메모리 시작 주소) 몇 바이트를" 전송하라는 명령만 받으면 나머지를 스스로 처리한다.
DMA가 동작하려면 반드시 캐시 일관성(cache coherence) 문제를 다뤄야 한다. CPU는 메모리를 캐시를 통해 읽고 쓰므로, DMA가 메모리를 직접 갱신하면 CPU 캐시에 남아 있는 옛 값과 불일치가 생긴다. 그래서 DMA 수신(장치→메모리) 전에는 해당 영역의 캐시를 무효화(invalidate)하고, DMA 송신(메모리→장치) 전에는 캐시를 메모리로 비워(flush) 최신 값이 반영되게 해야 한다. 현대 x86은 하드웨어가 DMA 일관성을 자동 보장하지만(coherent DMA), ARM 임베디드 등에서는 드라이버가 명시적으로 캐시를 관리하거나 비캐시(non-cacheable) DMA 버퍼를 할당해야 하며, 이를 누락하면 "간헐적으로 데이터가 깨지는" 추적하기 어려운 버그가 된다.
전송 절차는 다음과 같다. CPU는 DMAC의 레지스터에 전송 방향, 메모리 시작 주소, 전송 길이(카운트)를 설정하고 전송을 지시한 뒤 즉시 다른 작업으로 전환한다. DMAC는 장치가 데이터를 준비하면 버스 사용권(bus grant)을 CPU로부터 획득해 메모리에 직접 바이트를 쓰고 카운트를 감소시키며, 카운트가 0이 되면 전송 완료 인터럽트 하나를 CPU에 발생시킨다. CPU는 수천~수백만 바이트의 전송 전체에 대해 처음과 끝, 단 두 번만 개입하는 셈이다.
sequenceDiagram
participant CPU
participant DMAC as "DMA 컨트롤러"
participant DEV as "I/O 장치"
participant MEM as "주기억장치"
CPU->>DMAC: "전송 설정(주소·길이·방향)"
Note over CPU: "다른 작업 수행(블록 안 됨)"
DEV->>DMAC: "데이터 준비 완료(DREQ)"
DMAC->>CPU: "버스 사용권 요청(HOLD)"
CPU-->>DMAC: "버스 양도(HLDA)"
loop "블록 전송"
DEV->>DMAC: "데이터"
DMAC->>MEM: "메모리에 직접 쓰기"
DMAC->>DMAC: "카운트 감소"
end
DMAC->>CPU: "전송 완료 인터럽트"
CPU->>CPU: "ISR에서 완료 처리"
DMAC가 CPU와 버스를 공유하는 방식에 따라 세 가지 전송 모드가 있다. 첫째, 사이클 스틸링(cycle stealing)은 CPU가 버스를 쓰지 않는 유휴 사이클을 "훔쳐" 한 번에 한 워드씩 전송하는 방식으로, CPU 성능에 거의 영향을 주지 않지만 전송이 느리다. 둘째, 버스트 모드(burst/block mode)는 DMAC가 버스를 독점해 블록 전체를 한 번에 전송하는 방식으로, 전송은 가장 빠르지만 그동안 CPU가 메모리에 접근하지 못해 멈출 수 있다. 셋째, 투명 모드(transparent mode)는 CPU가 명백히 버스를 쓰지 않는 구간에만 전송해 CPU에 전혀 영향을 주지 않지만 가장 느리다. 예를 들어 실시간성이 중요한 오디오 스트리밍은 사이클 스틸링을, 대용량 디스크 백업은 버스트 모드를 선택하는 식으로 응용 특성에 맞춰 모드를 고른다.
현대 시스템에서는 단순 DMAC를 넘어 각 장치가 스스로 버스 마스터가 되는 버스 마스터링 DMA가 표준이며, PCIe 장치(NVMe SSD, GPU, NIC)는 모두 자체 DMA 엔진을 내장한다. 특히 디스크립터 기반 스캐터-게더(scatter-gather) DMA는 물리적으로 흩어진 여러 메모리 영역을 하나의 전송 명령으로 묶어 처리하여, 가상 메모리로 인해 연속되지 않은 사용자 버퍼도 한 번에 전송할 수 있게 한다. 이것이 제로카피 네트워킹과 고성능 스토리지의 하드웨어적 토대다.
고속 장치는 CPU와 DMA 엔진이 공유 메모리의 디스크립터 링(descriptor ring)을 통해 비동기로 협력한다. CPU(드라이버)는 전송할 버퍼의 주소·길이를 담은 디스크립터를 링에 채워 넣고 "생산자 인덱스"를 전진시키며, 장치의 DMA 엔진은 "소비자 인덱스"를 따라가며 디스크립터를 읽어 전송을 수행한 뒤 완료 플래그를 되돌려 쓴다. 이 구조 덕분에 CPU와 장치가 서로를 기다리지 않고 수천 건의 전송을 파이프라인처럼 중첩할 수 있으며, NIC의 송수신 링(TX/RX ring)과 NVMe의 제출·완료 큐(SQ/CQ)가 모두 이 원리를 따른다. 결국 "DMA가 빠르다"는 것은 단일 전송이 빠르다는 뜻이 아니라, 완료를 기다리지 않고 다수 전송을 동시에 진행(outstanding I/O)할 수 있다는 뜻으로 이해해야 한다.
5. 입출력 방식의 비교
세 가지 입출력 방식 — 프로그램 제어(폴링), 인터럽트 구동, DMA — 은 "CPU가 얼마나 개입하는가"라는 축에서 점진적으로 CPU를 해방시켜 온 진화 과정으로 이해해야 한다. 단순히 "DMA가 가장 우수하다"가 아니라, 데이터 크기·실시간성·구현 복잡도의 트레이드오프에 따라 적절한 방식이 달라진다는 점이 실무적 함의다.
| 구분 | 프로그램 제어(폴링) | 인터럽트 구동 | DMA |
|---|---|---|---|
| CPU 개입 | 전 과정(바쁜 대기) | 매 데이터 단위마다 | 시작·완료 2회만 |
| 데이터 이동 주체 | CPU | CPU | DMA 컨트롤러 |
| CPU 효율 | 매우 낮음 | 중간 | 매우 높음 |
| 응답 지연 | 폴링 주기에 종속 | 인터럽트 지연 수준 | 낮음(블록 단위) |
| 적합 상황 | 초고속·소량, 단순 장치 | 중저속·비정기 사건 | 고속·대용량 블록 전송 |
| 오버헤드 원천 | 바쁜 대기 | 문맥 저장/복원 | 버스 경합·설정 비용 |
세 방식의 선택은 "장치의 속도"와 "전송 단위의 크기"라는 두 변수의 조합으로 결정된다고 정리할 수 있다. 저속·소량이면서 즉시성이 필요한 단순 장치(온도 센서 한 바이트 읽기)는 설정 오버헤드가 없는 폴링이 간결하고, 중저속이면서 사건이 드문드문 발생하는 장치(키보드·마우스)는 인터럽트가 적합하며, 고속·대용량 블록 전송(디스크·네트워크·GPU)은 DMA가 유일한 현실적 선택이다. 중요한 것은 이 경계가 하드웨어 발전에 따라 끊임없이 이동한다는 점으로, 과거 인터럽트로 충분하던 1GbE가 100GbE로 빨라지자 DMA와 폴링(NAPI)의 조합으로 다시 옮겨간 것이 그 예다. 기술사는 특정 방식을 정답으로 외우기보다, 속도와 전송 단위라는 축 위에서 트레이드오프를 판단하는 사고를 갖춰야 한다.
폴링은 대부분 비효율적이지만 아주 짧고 즉각적인 전송에서는 오히려 유리할 수 있다. 인터럽트는 문맥 저장·복원과 벡터 분기에 수백~수천 사이클이 드는데, 전송할 데이터가 한두 바이트뿐이라면 이 오버헤드가 전송 자체보다 커지기 때문이다. 역설적으로 10GbE·100GbE 고속 네트워크에서는 패킷이 너무 빨리 쏟아져 인터럽트가 초당 수백만 번 발생하면 CPU가 ISR 처리만으로 포화되는 인터럽트 폭주(interrupt storm)가 생긴다. 이를 막기 위해 리눅스 NAPI는 인터럽트를 한 번 받으면 이후에는 폴링으로 전환해 여러 패킷을 몰아서 처리하고, DPDK는 아예 인터럽트를 끄고 전용 코어가 계속 폴링하는 방식으로 회귀했다. 즉 "폴링 → 인터럽트 → DMA"는 단순 대체가 아니라 상황에 따라 공존·회귀하는 스펙트럼이다.
구체적 수치로 보면, 디스크에서 4KB 블록을 읽을 때 폴링 방식은 CPU가 수 밀리초 내내 상태를 확인하며 수백만 사이클을 버리지만, DMA는 CPU가 수십 사이클로 전송을 설정한 뒤 완료 인터럽트 한 번(수천 사이클)만 처리하면 된다. NVMe SSD가 초당 수백만 IOPS를 내는 것은 이런 DMA + 완료 인터럽트 조합에 더해, 깊은 명령 큐(최대 64K 큐 × 64K 깊이)로 여러 전송을 동시에 진행하기 때문이다.
실제 적용 사례로 Apache Kafka의 브로커를 들 수 있다. Kafka는 디스크의 로그 세그먼트를 소비자에게 전송할 때 sendfile 시스템 콜을 사용하는데, 이는 디스크 DMA로 페이지 캐시에 올린 데이터를 사용자 공간을 거치지 않고 스캐터-게더 DMA로 곧바로 NIC에 전달한다. 전송 전 과정에서 CPU가 하는 일은 전송 설정과 완료 인터럽트 처리뿐이므로, 단일 브로커가 수십만 메시지를 초당 중계하면서도 CPU 사용률을 낮게 유지한다. 이는 인터럽트(완료 알림)와 DMA(실제 이동)가 결합해 처리량을 끌어올리는 전형적 구조다.
6. 심화: 현대 시스템의 인터럽트·DMA 진화
현대 고성능 컴퓨팅에서 인터럽트와 DMA는 멀티코어·가상화·보안 요구에 맞춰 크게 진화했다. 과거의 단일 PIC(8259)·단일 DMAC(8237) 구조로는 수십 코어와 수백만 IOPS를 감당할 수 없기 때문이다. 이 섹션에서는 실무에서 성능·보안을 좌우하는 핵심 발전을 짚는다.
첫째, MSI/MSI-X(Message Signaled Interrupt)는 물리적 인터럽트 핀 대신 장치가 약속된 메모리 주소에 특정 값을 써서 인터럽트를 전달하는 방식이다. 핀 기반 방식은 공유 라인으로 인한 식별 폴링과 라인 부족 문제가 있었으나, MSI-X는 장치당 최대 2,048개의 독립 벡터를 지원해 멀티큐 NIC·NVMe가 코어별로 전용 인터럽트 큐를 둘 수 있게 한다. 예컨대 64코어 서버의 100GbE NIC는 64개의 수신 큐 각각을 다른 코어의 MSI-X 벡터에 연결(RSS, Receive Side Scaling)하여, 인터럽트 처리를 코어 전체에 분산하고 캐시 지역성을 높인다.
둘째, IOMMU(Input-Output MMU, 인텔 VT-d·AMD-Vi)는 DMA의 보안·가상화 문제를 해결한다. 전통적 DMA는 물리 주소에 직접 접근하므로, 악성·결함 장치가 임의 메모리를 덮어쓰는 DMA 공격(예: Thunderbolt를 통한 DMA 탈취)이 가능했다. IOMMU는 장치가 사용하는 I/O 가상 주소를 실제 물리 주소로 변환하고 접근 범위를 제한하여, 장치가 허가된 영역만 접근하도록 격리한다. 이는 가상머신에 물리 장치를 직접 할당(PCI passthrough)하면서도 VM 간 메모리를 보호하는 핵심 기술이며, 클라우드의 SR-IOV 기반 네트워크 가상화의 토대가 된다.
셋째, 인터럽트 결합(interrupt coalescing)과 하이브리드 폴링은 인터럽트 폭주 문제의 실무적 해법이다. 장치가 여러 사건을 모아 한 번에 인터럽트를 발생시키거나(coalescing), 일정 시간 폴링한 뒤 인터럽트로 전환하는 방식으로 처리량과 지연 사이의 균형을 맞춘다. NVMe 드라이버의 하이브리드 폴링은 고IOPS 상황에서 완료 인터럽트를 끄고 짧게 폴링하여 인터럽트 오버헤드와 지연을 동시에 줄인다. 2024년 기준 리눅스 커널의 io_uring은 이런 완료 폴링(IOPOLL)과 DMA를 결합해 사용자 공간에서 수백만 IOPS를 달성하는 대표적 사례다.
넷째, 가상화 환경의 인터럽트 전달이 성능을 좌우한다. 가상머신에서 물리 인터럽트가 발생하면 전통적으로는 하이퍼바이저가 가로채 게스트에 주입(inject)했는데, 이 VM 진입/탈출(VM-exit)이 큰 오버헤드였다. 인텔 APICv·AMD AVIC 같은 가상 인터럽트 하드웨어 지원과 포스티드 인터럽트(posted interrupt)는 하이퍼바이저 개입 없이 인터럽트를 게스트 vCPU에 직접 전달하여 이 비용을 제거한다. SR-IOV로 물리 NIC의 가상 기능(VF)을 VM에 직접 할당하고, 그 VF의 DMA는 IOMMU로 격리하며, 인터럽트는 포스티드 인터럽트로 직접 전달하는 조합이 오늘날 클라우드 고성능 네트워킹의 표준 경로다.
7. 고려사항 및 시사점
인터럽트와 DMA는 반세기 넘은 기초 개념이지만, 고성능·실시간·보안 시스템을 설계하는 기술사 관점에서는 여전히 핵심 트레이드오프의 원천이다. 다음 항목들을 종합적으로 고려해야 한다.
인터럽트 지연 vs 처리량의 균형: 실시간 제어(로봇·자동차)는 결정적 인터럽트 지연(worst-case latency)이 생명이므로 ISR을 극도로 짧게 유지하고 상위 우선순위를 보장해야 한다. 반면 고처리량 서버는 인터럽트 결합·NAPI·폴링으로 인터럽트 횟수를 줄이는 것이 유리하다. 하나의 시스템이 두 요구를 모두 가지면(예: 자율주행 ECU) 우선순위 분리와 코어 격리(core isolation)를 병행해야 한다.
DMA 보안과 신뢰 경계: DMA는 본질적으로 메모리 직접 접근이므로 신뢰 경계를 무너뜨릴 수 있다. 외부 포트(Thunderbolt·USB4)나 클라우드의 패스스루 장치에는 반드시 IOMMU를 활성화하고, 기밀 컴퓨팅 환경에서는 장치 DMA에 대한 암호화·검증(예: PCIe IDE/TDISP)을 적용해야 한다. "성능을 위해 IOMMU를 끄는" 관행은 보안 사고의 빌미가 되므로 신중히 판단한다.
멀티코어 확장성과 인터럽트 친화성(affinity): 단일 코어에 인터럽트가 몰리면 그 코어만 포화되어 전체 성능이 제한된다. MSI-X 다중 벡터와 인터럽트 affinity 설정으로 부하를 분산하되, 인터럽트를 처리하는 코어와 데이터를 소비하는 코어를 일치시켜(캐시 지역성) NUMA 환경에서의 원격 메모리 접근을 최소화해야 한다.
전력·에너지 관점: 폴링은 CPU를 깨어 있게 유지해 전력을 소모하므로, 배터리 기반 IoT·임베디드에서는 인터럽트 기반 설계로 CPU를 저전력 슬립에 두었다가 사건 발생 시에만 깨우는 것이 필수다. 반대로 데이터센터의 지연 민감 워크로드는 전력을 감수하고 폴링을 선택한다. 전력과 성능은 동일 축의 양 끝이며, 워크로드 특성이 선택을 결정한다.
연계 기술과의 통합 설계: DMA는 제로카피 I/O, RDMA(원격 DMA), GPU Direct, 스마트NIC·DPU로 확장된다. 특히 AI 데이터센터에서 GPUDirect RDMA는 NIC가 GPU 메모리에 CPU·시스템 메모리를 경유하지 않고 직접 DMA하여 분산 학습의 통신 병목을 제거한다. 기술사는 단일 노드의 DMA를 넘어 노드 간 DMA(RDMA) 관점에서 전체 데이터 경로를 최적화할 수 있어야 한다.
참고자료
- Intel, "Intel 64 and IA-32 Architectures Software Developer's Manual, Vol.3 (System Programming): Interrupt and Exception Handling, I/O" — https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
- The Linux Kernel Documentation, "Interrupt Handling / MSI / DMA-API / IOMMU" — https://docs.kernel.org/core-api/dma-api.html
- PCI-SIG, "PCI Express Base Specification (MSI-X, Address Translation Services)" — https://pcisig.com/specifications
- Thunderclap, "DMA attacks and the IOMMU" (NDSS 2019) — https://www.ndss-symposium.org/ndss-paper/thunderclap/
한 줄 요약: 인터럽트는 CPU를 느린 장치의 기다림에서 해방하는 사건 주도 제어 전환이고, DMA는 데이터 이동을 전용 컨트롤러에 맡겨 CPU를 복사 작업에서 해방하는 기법으로, 현대 고성능 입출력은 "DMA로 블록을 전송하고 완료 시 인터럽트 한 번"의 조합 위에서 MSI-X·IOMMU·폴링 회귀로 확장된다.