Interrupts and Interrupt Handling
When you press a key, the keyboard doesn’t wait for the CPU to notice. It fires an electrical signal that forces the CPU to stop what it’s doing, handle the keypress, and then resume. That signal is an interrupt — and it’s one of the most fundamental mechanisms in all of computing.
Without interrupts, the OS would have to constantly poll every device to check if anything had happened. Polling wastes CPU cycles. Interrupts let the CPU do useful work until a device actually needs attention.
What Happens During an Interrupt
The sequence is hardware-enforced:
CPU executing process A
│
│ ← keyboard fires interrupt signal
│
▼
CPU finishes current instruction
│
▼
CPU saves current state (registers, instruction pointer, stack pointer)
onto the kernel stack
│
▼
CPU looks up the interrupt number in the Interrupt Descriptor Table (IDT)
│
▼
CPU jumps to the interrupt handler (ISR — Interrupt Service Routine)
│
▼
Handler runs: reads keypress from keyboard controller, queues it
│
▼
Handler executes IRET instruction
│
▼
CPU restores saved state
│
▼
Process A resumes — it never knew anything happened
The entire save-and-restore is done by hardware, not software. The CPU has dedicated circuitry for it.
The Interrupt Descriptor Table
Every interrupt is identified by a number (0–255 on x86). The Interrupt Descriptor Table (IDT) is an array of 256 entries, each pointing to the handler for that interrupt number. The OS sets up the IDT during boot.
IDT (256 entries)
┌────┬────────────────────────────┐
│ 0 │ → Divide-by-zero handler │ ← exception
│ 1 │ → Debug handler │ ← exception
│ 2 │ → NMI handler │ ← non-maskable interrupt
│ .. │ ... │
│ 14 │ → Page fault handler │ ← exception
│ .. │ ... │
│ 32 │ → Timer interrupt handler │ ← hardware IRQ 0
│ 33 │ → Keyboard handler │ ← hardware IRQ 1
│ .. │ ... │
│128 │ → System call handler │ ← software interrupt (Linux)
└────┴────────────────────────────┘
When an interrupt fires, the CPU uses the interrupt number as an index into the IDT, fetches the handler address, and jumps to it.
Three Kinds of Interrupts
Hardware interrupts come from devices: keyboard, disk controller, network card, timer chip. Each device has an IRQ (Interrupt Request) line connected to the CPU. Hardware interrupts are asynchronous — they can arrive at any point during execution.
Exceptions are synchronous — generated by the CPU itself in response to something the currently executing instruction did wrong. Examples:
- Division by zero (IDT entry 0)
- Invalid opcode
- Page fault (IDT entry 14) — when a virtual address has no valid mapping
- General protection fault — when code violates a privilege rule
Some exceptions are faults (the instruction can be retried after the OS fixes the problem — page faults work this way) and some are aborts (unrecoverable — double fault, machine check).
Software interrupts are deliberate. The int n instruction fires interrupt number n. Linux historically used int 0x80 to invoke system calls — user code executed int 0x80, which jumped to the kernel’s system call dispatcher. Modern x86 uses the faster syscall instruction instead.
Top Half and Bottom Half
Interrupt handlers run with interrupts disabled on that CPU core. This is necessary — you can’t be interrupted while handling an interrupt — but it means handler code must be fast. Stalling inside a handler delays every other interrupt.
Linux splits interrupt handling into two parts:
Top half (the actual ISR): runs immediately, with interrupts disabled. Does the bare minimum — acknowledges the hardware, reads data out of device registers, queues work.
Bottom half: deferred work that can run later, with interrupts re-enabled. Linux implements this via softirqs, tasklets, and workqueues, depending on the constraints.
Hardware interrupt fires
│
▼
Top half: ACK device, copy data to kernel buffer
(fast, interrupts disabled)
│
▼
Schedule bottom half
│
▼ (interrupts re-enabled, normal scheduling resumes)
▼
Bottom half: process data, wake up waiting processes
(can sleep, can be preempted)
A network card interrupt handler, for example, copies the packet out of the NIC’s buffer in the top half (before the NIC overwrites it), then processes the packet headers, routes it up the protocol stack, and wakes any blocked recv() calls in the bottom half.
Interrupt Priority and Masking
Not all interrupts are equal. The CPU has a priority level; interrupts below the current priority are masked (ignored) until the priority drops. This prevents a low-priority device interrupt from preempting a higher-priority one.
Some interrupts are non-maskable (NMI). They cannot be disabled. NMIs are reserved for catastrophic hardware conditions — memory errors, watchdog timer expiry, system management events. The CPU always handles them regardless of the current interrupt flag.
The Timer Interrupt
The most important interrupt in the OS is the timer interrupt, fired by the hardware clock at a fixed frequency (historically 100 Hz, now often 250 or 1000 Hz on Linux). Every timer tick, the kernel’s timer ISR runs and:
- Increments the system clock
- Decrements the current process’s remaining timeslice
- If the timeslice hits zero, sets a reschedule flag
This is how preemptive multitasking works. The OS doesn’t politely ask processes to yield — it interrupts them on a fixed schedule and decides whether to run them again. Without the timer interrupt, a process could hold the CPU indefinitely.
Interrupts and System Calls
System calls are a controlled form of interrupt. When user code calls read() or write(), the C library executes syscall (x86-64), which:
- Switches the CPU from user mode (ring 3) to kernel mode (ring 0)
- Jumps to the kernel’s system call entry point
- Dispatches based on the syscall number in
rax
The return path reverses the mode switch. From the process’s perspective, the system call is a function call that returned a value. From the CPU’s perspective, it was a controlled interrupt that briefly elevated privilege.
This is why system calls are expensive relative to ordinary function calls: the mode switch, the argument copying, and the save/restore of registers all add up. The vDSO mechanism avoids this for calls like gettimeofday() — the kernel maps a small piece of code into every process’s address space that can read kernel data structures directly from user mode, without a mode switch.