DEV Community

Ripan Deuri
Ripan Deuri

Posted on

Linux Kernel: Interrupt Handling - Code Walk Through (Part 3)

Table of Contents

Introduction

The previous post Linux Kernel: Interrupt Handling (Part 2) breaks down the interrupt handling in CPU once GIC asserts the IRQ line.

This post follows a complete IRQ journey on ARMv8-A with Linux 6.x, tracing the transition from GIC to hardware exception entry, through the kernel’s low-level assembly paths, into the IRQ and softirq subsystems, and finally back to user or kernel context via eret.

The Complete Interrupt Path : Interrupt from User Space

An interrupt arrives while a user application is executing:

Initial State:

  • CPU executing at EL0
  • SP_EL0 points to user stack
  • PSTATE.I = 0 (interrupts enabled)
  • Peripheral device asserts interrupt line #42 to GIC

Step 1: GIC Processing

Peripheral Device:

  • Asserts interrupt line 42 (e.g., network packet arrives)

GIC Distributor:

  • Checks GICD_ISENABLER[42]: enabled
  • Checks GICD_IPRIORITYR[42]: priority = 0xA0
  • Checks GICD_ITARGETSR[42]: routed to CPU 0
  • Transitions interrupt 42 to Pending state
  • Performs priority arbitration with other pending interrupts

GIC CPU Interface (CPU 0):

  • Interrupt 42 has sufficient priority
  • Checks ICC_PMR_EL1: priority threshold allows this interrupt
  • Asserts IRQ line to CPU 0 (level signal, held high)

Step 2: CPU Exception Recognition

CPU 0 Exception Logic (every cycle):

  • Checks: IRQ line asserted? YES
  • Checks: PSTATE.I == 0? YES (interrupts enabled)
  • Checks: CurrentEL? EL0
  • Decision: Take IRQ exception to EL1

Step 3: Hardware Exception Entry

CPU 0 (atomic hardware operation):

ELR_EL1 ← PC            // Save user-space PC
SPSR_EL1 ← PSTATE       // Save user-space PSTATE (I=0, EL=0, etc.)
ESR_EL1 ← syndrome      // For IRQ, not typically used

PSTATE.DAIF ← 1111b     // Mask all exceptions
PSTATE.EL ← 01b         // Switch to EL1
SPSel ← 1               // Switch to SP_EL1

SP ← SP_EL1             // Now using exception stack
PC ← VBAR_EL1 + 0x480   // Vector to el0_irq handler
Enter fullscreen mode Exit fullscreen mode

The CPU is now executing at EL1 with SP_EL1, at the address VBAR_EL1 + 0x480.

Step 4: Assembly Exception Handler Entry

  • Allocates struct pt_regs on SP_EL1
  • Save general purpose registers and exception state (ELR_EL1, SPSR_EL1, SP_EL0) to struct pt_regs

entry.S:

// At VBAR_EL1 + 0x480
entry_handler 0, t, 64, irq
Enter fullscreen mode Exit fullscreen mode

This expands to:

SYM_CODE_START_LOCAL(el0t_64_irq)
    kernel_entry 0, 64
    mov x0, sp
    bl el0t_64_irq_handler
    b ret_to_user
SYM_CODE_END(el0t_64_irq)
Enter fullscreen mode Exit fullscreen mode
  • kernel_entry saves all GPRs, exception state into pt_regs, sets up the stack, etc.
  • bl el0t_64_irq_handler jumps into C function defined in entry-common.c

entry-common.c:

asmlinkage void noinstr el0t_64_irq_handler(struct pt_regs *regs)
{
    __el0_irq_handler_common(regs);
}
Enter fullscreen mode Exit fullscreen mode

Then this calls el0_interruptdo_interrupt_handler

static void noinstr el0_interrupt(struct pt_regs *regs,
                                  void (*handler)(struct pt_regs *))
{
    enter_from_user_mode(regs);
    write_sysreg(DAIF_PROCCTX_NOIRQ, daif);

    if (regs->pc & BIT(55))
        arm64_apply_bp_hardening();

    irq_enter_rcu();
    do_interrupt_handler(regs, handler);
    irq_exit_rcu();
    exit_to_user_mode(regs);
}

