Interrupt Handling Overview
From hardware signal to kernel handler: the complete path
What happens when hardware sends an interrupt
On x86, the interrupt flow:
Hardware device asserts IRQ line
↓
Interrupt Controller (APIC/PIC)
Identifies interrupt source, routes to CPU
↓
CPU finishes current instruction
Saves: RIP, RFLAGS, RSP, CS, SS to kernel stack
Clears IF (disables hardware interrupts)
Looks up handler in IDT (Interrupt Descriptor Table)
↓
common_interrupt() (arch/x86/kernel/irq.c)
irq_enter_rcu() ← marks hardirq context
handle_irq() ← calls the high-level handler
irq_exit_rcu() ← exits hardirq context, runs pending softirqs
↓
irq_exit_rcu() checks if softirqs are pending
If yes: invoke_softirq() → __do_softirq()
The IDT and interrupt vectors
The x86 Interrupt Descriptor Table (IDT) has 256 entries, each pointing to a handler:
Vectors 0-31: CPU exceptions (divide by zero, page fault, etc.)
Vectors 32-127: Hardware IRQs (mapped by APIC)
Vector 128: System call (int 0x80, legacy)
Vectors 129-255: Other IPIs, local APIC, MSI
Each IDT entry specifies privilege level, segment, and handler address. When an interrupt fires, the CPU indexes into the IDT to find the handler.
Interrupt entry: irq_enter and irq_exit
The kernel tracks whether it's in interrupt context with per-CPU counters in preempt_count:
/* include/linux/preempt.h */
#define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS)
#define HARDIRQ_SHIFT (SOFTIRQ_SHIFT + SOFTIRQ_BITS)
/* preempt_count layout:
bits 0-7: preempt count
bits 8-15: softirq count
bits 16-19: hardirq count
bit 20+: NMI count
*/
#define in_hardirq() (hardirq_count()) /* in hardirq handler */
#define in_softirq() (softirq_count()) /* in softirq handler */
#define in_interrupt() (irq_count()) /* in any interrupt context */
irq_enter() increments the hardirq count; irq_exit() decrements it and runs pending softirqs if we're returning to process context:
/* kernel/softirq.c (simplified) */
void irq_exit_rcu(void)
{
/* Decrement hardirq nesting count */
preempt_count_sub(HARDIRQ_OFFSET);
/* Run softirqs if we're back in process context */
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
tick_irq_exit();
/* Note: rcu_irq_exit() and trace_hardirq_exit() are NOT called here —
* the _rcu suffix means the caller manages RCU idle-state transitions */
}
The irq_desc lookup
From the interrupt vector, the kernel finds the irq_desc (interrupt descriptor):
/* arch/x86/kernel/irq.c (simplified) */
__visible void __irq_entry common_interrupt(struct pt_regs *regs, u64 vector)
{
struct irq_desc *desc;
irq_enter_rcu();
/* Convert CPU vector → Linux IRQ number → irq_desc */
desc = __this_cpu_read(vector_irq[vector]);
if (likely(!IS_ERR_OR_NULL(desc)))
handle_irq(desc, regs);
irq_exit_rcu();
}
handle_irq(desc, regs) doesn't run one universal handler — it calls whichever flow handler is stored in desc->handle_irq for this specific interrupt (handle_level_irq, handle_edge_irq, handle_fasteoi_irq, and a handful of others). That indirection exists because of a design that didn't scale: the original Linux interrupt core used a single __do_IRQ() "super-handler" that tried to cope with every controller's quirks — level-triggered, edge-triggered, per-CPU, fast-EOI — inside one function, dispatching internally via hw_interrupt_type callback structs. As more interrupt controller types accumulated across architectures, that produced real duplication: x86 alone needed both an ioapic_level_irq and an ioapic_edge_irq variant that shared most of their low-level detail but differed in flow control. Analysis across architectures found the same handful of flow categories — level, edge, simple, fast-EOI — recurring everywhere, which is what made a single shared set of flow handlers usable by every architecture instead of each reinventing its own (Documentation/core-api/genericirq.rst).
Russell King had already identified the real categories — level, edge, simple, and (later) fast-EOI — while building ARM's interrupt handling for the 2.5/2.6 development series. Thomas Gleixner generalized that split into the current irq_desc/flow-handler/irq_chip architecture across all architectures in 2005–2006 (kernel/irq/handle.c's own header credits "Copyright (C) 2005-2006, Thomas Gleixner, Russell King" for that generalization, alongside the older "Copyright (C) 1992, 1998-2006 Linus Torvalds, Ingo Molnar" line for the file's original pre-genirq history — see IRQ Descriptor and irq_chip for the resulting structures in depth). The payoff of separating "how does this interrupt's lifecycle flow" (the flow handler) from "how do I talk to this specific piece of hardware" (the irq_chip ops) is exactly what makes common_interrupt() above so short: it doesn't need to know anything about IOAPIC, GIC, or MSI at all. That knowledge lives entirely behind desc->handle_irq and desc->irq_data.chip, which is also what makes it possible for one common_interrupt() to serve every interrupt-controller type a given architecture supports.
Interrupt context restrictions
Code running in hardirq context must follow strict rules:
/* DON'T: sleep in interrupt context */
irqreturn_t my_handler(int irq, void *dev)
{
msleep(1); /* BUG: may sleep */
kmalloc(sz, GFP_KERNEL); /* BUG: may sleep */
mutex_lock(&m); /* BUG: may sleep */
return IRQ_HANDLED;
}
/* DO: use non-sleeping alternatives */
irqreturn_t my_handler(int irq, void *dev)
{
kmalloc(sz, GFP_ATOMIC); /* never sleeps */
spin_lock(&s); /* doesn't sleep */
/* ... */
spin_unlock(&s);
return IRQ_HANDLED;
}
Hardware interrupts are disabled on the current CPU for the duration of the hardirq handler (the CPU cleared IF on entry). Interrupts on other CPUs are not affected.
/proc/interrupts
cat /proc/interrupts
# CPU0 CPU1 CPU2 CPU3
# 0: 21 0 0 0 IR-IO-APIC 2-edge timer
# 1: 2 0 0 1 IR-IO-APIC 1-edge i8042
# 16: 0 0 0 0 IR-IO-APIC 16-level uhci_hcd:usb3
# 24: 0 0 0 0 IR-PCI-MSI 524288-edge xhci_hcd
# LOC: 198432 197654 196543 198123 Local timer interrupts
# SPU: 0 0 0 0 Spurious interrupts
# PMI: 0 0 0 0 Performance monitoring interrupts
# RES: 1234 1567 1234 1345 Rescheduling interrupts
# CAL: 234 234 234 234 Function call interrupts
- First column: IRQ number (or name for special interrupts)
- CPU columns: per-CPU interrupt count since boot
- Controller: interrupt controller and routing
- Name: driver name from
request_irq()
# Watch interrupt rates
watch -d -n 1 'cat /proc/interrupts'
# IRQs per second
awk 'NR>1 {for(i=2;i<=NF-2;i++) if($i+0>0) {print $0; next}}' /proc/interrupts
IRQ affinity
Each IRQ can be steered to specific CPUs:
# Show IRQ affinity (bitmask)
cat /proc/irq/24/smp_affinity # hex bitmask
cat /proc/irq/24/smp_affinity_list # human-readable CPU list
# Pin IRQ 24 to CPUs 0-3
echo "f" > /proc/irq/24/smp_affinity # hex f = CPUs 0-3
echo "0-3" > /proc/irq/24/smp_affinity_list
# irqbalance daemon automatically balances IRQ affinity
systemctl status irqbalance
Further reading
- IRQ Descriptor and irq_chip — The irq_desc data structures
- request_irq and free_irq — Registering handlers
- Softirqs — What irq_exit runs when pending
Documentation/core-api/genericirq.rst— Generic IRQ subsystem docs