The Linux kernel runs on systems with hundreds of CPUs and must protect shared data structures from concurrent access while staying responsive. It provides a rich set of synchronization primitives, each with different performance characteristics and usage constraints. Choosing the wrong primitive can cause deadlocks, priority inversion, or subtle data corruption — understanding when to use each one is a core kernel programming skill.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/deelerdev/linux/llms.txt
Use this file to discover all available pages before exploring further.
Lock categories
The kernel divides locking primitives into three categories:| Category | Examples | May sleep? | Notes |
|---|---|---|---|
| Sleeping locks | mutex, rw_semaphore, semaphore | Yes | Only valid in preemptible task context |
| CPU-local locks | local_lock | No | Disables preemption or interrupts on one CPU |
| Spinning locks | spinlock_t, rwlock_t, raw_spinlock_t | No | Busy-wait; implicitly disable preemption |
PREEMPT_RT kernels spinlock_t and rwlock_t are converted to sleeping RT-mutex-based locks, which changes their context constraints. Use raw_spinlock_t only when you need a non-sleeping spinlock on all kernel configurations.
Spinlocks
Spinlocks are the most fundamental primitive. They busy-wait until the lock is available, disable preemption, and optionally disable interrupts. All spinlock operations are defined ininclude/linux/spinlock.h.
Choosing the right spinlock variant
irqsave/irqrestore variant saves the current interrupt-enable state into flags and restores it on unlock. Use this instead of spin_lock_irq whenever the interrupt state before the lock is unknown.
Reader-writer spinlocks
rwlock_t allows many concurrent readers but only one writer at a time:
The kernel community is actively removing
rwlock_t from most subsystems because the read-write overhead exceeds the benefit for short critical sections. Prefer RCU for read-heavy data structures.Mutexes
Mutexes are sleeping locks defined ininclude/linux/mutex.h. The holder is put to sleep when the mutex is contended, freeing the CPU for other work. Mutexes have strict semantics enforced by CONFIG_DEBUG_MUTEXES:
- Only one task may hold the mutex at a time.
- Only the owner may unlock it.
- Recursive locking is not permitted.
- A mutex must not be used in hardware or software interrupt context.
- A task must not exit while holding a mutex.
RCU: Read-Copy Update
RCU (Read-Copy Update) is a synchronization mechanism optimised for read-heavy workloads. Readers hold no locks, incur no cache traffic, and never block. Writers make a copy of the data, modify the copy, then atomically publish it; the old version is freed only after all pre-existing readers have finished.| API | Purpose |
|---|---|
rcu_read_lock() / rcu_read_unlock() | Mark the boundaries of an RCU read-side critical section. |
rcu_dereference(p) | Safely dereference an RCU-protected pointer inside a read-side section. |
rcu_assign_pointer(p, v) | Publish a new pointer value with the correct memory barrier. |
synchronize_rcu() | Sleep until all pre-existing RCU readers have completed (writer side). |
call_rcu(&head, func) | Asynchronous variant: calls func after a grace period (no sleeping). |
Atomic operations
The kernel provides atomic integer types and operations that execute as a single indivisible instruction without any lock:atomic_dec_and_test is commonly used for reference counting:
Prefer
refcount_t over atomic_t for reference counting. refcount_t adds overflow and underflow detection and is harder to misuse. It is defined in include/linux/refcount.h.set_bit(), clear_bit(), test_bit(), and test_and_set_bit().
Semaphores
struct semaphore is a counting semaphore defined in include/linux/semaphore.h. It allows a configurable number of concurrent holders.
Reader-writer semaphores
rw_semaphore allows multiple concurrent readers or one exclusive writer:
Choosing the right primitive
Short critical section with possible interrupt context
Short critical section with possible interrupt context
Use spinlock. If the lock is ever taken in a hardware interrupt handler, use
spin_lock_irqsave. If only in softirq/tasklet, use spin_lock_bh. For process-context-only code, use spin_lock.Longer critical section, process context only
Longer critical section, process context only
Use mutex. The lock holder sleeps if contended, which avoids wasting CPU cycles. Mutexes integrate with lockdep and priority inheritance (on RT kernels).
Read-heavy data, infrequent writes
Read-heavy data, infrequent writes
Use RCU. Readers have zero overhead and never block. Writers copy the data, update, and publish atomically. Use
synchronize_rcu or call_rcu to defer freeing the old version.Reference counting
Reference counting
Use refcount_t or, if you need additional atomic arithmetic,
atomic_t. Never use locks for simple reference counting — the atomic primitives are sufficient and far cheaper.Signalling between tasks
Signalling between tasks
Use completion (
include/linux/completion.h). One task calls wait_for_completion() and another calls complete(). This is cleaner than using a semaphore as a one-shot signal.Common locking pitfalls
Lock ordering violations
Lock ordering violations
Acquiring two or more locks in different orders on different code paths will deadlock. Establish a global lock ordering and always acquire locks in that order. Enable
CONFIG_LOCKDEP during development — lockdep tracks acquisition sequences and reports ordering violations.Sleeping in atomic context
Sleeping in atomic context
Calling any function that may sleep —
kmalloc(GFP_KERNEL), mutex_lock(), msleep(), copy_from_user() — while holding a spinlock or in interrupt context will hang or corrupt the kernel. Use GFP_ATOMIC for allocations inside spinlock-protected sections or interrupt handlers.Missing memory barriers
Missing memory barriers
On architectures with weak memory ordering (ARM, POWER), accesses may be reordered by the CPU or compiler. The RCU primitives (
rcu_assign_pointer, rcu_dereference) include the correct barriers. For other patterns, use smp_store_release() / smp_load_acquire() or the WRITE_ONCE() / READ_ONCE() macros.Forgetting to unlock on all error paths
Forgetting to unlock on all error paths
A function that acquires a lock must release it on every return path, including error paths. Use
goto labels with cleanup paths or the scoped_guard() macro (available since 6.4) to ensure the lock is always released.Further reading
Memory management
GFP flags determine whether an allocation may sleep, which directly affects which lock is safe to hold.
Filesystems
The VFS uses a combination of mutexes, spinlocks, and RCU to protect inodes, dentries, and superblocks.