static void noinstr __el0_irq_handler_common(struct pt_regs *regs)
{
    el0_interrupt(regs, handle_arch_irq);
}
Enter fullscreen mode Exit fullscreen mode
static void do_interrupt_handler(struct pt_regs *regs,
                                 void (*handler)(struct pt_regs *))
{
    struct pt_regs *old_regs = set_irq_regs(regs);
    if (on_thread_stack())
        call_on_irq_stack(regs, handler);
    else
        handler(regs);
    set_irq_regs(old_regs);
}
Enter fullscreen mode Exit fullscreen mode
  • on_thread_stack() checks if we are running on a process/task’s regular kernel stack.
  • If yes, call_on_irq_stack switches to the per-CPU IRQ stack before calling the actual handler.

call_on_irq_stack in entry.S:

SYM_FUNC_START(call_on_irq_stack)
    ...
    ldr_this_cpu x16, irq_stack_ptr, x17
    add sp, x16, #IRQ_STACK_SIZE
    ...
    blr x1        // Calls handler(regs) on the IRQ stack!
    ...
SYM_FUNC_END(call_on_irq_stack)
Enter fullscreen mode Exit fullscreen mode

After this, do_interrupt_handlerhandle_arch_irq() for GIC is called.

Step 5: Generic Interrupt Handling

drivers/irqchip/irq-gic-v3.c:

static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
Enter fullscreen mode Exit fullscreen mode

The flow reads IAR register and then call domain IRQ handler.

// software ack intr
irqnr = gic_read_iar(); // read_sysreg(ICC_IAR1);
Enter fullscreen mode Exit fullscreen mode
static void __gic_handle_irq(u32 irqnr, struct pt_regs *regs)
  generic_handle_domain_irq(gic_data.domain, irqnr)
Enter fullscreen mode Exit fullscreen mode

kernel/irq/irqdesc.c:

void generic_handle_domain_irq: calls irq_resolve_mappingto translate hardware IRQ number to Linux virtual IRQ irqnr -> struct irq_desc

int handle_irq_desc(struct irq_desc *desc): Calls generic_handle_irq_desc -> desc->handle_irq(desc)

Typically desc->handle_irq = handle_fasteoi_irq for GIC

kernel/irq/chip.c:

//simplified
void handle_fasteoi_irq(struct irq_desc *desc)
{

    // Call the device-specific interrupt handler
    handle_irq_event(desc);

    // Signal End of Interrupt to GIC
    desc->irq_data.chip->irq_eoi(&desc->irq_data);  // Writes ICC_EOIR1_EL1

}
Enter fullscreen mode Exit fullscreen mode
// The irq_eoi callback for GIC:
static void gic_eoi_irq(struct irq_data *d)
{
    write_gicreg(irqd_to_hwirq(d), ICC_EOIR1_EL1);
}
Enter fullscreen mode Exit fullscreen mode

handle_irq_event calls device driver's irq_handler.

Step 6: Device Driver Handler

The device driver's handler performs minimal work: acknowledge the device, schedule deferred processing (softirq), and return.

Step 7: Interrupt Exit and Softirq Processing

void el0_interrupt()
{
    irq_enter_rcu();
    do_interrupt_handler(regs, handler);
    irq_exit_rcu();
}
Enter fullscreen mode Exit fullscreen mode

irq_exit_rcu() (defined in kernel/softirq.c) checks for pending softirq and calls __do_softirq()

Softirq processing runs with interrupts enabled (local_irq_enable()). If another interrupt arrives during softirq processing, it will nest deeper on the IRQ stack, execute its handler, and return to the softirq processing.

Step 8: Return to Assembly

    bl el0t_64_irq_handler
    b ret_to_user
Enter fullscreen mode Exit fullscreen mode

Once el0t_64_irq_handler is done, control returns to assembly ret_to_user. It marks the final exit path from kernel back to user space

SYM_CODE_START_LOCAL(ret_to_user)
    ...
    kernel_exit 0 // macro: restores regs & PSTATE from pt_regs, ERET
SYM_CODE_END(ret_to_user)
Enter fullscreen mode Exit fullscreen mode

Step 9: ERET - Exception Return

The eret instruction performs these operations:

ERET:
    PC ← ELR_EL1                    // Jump to saved user-space PC
    PSTATE ← SPSR_EL1               // Restore all PSTATE fields
Enter fullscreen mode Exit fullscreen mode

Restoration all PSTATE fields includes:

  • PSTATE.I = 0 (unmask IRQs)
  • PSTATE.EL = 0 (return to EL0)
  • SPSel (return to SP_EL0 for user)

Execution resumes in user space at the exact instruction that was interrupted, with all registers and processor state restored.

Sequence Diagram

Flow Diagram for Linux interrupt handling


Top comments (0)