---
title: Interrupt Subsystem
url: https://doc.liz6.com/en/linux-kernel/04-interrupts-and-clocks/01-interrupt-subsystem
locale: en
area: linux-kernel
tags:
- linux-kernel
- interrupts-and-clocks
date: 2026-06-30
modified: 2026-07-16
description: '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'
---

# 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

```c
// 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

```c
// 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

<svg viewBox="0 0 720 400" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Hardware Interrupt to Kernel: Three-step process from device pulling the interrupt line to entering handle_irq_desc()">
  <defs>
    <marker id="irq-hw-arrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="400" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Hardware Interrupt to Kernel: From Device Pulling Line to Entering handle_irq_desc()</text>

  <rect x="40" y="46" width="640" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="71" text-anchor="middle" font-size="12.5" font-weight="600" fill="#3730a3">① Device pulls interrupt line → Interrupt Controller (APIC / GIC)</text>

  <line x1="360" y1="86" x2="360" y2="100" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-hw-arrow)"/>

  <rect x="40" y="100" width="640" height="40" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="125" text-anchor="middle" font-size="12.5" font-weight="600" fill="#3730a3">② Interrupt Controller sends interrupt message → CPU (via APIC bus / MSI)</text>

  <line x1="360" y1="140" x2="360" y2="154" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-hw-arrow)"/>

  <rect x="40" y="154" width="640" height="122" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="60" y="176" font-size="12.5" font-weight="700" fill="#3730a3">③ CPU Responds to Interrupt</text>
  <text x="76" y="198" font-size="11.5" fill="#4f46e5">· Hardware saves RIP / CS / RFLAGS (x86: push to stack)</text>
  <text x="76" y="220" font-size="11.5" fill="#4f46e5">· Switch to kernel stack (if entering from user mode)</text>
  <text x="76" y="242" font-size="11.5" fill="#4f46e5">· Jump to entry point in IDT (companion of entry_SYSCALL_64)</text>

  <line x1="360" y1="276" x2="360" y2="292" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-hw-arrow)"/>

  <rect x="40" y="294" width="170" height="40" rx="6" fill="#0d9488"/>
  <text x="125" y="319" text-anchor="middle" font-size="12" font-weight="600" fill="#ffffff">common_interrupt()</text>
  <line x1="210" y1="314" x2="248" y2="314" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-hw-arrow)"/>

  <rect x="250" y="294" width="150" height="40" rx="6" fill="#0d9488"/>
  <text x="325" y="319" text-anchor="middle" font-size="12" font-weight="600" fill="#ffffff">do_IRQ()</text>
  <line x1="400" y1="314" x2="438" y2="314" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-hw-arrow)"/>

  <rect x="440" y="294" width="240" height="40" rx="6" fill="#0d9488"/>
  <text x="560" y="319" text-anchor="middle" font-size="12" font-weight="600" fill="#ffffff">handle_irq_desc()</text>

  <rect x="60" y="352" width="600" height="34" rx="7" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="76" y="373" font-size="12" fill="#115e59">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).</text>
</svg>

### handle_irq_desc() Path

