R0KernelPath
module 079 / 262

Kernel Synchronization: Spinlocks, RCU, Atomics, Barriers

Kernel synchronization is the art of keeping shared data sane when dozens of CPUs and interrupt handlers all reach for it at once — get it wrong and the kernel silently corrupts itself.

~18h focused study10 concepts

Overview

Modern Linux runs on machines with 256 hardware threads, where any two CPUs can simultaneously touch the same linked list, counter, or pointer. The kernel has no luxury of "just use a mutex" — interrupt handlers cannot sleep, real-time tasks cannot stall, and a 10-nanosecond spin might be cheaper than a 1-microsecond context switch. This module is about how the kernel solves the hardest class of concurrent-programming problems: shared mutable state with strict latency and no user-space safety nets.

The synchronization primitives you will study here form a hierarchy of trade-offs. At the bottom sit raw memory barriers and atomic operations — single-instruction guarantees straight from the CPU. Above them sit spinlocks, which burn CPU cycles "spinning" rather than sleeping, making them safe for interrupt context. Then come RCU (Read-Copy-Update), the kernel's most elegant primitive, which allows thousands of readers to proceed with zero overhead while a single writer copies and patches. Finally, seqlocks and per-CPU variables round out the toolkit for specialized hot paths.

What makes kernel locking "brutal" is the sheer number of orthogonal concerns that collide simultaneously: hardware memory models that allow CPUs to reorder loads and stores; interrupt handlers that can preempt a critical section mid-instruction; NMIs (non-maskable interrupts) that fire even inside a spinlock; NUMA systems where a cache line crossing a socket boundary takes 300 ns; and the kernel's own lock-ordering rules that, if violated, cause deadlocks that only appear in production at 3 AM. lockdep, the kernel's runtime lock-dependency validator, is a piece of engineering art specifically designed to find these bugs before they escape.

By the end of this module you will not just know the API — you will understand why each primitive exists, what it costs, and when to reach for it. That understanding is what separates kernel contributors from kernel readers.

why this matters for the kernel

Every non-trivial kernel subsystem you will ever touch — the VFS, the memory allocator, the network stack, the scheduler — uses the primitives in this module. Before you can read a kernel patch, review someone else's driver, or submit your own change, you need to be able to answer: "Is this lock held when that function is called? Could an interrupt fire here? Is this read guaranteed to see the write on another CPU?" These are not theoretical questions; they are the actual review checklist maintainers use before accepting a patch to lkml.

Beyond kernel contribution, this material is foundational to the entire field of systems programming. The ideas here — lock-free data structures, the happens-before relation, read-side scalability, memory ordering — appear in databases, distributed systems, and high-frequency trading infrastructure. Studying them in the kernel is ideal because the kernel's constraints are the most extreme, its code is open and annotated, and its community has produced some of the best writing on the subject anywhere (LWN.net alone is a graduate-level resource on this topic). Mastering kernel synchronization gives you a mental model that makes every other concurrent system you encounter look simple by comparison.

RCU Grace Period: Reader Safety Through Deferred Reclamation

rendering diagram…

RCU write sequence: the writer publishes the new pointer atomically, then waits for a full grace period before freeing the old object. Reader CPU1 held a reference to the old pointer the entire time — synchronize_rcu() ensures it completes before the free.

The concepts

01Why Kernel Locking Is Different From Userspace

In userspace, when two threads conflict on a mutex, the loser sleeps — the OS scheduler parks it and gives the CPU to someone else. This is fine when you are a web server. In the kernel, large swaths of code cannot sleep:

  • Interrupt handlers (hardirq context) run on the current CPU's stack with interrupts disabled. If they block, the CPU is wedged forever.
  • Softirq / tasklet context is similarly non-sleepable.
  • Spinlock critical sections must complete without sleeping, or the system deadlocks.
  • Atomic context (any code that has called preempt_disable() or local_irq_disable()) cannot call anything that might sleep.

The kernel therefore has two broad categories of locks: sleeping locks (mutexes, semaphores, rw_semaphore) that can only be taken from process context where sleeping is legal, and spinning locks (spinlocks, rwlocks) that burn CPU cycles waiting and can be used anywhere — including interrupt handlers.

A second difference is priority inversion. The kernel's real-time (PREEMPT_RT) patches flip some spinlocks into sleeping mutexes to bound latency, which shows that even this division is not permanent — it is driven by scheduling policy.

A third brutal reality: the kernel runs on weakly-ordered hardware. x86 gives you a fairly strong memory model, but ARM and RISC-V do not. A store on CPU 0 might not be visible on CPU 1 until you emit a barrier instruction. The kernel's synchronization primitives embed the right barriers so that callers do not have to think about this — but when you write new locking code, you must.

