A CPU running a program has no inherent way of knowing that a keyboard key was just pressed, a network packet just arrived, or a disk transfer just completed — unless it stops what it’s doing to check, constantly, which would be enormously wasteful (this “constantly checking” approach is called polling, and it’s precisely the inefficient pattern that DMA and interrupts together were designed to eliminate for I/O). Interrupts are the mechanism that lets hardware events reach into the CPU’s normal instruction flow and say, effectively, “stop what you’re doing, something important just happened, deal with it now.” This article covers how interrupts and exceptions work, how CPUs prioritize and route them, and why precise, well-managed interrupt handling is foundational to virtually every real-time responsive behavior in modern computing.
Interrupts vs. Exceptions vs. Traps: Getting the Terminology Straight
These terms are sometimes used loosely, but there’s a meaningful distinction worth establishing:
- Interrupts (hardware interrupts): Asynchronous events generated by external hardware — a keyboard press, a completed DMA transfer, a network packet arrival, a timer firing. They can occur at essentially any point during program execution, unrelated to what instruction is currently running.
- Exceptions: Synchronous events generated by the CPU itself as a direct result of executing a particular instruction — a divide-by-zero error, an invalid opcode, a page fault (accessing memory not currently mapped), or a general protection fault. These are tied to a specific instruction and are reproducible if you re-run the same code under the same conditions.
- Traps (software interrupts): Deliberately triggered by software, typically via a special instruction, most commonly used to implement system calls — a user-mode program’s controlled, deliberate request to transition into the operating system kernel for a privileged operation.
| Type | Trigger Source | Synchronous or Asynchronous | Example |
|---|---|---|---|
| Hardware interrupt | External device | Asynchronous | Timer, keyboard, network card, DMA completion |
| Exception | CPU itself, during instruction execution | Synchronous | Divide by zero, page fault, illegal instruction |
| Trap / software interrupt | Deliberate software instruction | Synchronous | System call (e.g., syscall/int 0x80) |
Despite these distinctions, all three ultimately use very similar underlying hardware and software mechanisms to redirect CPU execution to a handler routine, which is why they’re often discussed together under the general umbrella of “interrupt handling.”
The Basic Interrupt Handling Flow
When an interrupt occurs, the CPU follows a fairly standardized sequence:
- Detect the interrupt signal. The CPU checks for pending interrupts, typically at defined points in its instruction cycle (commonly at instruction boundaries, so an interrupt doesn’t have to awkwardly interrupt a partially-completed instruction).
- Save current execution state. The CPU pushes critical state — most importantly the program counter (so it knows where to resume) and processor status flags — onto a stack or dedicated save area, so normal execution can be precisely resumed later.
- Identify the interrupt source and look up its handler. Using a mechanism called an interrupt vector (explained below), the CPU determines which specific handler routine corresponds to this particular interrupt.
- Transfer control to the Interrupt Service Routine (ISR). Execution jumps to the appropriate handler code, often with an accompanying privilege level change (from user mode to kernel/supervisor mode, since interrupt handlers frequently need privileged access).
- Execute the ISR. The handler does whatever work is needed — reading data from a device, updating a data structure, waking up a waiting process, and so on.
- Restore saved state and resume. Once the ISR completes, the CPU restores the previously saved program counter and status flags, and normal program execution resumes exactly where it left off, as if nothing had happened (from the interrupted program’s perspective).
Normal Program Execution
|
| <-- Interrupt signal arrives -->
v
[Save PC, flags, state]
v
[Look up handler via interrupt vector]
v
[Jump to Interrupt Service Routine (ISR)]
v
[ISR executes: service the device/event]
v
[Restore saved state]
v
Resume Normal Program Execution (exactly where it left off)
Interrupt Vectors and the Interrupt Vector Table
Rather than having a single generic handler that has to figure out what caused every interrupt, modern CPUs use an interrupt vector table (IVT), sometimes called the Interrupt Descriptor Table (IDT) in x86 terminology — a data structure, typically stored in memory, that maps each possible interrupt or exception number to the memory address of its specific handler routine.
When an interrupt occurs, the hardware (or the interrupt controller, discussed next) provides an interrupt number, which the CPU uses as an index into this table to directly jump to the correct handler, without needing generic dispatch logic to figure out the cause. This is dramatically faster and cleaner than a single monolithic handler that has to poll every possible device to figure out what happened.
x86 systems, for example, reserve the first 32 vector numbers for CPU-generated exceptions (like divide error, page fault, general protection fault), with higher-numbered vectors available for hardware interrupts and software-triggered interrupts.
Interrupt Controllers: Managing Multiple Sources
In any real system, many different devices can generate interrupts, and the CPU needs a way to manage, prioritize, and route all of them. This is handled by dedicated interrupt controller hardware:
- PIC (Programmable Interrupt Controller): The classic approach (e.g., Intel’s 8259 PIC in early PC-compatible systems), managing a limited number of interrupt lines with fixed or software-configurable priority.
- APIC (Advanced Programmable Interrupt Controller): The modern standard in x86 systems, supporting many more interrupt sources, multiprocessor/multicore interrupt routing (so interrupts can be directed to specific cores), and more sophisticated priority and masking schemes. Consists of a Local APIC per core and an I/O APIC that routes external device interrupts.
- GIC (Generic Interrupt Controller): ARM’s standard interrupt controller architecture, serving an analogous role for ARM-based systems, including sophisticated support for interrupt prioritization and routing across many cores in modern multicore ARM SoCs.
Interrupt Priority and Masking
Not all interrupts are equally urgent, and systems need mechanisms to manage this:
- Priority levels: Interrupt controllers typically support assigning different priority levels to different interrupt sources, so a higher-priority interrupt (say, a critical hardware fault) can preempt the handling of a lower-priority one (say, a routine timer tick), rather than having to wait for it to finish.
- Interrupt masking: The CPU (or specific interrupt lines) can be temporarily “masked” or disabled, preventing certain interrupts from being serviced during critical sections of code where an interruption would be problematic — for example, while the kernel is in the middle of manipulating a critical data structure that must not be touched by a concurrently running handler.
- Non-maskable interrupts (NMI): A special category reserved for the most critical events (like hardware failure detection) that cannot be masked/disabled by software, ensuring they’re always serviced regardless of what else is happening.
Precise vs. Imprecise Interrupts
Recall the concept of precise exceptions from the out-of-order execution article — this connects directly here. In a modern out-of-order superscalar CPU, ensuring that an interrupt or exception can be handled “precisely” (meaning the CPU can cleanly identify a specific point in program order such that everything before it is complete and nothing after it has taken effect) is essential for correctness and for the operating system to be able to resume execution reliably afterward. This is one of the key reasons the reorder buffer and in-order retirement discipline exist — they make it possible to interrupt an aggressively out-of-order-executing CPU at a precise, well-defined point despite all the internal parallelism happening under the hood.
Interrupt Service Routines (ISRs): Design Considerations
Because ISRs run in a special context — often with interrupts disabled, at elevated privilege, and potentially interrupting arbitrary code at an arbitrary point — they’re held to a stricter set of design constraints than ordinary code:
- Keep them short and fast. Long-running ISRs can delay the servicing of other pending interrupts and degrade overall system responsiveness.
- Defer non-urgent work. Many operating systems split interrupt handling into two halves: a minimal, fast “top half” that runs immediately in interrupt context to handle only the most time-critical work (like acknowledging the device and copying data out of a hardware buffer), and a “bottom half” (variously called deferred procedure calls, tasklets, softirqs, or similar depending on the OS) that runs later, in a less restrictive context, to complete the remaining work.
- Avoid blocking operations. ISRs generally cannot safely sleep, wait on locks that might be held by the interrupted code, or perform operations that could themselves trigger further interrupts in problematic ways.
Real-World Applications and Examples
- Timer interrupts: Drive the operating system’s scheduler, giving it a regular opportunity to reconsider which process/thread should run next — this is fundamental to preemptive multitasking.
- I/O completion interrupts: As covered in the DMA article, devices signal transfer completion via interrupts, letting the CPU move on to other work during the transfer and only respond once data is actually ready.
- Page faults: A CPU-generated exception used to implement virtual memory — when a program accesses memory that isn’t currently mapped to physical RAM (perhaps because it’s been swapped to disk, or because of lazy/on-demand memory allocation), the CPU raises a page fault exception, and the OS’s handler decides how to resolve it (load the page from disk, allocate new physical memory, or terminate the process if the access was genuinely invalid).
- Inter-processor interrupts (IPIs): In multicore systems, one core can send an interrupt directly to another core — used for coordination tasks like TLB invalidation broadcasts, cache coherence protocol assistance, or waking up an idle core.
Performance Considerations
- Interrupt latency — the time between an interrupt being raised and its handler actually starting to run — matters enormously for real-time and latency-sensitive systems, and is affected by interrupt masking duration, priority scheme, and how deep/complex any preceding interrupt handling is.
- Interrupt coalescing/batching, briefly mentioned in the DMA article, reduces the overhead of handling extremely frequent interrupts (common in high-speed networking) by grouping multiple events into fewer interrupt deliveries, trading a small amount of latency for significantly reduced CPU overhead.
- Interrupt storms, where a malfunctioning or misconfigured device generates interrupts far more frequently than it should, can severely degrade system performance by consuming CPU time almost entirely in interrupt handling, starving normal program execution.
Advantages of Interrupt-Driven Design
- Eliminates the need for wasteful polling, letting the CPU do useful work instead of constantly checking device status.
- Enables responsive handling of asynchronous, unpredictable events (user input, network traffic, hardware faults).
- Forms the foundation of preemptive multitasking via timer interrupts, letting an OS fairly share CPU time across many processes.
- Works cleanly alongside DMA, letting the CPU be notified only when a transfer genuinely needs attention rather than needing to babysit it.
Limitations and Trade-offs
- Overhead per interrupt. Saving/restoring state and the general dispatch mechanism has a real, non-zero cost, which is why extremely high-frequency events sometimes benefit from coalescing or polling hybrid approaches (some high-performance networking code deliberately switches to polling under heavy sustained load, a technique sometimes called “interrupt mitigation” or exemplified by mechanisms like Linux’s NAPI).
- Complexity of correct handling. Writing correct ISR and interrupt-handling code is notoriously tricky, given the constraints around what can and can’t safely be done in interrupt context, and the potential for subtle race conditions with the interrupted code.
- Interrupt storms and priority inversion are real operational risks that require careful system design to avoid or mitigate.
Common Misconceptions
“Interrupts and exceptions are the same thing.” As covered above, they’re related but distinct — interrupts are asynchronous and externally triggered, while exceptions are synchronous and generated by the CPU itself as a direct consequence of the currently executing instruction.
“Polling is always worse than interrupts.” Not universally true — under extremely high-frequency event conditions, the per-event overhead of interrupt handling can actually exceed the cost of periodic polling, which is exactly why some high-throughput networking and storage code deliberately uses polling or hybrid interrupt/polling schemes under heavy load.
“An interrupt handler runs ‘whenever it wants’ relative to the interrupted program.” In practice, interrupt delivery is tightly constrained by the CPU’s own timing (checked at instruction boundaries) and by whatever masking is currently in effect — it isn’t an arbitrary mid-instruction intrusion, and the whole architecture is built specifically to guarantee clean, resumable transitions.
A Detailed Walkthrough: Handling a Keystroke
To make the abstract interrupt flow more tangible, consider what happens, at a simplified architectural level, when a user presses a key on a keyboard connected to a modern system:
- Hardware event. The keyboard controller detects the keypress and signals an interrupt request to the system’s interrupt controller (an APIC, in a modern x86 system).
- Interrupt controller routing. The interrupt controller, based on its current priority and masking configuration, determines that this interrupt should be delivered now (rather than deferred behind a higher-priority pending interrupt) and signals the appropriate CPU core.
- CPU acknowledges and saves state. At the next available instruction boundary, the CPU recognizes the pending interrupt, saves its current program counter and relevant status flags (so whatever program was running can resume exactly where it left off), and looks up the correct handler address using the interrupt vector table, indexed by the keyboard interrupt’s assigned vector number.
- ISR executes (top half). The keyboard interrupt handler runs, typically doing minimal, fast work: reading the specific key code from the keyboard controller’s hardware buffer, storing it into an operating-system-managed input queue, and acknowledging the interrupt to the controller so further keyboard interrupts can be delivered.
- Deferred work (bottom half), if applicable. More involved processing — like updating on-screen text, triggering application-level event callbacks, or handling more complex input processing logic — is often deferred to run outside the strict, time-critical interrupt context, in a less constrained execution environment.
- State restoration and resumption. Once the immediate interrupt handling completes, the CPU restores the previously saved program counter and flags, and the originally interrupted program resumes exactly as if the interruption had never happened, aside from the small amount of elapsed real time.
This entire sequence typically completes in a matter of microseconds on modern hardware — fast enough that, from a human perspective, keystrokes feel instantaneous, even though a genuinely complex, multi-layered hardware and software process is happening behind every single keypress.
Spurious Interrupts and Interrupt Acknowledgment
A subtlety worth understanding: interrupt controllers and devices generally require explicit acknowledgment from the handling software once an interrupt has been serviced, to signal that the device can safely generate further interrupts and that the interrupt controller can consider the current interrupt fully handled. Failing to properly acknowledge an interrupt can, depending on the specific hardware and interrupt controller design, either cause the same interrupt to be redelivered unnecessarily, or worse, cause the affected interrupt line to become “stuck,” preventing further legitimate interrupts of that type from being delivered at all — a class of bug that can be notoriously difficult to diagnose, since the system may appear to simply stop responding to a particular device with no obvious error message.
Related to this, spurious interrupts — interrupts that fire without a genuinely corresponding, currently-pending hardware event, often due to specific hardware race conditions or edge cases in interrupt controller behavior — are a recognized phenomenon that robust interrupt handler code needs to account for defensively, typically by verifying that a genuine pending condition actually exists before doing substantive work, rather than assuming every interrupt delivery necessarily corresponds to real, actionable device state.
Interrupt Handling in Real-Time Systems
Real-time operating systems (RTOS), used in contexts like industrial control systems, automotive systems, and aerospace applications, place extraordinarily strict requirements on interrupt latency — the maximum time between an interrupt occurring and its handler beginning execution needs to be not just low on average, but bounded and predictable in the worst case, since a real-time system’s correctness often depends on guaranteed timing behavior, not just typically-fast behavior. This drives real-time system design toward careful, deliberate interrupt priority schemes, strict limits on how long interrupts can remain masked during critical sections, and often specialized hardware and operating system designs (dedicated real-time kernels, or real-time extensions layered onto general-purpose operating systems) specifically engineered to provide these hard timing guarantees — a meaningfully different design goal than general-purpose desktop or server systems, which typically optimize for good average-case responsiveness and throughput rather than strict worst-case timing bounds.
Wrapping Up
Interrupts and exception handling form the nervous system of a modern computer — the mechanism that lets the outside world (devices, timers, hardware faults) and the CPU’s own internal error conditions reach into an otherwise self-contained stream of sequential instruction execution and demand attention, precisely and safely. Combined with DMA for efficient data movement and built on the precise-state guarantees that out-of-order CPUs work hard to preserve, interrupt handling is what allows a single processor to feel like it’s doing dozens of things “at once” — responding to your keystrokes, streaming audio, downloading a file, and running your applications — when in reality, it’s just extremely good at knowing when, and how, to stop what it’s doing.