
인터럽트(Interrupt): CPU를 깨우는 알람
CPU가 100% 바쁠 때 마우스를 움직이면 반응할까요? 폴링(Polling) vs 인터럽트(Interrupt). 엄마가 피자 다 됐다고 소리치는 이유.

CPU가 100% 바쁠 때 마우스를 움직이면 반응할까요? 폴링(Polling) vs 인터럽트(Interrupt). 엄마가 피자 다 됐다고 소리치는 이유.
맥북 배터리는 왜 오래 갈까? 서버 비용을 줄이려면 AWS Graviton을 써야 할까? 복잡함(CISC)과 단순함(RISC)의 철학적 차이를 정리해봤습니다.

AI 시대의 금광, 엔비디아 GPU. 도대체 게임용 그래픽카드로 왜 AI를 돌리는 걸까? 단순 노동자(CUDA)와 행렬 계산 천재(Tensor)의 차이로 파헤쳐봤습니다.

빠른 SSD를 샀는데 왜 느릴까요? 1차선 시골길(SATA)과 16차선 고속도로(NVMe). 인터페이스가 성능의 병목이 되는 이유.

LP판과 USB. 물리적으로 회전하는 판(Disc)이 왜 느릴 수밖에 없는지, 그리고 SSD가 어떻게 서버의 처리량을 100배로 만들었는지 파헤쳐봤습니다.