/* This is WRONG — sleeping inside a spinlock */
static DEFINE_SPINLOCK(my_lock);

void bad_function(void)
{
    spin_lock(&my_lock);        /* interrupts disabled, preemption off */
    kmalloc(4096, GFP_KERNEL);  /* BUG: GFP_KERNEL may sleep! */
    spin_unlock(&my_lock);
}

/* CORRECT: use GFP_ATOMIC in atomic context */
void good_function(void)
{
    spin_lock(&my_lock);
    kmalloc(4096, GFP_ATOMIC);  /* never sleeps; may fail */
    spin_unlock(&my_lock);
}

02Atomic Operations and atomic_t

The simplest building block of kernel synchronization is the atomic operation: a read-modify-write that the hardware guarantees completes without interruption on that CPU, and (with barriers) is visible to all other CPUs in a defined order.

Linux wraps these in atomic_t (32-bit) and atomic64_t (64-bit). The implementation maps to a single locked instruction on x86 (lock xadd, lock cmpxchg, etc.) and to the ldxr/stxr exclusive-access loop on ARM64.

Key API:

atomic_t v = ATOMIC_INIT(0);
atomic_set(&v, 5);           // v = 5 (not SMP-safe for compound ops)
atomic_read(&v);             // read current value
atomic_inc(&v);              // v++, atomic
atomic_dec_and_test(&v);     // --v; return (v == 0)
atomic_add(n, &v);           // v += n
atomic_cmpxchg(&v, old, new);// if v==old: v=new; return old

Why atomic_read is not just v.counter: On platforms where int access is not naturally atomic (rare today but must be portable), a plain read could tear. More importantly, atomic_read implies a compiler barrier — the compiler cannot cache the value in a register across the read.

Atomics do NOT replace locks for compound operations. atomic_inc(&v); if (atomic_read(&v) == 1) { ... } is still racy because another CPU can increment between the two operations. Atomics are correct for: reference counting, statistics counters, flags, and the compare-and-swap primitive used to build lock-free structures.

Memory ordering of atomics: By default, atomic_inc on x86 is a fully-ordered locked instruction. On ARM64, the kernel uses staddl (acquire-release). For performance, the kernel exposes relaxed variants: atomic_inc_relaxed, atomic_add_return_acquire, etc., which map to weaker barrier instructions where the architecture supports them.

/* Reference counting — the canonical use of atomic_t */
struct my_object {
    atomic_t refcount;
    /* ... other fields ... */
};

void my_get(struct my_object *obj)
{
    atomic_inc(&obj->refcount);
}

void my_put(struct my_object *obj)
{
    /* dec_and_test: returns true when counter hits zero */
    if (atomic_dec_and_test(&obj->refcount))
        kfree(obj);  /* last reference dropped */
}

03Memory Barriers: smp_mb, smp_rmb, smp_wmb, and Friends

Modern CPUs and compilers reorder memory operations for performance. This is invisible in single-threaded code but catastrophic in SMP (multi-processor) code without explicit barriers.

The three culprits:

  1. Compiler reordering — the compiler may move a store before a load if it thinks the locations do not alias.
  2. CPU out-of-order execution — the CPU may retire stores to its store buffer before they reach the cache-coherent domain.
  3. CPU store-load reordering — on weakly-ordered ISAs (ARM, RISC-V, POWER), a load can be satisfied from the store buffer of a different CPU's pending writes if the cache line is shared.

The Linux barrier API:

MacroMeaning
barrier()Compiler-only fence. Zero cost at runtime.
smp_rmb()Read memory barrier. All prior loads complete before any subsequent load.
smp_wmb()Write memory barrier. All prior stores complete before any subsequent store.
smp_mb()Full memory barrier. All loads/stores before are globally visible before any load/store after.
smp_store_release(p, v)Store with release semantics (pairs with smp_load_acquire).
smp_load_acquire(p)Load with acquire semantics.

On x86, smp_rmb() and smp_wmb() compile to just a compiler barrier (x86 already orders loads and stores among themselves). smp_mb() compiles to mfence or a locked instruction. On ARM64, they compile to dmb ish (data memory barrier, inner-shareable domain).

Acquire/release pairs are the preferred modern idiom. A release store (smp_store_release) guarantees all prior operations are visible before the store. An acquire load (smp_load_acquire) guarantees the load completes before any subsequent operation. Used together they form a happens-before edge: anything done before the release is seen after the acquire.

Classic example — flag-based communication:

CPU 0 (writer):           CPU 1 (reader):
  data = 42;              while (!READ_ONCE(flag)) ;
  smp_wmb();              smp_rmb();
  WRITE_ONCE(flag, 1);    printk("%d\n", data); // must print 42

