Why architecture matters here
Architecture matters here because the read/write asymmetry of RCU is only a win when the workload matches it, and it is a spectacular win when it does. Kernel data structures — the dentry cache, network routing tables, the list of loaded modules, IPC namespaces — are read millions of times per second and modified occasionally. Making the read path lock-free and contention-free removes the single biggest scalability bottleneck on many-core machines: the shared cache line that every reader-writer lock forces every reader to touch. RCU readers scale linearly with cores because they share nothing.
The reason readers can be free is the same reason writers are expensive and subtle: RCU never mutates published data in place, so there is nothing a reader can observe half-updated. A reader dereferencing the shared pointer gets a fully-formed old version or a fully-formed new version, never a torn intermediate. But this means every update allocates a new copy and retires the old one, and the old one cannot be freed immediately — freeing it while a reader still holds a pointer to it is a use-after-free, the exact bug RCU must prevent. The architecture's core problem is therefore safe deferred reclamation: knowing when the last reader of the old version has gone.
That knowledge comes from a beautifully cheap observation. RCU read-side critical sections are non-preemptible (in the classic kernel variant) or otherwise bounded, so if every CPU has passed through a quiescent state — a context switch, a return to idle, a point where it provably holds no RCU-protected pointer — since the update was published, then no reader can still hold the old version. The grace period is simply the time until all CPUs have passed through such a state. No reader ever announced itself, no lock was taken, yet the writer can prove safety. Understanding this is understanding why RCU gives near-zero read cost without ever risking a dangling pointer.
It is worth contrasting this with the alternatives it displaces, because the asymmetry only looks free once you see what everyone else pays. A reader-writer lock lets many readers proceed together, but each still executes an atomic operation to acquire and release the lock, and that atomic touches a shared cache line that every core must bounce between them — so read throughput stops scaling exactly when you add cores, the opposite of what you wanted. Atomic reference counting has the same disease: every read increments and decrements a shared counter, writing to shared memory on the hottest path in the system. Hazard pointers avoid the shared write but require each reader to publish which pointer it is using, adding stores and fences. RCU alone lets the reader touch nothing shared at all, and it pays for that by making the writer wait out a grace period — a trade that is lopsidedly good precisely when reads vastly outnumber writes.
The architecture: every piece explained
Top row: the read and publish paths. Readers mark their critical section with rcu_read_lock and rcu_read_unlock — in the classic non-preemptible variant these mostly disable preemption and act as compiler barriers, doing no atomic work. Inside, a reader uses rcu_dereference to load the shared pointer to the current version, with the dependency-ordering barrier that guarantees it sees a fully-initialized structure. The writer builds a new version by copying the current one, mutating the copy, and then publishing it with rcu_assign_pointer — an atomic store with a release barrier so that a reader which sees the new pointer also sees all the writes that built the new version. After the swap, the old version is still reachable by readers that dereferenced the pointer before the swap.
Middle row: reclamation. The grace period is the interval the writer must wait before the old version is unreachable by any reader. The reclaimer frees the old version only after the grace period ends. Detection relies on quiescent states: the RCU subsystem tracks, per CPU, whether that CPU has passed through a state in which it cannot be inside a read-side critical section (a context switch, idle, or user-mode transition). Once every CPU has reported a quiescent state since the update, the grace period is complete — cheaply, without readers ever signaling.
Bottom row: ordering and the writer's two choices. The publish barrier is the memory-ordering discipline that makes the pointer swap safe: release on the writer paired with the dependency/acquire on rcu_dereference. The writer then chooses how to reclaim: synchronize_rcu blocks the writer until a grace period elapses, then it frees the old version inline — simple but the writer waits; or call_rcu registers a callback that the RCU subsystem invokes after the next grace period, so the writer returns immediately and reclamation happens asynchronously — essential on paths that must not block.
The ops strip names what to watch. Grace-period latency is how long reclamation is deferred. Callback backlog is the queue of call_rcu callbacks awaiting a grace period — a growing backlog means memory retired but not yet freed. Reader duration bounds how long a grace period can take, since one long-running reader stretches it. Reclaim lag is the gap between retiring and freeing, the direct measure of RCU's memory overhead.
The two reclamation choices, synchronize_rcu and call_rcu, deserve a sharper contrast because picking wrong is a common mistake. synchronize_rcu is the honest, simple option: the writer publishes, then blocks until a grace period has demonstrably elapsed, then frees inline, and when it returns you know the old version is gone. That is perfect for slow, infrequent updates on a thread that can afford to wait — module unload, a rare configuration swap. It is disastrous on a hot path, because blocking a writer for a whole grace period, which can be milliseconds, serializes updates and can even deadlock if the waiting thread is itself needed to make other CPUs reach quiescence. call_rcu is the answer there: it queues a callback and returns instantly, so the writer never waits and reclamation drains asynchronously in the background. The price is that memory is retired but not yet freed, which is exactly what the callback-backlog metric measures — and why a writer that fires call_rcu in a tight loop must watch that backlog for unbounded growth.
End-to-end flow
Trace an update to a routing table protected by RCU. A packet-forwarding thread is a reader: it calls rcu_read_lock, uses rcu_dereference to load the current routing table pointer, looks up the destination, and calls rcu_read_unlock — all with no atomic operations and no shared writes, so a hundred cores forward packets simultaneously without contending on a single byte.
Now a route changes. The writer thread takes its own update-side lock (writers still serialize against each other, just not against readers), copies the routing table, applies the change to the copy, and calls rcu_assign_pointer to swap the shared pointer to the new table. The release barrier ensures any reader that now loads the new pointer also sees the fully-built new table. From this instant, new readers use the new table; readers that already dereferenced the pointer keep using the old table, safely, because the old table was never touched.
The writer must now free the old table, but not yet. It calls call_rcu with a callback to free the old table and returns immediately — the forwarding path must never block on reclamation. The old table sits retired. Meanwhile the RCU subsystem watches for a grace period: it waits until every CPU has passed through a quiescent state — the forwarding threads finish their current critical sections and context-switch or go idle, one CPU at a time. Once all CPUs have reported, RCU knows no thread can still hold a pointer to the old table.
The grace period completes; RCU invokes the callback; the old routing table is freed. No reader ever waited, no reader ever signaled, and the memory was reclaimed exactly when it became provably safe — potentially milliseconds after retirement, which is the price. If routes changed in a tight loop, callbacks would pile into the backlog faster than grace periods complete, and memory would accumulate; the operational signal for that is a rising callback backlog, which tells you to batch updates or throttle the writer. In the steady state, though, this is the ideal: readers at full speed, writers paying a bounded deferred cost, and not a single dangling pointer.
The subtle moment in that trace is the grace period, so it is worth dwelling on what it is and is not. It is not a fixed delay or a timer; it is a condition — the point at which every CPU has been observed passing through a quiescent state since the update was published. Because a classic RCU read section cannot span a context switch, once a given CPU context-switches or goes idle you know for certain it is no longer inside any read section that predates the switch, and therefore cannot hold the old pointer. The grace period ends when the last straggler CPU has done so. This is why a single misbehaving reader that stays in its critical section — or worse, blocks inside it in the non-sleepable variant — can stretch the grace period and back up every pending free behind it. The reader path is free, but it carries an obligation: get out promptly, because the whole reclamation engine is waiting on the slowest reader to reach quiescence.