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_israbove): 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.