Interrupts and Exceptions
- Interrupts are asynchronous electrical signals generated by hardware devices to capture the processor’s attention without requiring continuous polling.
- Hardware devices route these signals into input pins on an interrupt controller, which multiplexes multiple lines into a single line directly connected to the processor.
- Each signal is identified by a unique numeric value known as an Interrupt Request (IRQ) line, allowing the kernel to distinguish between devices (e.g., keyboard versus hard drive).
- Exceptions are synchronous interrupts generated by the processor itself during instruction execution.
- Exceptions trigger in response to programming errors (e.g., divide by zero), abnormal conditions (e.g., page faults), or software interrupts (e.g., system calls trapping into the kernel).
To process these asynchronous hardware signals without stalling system execution, the kernel relies on specialized operational functions.
Interrupt Handlers (Top Halves)
- An Interrupt Service Routine (ISR), or interrupt handler, is a standard C function that the kernel executes in response to a specific interrupt.
- ISRs operate in a special operational mode called interrupt context (or atomic context).
- Code executing in interrupt context cannot block or sleep, strictly limiting the functions available for use.
- An ISR must execute as quickly as possible to acknowledge the hardware and resume the interrupted code.
- During handler execution, the corresponding interrupt line is masked on all processors to prevent concurrent nested interrupts on the same line.
- Because the interrupt line is masked, interrupt handlers in Linux do not need to be reentrant.
Because ISRs must execute quickly but tasks like network or disk I/O require extensive data processing, the kernel splits interrupt handling into two distinct phases.
Top Halves Versus Bottom Halves
- Top Half (Interrupt Handler): Executes immediately upon receipt of the interrupt.
- Performs time-critical, hardware-specific work, such as acknowledging the interrupt receipt to the hardware and copying data from device buffers into main memory.
- Runs with at least the current interrupt line disabled.
- Bottom Half: Executes deferred work at a more convenient time.
- Performs data processing and protocol stack pushes.
- Runs with all interrupts enabled, ensuring system latency remains low.
To deploy a top half that responds to hardware, drivers must formally register their ISRs with the kernel’s interrupt subsystem.
Registering and Freeing Handlers
- Drivers allocate an interrupt line and register their ISR using
request_irq():irq: The numeric IRQ line to allocate.handler: A function pointer to the ISR.flags: A bitmask dictating handler behavior, utilizing macros such asIRQF_DISABLED,IRQF_SAMPLE_RANDOM,IRQF_TIMER, andIRQF_SHARED.name: An ASCII string identifying the device, utilized by/proc/irqand/proc/interrupts.dev: A unique cookie passed back to the handler, mandatory for shared IRQ lines to distinguish between devices.
- The
request_irq()function can sleep because it callskmalloc()to create/procentries, meaning it must be executed strictly from process context. - To unregister a handler, drivers invoke
free_irq(unsigned int irq, void *dev). - On shared lines,
free_irq()uses the uniquedevcookie to remove the specific handler and disables the IRQ line only when the final handler is removed.
Once registered, the ISR must adhere to specific architectural signatures and return types to communicate hardware status back to the kernel.
Handler Implementation and Shared Lines
- Function Prototype: Handlers must match the signature
static irqreturn_t intr_handler(int irq, void *dev). - Return Values: The
irqreturn_ttype communicates interrupt ownership to the kernel:IRQ_NONE: Returned when the handler detects that its associated hardware did not originate the interrupt.IRQ_HANDLED: Returned when the handler successfully processes an interrupt originated by its device.- The macro
IRQ_RETVAL(val)returnsIRQ_HANDLEDifvalis nonzero, orIRQ_NONEotherwise.
- Shared Handlers: Multiple devices can share a single IRQ line if they adhere to strict rules:
- All registering drivers must specify the
IRQF_SHAREDflag. - The
devcookie must be unique (often a pointer to the device structure) and cannot beNULL. - The handler must check hardware status registers to quickly confirm if its specific device generated the interrupt, exiting with
IRQ_NONEif it did not. - The kernel sequentially invokes each registered handler on a shared line until one acknowledges the interrupt.
- All registering drivers must specify the
As these handlers process hardware state, they run in a strict operational environment distinct from standard user processes.
Interrupt Context and Stacks
- Interrupt Context: Unlike process context, interrupt context is not associated with a backing process.
- The
currentmacro points to the interrupted process but holds no relevance for the handler. - Because there is no backing process to reschedule, code in interrupt context absolutely cannot sleep or block.
- The
- Kernel Stacks:
- Historically, interrupt handlers shared the stack of the interrupted process, which is tightly limited (e.g., 8KB on 32-bit architectures).
- To reduce memory pressure, modern configurations employ 1-page kernel stacks and introduce dedicated interrupt stacks.
- These interrupt stacks provide one 1-page stack per processor exclusively for interrupt handlers.
The transition into this restrictive context follows a precise hardware-to-software execution path initiated by the interrupt controller.
The Interrupt Execution Path
- When a device generates an interrupt, it sends a signal to the interrupt controller, which triggers a designated pin on the processor.
- The processor interrupts execution, disables the interrupt system, and jumps to an architecture-specific assembly entry point.
- The initial assembly routine saves the IRQ value and the interrupted task’s registers into a
pt_regsstructure on the stack. - The kernel then invokes the C function
do_IRQ(), which extracts the IRQ number, acknowledges the interrupt to the controller, and disables delivery on that line. do_IRQ()ensures a valid handler exists and callshandle_IRQ_event()to process the action chain.- Unless
IRQF_DISABLEDwas set during registration,handle_IRQ_event()re-enables local processor interrupts before looping through and executing all handlers registered on the line. - If
IRQF_SAMPLE_RANDOMwas specified, the kernel invokesadd_interrupt_randomness()to feed the timing into the entropy pool. - Execution returns to the assembly routine
ret_from_intr(), which checks theneed_reschedflag andpreempt_countto determine if it is safe to invokeschedule()before restoring registers and resuming the interrupted code.
To protect data structures modified during this execution path from concurrent access, the kernel provides strict mechanisms to control interrupt delivery.
Interrupt Control and Synchronization
- Local Interrupt Control: Disabling local interrupts prevents preemptive concurrent access from an ISR on the current processor.
local_irq_disable()andlocal_irq_enable()unconditionally clear and set the allow-interrupts flag on the issuing processor (e.g., viacliandstiassembly instructions on x86).local_irq_save(flags)saves the current interrupt state into an opaqueunsigned long, then disables interrupts.local_irq_restore(flags)restores the exact interrupt state previously saved, preventing the erroneous enablement of interrupts that were already disabled prior to the critical section.- The
flagsvariable must be passed by value within the same stack frame due to architecture-specific stack behaviors.
- Global Interrupt Control: The legacy global
cli()lock was deprecated to mandate fine-grained locking (e.g., spin locks) alongside local interrupt control, ensuring better SMP scalability. - Line-Specific Control: The kernel can mask specific IRQ lines across the entire system.
disable_irq(irq)disables the line and blocks until any currently executing handlers on that line complete.disable_irq_nosync(irq)disables the line immediately without waiting for existing handlers to exit.enable_irq(irq)enables the line; calls nest, meaning two disables require two enables to re-activate the line.synchronize_irq(irq)blocks until a specific interrupt handler finishes executing.
- Context Status Macros:
irqs_disabled()returns nonzero if local interrupt delivery is currently disabled.in_interrupt()returns nonzero if the kernel is in interrupt context (executing an ISR or a bottom half).in_irq()returns nonzero strictly if the kernel is executing an ISR.
Mastering these synchronization primitives ensures that the delicate interplay between asynchronous hardware interrupts and kernel processes remains free of race conditions.