Arm Linux Interrupt Handling Explained: The Complete Call Chain from GIC to irq_desc

This article focuses on the main Arm Linux IRQ path. It explains how the GIC hands a hardware interrupt to the kernel’s generic interrupt layer, which then routes it to the device driver’s action chain. The core challenge is that the interrupt path is long, involves many objects, and contains mapping relationships that are hard to trace. Keywords: GIC, irq_domain, irq_desc.

Technical specifications provide a quick snapshot

Parameter Details
Topic Main Arm Linux interrupt path analysis
Architecture ARM32 / GIC v2 / I.MX6ULL
Core languages C, plus a small amount of ARM exception-context logic
Key protocols and mechanisms IRQ, SGI/PPI/SPI, EOI, irq_domain
Key objects irq_desc, irq_data, irq_chip, irqaction
Core dependencies Linux kernel IRQ subsystem, drivers/irqchip, drivers/gpio
Reference scenarios GIC primary interrupts, GPIO cascaded interrupts, driver request_irq
GitHub stars Not provided in the original input

The GIC entry point does not process device logic; it only dispatches

After Linux saves the processor state in the assembly exception entry, it enters gic_handle_irq(). This step has a very narrow responsibility: read the interrupt ID from the GIC’s GICC_IAR, identify the interrupt type, and pass any processable hardware interrupt number to the generic interrupt layer.

For SGIs where irqnr < 16, Linux writes the end-of-interrupt signal back to GICC_EOIR early at the GIC layer. For common peripheral PPIs and SPIs, it continues into handle_domain_irq(). This shows that the GIC behaves more like a first-level router than the place where final device logic runs.

static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
    u32 irqstat, irqnr;

    irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK); // Read the GIC acknowledge register
    irqnr = irqstat & GICC_IAR_INT_ID_MASK;             // Extract the hardware interrupt number

    if (likely(irqnr > 15 && irqnr < 1021))
        handle_domain_irq(gic->domain, irqnr, regs);    // Hand off to the generic interrupt layer
}

The purpose of this code is to forward the hwirq returned by the GIC into the Linux generic IRQ framework.

GIC interrupt state transition diagram AI Visual Insight: This diagram shows how a GIC v2 interrupt transitions from Pending to Active and then to Inactive. Reading the IAR moves the interrupt into the Active state, while writing EOIR releases the priority and completes the lifecycle. This directly explains why Linux separates irq_ack from irq_eoi.

The core value of irq_domain is mapping hardware IRQs to virtual IRQs

The key role of handle_domain_irq(domain, hwirq, regs) is not to “handle” the interrupt directly, but to translate it. Linux does not use a controller-local hwirq as a global index. Instead, it relies on irq_domain to convert hwirq into virq.

You can think of irq_domain as the management boundary for an interrupt controller. The GIC, GPIO, and some cascaded peripherals can each own their own domain. Internally, a domain stores an operation set in irq_domain_ops, along with reverse-mapping structures such as linear_revmap and revmap_tree.

irq_domain cascade relationship diagram AI Visual Insight: This diagram shows the hierarchical interrupt topology among controllers such as GIC, GPIO, UART, and Ethernet. The key is not simply which device generates the interrupt, but which controller is responsible for managing and forwarding it. That is the essence of how irq_domain supports cascaded controllers.

int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
                        bool lookup, struct pt_regs *regs)
{
    unsigned int irq = hwirq;

    irq_enter(); // Mark entry into hard IRQ context
    if (lookup)
        irq = irq_find_mapping(domain, hwirq); // Map hwirq to virq

    if (unlikely(!irq || irq >= nr_irqs))
        ack_bad_irq(irq); // Fallback handling for an invalid interrupt number
    else
        generic_handle_irq(irq); // Enter generic IRQ dispatch

    irq_exit(); // Exit hard IRQ context
    return 0;
}

The purpose of this code is to translate hwirq → virq and pass the request to generic_handle_irq().

irq_desc is the true software landing point for Linux interrupts

Once Linux has the virq, it can locate the corresponding irq_desc. This object is not just a descriptor. It is the central in-kernel entry point for the interrupt. It holds three classes of critical data at the same time: controller operations, flow-control handlers, and the device driver’s action chain.

The embedded irq_data links to irq_chip and irq_domain; handle_irq determines which flow-control strategy the current virq uses; and the action linked list stores device handlers registered through request_irq() or request_threaded_irq().

struct irq_desc {
    struct irq_data irq_data;              // Interrupt controller data and mapping information
    irq_flow_handler_t handle_irq;         // Current interrupt flow-control handler
    struct irqaction *action;              // Handler chain registered by device drivers
};

This structure definition shows that irq_desc is the core hub that connects the controller layer to device drivers.

irq_desc relationship diagram AI Visual Insight: This diagram connects the reference relationships among irq_desc, irq_data, irq_chip, irq_domain, and irqaction. It highlights that Linux interrupt handling is not a single direct function call, but a layered model built from descriptors, mappings, controller operations, and driver callbacks.

generic_handle_irq ultimately reaches the device action chain through handle_irq

generic_handle_irq() is intentionally thin. It only looks up irq_desc, then calls desc->handle_irq(irq, desc). The real strategy differences come from the function bound to the handle_irq pointer.

