This content originally appeared on DEV Community and was authored by Ripan Deuri
Table of Contents
- Introduction
-
The Complete Interrupt Path : Interrupt from User Space
- Step 1: GIC Processing
- Step 2: CPU Exception Recognition
- Step 3: Hardware Exception Entry
- Step 4: Assembly Exception Handler Entry
- Step 5: Generic Interrupt Handling
- Step 6: Device Driver Handler
- Step 7: Interrupt Exit and Softirq Processing
- Step 8: Return to Assembly
- Step 9: ERET - Exception Return
- Sequence Diagram
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
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_regson 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
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)
-
kernel_entrysaves all GPRs, exception state into pt_regs, sets up the stack, etc. -
bl el0t_64_irq_handlerjumps into C function defined inentry-common.c
entry-common.c:
asmlinkage void noinstr el0t_64_irq_handler(struct pt_regs *regs)
{
__el0_irq_handler_common(regs);
}
Then this calls el0_interrupt → do_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);
}
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);
}
-
on_thread_stack()checks if we are running on a process/task’s regular kernel stack. - If yes,
call_on_irq_stackswitches 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)
After this, do_interrupt_handler → handle_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)
The flow reads IAR register and then call domain IRQ handler.
// software ack intr
irqnr = gic_read_iar(); // read_sysreg(ICC_IAR1);
static void __gic_handle_irq(u32 irqnr, struct pt_regs *regs)
generic_handle_domain_irq(gic_data.domain, irqnr)
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
}
// The irq_eoi callback for GIC:
static void gic_eoi_irq(struct irq_data *d)
{
write_gicreg(irqd_to_hwirq(d), ICC_EOIR1_EL1);
}
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();
}
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
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)
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
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
This content originally appeared on DEV Community and was authored by Ripan Deuri
Ripan Deuri | Sciencx (2025-11-23T18:33:23+00:00) Linux Kernel: Interrupt Handling – Code Walk Through (Part 3). Retrieved from https://www.scien.cx/2025/11/23/linux-kernel-interrupt-handling-code-walk-through-part-3/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.
