Kernel Spinlocks

A spinlock is the Linux kernel’s primitive for mutual exclusion over a critical section that is short and runs in a context that must not sleep. A CPU that wants the lock and finds it held does not block and yield the processor; it busy-waits — it “spins” in a tight loop reading the lock word until the holder releases it. Because the waiter burns a whole CPU while spinning, spinlocks are only correct and only efficient when the hold time is tiny (a handful of cache-line accesses, never an I/O wait or a memory allocation that can reclaim). The defining constraint is captured in one iron rule: you must never sleep while holding a spinlock. On a non-PREEMPT_RT kernel, taking a spinlock_t also disables kernel preemption on the holding CPU, so the holder cannot be scheduled out mid-critical-section. The generic type is spinlock_t, manipulated through spin_lock(), spin_unlock(), and spin_trylock() (per the kernel’s own locking documentation).

This note covers the contract and API of spinlocks: what they guarantee, why the no-sleep rule exists, and how spinlock_t differs from raw_spinlock_t. The MCS-based machinery that actually implements the spin is in Queued Spinlocks and qspinlock; the interrupt- and bottom-half-disabling variants are in Spinlock irqsave and bh Variants; the preemption counter that makes the no-sleep guarantee enforceable is in Preemption Disabling and preempt_count.

Mental Model

The right way to think about a spinlock is as a doorway that one CPU can occupy at a time, where everyone else queues at the threshold without sitting down. Contrast it with a mutex, which is a doorway with a waiting room: if the door is occupied, you take a seat (the scheduler puts your task to sleep and runs someone else) and a bell wakes you when it’s your turn. The spinlock has no waiting room. If the door is occupied you stand at the threshold tapping your foot — consuming a CPU — until it opens. That is only sensible if the person inside will be out in microseconds. If they might go take a nap inside (sleep), everyone queued outside is stuck burning fuel indefinitely, and on a single-CPU machine the napper can never even be woken because the only CPU is busy spinning.

flowchart TB
  A["CPU wants spinlock"] --> B{"lock word == 0<br/>(free)?"}
  B -->|"yes"| C["atomic cmpxchg<br/>0 -> locked<br/>(fast path)"]
  C --> D["preemption disabled<br/>on this CPU<br/>enter critical section"]
  B -->|"no (held)"| E["busy-wait / spin<br/>(slow path:<br/>queued, see qspinlock)"]
  E --> B
  D --> F["spin_unlock():<br/>store-release lock = 0<br/>preemption re-enabled"]
  F --> G["next waiter proceeds"]

The spinlock lifecycle. What it shows: acquisition first tries an atomic compare-and-swap on the lock word (the uncontended fast path); failure drops into a busy-wait loop. Crucially, between acquire and release, preemption is disabled on the holding CPU — the holder runs to completion of the critical section and cannot be scheduled away. The insight to take: the spin is pure wasted CPU, so the entire design only pays off when the critical section is short enough that spinning is cheaper than the context-switch cost a sleeping lock would incur. The moment a holder could sleep, this model collapses: the spinning waiters would burn CPU for the holder’s entire sleep duration.

The Iron Rule: You Cannot Sleep While Holding a Spinlock

This is the single most important fact about spinlocks, and it is not a stylistic preference — it is a correctness requirement that the kernel actively enforces in debug builds. Two independent reasons make it absolute.

Reason one: deadlock by self-starvation. Suppose a task holds a spinlock and then calls something that sleeps — kmalloc(GFP_KERNEL) (which may block on memory reclaim), mutex_lock(), copy_to_user() (which may fault and wait on disk), or msleep(). The scheduler picks another task on that CPU. If that other task tries to take the same spinlock, it spins. But the original holder is asleep and will only release the lock when it is scheduled back in — and it can only be scheduled back in if the spinning task yields the CPU, which it never will, because a spinning task does not yield. On a uniprocessor (CONFIG_SMP=n) build this is an instant hard hang of that CPU. On SMP it is a latent deadlock that fires whenever the contending task lands on the holder’s CPU.

Reason two: preemption is disabled, so sleeping is already forbidden. Taking a spinlock_t calls preempt_disable() as part of the acquire path. The implementation is explicit: in include/linux/spinlock_api_smp.h, __raw_spin_lock() begins with preempt_disable(); before it ever touches the lock word (spinlock_api_smp.h, v6.12). Sleeping (calling schedule() to yield) inside a preempt_disable() region is itself illegal: the scheduler’s __schedule() path and the might_sleep() debug check assume preemption is enabled when a voluntary sleep happens. A CONFIG_DEBUG_ATOMIC_SLEEP kernel catches a sleep inside any spinlock with a loud BUG: sleeping function called from invalid context splat that names the offending file and line.

The practical corollary, stated in the kernel’s lock-types documentation: “Sleeping lock types cannot nest inside CPU local and spinning lock types.” You may take a spinlock while holding a mutex, but never a mutex while holding a spinlock — the ordering is one-directional. Any allocation inside a spinlock must use GFP_ATOMIC (which never reclaims and never sleeps); any user-memory copy must be hoisted outside the locked region.

