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_irq()      (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();
}

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