<svg viewBox="0 0 720 420" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="handle_irq_desc branching to generic, fasteoi, and edge flow handlers based on interrupt trigger type">
  <defs>
    <marker id="irq-tree-arrow" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="420" fill="#ffffff"/>
  <text x="360" y="26" text-anchor="middle" font-size="16.5" font-weight="700" fill="#1f2933">handle_irq_desc() Path: Branching to corresponding flow handler based on interrupt trigger type</text>

  <rect x="280" y="44" width="160" height="36" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="360" y="66" text-anchor="middle" font-size="12.5" font-weight="700" fill="#3730a3">handle_irq_desc(desc)</text>

  <line x1="360" y1="80" x2="135" y2="112" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-tree-arrow)"/>
  <line x1="360" y1="80" x2="360" y2="112" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-tree-arrow)"/>
  <line x1="360" y1="80" x2="585" y2="112" stroke="#475569" stroke-width="1.6" marker-end="url(#irq-tree-arrow)"/>

  <!-- Column A: generic -->
  <rect x="40" y="112" width="190" height="32" rx="6" fill="#e2e8f0"/>
  <text x="135" y="132" text-anchor="middle" font-size="10.5" font-weight="600" fill="#334155">generic_handle_irq_desc(desc)</text>
  <line x1="135" y1="144" x2="135" y2="160" stroke="#475569" stroke-width="1.4" marker-end="url(#irq-tree-arrow)"/>
  <rect x="40" y="160" width="190" height="46" rx="6" fill="#e2e8f0"/>
  <text x="135" y="180" text-anchor="middle" font-size="11" font-weight="700" fill="#334155">desc-&gt;handle_irq(desc)</text>
  <text x="135" y="196" text-anchor="middle" font-size="10" fill="#64748b">High-level flow handler</text>

  <!-- Column B: fasteoi (most common, highlighted cyan) -->
  <rect x="250" y="112" width="220" height="44" rx="6" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="360" y="130" text-anchor="middle" font-size="12" font-weight="700" fill="#0f766e">handle_fasteoi(desc)</text>
  <text x="360" y="146" text-anchor="middle" font-size="10" fill="#115e59">Most common · level-triggered</text>
  <line x1="360" y1="156" x2="360" y2="170" stroke="#475569" stroke-width="1.4" marker-end="url(#irq-tree-arrow)"/>
  <rect x="250" y="170" width="220" height="40" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="188" text-anchor="middle" font-size="11.5" font-weight="700" fill="#0f766e">mask_irq(desc)</text>
  <text x="360" y="203" text-anchor="middle" font-size="10" fill="#115e59">Mask this interrupt</text>
  <line x1="360" y1="210" x2="360" y2="224" stroke="#475569" stroke-width="1.4" marker-end="url(#irq-tree-arrow)"/>
  <rect x="250" y="224" width="220" height="58" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="242" text-anchor="middle" font-size="11.5" font-weight="700" fill="#0f766e">handle_irq_event(desc)</text>
  <text x="360" y="258" text-anchor="middle" font-size="10" fill="#115e59">Traverse action list → action-&gt;handler()</text>
  <text x="360" y="272" text-anchor="middle" font-size="10" fill="#115e59">IRQF_WAKE_THREAD → Wake kernel thread</text>
  <line x1="360" y1="282" x2="360" y2="296" stroke="#475569" stroke-width="1.4" marker-end="url(#irq-tree-arrow)"/>
  <rect x="250" y="296" width="220" height="40" rx="6" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="314" text-anchor="middle" font-size="11.5" font-weight="700" fill="#0f766e">unmask_irq(desc)</text>
  <text x="360" y="329" text-anchor="middle" font-size="10" fill="#115e59">Restore</text>

  <!-- Column C: edge -->
  <rect x="490" y="112" width="190" height="44" rx="6" fill="#e2e8f0"/>
  <text x="585" y="130" text-anchor="middle" font-size="11.5" font-weight="700" fill="#334155">handle_edge_irq(desc)</text>
  <text x="585" y="146" text-anchor="middle" font-size="10" fill="#64748b">edge-triggered</text>
  <line x1="585" y1="156" x2="585" y2="170" stroke="#475569" stroke-width="1.4" marker-end="url(#irq-tree-arrow)"/>
  <rect x="490" y="170" width="190" height="46" rx="6" fill="#e2e8f0"/>
  <text x="585" y="190" text-anchor="middle" font-size="10.5" font-weight="600" fill="#334155">ack_irq(desc) →</text>
  <text x="585" y="205" text-anchor="middle" font-size="10.5" font-weight="600" fill="#334155">handle_irq_event() → …</text>

  <rect x="60" y="356" width="600" height="50" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="76" y="376" font-size="12" fill="#115e59">handle_fasteoi is the most common path (level-triggered): Mask interrupt first → Traverse all registered actions and call handlers → Restore;</text>
  <text x="76" y="394" font-size="12" fill="#115e59">If an action requires IRQF_WAKE_THREAD, wake the corresponding kernel thread to execute the bottom half.</text>
</svg>

### 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

```c
// 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)

```c
// 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;
};
```

### Threaded IRQ: Currently Recommended Approach

```c
// 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

```c
// 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

```c
// 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

```bash
# 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*
