Frontend Labs
⋯

52. Interrupts and kernel concurrency: top/bottom halves, spinlocks vs. mutexes

52.1 Registering an interrupt handler

#include <linux/interrupt.h>
#include <linux/gpio.h>

static irqreturn_t button_isr(int irq, void *dev_id) {
    (void)irq; (void)dev_id;
    printk(KERN_INFO "button: pressed\n");
    return IRQ_HANDLED;    // tells the kernel this handler claimed the interrupt
}

static int irq_number;

static int __init button_init(void) {
    irq_number = gpio_to_irq(17);   // the IRQ line wired to GPIO 17
    return request_irq(irq_number, button_isr, IRQF_TRIGGER_RISING, "button_handler", NULL);
}

static void __exit button_exit(void) {
    free_irq(irq_number, NULL);
}

52.2 The rule that changes everything: an interrupt handler cannot sleep

Code running in interrupt context (an ISR, or the top half of one) runs with normal scheduling paused on that CPU — it cannot call anything that might block: no mutex_lock, no kmalloc(..., GFP_KERNEL) (use GFP_ATOMIC instead), no blocking I/O. Calling a sleeping function from interrupt context doesn't error gracefully — it produces a kernel BUG ("scheduling while atomic" or "sleeping function called from invalid context") and can wedge or crash the whole machine. This is why real interrupt handling is split in two:

  • Top half (the ISR itself, like button_isr above): do the absolute minimum — acknowledge the hardware, grab the data that would otherwise be lost, and return, as fast as possible.
  • Bottom half (a tasklet or a workqueue): the actual work, deferred to run later in a context that's allowed to sleep.
#include <linux/workqueue.h>

static struct work_struct button_work;

static void button_work_fn(struct work_struct *work) {
    (void)work;
    // safe here to sleep, take a mutex, allocate with GFP_KERNEL, etc. —
    // this runs in process context, not interrupt context
    printk(KERN_INFO "button: doing the slow part\n");
}

static irqreturn_t button_isr(int irq, void *dev_id) {
    (void)irq; (void)dev_id;
    schedule_work(&button_work);   // defer the real work — return from the ISR immediately
    return IRQ_HANDLED;
}

// in the module's init function: INIT_WORK(&button_work, button_work_fn);

52.3 Spinlocks vs. mutexes: the rule that governs which lock to use

Section 20's pthread_mutex_t assumed something the kernel can't always assume: that the thread holding a lock is free to be put to sleep while another thread waits. That's false the moment an interrupt handler needs the same data a normal kernel thread does — the ISR cannot sleep, so it cannot wait on a mutex.

Can sleep while waiting? Use when
mutex_lock() Yes Data only ever touched from process context (never from an ISR)
spin_lock() / spin_lock_irqsave() No — busy-waits Data shared with an interrupt handler; _irqsave variant also disables local interrupts, since the ISR could otherwise preempt the very code holding the lock, on the same CPU
atomic_t / atomic_inc() etc. N/A — single indivisible op A simple counter or flag, no larger critical section needed
#include <linux/spinlock.h>

static DEFINE_SPINLOCK(button_lock);
static int press_count;

static irqreturn_t button_isr(int irq, void *dev_id) {
    (void)irq; (void)dev_id;
    unsigned long flags;
    spin_lock_irqsave(&button_lock, flags);   // disable interrupts on this CPU + acquire the lock
    press_count++;
    spin_unlock_irqrestore(&button_lock, flags);
    return IRQ_HANDLED;
}

War story. "Sleeping in atomic context" — calling a mutex or a blocking allocation from inside a spinlock-held region or an interrupt handler — is one of the most common kernel bugs new driver authors write, and it's exactly analogous to Section 46.13's "mutable global state that quietly breaks under threads": it often works fine in testing (the sleep path is rarely hit) and then wedges a production machine the first time the unlucky timing actually occurs.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36