Interrupt Handling: From Hardware Pin to Your Handler

2026-04-25

You already know how context switches work. Interrupts are why most of them happen. An interrupt is a hardware or software signal that forces the CPU to stop what it's doing, save state, and jump to a handler. Understanding the full path — from electrical pin to your C function — is essential for systems work.

The Interrupt Descriptor Table (IDT). On x86-64, the CPU consults the IDT when an interrupt fires. The IDT holds 256 entries (called gate descriptors), each pointing to a handler address, a code segment selector, and privilege information. The CPU loads the IDT's base address from the IDTR register (set by lidt). Entry 0 is #DE (divide error), entry 14 is #PF (page fault), entries 32–255 are available for device IRQs and software use.

Hardware path. A device (say, your NIC) asserts an interrupt line. On modern systems, this goes through the I/O APIC, which routes it to a specific CPU's Local APIC based on a configurable routing table. The Local APIC signals the core, which — between instructions — checks for pending interrupts. If IF (interrupt flag) is set in RFLAGS, the CPU:

This entire hardware sequence takes roughly 50–100 CPU cycles — about 20–40 ns on a 2.5 GHz core. That's the minimum interrupt latency before your handler code even runs.

Top half vs. bottom half. Linux splits interrupt handling in two. The top half (hardirq) runs with interrupts disabled, does the absolute minimum — acknowledge the device, copy data from a hardware buffer — and schedules a bottom half. Bottom halves (softirqs, tasklets, or workqueues) run with interrupts re-enabled, handling the heavier processing. This keeps interrupt-disabled time short.

Real-world example. When a network packet arrives: the NIC raises IRQ → top half copies the packet descriptor from the ring buffer and calls napi_schedule() → bottom half (NAPI softirq) processes packets in a polling loop, feeding them up the network stack. On a busy 10 Gbps NIC handling 14.8 million packets/sec, you get one interrupt roughly every 67 nanoseconds if uncoalesced — which is why interrupt coalescing exists.

Rule of thumb: If your top-half handler takes more than 1 microsecond, you're holding off other interrupts too long. Defer the work.

You can inspect interrupt counts per CPU in /proc/interrupts and see which cores are handling which devices. Use /proc/irq/<N>/smp_affinity to pin interrupts to specific cores — critical for low-latency workloads where you want to keep a core's cache warm for network processing.

See it in action: Check out How Interrupts Work in Modern Computers by BitLemon to see this theory applied.
Key Takeaway: Interrupts are the bridge between hardware events and software handlers — the CPU hardware pushes minimal state and jumps to your IDT entry, so keep top-half handlers under a microsecond and defer everything else to bottom halves.

All newsletters