Interrupts
Interrupt handling is a critical aspect of kernel module development, as it enables devices to communicate with the CPU asynchronously. Interrupts are high-priority events that require immediate attention, but the kernel imposes strict constraints on code executed in interrupt context to ensure system stability and responsiveness. Understanding these constraints and following safe practices is essential for writing robust kernel modules.
## Interrupt Context Constraints¶
When a device triggers an interrupt, the CPU pauses its current task and executes an interrupt handler (also called an interrupt service routine). This handler runs in interrupt context, which has several limitations:
- No blocking: The handler cannot sleep or call functions that may block (e.g., schedule(), kmalloc() with GFP_KERNEL). This is because the CPU cannot context-switch out of interrupt context.
- Atomic operations: Only atomic operations or spinlocks can be used to protect shared data.
- Limited stack usage: The interrupt stack is small, so handlers must avoid deep recursion or large allocations.
To check if code is running in interrupt context, use the in_interrupt() macro. For device-specific interrupts, in_irq() or in_softirq() can provide more granular information.
Example: Protecting shared data with a spinlock:
spinlock_t my_lock;
irqreturn_t my_irq_handler(int irq, void *dev_id) {
spin_lock(&my_lock);
// Critical section: modify shared data
spin_unlock(&my_lock);
return IRQ_HANDLED;
}
## Safe Practices for Interrupt Handlers¶
- Minimize work in handlers: Perform only essential processing (e.g., acknowledging the interrupt) and defer complex tasks to process context.
- Use
irqreturn_tfor return values: Handlers must returnIRQ_HANDLED(interrupt was processed),IRQ_NONE(no action taken), orIRQ_WAKE_THREAD(defer processing to a thread). - Register handlers carefully: Use
request_irq()to register handlers, specifying the IRQ number, handler function, and flags (e.g.,IRQF_SHAREDfor shared interrupts). Always free resources withfree_irq().
Example: Registering an interrupt handler:
int my_request_irq(struct device *dev) {
int ret = request_irq(pdev->irq, my_irq_handler, IRQF_SHARED, "my_device", dev);
if (ret) {
dev_err(dev, "Failed to request IRQ %d\n", pdev->irq);
return ret;
}
return 0;
}
## Deferring Work to Process Context¶
For tasks that cannot be completed in interrupt context (e.g., sleeping, long computations), use irq_work to defer work to process context:
struct irq_work my_irq_work;
irqreturn_t my_irq_handler(int irq, void *dev_id) {
irq_work_queue(&my_irq_work);
return IRQ_HANDLED;
}
void my_irq_work_func(struct irq_work *work) {
// Safe to sleep or call blocking functions here
msleep(100);
}
## Interrupt Affinity and CPU Masking¶
You can control which CPU handles an IRQ using set_cpus_allowed() or irqd_affinity. This is useful for load balancing or isolating interrupts to specific cores.
Example: Setting IRQ affinity:
struct irq_data *irq_data = ...; // Obtain from irq number
irqd_set_affinity(irq_data, cpumask_of(0)); // Bind to CPU 0
Key takeaways¶
- Interrupt context is non-blocking: Avoid sleeping, use atomic operations, and protect shared data with spinlocks.
- Defer complex work: Use
irq_workto move tasks to process context when necessary. - Register and free IRQs properly: Always pair
request_irq()withfree_irq()to prevent resource leaks. - Control IRQ affinity: Use
irqd_affinityto optimize interrupt handling on specific CPUs.