Second Edition coming soon. Now with AI: Machine Learning, Deep Learning & LLMs Get notified →

How interrupt handlers work

Typing on a computer is something we totally take for granted. Press a key and the letter appears instantly on screen, no matter what else the system is doing. In fact, it’s so normal that a laggy input is a sure sign of problems. Making this happen seamlessly requires an elegant interplay between software and hardware known as an interrupt.

When you press a key on your keyboard, your CPU literally stops what it’s doing. It saves everything it was working on, jumps to completely different code, handles your keypress, and then resumes exactly where it left off.

What is an Interrupt?

An interrupt is a signal that demands the CPU’s immediate attention. When one arrives, the processor suspends its current work, handles whatever triggered the interrupt, then resumes as if nothing happened.

There are two main flavours. Hardware interrupts come from physical devices such as keyboards, network cards, or disk controllers. They’re the hardware shouting “hey! Something happened that you need to deal with!” These are fundamentally asynchronous because they can arrive at any point in execution, completely outside normal program flow.

Software interrupts are triggered by the processor itself when something prevents it from continuing. For example, attempting to divide by zero is mathematically impossible. When it finds itself being asked to divide by zero, the processor can’t proceed, so it raises an interrupt to report the error. These are synchronous because they occur within normal program flow. You’ll probably know them by the name exceptions.

If you’ve read about operating system kernels, you’ll recognise that system calls use this same interrupt mechanism to switch from user mode to kernel mode. Same underlying hardware feature, different uses.

Why Do We Need Interrupts?

Before digging into how interrupt handlers work, consider the alternative: polling. Instead of waiting for devices to signal when they need attention, the CPU could just constantly check each device. “Got any data for me? No? How about now? Now?”

This works, but it’s wasteful. The CPU spends enormous time asking devices that have nothing to say. It’s like standing by your front door all day asking “is anyone there?” instead of installing a doorbell.

Interrupts flip this around. The CPU chugs along doing useful work until something calls its attention. A network card sits quietly until a packet arrives, then interrupts the processor: “data’s here!” The processor handles it and goes back to whatever it was doing. No wasted cycles asking about data that hasn’t yet arrived.

This is why your computer feels responsive even when it’s doing intensive work. Interrupts ensure important events (like your keystrokes) get handled promptly, regardless of what else is running.

The Interrupt Handling Process

When an interrupt arrives, a carefully choreographed sequence unfolds.

First, a device asserts its interrupt line. This is a physical electrical signal that travels to the processor. The CPU finishes its current instruction (it doesn’t abandon work literally mid-instruction) and acknowledges the interrupt.

Next, the processor saves its current state. It pushes register contents, the program counter, and various flags onto the stack. This is absolutely crucial because we need to restore everything to exactly how it was when we return to the interrupted code.

Now the processor needs to figure out what to do. Every interrupt is assigned a number, and this number indexes into the Interrupt Vector Table (IVT) or Interrupt Descriptor Table (IDT). Those are fancy names for an array of function pointers that the operating system sets up during boot time. Entry 0 might point to the divide-by-zero handler while entry 14 points to the page fault handler. Entries 32 and above typically handle hardware device interrupts.

Interrupt process flowchart showing interrupt request, save state, handle interrupt, restore state The interrupt handling process. Source: Wikimedia Commons, CC BY-SA 4.0.

The processor fetches the handler’s address from the table and jumps to it. This handler code, often called an Interrupt Service Routine (ISR), actually deals with the interrupt. For a keyboard interrupt, it will read the scancode from the keyboard controller and add the corresponding character to a buffer.

When the handler finishes, it executes a special return-from-interrupt instruction (iret on x86). This pops the saved state back into the registers and resumes the original code exactly where it left off. The interrupted program never knows it was interrupted.

The Constraints of Handler Code

Writing an interrupt handler isn’t like writing normal code. Handlers run in a peculiar context with strict constraints.

Handlers must be fast. While a handler is running, the interrupted code is frozen. Take too long and the system becomes unresponsive. Worse, other interrupts might be disabled while the current one is being handled, meaning the system could miss important events. If you’ve ever noticed your phone run slow and miss keypresses as you furiously type out a text message, this might be what’s happening.

Handlers cannot sleep or block. Normal code can wait for a resource to become available such as reading a file from disk. Handlers can’t because there’s no “normal execution” to return to while waiting. The handler is the execution context.

Handlers cannot cause page faults. If the handler’s code isn’t in physical memory, the processor would try to handle the page fault via an interrupt… but we’re already in an interrupt handler. This means nested interrupts at best, system crashes at worst. The kernel dodges this by ensuring that interrupt handler code is always resident in memory.

Handlers have limited stack space. They typically run on a special interrupt stack that’s smaller than regular stacks. Deep recursion or large local variables can overflow it.