For GIC primary interrupts, a common binding is handle_fasteoi_irq. It performs lock protection, state checks, and statistics updates, then calls handle_irq_event() when conditions allow. That function walks the desc->action list and executes each driver-registered handler in order.

int generic_handle_irq(unsigned int irq)
{
    struct irq_desc *desc = irq_to_desc(irq); // Look up the descriptor by virq
    if (!desc)
        return -EINVAL;

    desc->handle_irq(irq, desc);              // Call the flow-control handler
    return 0;
}

The purpose of this code is to dispatch the virq to its corresponding interrupt flow-control function.

handle_fasteoi_irq turns controller-level handling into driver callbacks

handle_fasteoi_irq has two key jobs. First, it checks whether desc->action exists. Second, after device handling completes, it writes EOI so the GIC can leave the Active state.

This also explains a common misconception: the interrupt handler registered by a driver is not attached directly to the GIC entry point. It lives in the irq_desc->action chain and is triggered indirectly by flow-control functions such as handle_fasteoi_irq.

void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
{
    if (unlikely(!desc->action)) {
        mask_irq(desc);                      // Mask the interrupt first if no device handler exists
        return;
    }

    handle_irq_event(desc);                 // Walk the action chain and call driver handlers
    cond_unmask_eoi_irq(desc, chip);        // Write EOI and complete the interrupt on the GIC side
}

The purpose of this code is to convert controller-level interrupt handling into specific device-driver callbacks.

Cascaded interrupts skip action and forward directly to the next domain

Secondary interrupt controllers such as GPIO are usually not the final consumers of an interrupt. Their parent virq is often bound through irq_set_chained_handler() to a chained handler such as mx3_gpio_irq_handler.

In this case, the parent irq_desc->handle_irq no longer walks its own action list. Instead, it reads the GPIO status register, calculates which pin triggered the interrupt, and forwards it to the next-level virq through irq_find_mapping(port->domain, irqoffset). In other words, a cascaded node is responsible for splitting and forwarding, not for running device actions.

static void mxc_gpio_irq_handler(struct mxc_gpio_port *port, u32 irq_stat)
{
    while (irq_stat != 0) {
        int irqoffset = fls(irq_stat) - 1; // Find the triggered GPIO bit
        generic_handle_irq(irq_find_mapping(port->domain, irqoffset));
        irq_stat &= ~(1 << irqoffset);     // Clear the bit that has already been handled
    }
}

The purpose of this code is to split a GPIO port-level interrupt into per-pin interrupts and keep dispatching them.

NR_IRQS is not the real upper bound in modern kernels

One important correction from the original analysis is this: you should no longer treat NR_IRQS as the true number of interrupt descriptors. Modern kernels often use sparse or dynamic allocation. The correct global reference is nr_irqs, and you should query descriptors through irq_to_desc().

That means irq_desc[NR_IRQS] is closer to a legacy interface. For debugging modules, driver troubleshooting, and exception enumeration, the correct approach is to iterate over nr_irqs rather than assuming a static array covers every virq.

One main call chain is enough to explain most issues

From the GIC to the driver, the main path can be reduced to this sequence: read IAR, map hwirq, locate irq_desc, execute the flow-control handler, walk the action chain, and write EOI. Once you understand that path, interrupt debugging becomes much simpler.

gic_handle_irq(regs)
  -> handle_domain_irq(gic->domain, irqnr, regs)
  -> irq_find_mapping(domain, hwirq)
  -> generic_handle_irq(irq)
  -> desc->handle_irq(irq, desc)
  -> handle_irq_event(desc)
  -> action->handler(irq, dev_id)

This call chain summarizes the core path for a normal Arm Linux peripheral interrupt from the controller to the driver.

Overall interrupt flow diagram 1 AI Visual Insight: This diagram shows the layered flow from the exception entry to the generic interrupt layer and then to the driver handler. It helps distinguish hardware interrupt controller handling from device-driver business logic, so you do not conflate the roles of the GIC, GPIO, and the final device callback.

Overall interrupt flow diagram 2 AI Visual Insight: This diagram further fills in where softirqs, threaded interrupts, and context switches fit into the larger flow. It reminds developers that the end of hard IRQ handling does not always mean the work is done; longer tasks may continue through softirq or threaded IRQ execution.

The FAQ section answers common questions in a structured way

Why can’t you use a GIC interrupt number directly as the irq in a driver?

Because the GIC returns an hwirq in the controller’s local namespace, while Linux device drivers use the global virq. irq_domain maps one to the other, so drivers should program against the virq.

Why do some irq_desc objects execute the action chain while others do not?

Final device nodes usually walk the action list, but cascaded controller nodes often only forward the interrupt. For example, a GPIO port-level interrupt first identifies the specific pin, then calls the next-level generic_handle_irq().

When debugging interrupt descriptors, should you use NR_IRQS or nr_irqs?

In modern kernels, prefer nr_irqs together with irq_to_desc(). NR_IRQS may be only a static reserved value and may not represent the actual number of usable virq entries in the system.

Core summary: This article reconstructs the main Arm Linux interrupt handling path, systematically explains how gic_handle_irq, handle_domain_irq, irq_domain, irq_desc, and handle_fasteoi_irq work together, and clarifies the difference between NR_IRQS and the dynamic nr_irqs. The goal is to help developers build a complete mental model from hardware interrupt numbers to device-driver handler functions.