여러분이 방에서 게임을 하고 있는데 배달 피자를 시켰습니다. 도착을 확인하는 방법은 두 가지가 있습니다.
누가 봐도 2번이 효율적입니다. 컴퓨터도 똑같습니다. CPU가 주변 장치(키보드, 마우스, 네트워크 등)를 다루는 방식이 바로 인터럽트입니다.
저는 이 개념을 처음 배웠을 때 "그냥 이벤트 리스너 같은 거 아냐?"라고 생각했습니다. 그런데 깊이 파고들면서 이게 단순한 소프트웨어 패턴이 아니라, 하드웨어 레벨에서 작동하는 물리적인 신호 시스템이라는 걸 이해했습니다. 그 순간 왜 Node.js가 "이벤트 기반"을 강조하는지, 왜 async/await가 효율적인지가 머릿속에 쏙 들어왔습니다.
CPU는 눈코 뜰 새 없이 바쁜 녀석입니다. 그런데 만약 키보드 입력을 받기 위해 계속 "키 눌렀니? 안 눌렀어? 지금은?" 하고 물어봐야 한다면?
초창기 컴퓨터는 실제로 이 방식을 썼습니다. CPU가 Busy Waiting 루프를 돌면서 장치의 상태 레지스터를 계속 확인했죠.
// 옛날 방식: 폴링으로 키보드 입력 대기
while (1) {
if (keyboard_status_register & KEY_PRESSED) {
char key = keyboard_data_register;
process_key(key);
break;
}
}
문제가 뭘까요?
저는 처음에 "그래도 CPU 빠르니까 괜찮지 않나?"라고 생각했습니다. 그런데 Linux 서버에서 /proc/interrupts 파일을 보는 순간 생각이 바뀌었습니다.
cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 142 0 0 0 IO-APIC 2-edge timer
1: 10 0 0 0 IO-APIC 1-edge i8042
8: 1 0 0 0 IO-APIC 8-edge rtc0
9: 0 0 0 0 IO-APIC 9-fasteoi acpi
12: 156 0 0 0 IO-APIC 12-edge i8042
...
보세요. 제 서버는 지금 이 순간에도 타이머, 키보드, 디스크, 네트워크 등 수십 개의 장치가 인터럽트를 걸고 있습니다. 만약 CPU가 이것들을 폴링으로 확인했다면? 아무 일도 못 했을 겁니다.
그래서 컴퓨터 설계자들은 생각했습니다. "CPU가 물어보는 게 아니라, 장치가 말을 걸면 어떨까?"
이것이 인터럽트의 핵심입니다. 주변 장치들은 CPU에게 물리적인 신호선(Interrupt Request Line, IRQ)을 통해 신호를 보낼 수 있습니다.
이 덕분에 CPU 사용률이 100%여도, 마우스를 움직이면 즉각 반응하는 것입니다. 마우스 인터럽트가 우선순위가 높거든요.
저는 이 과정을 처음 이해했을 때 "아, 이게 OS의 멀티태스킹 기반이구나"라는 깨달음을 얻었습니다. 우리가 당연하게 여기는 "여러 프로그램 동시 실행"은 사실 인터럽트 없이는 불가능한 거였습니다.
CPU가 인터럽트 신호를 받으면 "누가 보낸 신호지?"를 알아야 합니다. 이때 사용하는 게 Interrupt Vector Table (IVT)입니다.
생각해보세요. 병원 응급실에 환자가 오면 "심장마비"는 심장내과, "골절"은 정형외과로 보내야 합니다. CPU도 마찬가지입니다.
timer_handler() 실행keyboard_handler() 실행mouse_handler() 실행IVT는 메모리의 특정 주소(예: x86에서는 0x00000000부터)에 저장된 함수 포인터 배열입니다.
// IVT의 개념적 구조 (실제는 더 복잡)
void (*interrupt_vector_table[256])(void);
interrupt_vector_table[0] = timer_handler;
interrupt_vector_table[1] = keyboard_handler;
interrupt_vector_table[12] = mouse_handler;
x86 시스템에서는 256개의 인터럽트 벡터를 가질 수 있습니다. 031번은 CPU가 예약한 예외(Exception)용이고, 32255번은 하드웨어 장치나 소프트웨어가 사용합니다.
제가 커널 드라이버를 작성할 때 "왜 IRQ 충돌이 나면 장치가 안 잡히는지" 이해가 안 갔었습니다. 그런데 IVT를 알고 나니 명확했습니다. 두 장치가 같은 IRQ 번호를 쓰면 같은 핸들러를 호출하니까 하나밖에 작동 안 하는 거였습니다. (요즘은 APIC 덕분에 IRQ 공유가 가능하지만요.)
병원 응급실에는 트리아지(Triage) 시스템이 있습니다. 심장마비 환자가 오면 감기 환자보다 먼저 봐야죠.
CPU의 인터럽트도 똑같습니다. 모든 인터럽트가 동등하지 않습니다.
만약 디스크 인터럽트를 처리하는 중에 타이머 인터럽트가 오면? 더 높은 우선순위가 낮은 우선순위를 가로챕니다(Preemption).
이것도 실제로 보면 확 와닿습니다. Linux에서 watch -n 1 cat /proc/interrupts를 실행해보면 타이머 인터럽트가 매초 엄청나게 많이 발생하는 걸 볼 수 있습니다. 이게 바로 OS의 심장 박동입니다.
저는 여기서 "왜 게임할 때 마우스는 부드러운데 디스크 복사는 멈춘 것처럼 느려질까?"라는 의문이 풀렸습니다. 마우스 인터럽트는 우선순위가 높아서 즉각 처리되지만, 디스크 I/O는 우선순위가 낮아서 CPU 시간을 나중에 받는 거였습니다.
인터럽트에는 두 종류가 있습니다.
대부분의 하드웨어 인터럽트는 CPU가 무시할 수 있습니다. 예를 들어, 은행 ATM 프로그램이 돈을 인출하는 중에는 키보드 인터럽트를 잠시 막아야 합니다. 안 그러면 중간에 다른 버튼을 눌러서 트랜잭션이 꼬일 수 있거든요.
// 크리티컬 섹션: 인터럽트를 잠시 막음
cli(); // Clear Interrupt Flag (인터럽트 비활성화)
// 중요한 작업 (예: 돈 인출)
withdraw_money(account, amount);
sti(); // Set Interrupt Flag (인터럽트 재활성화)
하지만 절대 무시하면 안 되는 인터럽트도 있습니다.
이런 건 화재 경보와 같습니다. 아무리 중요한 회의 중이어도 불이 나면 대피해야죠.
저는 서버 IDC에서 정전 사고를 겪었을 때 이게 얼마나 중요한지 뼈저리게 느꼈습니다. UPS(무정전 전원 공급 장치)에서 NMI를 보내면, OS는 무조건 "지금 당장 디스크에 데이터 쓰고 셧다운!" 해야 합니다. 이걸 무시하면 데이터가 날아갑니다.
인터럽트는 꼭 마우스 같은 장치만 거는 게 아닙니다.
저는 "시스템 콜이 인터럽트의 일종"이라는 걸 알았을 때 충격을 받았습니다.
// 파일 읽기 (시스템 콜)
int fd = open("/etc/passwd", O_RDONLY); // 이 순간 INT 0x80 (x86) 발생!
open() 함수를 호출하면 내부적으로 소프트웨어 인터럽트를 발생시킵니다. 그러면 CPU는 User Mode에서 Kernel Mode로 전환하고, OS의 파일 시스템 코드를 실행합니다.
이게 바로 "왜 사용자 프로그램이 하드웨어를 직접 못 건드리는지"의 답이었습니다. 모든 하드웨어 접근은 OS가 중간에서 인터럽트로 가로채서 권한을 검사합니다.
인터럽트 얘기를 하다 보면 빠질 수 없는 게 DMA입니다.
옛날에는 디스크에서 데이터를 읽으면 이런 과정을 거쳤습니다.
문제는 "한 바이트씩 읽는 게 CPU 시간 낭비"라는 겁니다.
그래서 나온 게 DMA (Direct Memory Access) 컨트롤러입니다.
// DMA 없을 때
for (int i = 0; i < 1024; i++) {
memory[i] = disk_read_byte(); // CPU가 일일이 복사
}
// DMA 있을 때
dma_start(disk_address, memory_address, 1024); // DMA 컨트롤러에게 위임
// CPU는 다른 일 함
wait_for_interrupt(); // 끝나면 인터럽트로 알림
저는 AWS EC2에서 "왜 EBS 볼륨은 인스턴스 타입에 따라 성능이 다를까?"가 궁금했습니다. 알고 보니 DMA 채널 수와 관련이 있더군요. 고성능 인스턴스는 DMA 채널이 많아서 여러 디스크를 동시에 빠르게 읽을 수 있습니다.
이 인터럽트 개념은 이벤트 기반(Event-driven) 프로그래밍의 모태가 됩니다.
자바스크립트의 onClick, onLoad가 바로 인터럽트 철학을 소프트웨어로 옮긴 것입니다.
// ❌ 폴링 (Polling)
while(true) {
if (button.isClicked()) {
doSomething();
}
} // CPU 과부하로 컴퓨터 멈춤
// ✅ 인터럽트 (Event Listener)
button.addEventListener('click', () => {
doSomething(); // 클릭될 때만 실행됨 (CPU 낭비 0%)
});
여러분이 짜는 addEventListener가 사실은 CPU의 인터럽트 방식을 흉내 낸 거라는 사실, 흥미롭지 않나요?
Node.js가 "싱글 스레드인데 어떻게 빠르냐"고 물어보는 사람이 많습니다. 답은 인터럽트 방식의 Event Loop입니다.
// Node.js 파일 읽기
const fs = require('fs');
console.log('1. 파일 읽기 시작');
fs.readFile('/large-file.txt', (err, data) => {
console.log('3. 파일 다 읽음!');
});
console.log('2. 다른 일 함');
// 출력 순서: 1 → 2 → 3
fs.readFile()이 호출되면:
이게 가능한 이유는 OS의 비동기 I/O가 인터럽트 기반이기 때문입니다.
저는 백엔드 API 서버를 만들 때 이 개념을 이해하고 나서 "왜 동기 함수를 쓰면 서버가 느려지는지" 확실히 와닿았습니다. 동기 함수는 폴링처럼 CPU가 기다리고 있어야 하지만, 비동기 함수는 인터럽트처럼 알림을 받으니까요.
실제로 여러분의 시스템에서 어떤 인터럽트가 발생하는지 볼 수 있습니다.
# 실시간으로 인터럽트 모니터링
watch -n 1 cat /proc/interrupts
타이머, 네트워크, 디스크, 키보드 인터럽트가 초당 수백~수천 번 발생하는 걸 볼 수 있습니다. 이게 바로 "컴퓨터가 반응하는 비결"입니다.
인터럽트 핸들러가 너무 오래 걸리면 시스템이 멈춥니다. 그래서 리눅스는 처리를 두 단계로 나눕니다.
Top Half (전반부):
Bottom Half (후반부):
이렇게 함으로써 CPU는 응급실 의사처럼 "일단 살려놓고(Top Half), 수술은 나중에(Bottom Half)" 처리합니다.
만약 네트워크 카드가 고장나서 1초에 10만 번 인터럽트를 보내면? CPU는 하루 종일 "딩동!" 소리만 듣다가 Livelock 상태에 빠집니다. (죽지는 않았는데 일도 못함). 최신 OS는 이를 감지하면 해당 IRQ 라인을 잠시 차단(Masking)하거나, 폴링 모드로 전환(NAPI)해서 시스템을 보호합니다.
| 구분 | 폴링 (Polling) | 인터럽트 (Interrupt) |
|---|---|---|
| 비유 | "도착했나요?" 계속 묻기 | "도착하면 벨 눌러주세요" |
| 주체 | CPU가 직접 확인 | 장치가 CPU를 호출 |
| 효율성 | 기다리느라 낭비 심함 | 매우 효율적 (할 일 하다가 반응) |
| 구현 | while 문 반복 | 하드웨어/소프트웨어 신호 |
결국 제가 이해한 인터럽트는 이겁니다. "중요한 일이 생기면 저를 흔들어 깨우세요. 그 전엔 제 할 일을 하고 있겠습니다."
이게 세상에서 가장 효율적인 일처리 방식입니다. 우리가 당연하게 쓰는 멀티태스킹, 비동기 프로그래밍, 이벤트 기반 아키텍처는 모두 이 철학에서 나왔습니다.
폴링은 불안한 사람입니다. 계속 확인해야 안심이 됩니다. 인터럽트는 여유로운 사람입니다. 필요할 때 연락 오는 걸 믿고 자기 일을 합니다.
여러분도 코드를 짤 때 이 철학을 적용해보세요. 무한 루프로 상태를 확인하는 코드 대신, 이벤트 리스너나 콜백으로 바꿔보세요. 그게 바로 CPU가 수십 년 전부터 해온 방식입니다.
You are playing games in your room and waiting for a pizza delivery. There are two ways to check for arrival.
Option 2 is obviously more efficient. Computers are the same. Interrupt is how the CPU handles peripherals (Keyboard, Mouse, Network).
When I first learned this concept, I thought "Isn't this just like an event listener?" But diving deeper, I realized this isn't just a software pattern—it's a physical signal system operating at the hardware level. That's when it clicked why Node.js emphasizes "event-driven" architecture and why async/await is so efficient.
The CPU is incredibly busy. But what if it had to constantly ask the keyboard "Did you press a key? How about now?"
Early computers actually used this approach. The CPU ran busy-waiting loops, continuously checking device status registers.
// Old way: Polling for keyboard input
while (1) {
if (keyboard_status_register & KEY_PRESSED) {
char key = keyboard_data_register;
process_key(key);
break;
}
}
What's wrong with this?
I initially thought "But CPUs are fast, so it's fine?" My perspective changed when I saw the /proc/interrupts file on a Linux server.
cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 142 0 0 0 IO-APIC 2-edge timer
1: 10 0 0 0 IO-APIC 1-edge i8042
8: 1 0 0 0 IO-APIC 8-edge rtc0
9: 0 0 0 0 IO-APIC 9-fasteoi acpi
12: 156 0 0 0 IO-APIC 12-edge i8042
...
Look at this. My server is constantly being interrupted by timers, keyboards, disks, networks—dozens of devices. If the CPU had to poll all of these? It would accomplish nothing.
Computer designers thought: "What if devices signal the CPU, instead of the CPU asking?"
This is the core of interrupts. Peripherals can send signals to the CPU through physical interrupt request lines (IRQ).
This is why even at 100% CPU usage, the mouse cursor moves instantly. Mouse interrupts have high priority.
When I understood this process, I had an "aha!" moment: "This is the foundation of OS multitasking." The "running multiple programs simultaneously" we take for granted is impossible without interrupts.
When the CPU receives an interrupt signal, it needs to know "Who sent this?" This is where the Interrupt Vector Table (IVT) comes in.
Think about it. When a patient arrives at the ER, "heart attack" goes to cardiology, "fracture" goes to orthopedics. The CPU works the same way.
timer_handler()keyboard_handler()mouse_handler()The IVT is an array of function pointers stored at a specific memory address (e.g., starting at 0x00000000 on x86).
// Conceptual IVT structure (actual implementation is more complex)
void (*interrupt_vector_table[256])(void);
interrupt_vector_table[0] = timer_handler;
interrupt_vector_table[1] = keyboard_handler;
interrupt_vector_table[12] = mouse_handler;
x86 systems can have 256 interrupt vectors. Vectors 0-31 are reserved by the CPU for exceptions, and 32-255 are used by hardware devices or software.
When I was writing kernel drivers, I didn't understand "why IRQ conflicts prevent devices from working." After learning about the IVT, it was clear: if two devices use the same IRQ number, they call the same handler, so only one works. (Modern APIC allows IRQ sharing, though.)
Hospital ERs have a triage system. A heart attack patient gets seen before a cold patient.
CPU interrupts work the same way. Not all interrupts are equal.
If a timer interrupt arrives while handling a disk interrupt? Higher priority preempts lower priority (Preemption).
This becomes vivid when you see it. Run watch -n 1 cat /proc/interrupts on Linux and you'll see timer interrupts firing thousands of times per second. This is the OS's heartbeat.
This answered my question: "Why is the mouse smooth during gaming, but disk copying seems frozen?" Mouse interrupts have high priority and get handled immediately, while disk I/O has lower priority and waits for CPU time.
There are two types of interrupts.
Most hardware interrupts can be ignored by the CPU. For example, when an ATM program is withdrawing money, it should temporarily block keyboard interrupts. Otherwise, pressing another button mid-transaction could corrupt it.
// Critical section: Temporarily disable interrupts
cli(); // Clear Interrupt Flag (disable interrupts)
// Critical operation (e.g., withdraw money)
withdraw_money(account, amount);
sti(); // Set Interrupt Flag (re-enable interrupts)
But some interrupts must never be ignored:
These are like fire alarms. No matter how important the meeting, you evacuate when there's a fire.
I learned how critical this was during a power outage at a server IDC. When the UPS sends an NMI, the OS must "write data to disk and shut down NOW!" Ignoring this means data loss.
It's not just mice that cause interrupts.
I was shocked to learn "system calls are a type of interrupt."
// Reading a file (system call)
int fd = open("/etc/passwd", O_RDONLY); // INT 0x80 (x86) fires here!
Calling open() internally triggers a software interrupt. The CPU then switches from User Mode to Kernel Mode and executes the OS's file system code.
This answered "why user programs can't directly access hardware." All hardware access is intercepted by the OS via interrupts to check permissions.
When discussing interrupts, DMA is unavoidable.
In the old days, reading data from disk worked like this:
The problem: "Reading byte-by-byte wastes CPU time."
Enter the DMA (Direct Memory Access) controller:
// Without DMA
for (int i = 0; i < 1024; i++) {
memory[i] = disk_read_byte(); // CPU copies each byte
}
// With DMA
dma_start(disk_address, memory_address, 1024); // Delegate to DMA controller
// CPU does other work
wait_for_interrupt(); // Notified via interrupt when complete
I wondered "why EBS volume performance varies by EC2 instance type." Turns out it's related to DMA channel count. High-performance instances have more DMA channels for simultaneous fast disk reads.
This interrupt concept is the ancestor of Event-driven Programming.
JavaScript's onClick, onLoad are basically the software implementation of the interrupt philosophy.
// ❌ Polling
while(true) {
if (button.isClicked()) {
doSomething();
}
} // Cycles wasted, computer freezes
// ✅ Interrupt (Event Listener)
button.addEventListener('click', () => {
doSomething(); // Only runs when clicked (0% CPU waste)
});
Isn't it fascinating that your addEventListener is actually mimicking the CPU's interrupt mechanism?
People often ask "How is Node.js fast with a single thread?" The answer: Interrupt-style Event Loop.
// Node.js file reading
const fs = require('fs');
console.log('1. Start reading file');
fs.readFile('/large-file.txt', (err, data) => {
console.log('3. File read complete!');
});
console.log('2. Do other stuff');
// Output order: 1 → 2 → 3
When fs.readFile() is called:
This works because OS's async I/O is interrupt-based.
After understanding this, I clearly saw "why synchronous functions slow down servers." Synchronous functions are like polling—the CPU waits. Asynchronous functions are like interrupts—they get notified.
You can actually see what interrupts are happening on your system.
# Real-time interrupt monitoring
watch -n 1 cat /proc/interrupts
You'll see timer, network, disk, and keyboard interrupts firing hundreds to thousands of times per second. This is "the secret to computer responsiveness."
If an interrupt handler takes too long, the system freezes. So Linux splits the work into two parts.
Top Half:
Bottom Half:
This allows the CPU to act like an ER doctor: "Stabilize first (Top Half), surgery later (Bottom Half)."
What if a broken network card fires 100,000 interrupts per second? The CPU spends all day answering the doorbell and enters a Livelock state. (Alive but useless). Modern OSs detect this and either Mask the IRQ line or switch to Polling mode (NAPI) to protect the system.