A Complete Guide to Linux Interrupt Management and Workqueues
This article provides an in‑depth, step‑by‑step analysis of Linux GICv3 interrupt architecture, the kernel driver initialization, hardware‑to‑virtual interrupt mapping, the full interrupt handling state machine, and the workqueue subsystem including its data structures, initialization, and usage patterns.
GIC Hardware Overview
The Generic Interrupt Controller (GIC) is ARM's universal interrupt controller that receives hardware interrupt signals, processes them, and forwards them to the appropriate CPU.
Four GIC versions exist (v1‑v4); this guide focuses on GICv3.
GICv3 Interrupt Types
SGI (Software Generated Interrupt): triggered by software via the GICD_SGIR register, used for inter‑processor communication (IPI).
PPI (Private Peripheral Interrupt): private to each core, e.g., local timers.
SPI (Shared Peripheral Interrupt): shared external device interrupts, such as button presses.
LPI (Locality‑specific Peripheral Interrupt): new in GICv3, message‑based, configuration stored in tables (e.g., PCIe MSI/MSI‑x).
Hardware interrupt numbers for each type are listed in the table below.
GICv3 Components
Distributor : manages SPI interrupts and forwards them to Redistributors.
Redistributor : handles SGI, PPI, and LPI, forwarding them to the CPU interface.
CPU Interface : delivers interrupts to the core.
Interrupt Routing
Early systems used a simple linear mapping (one interrupt number to one controller line). Modern systems with multiple controllers use the irq_domain hierarchy to map hardware interrupt IDs to Linux virtual IRQ numbers, allowing a tree‑like layout that fully utilizes available IRQ numbers.
Interrupt State Machine
Inactive – no Pending and no Active state.
Pending – hardware or software has raised the interrupt; it waits for a CPU to handle it.
Active – the CPU has acknowledged the interrupt and is processing it.
Active and pending – the same source raises a new interrupt while the previous one is still active.
Interrupt Handling Flow
Peripheral raises an interrupt and sends it to the Distributor.
Distributor forwards the interrupt to the appropriate Redistributor.
Redistributor sends the interrupt to the CPU interface.
CPU interface generates the proper exception for the processor.
The processor receives the exception and the software handles the interrupt.
GIC Driver in the Linux Kernel
The driver source file is drivers/irqchip/irq-gic-v3.c. The following sections walk through the key steps.
Device Tree
gic: interrupt-controller@51a00000 {
compatible = "arm,gic-v3";
reg = <0x0 0x51a00000 0 0x10000>,
<0x0 0x51b00000 0 0xC0000>,
<0x0 0x52000000 0 0x2000>,
<0x0 0x52010000 0 0x1000>;
#interrupt-cells = <3>;
interrupt-controller;
interrupts = <GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(6) | IRQ_TYPE_LEVEL_HIGH)>;
interrupt-parent = <&gic>;
};Key properties:
compatible : matches the GICv3 driver.
reg : physical base addresses for GICD, GICR, GICC, GICH, GICV.
#interrupt-cells : number of cells in the interrupts property.
interrupt-controller : marks the node as an interrupt controller.
interrupts : encodes type, number, and flags.
Driver Initialization
1. IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gic_of_init); registers the driver with the __irqchip_of_table section.
2. gic_of_init maps the Distributor registers, validates the GIC version, reads the number of Redistributor regions, allocates resources, and finally calls gic_init_bases.
static int __init gic_of_init(struct device_node *node, struct device_node *parent) {
dist_base = of_iomap(node, 0); // (1)
if (!dist_base) {
pr_err("%pOF: unable to map gic dist registers
", node);
return -ENXIO;
}
err = gic_validate_dist_version(dist_base); // (2)
if (err) {
pr_err("%pOF: no distributor detected, giving up
", node);
goto out_unmap_dist;
}
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions)) // (3)
nr_redist_regions = 1;
// ... allocate and map each redistributor region (4)
// ... read redistributor-stride (5)
err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions, redist_stride, &node->fwnode); // (6)
// ... further initialization (7)
return 0;
}Key steps inside gic_init_bases:
Read GICD_TYPER to discover the number of interrupt lines and ID bits.
Create the IRQ domain with irq_domain_create_tree.
Initialize VLPIs, set the IRQ handler with set_handle_irq(gic_handle_irq), and optionally initialize the ITS.
Initialize SMP support, the Distributor, and the CPU interface.
Interrupt Mapping Structures
The kernel uses several core structures:
struct irq_desc : describes a peripheral interrupt; contains irq_common_data, irq_data, handler pointer, and action list.
struct irq_data : holds hardware data such as irq (virtual IRQ), hwirq (hardware IRQ), chip pointer, and domain pointer.
struct irq_chip : defines hardware operations (startup, shutdown, enable, disable, ack, mask, unmask, set_affinity, etc.).
struct irq_domain : maps hwirq to virq, stores name, ops, and reverse‑map tables.
struct irq_domain_ops : provides match, map, xlate callbacks for domain handling.
struct irqaction : represents a driver‑registered interrupt handler (function pointer, device ID, flags, name).
Creating the Mapping
During driver load, __irq_domain_add creates an irq_domain and adds it to the global list. Device drivers later call irq_of_parse_and_map which internally:
Parses the device tree node ( of_irq_parse_one).
Finds the matching irq_domain ( irq_find_matching_fwspec).
Translates to hardware IRQ and type ( gic_irq_domain_translate).
Allocates a virtual IRQ and descriptor ( irq_domain_alloc_descs).
Establishes the hwirq ‑to‑ virq mapping ( gic_irq_domain_alloc).
Sets the appropriate handler in irq_desc (shared handle_fasteoi_irq or private handle_percpu_devid_irq).
Interrupt Registration
Drivers register handlers with request_irq (or request_threaded_irq for threaded handling). Example macro:
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}Important flag definitions (e.g., IRQF_SHARED, IRQF_PERCPU, IRQF_ONESHOT) are listed in the source.
Interrupt Processing
When an interrupt occurs, the CPU jumps to the appropriate exception vector ( el0_irq or el1_irq). The generic steps are:
Save processor state (PSTATE, registers) on the stack.
Enable interrupts (clear DAIF).
Call the architecture‑specific handler ( handle_arch_irq), which points to gic_handle_irq. gic_handle_irq reads the interrupt ID ( gic_read_iar()), distinguishes SGI, SPI/PPI, or LPI, acknowledges the interrupt, and forwards to handle_domain_irq. handle_domain_irq looks up the Linux IRQ number via the domain, calls generic_handle_irq, which invokes the registered irq_desc->handle_irq (either shared or private).
The handler eventually calls the driver’s irqaction->handler (or wakes a threaded handler).
After processing, the kernel restores the saved state and returns from the exception.
Workqueue Subsystem
Workqueues provide a mechanism to defer work to kernel threads, allowing sleeping and rescheduling, unlike softirqs or tasklets.
Key Data Structures
struct work_struct : the smallest unit of work; contains state, list entry, and the function pointer ( func) to execute.
struct workqueue_struct : a collection of works; can be bound (per‑CPU) or unbound (migratable). Holds lists of pool workqueues, workers, and rescue structures.
struct pool_workqueue : mediates between a workqueue and a worker pool; tracks active and delayed works.
struct worker_pool : a set of kernel threads (workers) that execute works; maintains worklist, idle list, busy hash, timers, and management structures.
struct worker : represents a kernel thread; holds the current work, function, associated pool, and scheduling list.
Workqueue Initialization
The kernel performs two phases:
workqueue_init_early : allocates worker_pool and workqueue_struct, links them via alloc_and_link_pwqs which creates a pool_workqueue.
workqueue_init : creates an initial worker for each pool using create_worker, which spawns a kernel thread named kworker/XX:YY (or kworker/uXX:YY for unbound pools), adds it to the pool, and puts it into the idle state.
Using Workqueues
Drivers typically use the default system workqueue. The workflow is:
Initialize a work_struct with INIT_WORK(my_work, my_func).
Schedule it with schedule_work(&my_work), which adds the work to system_wq.
The scheduler determines the appropriate pool_workqueue (CPU‑bound or node‑bound) and either enqueues the work directly or places it on the delayed list if the pool is saturated.
If no worker is active, wake_up_worker wakes a kernel thread.
The worker_thread function runs, sets the PF_WQ_WORKER flag, and enters the woke_up routine.
In recheck, the worker processes pending works by calling process_one_work, which invokes the func stored in each work_struct.
If the worklist becomes empty, the worker sleeps on the idle list until woken again.
Summary
The article walks through the complete lifecycle of Linux interrupts: from the GICv3 hardware model, through device‑tree parsing, driver initialization, hardware‑to‑virtual IRQ mapping, and the detailed state machine that processes an interrupt. It then shifts focus to the workqueue subsystem, explaining its architecture, core data structures, initialization phases, and how drivers schedule deferred work safely in process context.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Linux Code Review Hub
A professional Linux technology community and learning platform covering the kernel, memory management, process management, file system and I/O, performance tuning, device drivers, virtualization, and cloud computing.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
