On this page

Interrupt Subsystem

Coverage: irq_desc/irq_chip/irq_domain → Interrupt handling flow (top/bottom half) → softirq/tasklet/workqueue → threaded IRQ → MSI/MSI-X → APIC/IOMMU interrupt remapping Kernel version: 2.6 ~ 6.x

Overview

The interrupt subsystem is the part of the kernel most tightly coupled with hardware. When a network card receives a packet, a disk completes a DMA operation, or a timer expires, the hardware sends an interrupt signal to the CPU via the interrupt controller (APIC/GIC). The kernel's interrupt subsystem is responsible for: routing interrupts to the correct CPU, dispatching them to the corresponding device driver handler, and deferring time-consuming work to the bottom half.

The core abstractions of the modern interrupt subsystem are three layers:

  • irq_chip: Hardware interrupt controller (IO-APIC, GIC, etc.)
  • irq_domain: Mapping from hardware interrupt numbers to Linux IRQ numbers
  • irq_desc: Descriptor for each Linux IRQ (handler, statistics, affinity)

Hardware Interrupt vs. Linux IRQ

Hardware Interrupt Number (hwirq):
  Interrupt line number inside the interrupt controller
  Different controllers can have the same hwirq

Linux IRQ Number (virq):
  Unified virtual interrupt number in the kernel
  Mapped via irq_domain: virq = irq_domain->map(hwirq)
  The numbers seen in /proc/interrupts are Linux IRQ numbers

irq_desc

// include/linux/irqdesc.h
struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data         irq_data;
    unsigned int __percpu   *kstat_irqs;     // per-CPU interrupt count (/proc/interrupts)
    irq_flow_handler_t      handle_irq;      // High-level handler (handle_fasteoi, ...)
    struct irqaction        *action;         // Linked list of handlers registered by drivers
    unsigned int            depth;           // Disable counter
    unsigned int            istate;          // Internal state
};

irqaction: Handler registered by the driver

// include/linux/interrupt.h
struct irqaction {
    irq_handler_t           handler;         // Handler provided by the driver
    unsigned long           flags;           // IRQF_SHARED, IRQF_ONESHOT, ...
    const char              *name;           // Name displayed in /proc/interrupts
    void                    *dev_id;         // Device identifier (must be unique for shared interrupts)
    struct irqaction        *next;           // Next action for shared interrupts
};

Interrupt Handling Flow

Hardware to Kernel

Hardware Interrupt to Kernel: From Device Pulling Line to Entering handle_irq_desc() ① Device pulls interrupt line → Interrupt Controller (APIC / GIC) ② Interrupt Controller sends interrupt message → CPU (via APIC bus / MSI) ③ CPU Responds to Interrupt · Hardware saves RIP / CS / RFLAGS (x86: push to stack) · Switch to kernel stack (if entering from user mode) · Jump to entry point in IDT (companion of entry_SYSCALL_64) common_interrupt() do_IRQ() handle_irq_desc() Three steps to handle_irq_desc(): Save context → Find entry → Hand over to core dispatcher, then branch based on interrupt trigger type (see next figure).

handle_irq_desc() Path

handle_irq_desc() Path: Branching to corresponding flow handler based on interrupt trigger type handle_irq_desc(desc) generic_handle_irq_desc(desc) desc->handle_irq(desc) High-level flow handler handle_fasteoi(desc) Most common · level-triggered mask_irq(desc) Mask this interrupt handle_irq_event(desc) Traverse action list → action->handler() IRQF_WAKE_THREAD → Wake kernel thread unmask_irq(desc) Restore handle_edge_irq(desc) edge-triggered ack_irq(desc) → handle_irq_event() → … handle_fasteoi is the most common path (level-triggered): Mask interrupt first → Traverse all registered actions and call handlers → Restore; If an action requires IRQF_WAKE_THREAD, wake the corresponding kernel thread to execute the bottom half.

Shared Interrupts

IRQF_SHARED: Multiple devices share the same interrupt line
  → All registered handlers are called in sequence
  → Each handler checks "is this interrupt from my device?"
     (Read device register; if no match → return IRQ_NONE)
  → If all handlers return IRQ_NONE → Spurious interrupt
     + Increment spurious count → If it reaches 100k times → Disable this interrupt

Bottom Half