The API Surface

The generic, preemption-disabling lock is spinlock_t. You define one statically with DEFINE_SPINLOCK(my_lock) or initialize an embedded one at runtime with spin_lock_init(&obj->lock). The core operations, all declared in include/linux/spinlock.h, are:

spinlock_t my_lock = __SPIN_LOCK_UNLOCKED(my_lock);  /* or DEFINE_SPINLOCK(my_lock) */
 
spin_lock(&my_lock);        /* acquire; spins if held; disables preemption */
/* ... short critical section, no sleeping ... */
spin_unlock(&my_lock);      /* release; store-release; re-enables preemption */
 
if (spin_trylock(&my_lock)) {   /* non-blocking attempt */
    /* got it */
    spin_unlock(&my_lock);
} else {
    /* lock was held; do something else, do NOT spin */
}

Walking the call chain shows there is almost no overhead in the common case. On a non-PREEMPT_RT SMP kernel, spin_lock() is a static __always_inline wrapper that forwards to raw_spin_lock(&lock->rlock) (spinlock.h, v6.12). That expands through _raw_spin_lock() to __raw_spin_lock(), which does exactly three things: preempt_disable(), a lockdep annotation (spin_acquire(), compiled out unless CONFIG_DEBUG_LOCK_ALLOC is on), and LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock). do_raw_spin_lock() finally calls arch_spin_lock(&lock->raw_lock), which on every modern SMP architecture is queued_spin_lock() — the qspinlock fast path, a single atomic_try_cmpxchg_acquire() (asm-generic/qspinlock.h, v6.12). So an uncontended acquire is: disable preemption, one atomic compare-and-swap with acquire ordering, done.

spin_unlock() mirrors this: it calls arch_spin_unlock()queued_spin_unlock(), which is smp_store_release(&lock->locked, 0) — a plain store with release ordering, not an expensive atomic — followed by preempt_enable(). The asymmetry is deliberate: acquiring a lock needs an atomic read-modify-write to win a race against other CPUs, but releasing it only needs to publish a single byte that the next waiter is already watching.

spin_trylock() forwards to raw_spin_trylock()do_raw_spin_trylock()arch_spin_trylock(), i.e. queued_spin_trylock(): it reads the lock word, and if it is zero, attempts the cmpxchg; if the word is non-zero (held or contended) it returns 0 immediately without spinning. spin_trylock() is what you reach for when you can do useful work elsewhere rather than wait, or to acquire locks out of your normal order without risking a deadlock (try the second lock; if it fails, drop the first and restart).

Read-side query helpers exist too: spin_is_locked(&lock) (is anyone holding it — racy, for assertions only), spin_is_contended(&lock) (is someone waiting behind the holder), and assert_spin_locked(&lock) (a debug assertion that the current path holds the lock). These do not acquire anything; they only inspect the word.

The Memory-Ordering Guarantee

A spinlock is not only mutual exclusion — it is also a memory barrier. The acquire uses acquire semantics (atomic_try_cmpxchg_acquire) and the release uses release semantics (smp_store_release). This means writes inside the critical section by the holder are guaranteed visible to the next CPU that acquires the same lock, in program order. The header comment in spinlock.h works through the subtle cases — notably that spin_unlock() followed by spin_lock() on the same lock provides an ordering strong enough that a third CPU observing the second lock’s effects also observes the first’s. The one trap the kernel calls out: a lock acquire is an ACQUIRE and a release is a RELEASE, but the two together do not form a full barrier; code that needs store-after-unlock to be ordered against load-after-lock must reach for smp_mb__after_unlock_lock(). See Memory Barriers in the Linux Kernel and Acquire Release and Fence Semantics for the full model; Compare-and-Swap and cmpxchg in the Kernel for the atomic the fast path rests on.

spinlock_t versus raw_spinlock_t — The PREEMPT_RT Split

This distinction looks pedantic on a normal kernel and becomes load-bearing on a real-time one. On a standard (!CONFIG_PREEMPT_RT) kernel the two are nearly identical: include/linux/spinlock_types.h literally defines spinlock_t as a wrapper around a raw_spinlock_t — “Non PREEMPT_RT kernels map spinlock to raw_spinlock” (spinlock_types.h, v6.12). Both spin; both disable preemption; the only practical difference is debug/lockdep wait-type annotation (LD_WAIT_CONFIG for spinlock_t, LD_WAIT_SPIN for raw_spinlock_t).

On a PREEMPT_RT kernel the semantics diverge sharply, and this is the whole point of the type distinction. Under PREEMPT_RT, spinlock_t is redefined entirely — it becomes a struct rt_mutex_base, i.e. a sleeping, priority-inheriting mutex: “PREEMPT_RT kernels map spinlock to rt_mutex.” A spin_lock() on RT calls rt_spin_lock(), which can block the calling task and let the scheduler run something else; preemption is not disabled (spinlock_rt.h, v6.12; locktypes doc). This is how PREEMPT_RT achieves bounded latency: almost all spinlocks stop disabling preemption, so a high-priority task can preempt a low-priority lock holder, and priority inheritance prevents the classic priority-inversion stall.

