Skip to content

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

  1. Minimize work in handlers: Perform only essential processing (e.g., acknowledging the interrupt) and defer complex tasks to process context.
  2. Use irqreturn_t for return values: Handlers must return IRQ_HANDLED (interrupt was processed), IRQ_NONE (no action taken), or IRQ_WAKE_THREAD (defer processing to a thread).
  3. Register handlers carefully: Use request_irq() to register handlers, specifying the IRQ number, handler function, and flags (e.g., IRQF_SHARED for shared interrupts). Always free resources with free_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_work to move tasks to process context when necessary.
  • Register and free IRQs properly: Always pair request_irq() with free_irq() to prevent resource leaks.
  • Control IRQ affinity: Use irqd_affinity to optimize interrupt handling on specific CPUs.