The interrupt handler only does the most urgent tasks (acknowledge interrupt, mask device, copy data). Time-consuming operations (processing protocol stack, writing back cache) are handed over to the bottom half.

softirq

// kernel/softirq.c
// Predefined softirq types (determined at compile time):
enum {
    HI_SOFTIRQ    = 0,   // High-priority tasklet
    TIMER_SOFTIRQ = 1,   // Timer expiration
    NET_TX_SOFTIRQ = 2,  // Network transmission
    NET_RX_SOFTIRQ = 3,  // Network reception (NAPI)
    BLOCK_SOFTIRQ = 4,   // Block layer
    TASKLET_SOFTIRQ = 6, // Regular tasklet
    RCU_SOFTIRQ   = 9,   // RCU callbacks
    NR_SOFTIRQS
};

// softirq runs in interrupt context (but with interrupts enabled)
// Triggered by irq_exit() check:
//   if (in_interrupt()) return;  // Not in nested hardware interrupt
//   do_softirq() → __do_softirq() → Traverse softirq bitmap

// ksoftirqd: Per-CPU kernel thread
//   When softirq processing is too heavy (MAX_SOFTIRQ_RESTART) → Hand over to ksoftirqd
//   → Yield CPU to user processes

tasklet (Deprecated, replaced by threaded IRQ)

// kernel/softirq.c
// tasklet is based on softirq (HI_SOFTIRQ or TASKLET_SOFTIRQ)
// Tasklets of the same type do not run simultaneously (simplifies synchronization)
// ⚠️ Deprecated in kernels 2022+: New code should use threaded IRQ

struct tasklet_struct {
    void (*func)(unsigned long);
    unsigned long data;
};
// kernel/irq/manage.c
// Each interrupt can have its own kernel thread
// Handler returns IRQ_WAKE_THREAD → Kernel schedules threaded_fn to run

request_threaded_irq(irq, handler, thread_fn, flags, name, dev);
//   handler: Runs in hardware interrupt context (does minimal work)
//   thread_fn: Runs in an independent kernel thread (can sleep, can take mutex)
//
// Advantages:
//   - Reduces interrupt disable time
//   - Bottom half can sleep (take mutex, perform IO)
//   - Can be managed by real-time scheduling policies

Workqueue: Heavier Bottom Half

// kernel/workqueue.c
// Workqueue runs in process context (kernel thread kworker)
// Suitable for: Work that requires sleeping or long execution times

// Main differences from tasklet/softirq:
//   workqueue: Process context, can sleep
//   tasklet/softirq: Interrupt context, cannot sleep

schedule_work(&work);          // System workqueue
queue_work(my_wq, &work);     // Dedicated workqueue

MSI / MSI-X

// kernel/irq/msi.c
// Message Signaled Interrupts
// Device writes directly to a memory address → Interrupt Controller (not via interrupt line)
// Advantages:
//   - No need to share (each MSI has a unique address)
//   - Higher performance (PCIe posted write vs. slow INTx level signal)
//   - MSI-X allows devices to have multiple interrupt vectors (e.g., one per queue for NVMe)

// Enable MSI for PCI device:
pci_alloc_irq_vectors(pdev, 1, 16, PCI_IRQ_MSI);
  → Device allocates 16 MSI vectors
  → Returns Linux IRQ numbers (one per vector)

Interrupt Affinity and Load Balancing

# View interrupt distribution
cat /proc/interrupts

# Set interrupt affinity (which CPU handles which interrupt)
echo 2 > /proc/irq/<N>/smp_affinity  # Bind to CPU 1

# irqbalance daemon automatically balances
#   Based on interrupt counts and CPU load
#   Certain interrupts (e.g., NVMe) are directly bound to NUMA-local CPUs

References and Further Reading

  • Kernel Documentation: Documentation/core-api/irq/, Documentation/PCI/msi-howto.rst
  • LWN: "The design of the interrupt subsystem", "Threaded interrupt handlers"
  • Source Code:
    • kernel/irq/ — Interrupt core (handle, manage, chip, domain, msi)
    • kernel/softirq.c — softirq and tasklet
    • kernel/workqueue.c — workqueue
    • arch/x86/kernel/irq.c — x86 interrupt entry
    • arch/x86/kernel/apic/ — APIC driver

Keywords: irq_desc, irq_chip, irq_domain, softirq, tasklet, threaded IRQ, workqueue, MSI/MSI-X, interrupt affinity