raw_spinlock_t, by contrast, stays a true busy-wait spinning lock in every configuration, including PREEMPT_RT: “raw_spinlock_t is a strict spinning lock implementation in all kernels.” It always disables preemption (and, with _irqsave, interrupts). It is reserved for the genuinely atomic core of the kernel — the scheduler’s own run-queue lock, the low-level timer code, the parts of interrupt entry/exit and the architecture code that cannot tolerate being preempted or made to sleep even on RT. Converting a hot path from spinlock_t to raw_spinlock_t is not an optimization; it is a contract that the critical section is genuinely non-preemptible and you accept the RT latency cost. See Raw Spinlocks and PREEMPT_RT for the full treatment.

Uncertain

Verify: that all spinlock_t instances become rt_mutex-based sleeping locks under PREEMPT_RT with no exceptions, and the precise set of contexts where raw_spinlock_t is mandated. Reason: the locking/locktypes.html render fetched is the 7.x/latest docs page, not a v6.12-pinned render; the spinlock_rt.h source was read directly at v6.12 and confirms rt_spin_lock() mapping, but the exhaustive RT-conversion policy lives across many subsystems. To resolve: cross-check Documentation/locking/locktypes.rst at the v6.12 tag and the PREEMPT_RT conversion patches. uncertain

Failure Modes and How They Present

Sleeping in atomic context. The most common bug. You call mutex_lock(), kmalloc(GFP_KERNEL), copy_from_user(), vmalloc(), msleep(), or any function that might sleep, while holding a spinlock. On a CONFIG_DEBUG_ATOMIC_SLEEP=y kernel the symptom is a BUG: sleeping function called from invalid context at <file>:<line> followed by a stack trace and the value of preempt_count. On a production kernel without that debug option, the symptom is far worse and rarer to reproduce: an intermittent hang or a soft-lockup warning when the contending task happens to spin against a sleeping holder.

Double-acquire / recursion. Spinlocks are not recursive. If a function holds a spinlock and calls (directly or via an interrupt) another function that takes the same lock, the second acquire spins forever on a lock its own CPU already holds — an instant self-deadlock. This is exactly why the irqsave/bh variants exist: if an interrupt handler takes lock L, then any process-context code that takes L must disable that interrupt first (spin_lock_irqsave), or the interrupt can fire mid-critical-section and self-deadlock. That whole family is in Spinlock irqsave and bh Variants.

Holding too long. Even when correct, a spinlock held across a long computation hurts: every contending CPU is burning cycles, and on a !PREEMPT_RT kernel preemption is off so even unrelated high-priority work on the holder’s CPU is stalled. spin_needbreak() exists for loops that legitimately must hold a lock across many iterations — it lets the holder voluntarily drop and re-take the lock at a safe point if a higher-priority task is waiting.

Forgetting _irqsave against an IRQ handler. Symptom: rare deadlocks under load that lockdep flags as an inconsistent {IN-HARDIRQ-W} -> {HARDIRQ-ON-W} usage. The fix is the irqsave variant; see lockdep Runtime Lock Validator.

Alternatives and When to Choose Them

The decision is almost never about performance first — it is about context, which is the cross-cutting theme of the Linux Kernel Synchronization MOC. If the critical section can sleep (waits on I/O, allocates with GFP_KERNEL, copies user memory), you cannot use a spinlock and must use a mutex or semaphore. If you are in hardirq/softirq context or already hold another spinlock, you cannot sleep, so a spinlock (or its irqsave variant) is mandatory. Within the spinlock family: use plain spin_lock() when no interrupt handler touches the lock; spin_lock_bh() when a softirq does; spin_lock_irqsave() when a hardirq does (Spinlock irqsave and bh Variants). If the data is read-mostly, a seqlock or RCU beats any spinlock by removing reader-side contention entirely. If the data is a single word, an atomic removes the lock altogether. rwlocks exist but usually lose to a plain spinlock because of their own overhead unless the read/write asymmetry is extreme.

Production Notes

In real kernel code, spinlocks are everywhere short critical sections guard shared state touchable from interrupt context: device-driver registers, the per-CPU run queue (rq->lock, a raw_spinlock_t), wait-queue heads, the slab allocator’s per-node lists, network sk_buff queue locks. The discipline that experienced kernel developers internalize: acquire late, release early, never sleep in between, and pick the narrowest variant that closes the relevant interrupt race. Lockdep (lockdep Runtime Lock Validator) runs in debug kernels and CI to catch ordering violations and bad nesting before they reach production; CONFIG_DEBUG_ATOMIC_SLEEP catches the no-sleep-rule violations. The combination is why a vanishingly small fraction of spinlock bugs survive to a shipped kernel — the rules are mechanically enforced, not merely documented.

See Also