Skip to content

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