Without smp_wmb() on CPU 0, the flag store could become globally visible before the data store on a weakly-ordered CPU, and the reader would see flag==1 but stale data.

/* Producer/consumer with explicit barriers (educational — 
   prefer smp_store_release/smp_load_acquire in real code) */

static int shared_data __cacheline_aligned;
static int ready;  /* flag */

void producer(void)
{
    shared_data = 42;       /* 1. write payload */
    smp_wmb();              /* 2. ensure payload visible before flag */
    WRITE_ONCE(ready, 1);   /* 3. publish */
}

void consumer(void)
{
    while (!READ_ONCE(ready)) /* spin until flag seen */
        cpu_relax();
    smp_rmb();              /* ensure flag load completes before data load */
    pr_info("data = %d\n", shared_data); /* guaranteed to see 42 */
}

04Spinlocks: The Workhorse of Interrupt-Safe Locking

A spinlock is a lock that, when contended, repeatedly tests the lock variable in a tight loop ("spins") rather than sleeping. This makes it usable in contexts where sleeping is illegal — including interrupt handlers.

How it works on x86: The spin_lock implementation ultimately executes something like:

1:  lock bts dword [rdi], 0    ; atomically set bit 0; CF = old value
    jc 1b                       ; if was set (locked), spin

On ARM64 it uses the load-exclusive / store-exclusive (ldaxr/stxr) loop. Modern kernels use a ticket spinlock (or on ARM64, a queued spinlock / qspinlock) to ensure fairness — CPUs acquire the lock in the order they arrived, preventing starvation.

The API:

DEFINE_SPINLOCK(lock);        /* static allocation */
spinlock_t lock;
spin_lock_init(&lock);        /* dynamic allocation */

spin_lock(&lock);             /* acquire; disables preemption */
spin_unlock(&lock);           /* release; re-enables preemption */

spin_lock_irqsave(&lock, flags);   /* also disables local IRQs */
spin_unlock_irqrestore(&lock, flags);

spin_lock_irq(&lock);         /* disable IRQs (don't save flags) */
spin_unlock_irq(&lock);

spin_lock_bh(&lock);          /* disable softirq only */
spin_unlock_bh(&lock);

Which variant to use:

  • spin_lock / spin_unlock: safe when neither your code nor anything that shares this lock runs in interrupt context.
  • spin_lock_irqsave: use when the lock is shared with an interrupt handler. This disables local interrupts so the handler cannot fire and try to take the same lock (which would deadlock, since spinlocks are not reentrant).
  • spin_lock_bh: use when the lock is shared with a softirq/tasklet but not a hardirq.

Critical rules:

  1. Never sleep inside a spinlock critical section.
  2. Never call a function that might sleep (kmalloc with GFP_KERNEL, copy_from_user, msleep, schedule) while holding a spinlock.
  3. Keep critical sections short. Every nanosecond you hold a spinlock, other CPUs wanting it are burning cycles.
  4. Spinlocks are not reentrant. Taking a lock you already hold = deadlock.

Read/write spinlocks (rwlock_t) allow multiple concurrent readers but exclusive writers, using read_lock/read_unlock and write_lock/write_unlock. In practice these are less common now because RCU is preferred for read-heavy workloads.

/* Driver with IRQ-safe spinlock protecting a hardware FIFO */
struct my_dev {
    spinlock_t  lock;
    struct list_head tx_queue;
};

/* Called from process context (write syscall) */
int my_enqueue(struct my_dev *dev, struct packet *pkt)
{
    unsigned long flags;

    /* IRQ save: the tx interrupt handler also touches tx_queue */
    spin_lock_irqsave(&dev->lock, flags);
    list_add_tail(&pkt->list, &dev->tx_queue);
    spin_unlock_irqrestore(&dev->lock, flags);
    return 0;
}

/* Called from hardirq context (tx complete interrupt) */
irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_dev *dev = data;
    struct packet *pkt;

    spin_lock(&dev->lock);  /* no irqsave needed: IRQs already off in hardirq */
    pkt = list_first_entry_or_null(&dev->tx_queue, struct packet, list);
    if (pkt)
        list_del(&pkt->list);
    spin_unlock(&dev->lock);

    if (pkt)
        kfree(pkt);
    return IRQ_HANDLED;
}

05RCU: Read-Copy-Update — The Crown Jewel

RCU is the most intellectually beautiful synchronization primitive in the Linux kernel. Its core insight: if readers never modify shared data and are short enough, you can let them run completely lock-free, and defer the destruction of old data until all active readers finish.

