Focus of this lesson: this Linux kernel spinlocks tutorial explains, in plain language, why a plain spinlock can freeze your driver, and how spin_lock_irq(), spin_lock_irqsave(), and spin_lock_bh() each solve a different flavor of the same problem. It’s part of our free Linux kernel development course and pairs well with our free Linux device drivers course material on interrupt handling.
Prerequisites
Before starting this Linux kernel spinlocks tutorial, you should be comfortable with:
- Writing a basic Linux kernel module (
module_init,module_exit) - The idea of process context vs interrupt context
- Basic C pointers and structures
- Compiling a kernel module against your running kernel headers
If any of these feel new, check the earlier lectures in our free Linux kernel development course before continuing.
Why Do We Even Need Spinlocks?
Modern Linux runs on multi-core CPUs, and a single driver’s code can be entered from more than one place at the same time — a read() call from user space on one core, and a hardware interrupt firing on another core. Whenever two code paths can touch the same shared data structure at the same time, you have a race condition. A spinlock is the kernel’s simplest tool to stop that: only one CPU core can hold the lock at a time, and every other core that wants it simply loops (“spins”) until the lock is free.
That sounds simple enough — until interrupts enter the picture. That’s where this Linux kernel spinlocks tutorial really begins.
| Process Context e.g. your read()/write() syscall |
↔ | Interrupt Context e.g. your IRQ handler |
| Both may touch the same shared data — a spinlock alone is not always enough to protect it safely. | ||
The Hidden Danger: How a Plain Spinlock Can Freeze Your Driver
Imagine your driver’s read handler takes a plain spinlock to protect a shared buffer, does some work, and releases it. On the surface this looks correct. Now picture this exact sequence on a single-core system:
- Your read handler acquires the plain spinlock.
- While the lock is still held, a hardware interrupt fires on that same core.
- The interrupt handler for your device also needs the same spinlock, so it starts spinning, waiting for the lock to free up.
- But the code that would release the lock — your original read handler — cannot resume, because the CPU is now stuck servicing the interrupt handler.
The result is a lock that can never be released: a self-deadlock. This is the single biggest gotcha that every Linux kernel spinlocks tutorial needs to cover before showing you any code.
Linux Kernel Spinlock Variants Compared
The kernel gives us several spinlock flavors, each disabling a different amount of “interruption” before grabbing the lock. This table is the core reference for this Linux kernel spinlocks tutorial — bookmark it.
| API Pair | What It Disables | Typical Use | Relative Overhead |
|---|---|---|---|
spin_lock() / spin_unlock() |
Nothing extra (just kernel preemption) | Process context only, no interrupt sharing | Lowest |
spin_lock_irq() / spin_unlock_irq() |
Hardware interrupts on the local core | Data shared with your own IRQ handler, IRQ state known to be on | Medium |
spin_lock_irqsave() / spin_unlock_irqrestore() |
Hardware interrupts, saving prior IRQ state | General-purpose, unknown/mixed caller context | Medium-high |
spin_lock_bh() / spin_unlock_bh() |
Softirqs / tasklet-class deferred work on the local core | Data shared between process context and a softirq/timer, no hardware IRQ sharing | Medium |
spin_lock_irqsave(). It is always safe, because it never assumes what the interrupt state was before you took the lock.
Using spin_lock_irq() to Protect Shared Data From Your Own IRQ Handler
spin_lock_irq() takes the lock and disables hardware interrupts on the local CPU core in one step. It’s the right tool when your process-context code and your own interrupt handler both touch the same structure, and you are certain interrupts were already enabled when you called it.
#include <linux/spinlock.h>
static spinlock_t evt_lock;
static struct event_queue evt_q;
/* Called from a user read() on this character device */
static ssize_t mychar_read(struct file *filp, char __user *buf,
size_t count, loff_t *pos)
{
struct event_node *node;
spin_lock_irq(&evt_lock);
node = dequeue_event(&evt_q);
spin_unlock_irq(&evt_lock);
if (!node)
return 0;
/* copy_to_user() and cleanup happen outside the lock */
return copy_event_to_user(node, buf, count);
}
/* Our device's interrupt handler, same lock, same queue */
static irqreturn_t mychar_irq_handler(int irq, void *dev_id)
{
struct event_node *node = alloc_event_node();
spin_lock(&evt_lock); /* plain spin_lock() is fine here — */
enqueue_event(&evt_q, node); /* we are already in IRQ context */
spin_unlock(&evt_lock);
return IRQ_HANDLED;
}
Notice the handler itself uses plain spin_lock(), not spin_lock_irq(). Since the CPU is already inside an interrupt when the handler runs, there’s no need to disable interrupts a second time — that would just add overhead for no benefit.
The Interrupt-Mask Corruption Trap (and Why irqsave Exists)
spin_lock_irq() has one silent assumption baked into it: it assumes interrupts were fully enabled right before you called it, and it will unconditionally re-enable them when you unlock. In a small driver that assumption usually holds. But in a larger codebase, a different code path may have deliberately masked off only some interrupts before reaching your function — maybe a diagnostics routine, maybe another subsystem sharing the same core.
If your locked section calls spin_lock_irq() / spin_unlock_irq() in that situation, it doesn’t restore the original, selective interrupt mask — it blindly turns every interrupt back on. That can quietly re-enable an interrupt source someone else intentionally kept off, causing a hard-to-reproduce bug far away from your driver.
spin_lock_irqsave() and spin_unlock_irqrestore(): The Safe Default
This pair fixes the problem above by capturing the exact interrupt state into a local variable before locking, and restoring precisely that state on unlock — never assuming “everything was on.”
#include <linux/spinlock.h>
static spinlock_t stats_lock;
static struct driver_stats stats;
void update_packet_stats(unsigned int bytes)
{
unsigned long flags;
spin_lock_irqsave(&stats_lock, flags);
stats.total_bytes += bytes;
stats.total_packets++;
spin_unlock_irqrestore(&stats_lock, flags);
}
The flags variable must be a plain local unsigned long — never a global, and never shared between calls. Each lock/unlock pair needs its own flags so the correct state is restored every time.
spin_lock_bh(): Locking Against Softirqs and Deferred Work
Not every shared-data race involves a hardware interrupt. If your process-context code shares a structure with a timer callback or a piece of deferred bottom-half work, you don’t need to disable hardware interrupts — you only need to stop that deferred work from running on the local core while you’re in the critical section. That’s exactly what spin_lock_bh() does.
#include <linux/spinlock.h>
static spinlock_t list_lock;
static LIST_HEAD(pending_list);
/* Called from process context (e.g. an ioctl) */
void add_pending_item(struct pending_item *item)
{
spin_lock_bh(&list_lock);
list_add_tail(&item->node, &pending_list);
spin_unlock_bh(&list_lock);
}
/* Called from a timer's deferred bottom-half context */
static void pending_list_timer_cb(struct timer_list *t)
{
spin_lock(&list_lock);
process_pending_list(&pending_list);
spin_unlock(&list_lock);
}
tasklet API used for deferred bottom-half work is now marked deprecated in mainline Linux and is gradually being replaced by BH workqueues (system_bh_wq, introduced via queue_work()/INIT_WORK() instead of tasklet_schedule()). If you’re writing a new driver today, prefer BH workqueues or threaded interrupt handlers over tasklets. spin_lock_bh() itself is unaffected by this change — it still correctly protects data shared with any softirq-class deferred work, tasklet-based or not.
Real-World Use Cases
| Scenario | Recommended Lock |
|---|---|
| Network driver’s transmit ring shared with its own IRQ handler | spin_lock_irqsave() |
| Character device event queue read from user space and a device IRQ | spin_lock_irq() in process context, plain spin_lock() in the handler |
| Statistics list updated by a periodic timer and read via sysfs | spin_lock_bh() |
Common Mistakes and Troubleshooting Tips
- Using plain spin_lock() from process context when an IRQ handler shares the lock. This is the classic self-deadlock described earlier. If in doubt, use the irq or irqsave variant.
- Mixing spin_lock_irq() with spin_unlock_irqrestore() (or vice versa). Always pair the exact matching lock/unlock function.
- Reusing one
flagsvariable across multiple nested locks. Eachspin_lock_irqsave()call needs its own localflags. - Holding a spinlock across a sleeping call (like
kmalloc(GFP_KERNEL)orcopy_to_user()). Spinlocks must never be held while the code might sleep. - Long critical sections. Every core waiting on your lock is doing nothing else — keep the locked section as short as possible.
Best Practices
- Default to
spin_lock_irqsave()unless you can prove the calling context is always the same. - Keep critical sections tiny — copy data in, unlock, then do the heavy lifting.
- Never call blocking or sleeping functions while holding any spinlock.
- Document, next to the lock declaration, exactly which contexts (process, IRQ, softirq) can take it.
- Prefer per-CPU data or lock-free structures over spinlocks on extremely hot paths, where appropriate.
Performance Considerations
Every spinlock variant beyond the plain form adds a small but real cost, because disabling and re-enabling interrupts (or softirqs) is not free on any architecture. On latency-sensitive paths — the networking receive path is the textbook example — kernel developers deliberately choose the cheapest variant that is still correct, which is exactly why understanding this table matters, not just memorizing the API names.
Security Considerations
Long or unbounded critical sections under a spinlock are effectively a local denial-of-service risk: every other core waiting on that lock is fully consumed, and with hardware interrupts disabled you can also delay time-sensitive events like watchdog resets. Always bound the work done inside a locked section, and never let a locked section’s duration depend on user-controlled input size without limits.
Summary / Key Takeaways
- A plain spinlock can self-deadlock if the same lock is ever needed inside an interrupt handler on the same core.
spin_lock_irq()disables hardware interrupts on the local core but assumes they were enabled beforehand.spin_lock_irqsave()is the safe, general-purpose choice — it saves and restores the exact prior interrupt state.spin_lock_bh()protects data shared with softirq-class deferred work, not hardware interrupts.- Tasklets are deprecated in modern kernels in favor of BH workqueues, but
spin_lock_bh()remains valid.
Conclusion
This Linux kernel spinlocks tutorial covered the full reasoning path: why a plain spinlock is dangerous near interrupt handlers, how spin_lock_irq() and spin_lock_irqsave() close that gap, and when spin_lock_bh() is the more appropriate, lighter-weight tool. Pick the narrowest lock variant that is provably correct for your caller context — that single habit prevents most of the locking bugs seen in real driver code. Keep practicing with the exercises in our free Linux kernel development course, and continue to the next lecture to see these locks used inside a complete character driver.
FAQ
Ask whether your own hardware IRQ handler ever touches the same data. If yes, use spin_lock_irq() or spin_lock_irqsave(). If only a softirq/timer shares it, use spin_lock_bh(). If neither, plain spin_lock() is enough.
Yes — it is always correct because it never assumes the prior interrupt state. The only downside is a very small extra overhead compared to spin_lock_irq().
No. Spinlocks are meant for very short critical sections and must never be held across any call that can sleep, such as memory allocation with GFP_KERNEL.
Not yet. Tasklets are marked deprecated since kernel 6.9 and are being converted to BH workqueues driver by driver, but the tasklet API itself still exists in mainline kernels as of this writing.
No. It only disables softirq-class deferred work on the local core. Hardware interrupts can still fire while you hold a lock taken with spin_lock_bh().
Because spin_lock_irq() doesn’t save any prior state to restore — it always unconditionally re-enables interrupts on unlock. Mixing it with irqrestore would use an uninitialized flags value.
Our free Linux kernel development course and free Linux device drivers course on EmbeddedPathashala include hands-on lab exercises that build on this exact Linux kernel spinlocks tutorial.
More lessons like this Linux kernel spinlocks tutorial are part of our free Linux kernel development course, free Linux device drivers course, and free embedded systems course.
Browse Free Courses Join the Community
2 Comments