Interrupt-Driven I/O and Exceptions

Polling left us with a machine that spends its life asking "ready yet? ready yet?" — a Ferrari idling in a car park. The fix is one of the most beautiful ideas in computer architecture: stop asking, and let the device tap the CPU on the shoulder when it actually has something to say. That tap is an interrupt, and it turns the processor from a nagging pollster into a busy worker who only looks up when the doorbell rings.

The gorgeous part is where the doorbell is wired. Remember the instruction-cycle FSM that loops forever? At the very end of each instruction — just before looping back to FETCH — the control unit adds one extra check: is an interrupt pending and enabled? If not, carry on. If so, the machine detours. And once you have that mechanism, the same hardware handles device interrupts, program errors (exceptions), and deliberate system calls (traps) — three things that turn out to be one thing.

The mechanism, step by step

When the end-of-instruction check finds a pending, enabled interrupt, the hardware performs a tightly choreographed detour. Follow the diagram as it builds:

  1. Save state. The current \text{PC} (where to come back to) and the processor status register \text{PSR} (condition flags, privilege, priority) are pushed onto the supervisor stack. This is the snapshot the machine will restore.
  2. Vector through the table. The interrupting device supplies a small vector number. The hardware uses it to index the interrupt vector table (IVT) — a table of handler addresses — and loads the matching address into the \text{PC}. The machine now begins fetching the handler.
  3. Run the handler (ISR). The interrupt service routine does the real work — reads the keystroke from \text{KBDR}, buffers it, acknowledges the device.
  4. Return. A special RTI (return-from-interrupt) instruction pops the saved \text{PSR} and \text{PC}, and the main program resumes exactly where it was — usually never even knowing it was paused.

Priority and masking: not all interrupts are equal

A disk finishing a transfer and a "the power supply is failing" alert should not be treated the same. So interrupts carry a priority level, and the PSR records the priority the CPU is currently running at. Two rules follow:

This is why the state is saved on a stack rather than a single fixed register: the stack is exactly the data structure that lets interrupts nest and unwind in the right order.

Interrupts, exceptions, and traps: one mechanism, three triggers

Here is the unification. All three bend the instruction stream through the same save-vector-handle-return machinery; they differ only in what sets them off:

KindTriggerTimingExample
Interruptan external deviceasynchronous — any timekeyboard key ready, timer tick
Exception / faultan error in the running instructionsynchronous — caused by an instructiondivide by zero, illegal opcode, page fault
Trap / syscallthe program asks on purposesynchronous — deliberateLC-3 \text{TRAP}, x86 \text{syscall}

A trap is best understood as a software interrupt: the LC-3 \text{TRAP x23} instruction deliberately vectors through a table to an OS routine, exactly like a device interrupt but requested by the program itself. This is how user code safely asks the operating system for a privileged favour (read a key, print a string, halt) without being trusted to do it directly — the trap is the guarded doorway between user and kernel.

Simulate it: the detour, then the payoff

First, watch a single interrupt play out — save, vector through the table, run the handler, RTI. Then compare the cycles polling wastes against the cycles interrupts save on a slow device.

// Part 1: one interrupt, start to finish. const IVT: Record<number, string> = { 0x01: "illegalOpcodeHandler", 0x80: "keyboardISR" }; let PC = 0x3005, PSR = 0x0302; // main program was here, at priority level 3 function deliver(vector: number): void { console.log(`END OF INSTRUCTION: interrupt vector 0x${vector.toString(16)} pending`); console.log(` 1. SAVE push PC=0x${PC.toString(16)}, PSR=0x${PSR.toString(16)} onto supervisor stack`); const handler = IVT[vector]; console.log(` 2. VECTOR IVT[0x${vector.toString(16)}] -> ${handler}; load it into PC`); console.log(` 3. ISR ${handler} runs, services the device`); console.log(` 4. RTI pop PSR, pop PC=0x${PC.toString(16)}; main resumes transparently`); } deliver(0x80); // Part 2: polling vs interrupts on a slow device (bigger gap = slower device). const items = 5; // characters to receive const gap = 1000; // CPU cycles between characters const handler = 25; // cycles to save/vector/run-ISR/RTI per character const pollingIO = items * gap; // CPU spins through every gap: all wasted const interruptIO = items * handler; // CPU only pays the handler per character const total = items * gap; console.log(""); console.log(`Polling: ${pollingIO} cycles on I/O = ${Math.round(100 * pollingIO / total)}% of the time (busy-wait).`); console.log(`Interrupts: ${interruptIO} cycles on I/O = ${(100 * interruptIO / total).toFixed(1)}% of the time.`); console.log(`Interrupts free ${pollingIO - interruptIO} cycles for real work -- a ${Math.round(pollingIO / interruptIO)}x reduction in I/O overhead.`);

Through this exact dance, millions of times a day. You press a key; the keyboard controller raises an interrupt line; at the end of the current instruction the CPU finishes the frame it was rendering, vectors to the OS keyboard handler, which stashes the character in a buffer and returns — all in well under a microsecond. The game never notices the pause. Multiply by every device — network packets, disk completions, the timer that drives multitasking itself — and you see that a modern computer is not really "running your program". It is running a storm of tiny interrupt handlers, with your program stitched into the gaps between them. Interrupts are what make one CPU feel like it is doing a thousand things at once.

On our simple machine the FSM checks for interrupts only at the end of the instruction cycle, so an instruction is never torn in half — the handler always sees a clean, consistent machine state, and RTI can resume perfectly. That "clean boundary" is doing enormous work. On a pipelined machine, though, several instructions are in flight at once, so "where exactly do we stop?" becomes genuinely hard — the machine must retire the right instructions and squash the rest so the handler still sees a tidy boundary. That is the whole problem of precise exceptions, and it is the direct descendant of this humble end-of-cycle check.