« Previous Lecture Next Lecture »
Linux Kernel Spinlocks Tutorial: A Complete Guide with Examples (Kernel 6.12+)
If you are searching for a free Linux kernel development course that actually explains kernel locking the way it works on modern kernels, this spinlock tutorial is for you. Spinlocks are one of the two workhorse locking primitives inside the Linux kernel (the other being the mutex), and understanding them correctly is essential for anyone writing device drivers, working on interrupt handlers, or debugging concurrency bugs in kernel space. This lecture is part of our free Linux device drivers course and our broader free embedded systems course track, and it is written for kernel 6.12 and later, including the mainline PREEMPT_RT changes.
What You Will Learn
- What a spinlock is, and why the kernel cannot always use a mutex
- How modern kernels implement spinlocks internally (ticket locks vs queued spinlocks)
- The full spin_lock() API family and when to use each variant
- A complete, original kernel module example that uses a spinlock safely
- How PREEMPT_RT changes spinlock behaviour on real-time kernels
- Common mistakes, debugging tips, and performance/security considerations
Prerequisites
- Basic C programming knowledge
- A working Linux kernel module build setup (kernel headers +
make/gcc) - Familiarity with what a kernel module is (insmod/rmmod, printk)
- Basic idea of process context vs interrupt context is helpful but not mandatory — we explain it below
What Is a Spinlock, and Why Does the Kernel Need It?
A spinlock is a low-level locking mechanism used to protect a shared resource — a variable, a linked list, a hardware register — from being accessed by more than one CPU at the same time. Unlike a mutex, a spinlock does not put the waiting thread to sleep. Instead, the CPU trying to acquire an already-locked spinlock simply loops — “spins” — checking the lock repeatedly until it becomes free.
This busy-waiting behaviour sounds wasteful, and in the wrong context it is. But it exists for a very specific reason: there are places in the kernel — interrupt handlers, softirqs, and other atomic contexts — where the code is not allowed to sleep. A mutex can put the calling thread to sleep while it waits, which is fine in normal process context but illegal inside an interrupt handler. A spinlock never sleeps, so it is safe to use anywhere, including interrupt context, as long as you follow the correct API variant.
| Can the critical section sleep? | → | Yes: use a mutex |
| Runs in interrupt/atomic context, or must never block? | → | Yes: use a spinlock |
Spinlock Internals: How Modern Kernels Implement It
Older kernel documentation (and older books) often describe spinlocks as a simple “test-and-set” loop. That is historically accurate but out of date. On any kernel you will actually build today, the implementation has evolved through two major generations:
| Implementation | Era | Key Idea |
|---|---|---|
| Simple test-and-set | Very old kernels | All spinning CPUs hammer the same cache line — poor scalability |
| Ticket spinlock | 2.6.25 onward | Each waiter gets a “ticket number”; lock is granted in FIFO order, fixing fairness |
| Queued spinlock (qspinlock) | 4.2 onward, default on most architectures today | Waiters queue on a per-CPU MCS-style node, drastically reducing cache-line contention on large multi-core systems |
You never call the internal ticket or queue logic directly — you always go through the standard spin_lock() / spin_unlock() API, and the kernel picks the right underlying implementation for your architecture automatically. What matters for driver authors is the API contract, which has stayed stable even as the internals were rewritten.
The Spinlock API Family
Include the header first:
#include <linux/spinlock.h>
Declaring and Initializing a Spinlock
/* Static initialization — most common in driver code */
static DEFINE_SPINLOCK(my_lock);
/* Dynamic initialization — used when the lock lives inside
* a structure allocated at runtime */
spinlock_t my_lock;
spin_lock_init(&my_lock);
Locking and Unlocking Variants
This is the part most tutorials skim over, and it is the part that actually causes bugs in real drivers. Choosing the wrong variant is one of the most common sources of deadlocks in kernel code.
| Function Pair | Use When |
|---|---|
spin_lock() / spin_unlock() |
You are certain interrupts are already disabled, or the critical section is never touched from interrupt context |
spin_lock_irq() / spin_unlock_irq() |
You know the current interrupt state and want to unconditionally disable/enable local interrupts |
spin_lock_irqsave() / spin_unlock_irqrestore() |
The safest general-purpose choice — saves and restores the previous interrupt state, so it is safe even if the interrupt state is unknown |
spin_lock_bh() / spin_unlock_bh() |
The data is also touched from a softirq/tasklet/bottom-half, but never from a hardware interrupt handler |
spin_trylock() |
You cannot afford to spin at all and want to fail fast if the lock is busy |
If a data structure can be touched from both process context and interrupt context, always protect it with spin_lock_irqsave() / spin_unlock_irqrestore() in the process-context path. This is the single safest default for driver code.
A Complete, Original Kernel Module Example
The following module demonstrates a spinlock protecting a shared counter that is incremented from a kernel timer (simulating interrupt-like asynchronous context) and read from a sysfs-style helper function. This example is written from scratch for this tutorial — it is not copied from any book or reference driver.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/spinlock.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
static DEFINE_SPINLOCK(counter_lock);
static unsigned long shared_counter;
static struct timer_list demo_timer;
/* Runs in softirq (timer) context — must not sleep */
static void demo_timer_fn(struct timer_list *t)
{
unsigned long flags;
spin_lock_irqsave(&counter_lock, flags);
shared_counter++;
spin_unlock_irqrestore(&counter_lock, flags);
mod_timer(&demo_timer, jiffies + HZ);
}
/* Runs in normal process context */
static unsigned long read_counter_safely(void)
{
unsigned long flags, value;
spin_lock_irqsave(&counter_lock, flags);
value = shared_counter;
spin_unlock_irqrestore(&counter_lock, flags);
return value;
}
static int __init spinlock_demo_init(void)
{
pr_info("spinlock_demo: module loaded\n");
timer_setup(&demo_timer, demo_timer_fn, 0);
mod_timer(&demo_timer, jiffies + HZ);
pr_info("spinlock_demo: counter at load = %lu\n", read_counter_safely());
return 0;
}
static void __exit spinlock_demo_exit(void)
{
del_timer_sync(&demo_timer);
pr_info("spinlock_demo: final counter = %lu\n", read_counter_safely());
pr_info("spinlock_demo: module unloaded\n");
}
module_init(spinlock_demo_init);
module_exit(spinlock_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Original spinlock demonstration module for EmbeddedPathashala");
Notice that spin_lock_irqsave() is used in both the timer callback and the reader function, even though the timer callback already runs in softirq context. This is the defensive, always-safe pattern: it guarantees correctness regardless of which context calls the function, at the small cost of disabling local interrupts briefly during the critical section.
PREEMPT_RT and Spinlocks: What Changed in Modern Kernels
One of the biggest changes affecting kernel locking in recent years is the merge of PREEMPT_RT into the mainline kernel starting with 6.12. This matters directly for spinlock behaviour:
- On a standard (non-RT) kernel,
spin_lock()disables kernel preemption and behaves exactly as described above — it never sleeps. - On a kernel built with PREEMPT_RT enabled, most regular
spinlock_tlocks are internally converted into “sleeping spinlocks” implemented on top of rt_mutex. This allows priority inheritance and lets a lower-priority task holding the lock be boosted so it does not block a higher-priority real-time task indefinitely. - Raw, always-spinning locks are still available on RT kernels via
raw_spinlock_tfor the rare cases (true hardware-critical sections) where sleeping is never acceptable, even under RT.
Standard kernelspin_lock() |
vs | PREEMPT_RT kernelspin_lock() |
| Busy-waits, never sleeps, preemption disabled | Sleeps if contended, backed by rt_mutex, supports priority inheritance |
If you are writing driver code that must behave identically on both kernel types, do not rely on the spinlock never sleeping — write the critical section as if it could, in principle, block briefly, and keep it as short as possible either way.
Real-World Use Cases
- Network drivers: protecting per-CPU packet ring buffers accessed from both NAPI poll (softirq) and control-path ioctls
- Character/misc device drivers: protecting a small shared device-context structure accessed from both a read() syscall and a completion interrupt handler
- Timer and workqueue callbacks: protecting counters or state flags updated periodically outside of process context
- Block layer and filesystem code: protecting short-lived metadata updates where mutex overhead would be disproportionate
Common Mistakes and Troubleshooting
| Mistake | Consequence & Fix |
|---|---|
Calling a sleeping function (e.g. kmalloc(GFP_KERNEL), msleep()) while holding a spinlock |
Causes a “scheduling while atomic” bug or kernel warning. Use GFP_ATOMIC if allocation is unavoidable, or restructure the code to allocate outside the lock. |
Using plain spin_lock() for data also touched by an interrupt handler |
Self-deadlock: the interrupt fires on the same CPU while the lock is held, and the handler spins forever. Use spin_lock_irqsave() instead. |
| Holding a spinlock for too long | Increases interrupt latency system-wide (interrupts are disabled with the irqsave variants). Keep critical sections as short as a few instructions. |
| Recursively acquiring the same non-recursive spinlock | Deadlock. If recursive acquisition seems necessary, redesign the locking — the kernel spinlock is not reentrant. |
Debugging tip: enable CONFIG_DEBUG_SPINLOCK and CONFIG_PROVE_LOCKING (lockdep) in your kernel config while developing. Lockdep will detect most ordering violations and atomic-context violations long before they cause a hard-to-reproduce production bug.
Best Practices
- Keep the critical section as small as possible — ideally just a few lines
- Default to
spin_lock_irqsave()unless you have a specific, verified reason to use a lighter variant - Never call any function that might sleep while holding a spinlock
- Prefer per-CPU data structures over a single global spinlock when scalability matters
- Document, next to the lock declaration, exactly what data it protects
Performance Considerations
On multi-core systems, an over-contended spinlock becomes a serialization bottleneck — every CPU spinning on the same lock wastes cycles it could spend on useful work. The queued spinlock implementation used by modern kernels reduces cache-line bouncing significantly compared to older ticket locks, but it cannot eliminate contention caused by poor lock granularity. If profiling shows a spinlock as a hotspot, consider splitting one global lock into several finer-grained locks, or switching to RCU (Read-Copy-Update) for read-mostly data.
Security Considerations
Improper locking is a common root cause of kernel race-condition vulnerabilities, which can lead to use-after-free or double-free bugs when exploited. Always ensure every code path that modifies shared state — including error-handling paths — takes the lock, and use lockdep during development to catch missing or inconsistent lock ordering before it becomes a security-relevant race condition in production.
Summary / Key Takeaways
- A spinlock busy-waits instead of sleeping, making it safe for atomic/interrupt context
- Modern kernels implement spinlocks as queued spinlocks internally, but the API you use has stayed stable
spin_lock_irqsave()is the safest general-purpose choice for driver code- On PREEMPT_RT kernels, regular spinlocks can sleep and support priority inheritance; use
raw_spinlock_twhen true non-sleeping behaviour is required - Keep critical sections short, never sleep while holding a spinlock, and use lockdep during development
Conclusion
Spinlocks remain one of the fundamental building blocks of Linux kernel concurrency, and getting them right is a core skill for any driver author. The API has stayed remarkably stable even as the internal implementation moved from simple test-and-set locks to ticket locks and now to queued spinlocks, and even as PREEMPT_RT changed what “spinlock” means under the hood on real-time kernels. If you master the decision between mutex and spinlock, and the different spin_lock() variants covered in this lecture, you already understand one of the trickiest parts of kernel programming. This lecture is part of our ongoing free Linux kernel development course — keep following the series for the next topics in kernel synchronization.
Frequently Asked Questions
1. What is the difference between a spinlock and a mutex in the Linux kernel?
A spinlock busy-waits without sleeping and is safe in interrupt/atomic context; a mutex can put the calling task to sleep and is only safe in process context where blocking is allowed.
2. Can a spinlock be used inside an interrupt handler?
Yes, that is one of its main purposes. Use spin_lock()/spin_unlock() inside the interrupt handler itself, and spin_lock_irqsave()/spin_unlock_irqrestore() in the process-context code that shares the same data.
3. Why does spin_lock_irqsave() exist if spin_lock() is simpler?
It protects against self-deadlock when the same lock might be taken from an interrupt handler on the same CPU. It saves the current interrupt state, disables interrupts, and restores the exact previous state on unlock.
4. Does PREEMPT_RT remove spinlocks from the kernel?
No. It changes the behaviour of the default spinlock_t so it can sleep and support priority inheritance, while still offering raw_spinlock_t for cases that must never sleep.
5. What happens if I sleep while holding a spinlock?
On a non-RT kernel this triggers a “scheduling while atomic” bug, which can hang or crash the system. Never call sleeping functions while holding a standard spinlock.
6. Are spinlocks recursive in Linux?
No. Attempting to acquire the same non-recursive spinlock twice from the same CPU causes a deadlock.
7. What replaced the old ticket spinlock implementation?
Most modern architectures now use queued spinlocks (qspinlock), which scale better on systems with many CPUs by reducing cache-line contention among waiters.
8. How do I debug a spinlock-related deadlock?
Enable CONFIG_DEBUG_SPINLOCK and CONFIG_PROVE_LOCKING (lockdep) in your kernel configuration; lockdep reports most lock-ordering and atomic-context violations automatically.

2 Comments