The three rules of RCU:

  1. Readers use rcu_read_lock() / rcu_read_unlock() (which on non-PREEMPT kernels just disables preemption — zero overhead on the hot path). They dereference RCU-protected pointers with rcu_dereference().
  2. Writers never modify data in place. They: (a) copy the object, (b) modify the copy, (c) atomically publish the new pointer with rcu_assign_pointer(), (d) call synchronize_rcu() (or call_rcu() for async), which waits until all CPUs have passed through a quiescent state — a point where no CPU is inside an RCU read-side critical section. Only then is the old object freed.
  3. The grace period: the interval between the pointer update and when the old object can be freed. After one grace period, no reader can hold a reference to the old object (because RCU read-side critical sections cannot span a quiescent state — a context switch, on preemptible kernels; or a schedule-out, on PREEMPT_RT).

Why this is so powerful: On a machine with 256 cores and a read-mostly data structure (like the routing table, which is read billions of times per second and updated rarely), replacing a global rwlock with RCU can give a 100x throughput improvement on the read side, because readers on all 256 cores proceed simultaneously with no cache-line contention.

The cost: Writers are slower (copy + grace period wait). The grace period can be milliseconds on a loaded system. RCU read-side sections must not sleep (on non-PREEMPT kernels) and must be short. Debugging RCU misuse is hard — rcu_dereference() without rcu_read_lock() is a common bug that lockdep/sparse can catch.

Variants:

  • synchronize_rcu() — synchronous, blocks until grace period.
  • call_rcu(&head, func) — async; func is called after grace period.
  • rcu_barrier() — wait for all pending call_rcu callbacks.
  • SRCU (Sleepable RCU) — allows sleeping in read-side critical sections at the cost of per-SRCU-domain state.
  • QRCU, Tasks RCU — for even more specialized contexts.
/* RCU-protected global pointer to a config struct */
struct config {
    int timeout;
    char name[64];
};

static struct config __rcu *global_cfg;  /* __rcu annotation for sparse */

/* READER — runs on hot path, potentially millions of times/sec */
void read_timeout(void)
{
    struct config *cfg;
    int t;

    rcu_read_lock();                        /* disable preemption */
    cfg = rcu_dereference(global_cfg);      /* load pointer safely */
    t = cfg->timeout;                       /* read field while lock held */
    rcu_read_unlock();                      /* re-enable preemption */

    pr_info("timeout = %d\n", t);
}

/* WRITER — called rarely (config reload) */
void update_config(int new_timeout)
{
    struct config *old_cfg, *new_cfg;

    new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);

    /* Copy old values, modify copy */
    old_cfg = rcu_dereference_protected(global_cfg,
                  lockdep_is_held(&my_writer_mutex));
    *new_cfg = *old_cfg;           /* copy */
    new_cfg->timeout = new_timeout; /* modify copy */

    rcu_assign_pointer(global_cfg, new_cfg); /* publish atomically */

    synchronize_rcu();  /* wait: all readers now see new_cfg or will */
    kfree(old_cfg);     /* safe to free: no reader holds old pointer */
}

06Seqlocks: Fast Writer, Safe Reader

A seqlock (sequence lock) is optimized for the case where writes are rare but must be fast, and readers can tolerate retrying. The canonical use in the kernel is jiffies_64 and xtime (wall clock time) — the timer interrupt updates them frequently but reads are also frequent.

How it works:

  • A sequence counter is embedded in the lock.
  • Writers take an exclusive spinlock, increment the counter (making it odd = write in progress), do the update, increment again (making it even = done).
  • Readers sample the counter before and after reading. If the counter changed (or was odd during the read), a writer was active — the reader retries. No reader-side lock at all.

Tradeoffs:

  • Read-side is wait-free in the uncontended case (just two reads of the counter).
  • Readers may loop indefinitely if writers are very frequent — starvation is theoretically possible but rare in practice.
  • The data being protected must be copyable atomically enough for the retry to be safe (no pointers that could be freed mid-read — use RCU for pointer-heavy structures).
  • Writers can never be starved by readers (contrast with rwlock where a flood of readers can lock out a writer).

API:

seqlock_t sl = SEQLOCK_UNLOCKED;

// Writer:
write_seqlock(&sl);
/* modify data */
write_sequnlock(&sl);

// Reader:
unsigned seq;
do {
    seq = read_seqbegin(&sl);
    /* read data into local copies */
} while (read_seqretry(&sl, seq));
/* Seqlock protecting a 64-bit timestamp on a 32-bit system
   (where a 64-bit read is not atomic without a lock) */

static seqlock_t time_lock = SEQLOCK_UNLOCKED;
static u64 current_ns;

/* Called from timer interrupt — must be fast */
void update_time(u64 ns)
{
    write_seqlock(&time_lock);
    current_ns = ns;
    write_sequnlock(&time_lock);
}

