From doorbell to address
In guide 2 you met the three ways to talk to a device, and you saw why interrupt-driven I/O beats polling: instead of the CPU standing at the door asking "are you done yet?" a thousand times, the device rings a doorbell when it is actually finished. We called that doorbell an interrupt and left it there. But a doorbell is useless if you do not know which door it came from and what to do about it. A real house has many doors — the keyboard, the disk, the network card, the timer — and they can all ring. This guide opens up the doorbell wiring and shows you exactly how the CPU turns a ring into the right action.
Here is the central idea, and everything else hangs off it. Every interrupt carries a small number called its interrupt number or vector — think of it as the door's apartment number. When the keyboard's controller raises an interrupt, it does not just shout "someone's here!"; it effectively says "this is interrupt number 33." The CPU takes that number and uses it as an index into a lookup table the kernel built at boot. That table is the heart of the whole mechanism: given a vector number, it tells the CPU the exact memory address of the code that handles that device. No searching, no guessing — one number, one table lookup, one address. The doorbell does not just ring; it ring tells you which room to run to.
The vector table: a directory of handlers
That lookup table has a real name. On x86-style hardware it is the interrupt descriptor table, usually shortened to IDT; the generic term is the interrupt vector table. Picture it as the building manager's directory taped inside the front desk: row 14 means "page fault, run this routine," row 33 means "keyboard, run that routine," and so on. The kernel fills this directory in during the boot process — long before any device is allowed to ring — and then points a special CPU register at it so the hardware knows where the directory lives. From then on the lookup is pure hardware: the CPU does the indexing itself, in nanoseconds, with no software involved in finding the address.
interrupt descriptor table (one row per vector)
vector | source | handler address
-------+---------------------+----------------
0 | divide-by-zero | 0x...A100
14 | page fault | 0x...B240
32 | timer | 0x...C080
33 | keyboard controller | 0x...C300 <-- key pressed
46 | disk controller | 0x...C7F0
CPU sees vector 33 -> jump to 0x...C300 (the keyboard handler)It is worth pausing on why a table beats the obvious alternative. You could imagine the CPU, on every interrupt, running a long if-else chain — "is it the keyboard? no. is it the disk? no..." — but that would make slow devices artificially slow and the cost would grow with the number of devices. A table makes every lookup cost the same, no matter how many devices you have: it is the difference between flipping to the right page of an index versus reading a book cover to cover to find one fact. This is the same instinct you saw with the page table and the hardware doing translation — when the hardware can do a direct lookup, you let it.
What actually happens when the bell rings
Let us trace a single keystroke, step by step, the way you would trace a page fault on paper. Suppose your program is busy adding numbers in a loop when you press the 'k' key. The keyboard controller latches the scan code and raises interrupt 33. From the CPU's point of view, an interrupt is a forced detour: it must drop what it is doing, run the right handler, and then return to exactly where it left off as if nothing happened. That "as if nothing happened" is the hard part, and it is why the steps below are so careful about saving state.
- Finish the current instruction. The CPU does not stop mid-instruction; it completes the add it was on so the machine is left in a clean, well-defined state. Only then does it check for a pending interrupt.
- Save just enough state. The hardware pushes the program counter (where to come back to) and the status flags onto a stack, then switches the CPU into kernel mode. This is a tiny, fast save — not a full context switch of every register yet.
- Look up the vector. The CPU takes the number 33, indexes into the interrupt descriptor table, reads out the handler's address, and jumps to it. The keyboard handler is now running.
- Service the device. The handler reads the scan code out of the controller's data register, stores it in a kernel buffer, and tells the controller "got it" so the controller can lower the interrupt line and is free to report the next key.
- Return from interrupt. A special instruction pops the saved program counter and flags back off the stack, switches the CPU back to the original mode, and execution resumes inside your loop at the very next instruction — your program never even noticed.
The two-part trick: top half and bottom half
There is a deep tension hiding in that trace. While a handler runs, further interrupts are usually blocked so the handler is not itself interrupted mid-job. That means the handler must be fast — every microsecond it spends is a microsecond the doorbell cannot ring for anyone else. But the work a device needs can be heavy: decoding a network packet, copying a disk block into the right place, waking up the process that was waiting for the data. You cannot do heavy work and stay fast at the same time. The classic solution is to split the interrupt service routine in two, the famous top half and bottom half.
Think of a busy restaurant. When a dish is ready, the kitchen rings a bell. The waiter who answers does the bare minimum at the bell — grab the plate, note which table it is for — and immediately gets out of the way so the bell is free for the next dish. Carrying the plate across the room, refilling the water, chatting with the table: all of that happens after, when there is breathing room. The top half is the waiter at the bell: it runs right now, with interrupts disabled, and does only the urgent, must-not-wait work — acknowledge the device, pull the data out of the controller before it is overwritten, and schedule the rest for later. Then it returns fast.
The bottom half is everything the waiter does afterward — the heavy, less-urgent processing. The kernel runs it a moment later, with interrupts re-enabled, so while it grinds through a network packet the doorbell is free to ring again. Different systems give the bottom half different names (softirqs, tasklets, work queues on Linux; deferred procedure calls on Windows), but the idea is identical everywhere: do the panic-now part instantly, defer the can-wait part. This split is the single most important reason a busy server can field hundreds of thousands of interrupts a second without choking — the door is almost never blocked for long.
Honest corners: priorities, sharing, and what an interrupt costs
Real machines have many devices and only a few interrupt lines, so two things complicate the tidy picture above. First, interrupts have priority levels: a network card finishing should not be allowed to delay the system timer that keeps your clock honest. The hardware lets a high-priority interrupt preempt a lower-priority handler, so urgent events jump the queue — the same instinct as priority scheduling, applied to hardware events. Second, several devices may share one interrupt line, so when the line goes high the kernel cannot tell from the line alone who rang. Its handler must politely ask each device on that line "was it you?" until it finds the culprit — a tiny dose of polling living inside the interrupt system.
Now the honest accounting, because guide 2 sold you interrupts as the cure for polling's waste, and that is true but not free. Every interrupt costs something: the forced detour, the state save and restore, the cache going cold, and the jump in and out of kernel mode. For a device that fires constantly — a 10-gigabit network card under load can raise an interrupt for nearly every packet — these costs pile up and the CPU can spend more time answering doorbells than doing work, a real condition called an interrupt storm. The fix in practice is to fight fire with a little polling: under heavy load high-end drivers switch to polling mode and let interrupts coalesce, so one bell answers for many packets. The lesson from guide 2 stands but with a footnote: interrupts win when events are occasional, and polling can win again when they are relentless.
One last connection back to guide 2, to keep the whole rung coherent. DMA does not abolish interrupts — it makes each one count for far more. Without DMA the CPU would take an interrupt for every tiny chunk of a transfer; with DMA the controller moves an entire block of data on its own and raises just one interrupt at the very end, saying "the whole block is in memory, come deal with it." So DMA and interrupts are partners, not rivals: DMA does the bulk carrying, and a single interrupt is the bell that tells the kernel the carrying is done. With the vector, the handler, and the top/bottom split now in hand, you are ready for the next guide, which steps up a level to the device driver — the code that knows what each specific device's bytes actually mean.