Before you start#
Read scheduler core, CPU, memory and I/O, and syscall entry. This chapter describes upstream v6.12; architecture, interrupt controller, kernel configuration and PREEMPT_RT change details. The source-only worked timeline requires no hardware experiment. The host observation reads counters and changes no IRQ masks or affinity.
Learning goals#
Distinguish hardware interrupt identifiers, Linux IRQ numbers and CPU vectors; follow a conditional generic IRQ action and IRQ-exit softirq path; explain hardirq, threaded IRQ, softirq and NMI context; and design a device teardown that respects in-flight handlers and object lifetime.
From a device event to a handler#
A device can signal that it needs attention or completed work. The signal reaches an interrupt controller, which delivers an interrupt according to the platform's routing and processor architecture. A hardware IRQ identifier is meaningful to its controller or domain; a Linux IRQ number, sometimes called a virtual IRQ, identifies a kernel IRQ descriptor. An x86 interrupt vector is a CPU entry identifier and is not automatically the same number as either. irqdomain maps hardware identifiers into Linux IRQ space under its domain rules; hierarchical domains can involve multiple controllers. Do not infer a physical wire or a universal one-to-one relationship from a /proc/interrupts label.
struct irq_desc holds the generic descriptor state for a Linux IRQ, including an irq_data relationship and registered actions. irq_data connects the IRQ to domain and controller-specific information; an irq_chip supplies operations such as masking, acknowledging or ending an interrupt as appropriate for that controller. A flow handler coordinates controller operations and registered actions for the trigger type and configuration. See v6.12 include/linux/irqdesc.h, irqdomain implementation, and generic IRQ documentation.
Masking prevents or limits delivery under the controller's rules; acknowledging records or clears a controller/device condition as specified; end-of-interrupt can tell a controller that servicing is complete. These are not interchangeable operations and the correct order depends on edge versus level triggering, controller and device. For a level-triggered source, failing to clear the underlying device condition can cause repeated delivery after unmasking. A generic flow handler cannot replace the device driver's obligation to handle its own status registers.
Source walkthrough#
- Controller and domain
Route a hardware event to a Linux IRQ descriptor.
- IRQ action
The primary handler checks its device and returns a result.
- Optional IRQ thread
A valid IRQ_WAKE_THREAD result wakes the registered thread.
- IRQ exit
Pending softirq work may run or be deferred under applicable rules.
On an x86 system, an IDT entry and APIC-related routing can lead into architecture IRQ handling; arm64 commonly uses a GIC and irqdomain-based mapping. The exact path depends on architecture, controller, interrupt type and configuration. An exception caused by an instruction, such as a page fault, is distinct from an external device interrupt. An NMI or similar non-maskable event has stronger nesting and context constraints and must not be assumed to follow an ordinary device IRQ's locking or masking path. v6.12 x86 IRQ code and arm64 IRQ code are architecture-specific starting points, not a single universal call chain.
For one concrete x86-64 source path, the DEFINE_IDTENTRY_IRQ(common_interrupt) wrapper in v6.12 arch/x86/include/asm/idtentry.h enters IRQ context and invokes the body on the appropriate stack. common_interrupt calls call_irq_handler, which looks up the per-CPU vector descriptor; the x86-64 path then calls generic_handle_irq_desc. If that descriptor is configured with handle_fasteoi_irq, v6.12 kernel/irq/chip.c shows that flow calling handle_irq_event and then its conditional unmask/EOI handling. handle_irq_event drops and reacquires the descriptor raw lock around the action dispatch. The IRQ-stack wrapper reaches irq_exit_rcu, which can process pending softirqs under the conditions discussed below. A different descriptor flow, special vector or error path can take a different route.
For an arm64 GICv3 example, v6.12 drivers/irqchip/irq-gic-v3.c handles non-special IRQ numbers with generic_handle_domain_irq(gic_data.domain, irqnr); an unexpected mapping triggers a warning and deactivation handling. Its NMI-related branches are separate. This source example is a contrast in mapping and entry, not a claim that all arm64 machines use GICv3 or the same flow handler.
Once a generic flow invokes registered actions, v6.12 kernel/irq/handle.c has __handle_irq_event_percpu iterate them. This excerpt is upstream v6.12 (SPDX GPL-2.0; tracing and checks omitted):
res = action->handler(irq, action->dev_id);
The action's primary handler receives the Linux IRQ number and the registered device-specific pointer. The handler must use that pointer only while its object remains valid. On a shared IRQ, each registered handler must determine whether its own device caused the event; a handler that did not find its device's condition normally returns IRQ_NONE, while a handled event returns the appropriate handled result. The generic code accumulates results for its accounting. A handler should not claim another device's interrupt simply because it was called.
The same function handles the threaded return (same file and SPDX notice; the missing-thread warning branch is omitted):
case IRQ_WAKE_THREAD:
__irq_wake_thread(desc, action);
break;
The actual source first checks that a thread function exists; returning IRQ_WAKE_THREAD without one triggers a warning rather than a valid deferred operation. request_threaded_irq registers a primary handler and thread function under its contract. The primary handler performs the urgent device-specific test and acknowledgement appropriate to its hardware, then can request the thread. IRQF_ONESHOT coordinates masking for the threaded handling under the generic IRQ rules; it does not by itself stop DMA or guarantee the device has been quiesced. request_irq is a common non-threaded registration interface. Registration can fail because of invalid parameters, sharing conflicts, unavailable resources or controller restrictions; a driver must unwind only resources already acquired.
The return values are defined in v6.12 include/linux/irqreturn.h, SPDX GPL-2.0 (kernel-doc comment omitted):
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
The header names three values; the dispatch loop in __handle_irq_event_percpu acts on each action's result and iterates over the registered actions. The enum does not show how a driver recognises its own event, and it cannot replace the device's own status check.
- IRQ_NONE
- Not this device event, or not handled; on a shared line the dispatch loop still visits the other actions.
- IRQ_HANDLED
- This device event was handled; no thread is requested by this action.
- IRQ_WAKE_THREAD
- Asks the core to wake the thread function; without one, the core warns instead.
Context and deferred work#
A hard IRQ handler runs in an interrupt context with restrictions that differ from an ordinary process. It must not perform operations that may sleep as though it were a normal thread. A threaded IRQ handler runs in a kernel thread and can use operations allowed by that thread's context, but must still follow its device, locking and teardown contracts. It is not automatically safe to hold a raw spinlock across a sleeping operation. PREEMPT_RT changes how many interrupts and softirqs are executed and how some ordinary spinlocks behave; raw spinlocks and NMI-specific constraints remain distinct. Do not project a non-RT locking rule onto every RT path without checking that configuration.
A softirq is a deferred kernel execution mechanism with its own context and per-CPU pending state. Networking's NAPI can use softirq processing to batch receive/transmit work and limit how much work one polling round performs. A tasklet is another deferred-work interface built on softirq machinery in this release; it is not an ordinary userspace thread and cannot be treated as an unrestricted sleeping callback. Workqueues, discussed later, provide a different process-context mechanism. NAPI, softirq and a device's hard interrupt handler are related in many network paths but are not synonyms or mandatory for every device.
At IRQ exit, v6.12 kernel/softirq.c includes this excerpt in __irq_exit_rcu (SPDX GPL-2.0-only; accounting and tick handling omitted):
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
The first condition checks that this is not still nested in an interrupt context and the second checks local pending work. Only then does the code invoke the applicable softirq path. Depending on configuration and state, work can run at IRQ exit or be deferred to ksoftirqd; PREEMPT_RT has different handling. The non-RT processing loop uses time and restart limits and can defer remaining work, but those limits are not an application latency guarantee. ksoftirqd is a kernel thread that processes deferred softirqs; seeing it in a process listing does not identify a single interrupt or prove why it ran.
A device and its ownership timeline#
Consider a hypothetical network device with a Linux IRQ registered using a primary handler and a threaded handler. At time 0 the device asserts an event. At time 1 the controller and architecture entry deliver it, and the generic flow eventually invokes the action. The primary handler reads the device status, recognizes its own event and performs the device-specific acknowledgement required by its contract. It returns IRQ_WAKE_THREAD. At time 2 generic code wakes the registered thread; with an appropriate oneshot configuration the line's masking is coordinated until the threaded work completes. The thread processes the work and returns; subsequent controller and device behavior depends on the flow and hardware.
Now suppose another device shares the line. If this device's status says the event was not its own, the primary handler returns IRQ_NONE; the generic dispatcher can continue through the other actions. If a driver instead always returns handled, it hides useful evidence about an unclaimed interrupt. Another counterexample is returning IRQ_WAKE_THREAD after failing to register a thread function: the source explicitly warns about that case. Neither example tells you how a particular device acknowledges its interrupt; consult that driver's code and hardware documentation.
A handler can race with device removal. A correct teardown prevents new device work, disables or quiesces the device's interrupt generation and DMA according to its hardware contract, unregisters the action with the correct IRQ and dev_id, and synchronizes in-flight handlers and threads before freeing objects they can access. free_irq has its own synchronization contract; it is not a substitute for stopping the device from generating further DMA or callbacks through another interface. A lock protecting a status field is also not a reference that keeps the device object alive after it is freed. Shared IRQs require using the matching dev_id; disabling an entire shared line can affect other devices and must not be used casually.
In __free_irq in v6.12 kernel/irq/manage.c, SPDX GPL-2.0, the wait for running handlers reads (earlier unregistration steps and the shared-IRQ debug branch are not shown):
/*
* Make sure it's not being used on another CPU and if the chip
* supports it also make sure that there is no (not yet serviced)
* interrupt in flight at the hardware level.
*/
__synchronize_irq(desc);
__synchronize_irq waits for hardirq handlers and then for threaded handlers to finish, so this call covers running handlers only. It does not stop the device from raising further interrupts or issuing DMA, and it does not free the driver's data; those remain the caller's obligations, as the removal paragraphs above explain.
- DriverDevice
Stop new work; quiesce interrupts and DMA
- DriverIRQ core
free_irq(irq, dev_id)
- IRQ coreHandler
__synchronize_irq waits for running handler and threads
- HandlerIRQ core
Return. Handler finishes
- IRQ coreDriver
Return. free_irq returns; driver may free state
Time runs downward. Dashed arrows show returns.
Read the text version
- Driver → Device: Stop new work; quiesce interrupts and DMA
- Driver → IRQ core: free_irq(irq, dev_id)
- IRQ core → Handler: __synchronize_irq waits for running handler and threads
- Handler → IRQ core (return): Handler finishes
- IRQ core → Driver (return): free_irq returns; driver may free state
Counters and causality#
/proc/interrupts typically presents per-CPU counts and labels; /proc/softirqs presents softirq counters. Their availability, labels and granularity vary by architecture, kernel and container restrictions. A rising counter supports the narrow claim that the displayed counter changed between two reads. It does not by itself measure interrupt latency, prove a particular handler's duration, identify one packet, or establish the v6.12 call chain on the running host. Reading counters can itself take time, and unrelated activity can change them between samples.
A useful investigation would pair a precise workload with timestamps, IRQ identity, configuration, CPU placement and appropriately configured tracepoints or device statistics. Tracing changes overhead and requires its own permission and safety analysis. A high count can be normal batching behavior or a symptom; without a baseline and workload it is not a diagnosis. Similarly, a quiet counter in a restricted container does not prove the physical machine has no interrupts.
Registration and removal are state machines#
Consider a driver probe that allocates a device structure, maps registers and then requests an interrupt. If IRQ registration fails, the driver must undo the successful earlier mapping and allocation in the right order. It must not call free_irq as though registration succeeded, nor free the structure while a separately registered callback can still reach it. Once registration succeeds, the handler may run as soon as the source is enabled; initialization of the data the handler reads must therefore precede exposing that source. The exact ordering of device masking, request and enabling depends on the device and IRQ API contract. A source trace that begins only at the handler omits this publication boundary.
During removal, imagine the hardware raises a final interrupt just as the driver sets a shutdown flag. A handler already running can have observed the old flag. The driver must protect the flag and any shared state with the appropriate lock or ordering, stop new hardware events, and synchronize the handler before releasing its backing object. If the threaded handler queued separate work, synchronizing the IRQ thread alone does not necessarily cancel that work. Likewise, a device can still write through DMA after its IRQ has been masked unless the hardware has been stopped and mappings handled correctly. Every independently asynchronous path needs its own lifetime accounting.
Why edge and level are not merely labels#
An edge-triggered event records a transition; a level-triggered source remains asserted while its condition holds. Controllers can have pending, active and masked states, and a device may have status and acknowledgement registers of its own. The generic IRQ layer chooses flow behavior based on configured trigger and chip operations, but it cannot infer all device-specific status semantics. If a driver clears the wrong register or enables the device before its status handler is ready, it may lose events or see repeated interrupts. Those are hypotheses to test against the actual controller and device documentation, not conclusions from a single counter. Interrupt storms, missed edges and shared-line claims can look similar in a coarse count while having very different causes.
Practice#
Source-only route: draw the timeline above with controller event, generic action, primary result, optional thread and completion. Add the IRQ_NONE shared-line branch and the missing-thread-function warning. For teardown, list which actor can still access the device object after each proposed step; identify why quiescing the device and waiting for handlers are separate requirements. This is a source-based model, not a measured trace.
Read-only host route: in an ordinary Linux shell run:
uname -m
head -25 /proc/interrupts
head -20 /proc/softirqs
If a file is empty, hidden or inaccessible, record that limitation; do not change container permissions, IRQ masks, IRQ affinity or kernel settings. On systems exposing the data, the output may contain CPU columns and counters. The exact names and values are host-specific and may disclose hardware or workload details, so review before sharing. If you take a second sample, note that background work can change counters even without a deliberately generated workload.
Deliver a short comparison of one observed counter or visibility failure and the source-only timeline. As a counterexample, explain why a counter increment cannot establish the duration of the hard handler or prove that a particular thread function ran. No files are created and no cleanup is needed. A disposable guest with a deliberately built test driver could provide stronger evidence, but no guest module or device manipulation is required here.
Knowledge check#
- Why is a hardware IRQ identifier not automatically equal to a Linux IRQ number or x86 vector?
- What happens if an action returns
IRQ_WAKE_THREADwithout a registered thread function? - Why can IRQ exit defer pending softirq work instead of always processing all of it immediately?
- Why does
free_irqalone not necessarily make a device's DMA and all other callbacks safe to free? - What can two
/proc/interruptscounter samples establish, and what can they not establish?
Answers#
- Controller domains map hardware identifiers into Linux IRQ descriptors, while architecture vectors describe CPU entry. Their relationship depends on the platform and routing.
- The v6.12 action path detects the missing thread function and warns; it cannot validly wake a thread that was never registered.
- Nesting, pending state, processing budgets, scheduler state and configuration—including PREEMPT_RT—affect when and where deferred work runs. Some work can move to
ksoftirqd. - IRQ synchronization covers the registered handler's contract, not every device engine or asynchronous interface. The driver must quiesce DMA and prevent other callbacks before freeing shared state.
- They can show that displayed counts changed over the interval. They do not independently show latency, a complete causal trace or the source revision used by the host.
Checkpoint#
Deliver the source-only action/teardown timeline and the labeled counter observation or visibility limitation. Pass when you distinguish hardware ID, Linux IRQ and vector; explain IRQ_NONE versus IRQ_WAKE_THREAD; and identify both handler synchronization and device quiescence. If uncertain, retrace __handle_irq_event_percpu and __irq_exit_rcu and revise the timeline. This is not certification.