/* Called from any context — readers retry if interrupted by writer */
u64 get_time_ns(void)
{
    unsigned seq;
    u64 ns;

    do {
        seq = read_seqbegin(&time_lock);
        ns  = current_ns;          /* may be torn if writer active */
    } while (read_seqretry(&time_lock, seq)); /* retry if seq changed */

    return ns;  /* guaranteed consistent */
}

07Per-CPU Variables: Eliminate Sharing Entirely

The best lock is no lock at all. Per-CPU variables give each CPU its own private copy of a variable, eliminating all contention. There is no sharing, therefore no synchronization needed between CPUs.

This is used heavily for: CPU-local statistics counters, per-CPU memory allocator caches (slab/slub), the scheduler's run queues, and the current task pointer.

Declaration:

DEFINE_PER_CPU(int, my_counter);
/* or dynamically: alloc_percpu(int) */

Access:

/* Safely read/write THIS CPU's copy */
int val = get_cpu_var(my_counter);  /* disables preemption */
get_cpu_var(my_counter)++;          /* modify */
put_cpu_var(my_counter);            /* re-enables preemption */

/* Raw access (caller ensures no preemption/migration) */
this_cpu_inc(my_counter);           /* optimized; uses this_cpu_* accessors */
this_cpu_add(my_counter, 5);

/* Access another CPU's copy (for aggregation) */
int cpu;
long total = 0;
for_each_online_cpu(cpu)
    total += per_cpu(my_counter, cpu);

The preemption danger: If a task reads get_cpu_var(x), then gets preempted and migrated to CPU 1, and then writes, it writes to CPU 0's variable while running on CPU 1 — a bug. get_cpu_var prevents this by disabling preemption. The this_cpu_* accessors are for code that has already guaranteed no migration (e.g., inside a spinlock-protected section or with preemption already disabled).

Memory layout: Per-CPU variables are allocated in a contiguous block per CPU at boot, with each CPU's block placed on a different NUMA node if possible. The kernel uses segment-register tricks on x86 (%gs base) or a dedicated tp register on RISC-V/ARM64 to efficiently address the current CPU's section.

/* Per-CPU stats counter — zero-overhead on the fast path */
DEFINE_PER_CPU(unsigned long, packets_received);

/* Called on every received packet — must be blazing fast */
void count_packet(void)
{
    /* this_cpu_inc is a single instruction on x86 (no lock prefix needed!) */
    this_cpu_inc(packets_received);
}

/* Called from /proc read — aggregates across all CPUs */
unsigned long total_packets(void)
{
    unsigned long total = 0;
    int cpu;

    for_each_online_cpu(cpu)
        total += per_cpu(packets_received, cpu);

    return total;
}

08Mutex vs Spinlock: Choosing the Right Primitive

Both mutex and spinlock protect critical sections, but they make opposite trade-offs:

Propertyspinlockmutex
Contended behaviorSpins (burns CPU)Sleeps (blocks)
Can be used in IRQ contextYesNo
Can sleep while heldNoYes
Overhead uncontended~1 ns (cache hit)~10-30 ns
Overhead contendedWastes CPUContext switch
Max hold timeMicrosecondsUnbounded

The kernel mutex API (not pthread_mutex — a different, kernel-internal type):

DEFINE_MUTEX(my_mutex);
mutex_lock(&my_mutex);      /* may sleep */
mutex_unlock(&my_mutex);
mutex_trylock(&my_mutex);   /* non-blocking; returns 1 if acquired */
mutex_lock_interruptible(&my_mutex); /* returns -EINTR if signal arrives */

Decision rule:

  • Use spinlock when: the critical section is short (< a few microseconds), the code might run in interrupt context, or you need the lock only on a single CPU path where spinning is cheaper than a context switch.
  • Use mutex when: the critical section might sleep (I/O, kmalloc with GFP_KERNEL, copy_from_user, condition waits), or when long hold times are expected.
  • Use RCU when: the data structure is read-mostly and you can tolerate the write complexity.
  • Use per-CPU when: each CPU logically has its own instance of the data.
  • Use atomics when: you need a single counter or flag with no compound operations.

A common mistake: using a spinlock for a long critical section because "mutexes are too slow." A mutex that sleeps releases the CPU; a spinlock that spins for 10 ms holds the CPU hostage, starving every other thread on that core.

/* A file-system-style object with two different locks:
   - spinlock for the fast-path inode cache lookup (IRQ-safe)
   - mutex for the slow-path disk read (may sleep) */
struct my_inode {
    spinlock_t   cache_lock;  /* protects: cached_data, cache_valid */
    struct mutex io_mutex;    /* protects: disk I/O in progress */
    char        *cached_data;
    bool         cache_valid;
};