These constraints mean handlers should do the absolute minimum necessary. To solve these issues, modern operating systems have developed a neat solution.

Top Half and Bottom Half

Real interrupt handlers split their work into two parts. The top half (or “hard” interrupt handler) runs immediately when the interrupt arrives. It does only what’s strictly necessary: acknowledge the interrupt to the hardware, grab data that would be lost otherwise, and schedule further processing. Then it returns immediately.

The bottom half (or “soft” interrupt handler) does the actual work later, in a context where sleeping and blocking are permitted. It processes the data, wakes up waiting processes, handles all the complex stuff the top half couldn’t do safely.

Tracing through our keyboard interrupt example:

  1. You press a key. The keyboard controller sends an interrupt to the CPU.
  2. The CPU saves state, looks up the keyboard handler, and jumps to it.
  3. The top half handler reads the scancode from the keyboard’s I/O port (must happen now or the data is lost), adds it to a small buffer, and schedules the bottom half.
  4. The handler returns. The interrupted program resumes.
  5. Later, when the kernel has a moment, the bottom half runs. It translates the scancode into a character, updates the terminal’s input buffer, and wakes any processes waiting for keyboard input.
  6. Eventually, your shell reads from its input buffer and sees your keypress.

Linux implements bottom halves through mechanisms called softirqs, tasklets, and workqueues, each with different tradeoffs between latency and flexibility. The details vary, but the principle is universal: do as little as possible in the interrupt context and defer everything else.

Interrupt Priorities and Nesting

Not all interrupts are equally urgent. A machine check error (your hardware is melting) should probably take priority over a keyboard press. Most systems implement interrupt priorities, where higher-priority interrupts can interrupt lower-priority handlers.

Some interrupts are so critical they cannot be ignored under any circumstances. The Non-Maskable Interrupt (NMI) handles catastrophic hardware failures. When an NMI arrives, the processor handles it immediately, regardless of what else is happening. By definition there is no way to disable or “mask” it.

For less critical interrupts, the processor can temporarily disable further interrupts while handling the current one. This is interrupt masking. It prevents unbounded nested interrupts but means interrupts can be delayed. Finding the right balance between responsiveness and complexity is one of the tricky aspects of OS design.

Interrupts, Polling, and DMA

Interrupts beat polling for infrequent events. But there’s a third option for bulk data transfers: Direct Memory Access (DMA).

It might occur to you that it seems a bit silly to interrupt the CPU just so that it can read the scancode from the keyboard hardware and put it into a buffer. Why can’t the hardware do that directly? It can!

With DMA, a device can transfer data directly to system memory without involving the CPU at all. The processor sets up the transfer (telling the DMA controller where to put the data) and then goes back to its own work. When the transfer completes, the device raises a single interrupt to say “all done!”

You wouldn’t actually bother doing this for a keyboard, which transfers a tiny amount of data at the cripplingly slow speed of human fingers. But for network cards rapidly receiving many packets or disk controllers reading big files, DMA is far more efficient than either polling or handling repeated interrupts. The CPU does a tiny amount of setup, then gets a single interrupt when potentially megabytes of data have arrived.

Interrupts are yet another example of how so much of operating system implementations involve software and hardware working together in interlocking, mutually dependent pieces. The architecture provides the mechanisms (interrupts, DMA), and the operating system uses them cleverly to maximise efficiency.

The Connection to Concurrency

Interrupt handlers are a form of concurrency. From the perspective of your CPU speedily crunching through a list of instructions, your keyboard presses are like meteors falling from the sky at random points. An interrupt can arrive at any time, including between any two instructions. This creates race conditions that can be incredibly subtle.

Imagine your code is updating a data structure when an interrupt arrives. The interrupt handler reads that same data structure, seeing it in a half-updated, inconsistent state. This is technically known as “utter chaos”.

This is why kernel code is peppered with mechanisms to disable interrupts around critical sections. It’s also why device drivers are notoriously difficult to write correctly. The handler could interrupt any code, including other handlers. Getting synchronisation right requires the kind of careful reasoning we explore in concurrent programming.

I’ll be honest. You’re unlikely to ever write a device driver or an interrupt handler yourself. I just think they’re really neat. But next time you feel your device becoming less responsive, give it a little breather and ease up on the keyboard presses.

Want to learn more?

This article is a taste of what's in The Computer Science Book. 250 pages covering the fundamentals every developer should know.

Get the book - $29

PDF, ePub & Kindle. Free v2 upgrade included.

or

Get a free chapter first

Enter your email and I'll send you Chapter 1 (Theory of Computation) as a PDF.

V2 Be first to know when the Second Edition launches with three new AI chapters: Machine Learning, Deep Learning, and LLMs.

No spam. Unsubscribe anytime.