char *fast_read(struct my_inode *ino)
{
    char *data = NULL;
    spin_lock(&ino->cache_lock);
    if (ino->cache_valid)
        data = ino->cached_data;
    spin_unlock(&ino->cache_lock);
    return data;              /* NULL means: go to slow path */
}

char *slow_read(struct my_inode *ino)
{
    mutex_lock(&ino->io_mutex);  /* may sleep waiting for disk */
    /* ... read from disk ... */
    spin_lock(&ino->cache_lock);
    ino->cache_valid = true;
    spin_unlock(&ino->cache_lock);
    mutex_unlock(&ino->io_mutex);
    return ino->cached_data;
}

09lockdep: The Kernel's Lock Validator

lockdep is a runtime lock dependency checker built into the kernel (enabled with CONFIG_LOCKDEP=y, automatically enabled when CONFIG_PROVE_LOCKING=y). It is one of the most impressive pieces of kernel infrastructure ever written.

What it does: lockdep tracks every lock acquisition and builds a directed graph of lock-acquisition order. If CPU 0 ever does A → B (acquires A then B) and CPU 1 does B → A, lockdep immediately prints a deadlock warning — even if the actual deadlock has not happened yet. It catches the potential for a circular lock dependency the first time the ordering is observed.

What it catches:

  • AB-BA deadlocks (classic deadlock)
  • Taking a sleeping lock (mutex) while holding a spinlock
  • Acquiring a lock in interrupt context that is also acquired in process context without proper irqsave
  • Lock-order inversion across interrupt boundaries
  • Taking an already-held non-reentrant lock

Example lockdep output:

======================================================
WARNING: possible circular locking dependency detected
------------------------------------------------------
kworker/0:1/37 is trying to acquire lock:
 (&b->lock){+.+.}, at: b_lock+0x14/0x30
but task is already holding lock:
 (&a->lock){+.+.}, at: a_lock+0x14/0x30
the existing dependency chain (in reverse order) is:
 -> #1 (&a->lock):
       lock_acquire+0xd4/0x110
       ...
-> #0 (&b->lock):
       ...

Lock classes: lockdep does not track individual lock instances — it tracks lock classes (all locks of the same type, e.g., all struct inode i_lock instances). This lets it validate ordering rules globally from a handful of observations.

Using lockdep annotations in your code:

lockdep_assert_held(&my_lock);    /* assert lock is held here */
lockdep_assert_irqs_disabled();   /* assert IRQs are off */

Building a kernel with lockdep: In make menuconfig, enable Kernel hacking → Lock debugging (spinlocks, mutexes, etc.). Boot the resulting kernel and run your subsystem's tests. Any detected issue will print a full stack trace to dmesg.

/* Correct lock ordering: always acquire lock_A before lock_B.
   lockdep will warn if any code path does B before A. */

static DEFINE_SPINLOCK(lock_A);
static DEFINE_SPINLOCK(lock_B);

void correct_function(void)
{
    spin_lock(&lock_A);   /* always first */
    spin_lock(&lock_B);   /* always second */
    /* ... critical section ... */
    spin_unlock(&lock_B);
    spin_unlock(&lock_A);
}

void buggy_function(void)
{
    spin_lock(&lock_B);   /* WRONG ORDER — lockdep will catch this */
    spin_lock(&lock_A);
    /* ... */
    spin_unlock(&lock_A);
    spin_unlock(&lock_B);
    /* If correct_function and buggy_function run concurrently
       on two CPUs, they will DEADLOCK. lockdep reports this
       the moment buggy_function is first called. */
}

/* Using lockdep assertion to document invariants */
void must_hold_A(void)
{
    lockdep_assert_held(&lock_A); /* BUG() if not held */
    /* ... */
}

10WRITE_ONCE / READ_ONCE and the Compiler Danger

Even without SMP concerns, the compiler itself is your enemy when writing concurrent code. Two specific problems:

1. Invented loads (load tearing): The compiler may read a variable multiple times even if you only wrote one load in C, if it needs to spill/reload a register. If another CPU changes the variable between the two reads, you get inconsistent values within a single "read."

2. Store sinking / load floating: The compiler may reorder accesses to different locations freely, as long as single-threaded semantics are preserved. In a concurrent context, this reordering is a bug.

READ_ONCE(x) and WRITE_ONCE(x, v) solve both problems by:

  • Emitting exactly one load or store in the generated assembly.
  • Acting as a compiler barrier (prevents the compiler from moving other accesses across them).
  • Marking the access as volatile-ish in a targeted way (unlike volatile, which is too broad and misleading).

When to use them:

  • Shared variables accessed without locks (e.g., the RCU "ready" flag, a stop condition checked in a polling loop).
  • Any variable that can be modified by another CPU, interrupt handler, or memory-mapped hardware register — without a full lock/barrier pair.
  • In lock-free algorithms where you are intentionally relying on hardware ordering (and providing your own smp_* barriers).

Do not use volatile in kernel code. The kernel style guide explicitly forbids it for synchronization purposes because it is both too weak (no barrier semantics) and too broad (prevents many useful compiler optimizations). Use READ_ONCE/WRITE_ONCE and explicit barriers instead.

/* Thread A sets a flag; Thread B polls it.
   Without READ_ONCE, the compiler may hoist the load out of the loop
   and spin forever even after the flag is set. */

static int stop_flag;  /* shared between CPU 0 and CPU 1 */

/* CPU 0: signal stop */
void request_stop(void)
{
    WRITE_ONCE(stop_flag, 1);  /* single store, compiler barrier */
}

/* CPU 1: worker loop */
void worker(void)
{
    while (!READ_ONCE(stop_flag)) {  /* compiler cannot cache this */
        do_work();
    }
    pr_info("stopped\n");
}

/* BAD version — compiler may transform while(!stop_flag) into:
   if (!stop_flag) while(1) { do_work(); }  <- infinite loop!
   because it "knows" stop_flag cannot change in this thread. */
void bad_worker(void)
{
    while (!stop_flag) {   /* no READ_ONCE — compiler can hoist */
        do_work();
    }
}

Key insights

  • ✦Kernel locking splits into two worlds: sleeping locks (process context only) and spinning locks (usable everywhere including IRQ handlers). Getting this wrong causes deadlocks that are difficult to reproduce.
  • ✦Memory barriers are not about performance tuning — on weakly-ordered CPUs (ARM, RISC-V, POWER), they are required for correctness. A missing smp_wmb() is a silent data-corruption bug.
  • ✦RCU's genius is making the read side free by shifting all complexity to the write side. For read-mostly data structures, this can provide 100x better scalability than rwlocks.
  • ✦WRITE_ONCE/READ_ONCE are about the compiler, not just the hardware. The compiler can legally transform a loop with an unprotected shared variable into an infinite loop — this is not a bug in the compiler.
  • ✦lockdep catches deadlocks before they happen by detecting circular lock-dependency graphs at the first ordering observation. Always develop with CONFIG_PROVE_LOCKING enabled.
  • ✦Per-CPU variables achieve zero contention by eliminating sharing entirely. The best synchronization is no synchronization.
  • ✦Spinlock critical sections must be microseconds-short. A spinlock held for milliseconds is a latency bomb; the correct tool for long critical sections is a mutex.
  • ✦Atomic operations are not a substitute for locks on compound check-then-act operations. atomic_read followed by atomic_inc is still a race condition.

Common pitfalls

  • ⚠Calling kmalloc(GFP_KERNEL) or any sleeping function inside a spinlock critical section — this can deadlock or corrupt the scheduler state. Always use GFP_ATOMIC in atomic contexts.
  • ⚠Using spin_lock() when the same lock is taken in an interrupt handler — the interrupt can fire while you hold the lock, try to take it again, and spin forever (deadlock). Always use spin_lock_irqsave() when sharing with ISRs.
  • ⚠Forgetting to call synchronize_rcu() before freeing old RCU-protected memory — readers may still hold stale pointers to the freed object, causing use-after-free.
  • ⚠Accessing a per-CPU variable without disabling preemption — a task can be migrated to another CPU between the per_cpu() address computation and the actual access, writing to the wrong CPU's slot.
  • ⚠Using volatile instead of READ_ONCE/WRITE_ONCE — volatile provides no barrier semantics and the kernel style guide prohibits it for concurrency. It will not prevent the compiler optimizations that READ_ONCE prevents.
  • ⚠Acquiring locks in different orders in different code paths — lockdep will catch this, but only if CONFIG_PROVE_LOCKING is enabled during testing. Always establish and document a global lock ordering.
  • ⚠Using an rcu_dereference() outside an rcu_read_lock() section — the pointer load gets no compiler ordering guarantee and sparse/lockdep will warn. Use rcu_dereference_protected() in writer paths instead.
  • ⚠Holding a mutex across a spin_lock — this violates the lock hierarchy (sleeping lock inside spinning lock is impossible) and will deadlock.

Practice

  1. 01
    medium

    Build a kernel module that implements a simple integer counter protected three different ways: with a spinlock, with an atomic_t, and with a per-CPU variable. Export /proc/counter_spin, /proc/counter_atomic, and /proc/counter_percpu. Write a user-space program that spawns 16 threads each incrementing via ioctl or /proc writes 1,000,000 times. Verify all three give the correct total of 16,000,000. Measure the wall-clock time for each approach and explain the performance difference.

  2. 02
    medium

    Write a kernel module that deliberately creates a circular lock dependency (AB-BA deadlock potential) between two spinlocks in two different functions, but structured so the actual deadlock never triggers during your test. Boot a kernel with CONFIG_PROVE_LOCKING=y, load your module, call both functions, and capture the lockdep warning from dmesg. Then fix the ordering and confirm the warning disappears.

  3. 03
    hard

    Implement a minimal RCU-protected linked list in a kernel module. Support three operations: rcu_list_add (writer), rcu_list_del (writer), and rcu_list_find (reader). Writers must hold a mutex for mutual exclusion among themselves. Readers must use rcu_read_lock(). Test with two writer kthreads (adding/removing nodes) and eight reader kthreads running concurrently for 30 seconds. Verify no use-after-free using CONFIG_KASAN=y.

  4. 04
    hard

    Write a user-space program (not a kernel module) that demonstrates memory reordering. Use two threads sharing two global variables X and Y, both initially 0. Thread 1 sets X=1 then reads Y. Thread 2 sets Y=1 then reads X. Run in a loop until you observe both threads reading 0 (which requires store-load reordering). Then add __sync_synchronize() (GCC full barrier) and confirm you can no longer observe that outcome. Run on x86 and on an ARM device if available and compare how often the reordering occurs.

  5. 05
    easy

    Find a real kernel subsystem that uses seqlocks (hint: look at kernel/time/timekeeping.c or fs/dcache.c) and trace the complete read path: where is read_seqbegin called, what data is read, where is read_seqretry called, and what triggers a retry in practice. Write a one-page explanation of why RCU would not be a good fit for this particular use case and why seqlocks are the right choice.

Resources

📄Linux Kernel Documentation: locking/
docsfree

Official kernel docs covering spinlocks, mutexes, RCU, lockdep, and memory barriers. Start here — written by the maintainers.

✎What is RCU, Fundamentally? (LWN.net)
articlefree

Paul McKenney's definitive three-part LWN series on RCU — required reading; no better explanation exists anywhere.

📄Linux Kernel Memory Barriers
docsfree

The authoritative 80-page document on Linux memory ordering. Dense but complete — bookmark and return repeatedly.

📕Is Parallel Programming Hard, And, If So, What Can You Do About It?
bookfree

Paul McKenney's free textbook — the deepest treatment of SMP concurrency in the Linux ecosystem, includes RCU, memory models, and lock-free algorithms.

📄Unreliable Guide to Locking (Rusty Russell)
docsfree

Excellent practical guide to kernel locking written with humor and precision. Great for building intuition quickly.

📕Linux Device Drivers, 3rd Edition (Corbet, Rubini, Kroah-Hartman)
bookfree

Chapter 5 covers concurrency and race conditions with practical driver examples. Slightly dated but conceptually sound.

✎A Formal Kernel Memory-Ordering Model (LKMM)
articlefree

LWN article introducing the Linux Kernel Memory Model — for when you want to go beyond intuition to formal proofs.

📄lockdep design documentation
docsfree

Explains how lockdep's lock-class graph and dependency checking actually work internally.

🔧Compiler Explorer (Godbolt)
toolfree

Use this to see exactly what assembly atomic_inc, smp_mb, and READ_ONCE generate on x86, ARM64, and RISC-V. Essential for building intuition about barrier costs.

Checkpoints — you can now…

  • ✓You can explain to a peer why a spinlock cannot be used with kmalloc(GFP_KERNEL) and which GFP flag to use instead.
  • ✓You can write correct RCU reader and writer code from memory, including rcu_read_lock/unlock, rcu_dereference, rcu_assign_pointer, and synchronize_rcu, and explain what happens during the grace period.
  • ✓You can describe the difference between smp_mb(), smp_rmb(), smp_wmb(), and smp_store_release/smp_load_acquire, and give a concrete example of a bug each one prevents.
  • ✓You can choose between spinlock, mutex, RCU, seqlock, per-CPU, and atomics for a given scenario and justify the choice in terms of context (IRQ-safe?), hold time, read/write ratio, and scalability.
  • ✓You can build a kernel with CONFIG_PROVE_LOCKING=y, trigger a lockdep warning by writing code with inconsistent lock ordering, and read the output to identify the offending code paths.
  • ✓You can explain why WRITE_ONCE/READ_ONCE are needed even on single-CPU systems and why volatile is not an acceptable substitute in kernel code.
  • ✓You can implement a per-CPU counter, correctly aggregate it across CPUs, and explain the preemption hazard that requires get_cpu_var/put_cpu_var.