How Read-Copy-Update Grace Periods Actually Work in Linux
A deep dive into Linux Read-Copy-Update grace periods, Tree RCU quiescent state tracking, dyntick-idle CPU accounting, memory barriers, and callback batching.

Introduction
Modern concurrent systems demand near-zero synchronization overhead for read-heavy workloads. In the Linux kernel, Read-Copy-Update (RCU) satisfies this requirement by decoupling read-side traversals from write-side updates. While readers traverse shared linked data structures without acquiring locks, allocating atomic reference counts, or invalidating peer processor cache lines, writers update structures by publishing modified copies and deferring memory reclamation.
The mechanism that guarantees memory safety without read-side lock coordination is the grace period. Understanding how Read-Copy-Update Grace Periods work requires peeling back the coordination fabric between CPU scheduling interrupts, memory barriers, and hierarchical state tracking. Before any node removed by an RCU writer can be freed, every core in the system must pass through a quiescent state, ensuring that no active reader retains a reference to the old allocation.
Reader Core 0: [ rcu_read_lock() ... Read Node X ... rcu_read_unlock() ]
|
Writer Core 1: [ Remove Node X ] --> [ Synchronous/Async Wait ] --> [ kfree(Node X) ]
|<--- Grace Period ------>|
(All CPUs pass through QS)
At scale across hundreds of physical cores and non-uniform memory access (NUMA) sockets, orchestrating this synchronization without degrading throughput is a masterclass in systems design. This guide examines how the Linux kernel coordinates quiescent state detection, manages Tree RCU hierarchies, tracks energy-efficient dyntick-idle cores, and orders memory barriers to safely reclaim memory.
The Anatomy of Quiescent States and Grace Periods
An RCU reader marks the boundary of its interaction with protected pointers using rcu_read_lock() and rcu_read_unlock(). In non-preemptible kernels (CONFIG_PREEMPT_NONE), these markers compile down to pure compiler barriers (barrier()), imposing zero instruction overhead and zero memory bus traffic. In preemptible kernels (CONFIG_PREEMPT_RCU), rcu_read_lock() increments an execution nesting counter (current->rcu_read_lock_nesting) on the caller's task structure, preventing preemption events from masking active readers.
A quiescent state (QS) represents an execution window where a given processor is guaranteed not to be inside an RCU read-side critical section that began prior to the start of the current grace period. In non-preemptible kernels, a quiescent state occurs whenever a CPU:
- Executes a voluntary context switch (
schedule()). - Transitions into the user-space execution domain via an interrupt or syscall exit.
- Enters the idle loop (
cpuidle).
A grace period is the global duration required for every processor across the system to record at least one quiescent state. Any reader critical section executing when a grace period begins must terminate before that grace period can finish. Any reader entering a critical section after the grace period starts will only see pointers published after the removal, meaning it cannot observe pointers waiting in the current grace period's deletion queue.
CPU 0: ---[ RCU Critical Section ]---(Context Switch: QS)------------------->
CPU 1: --------(User Space: QS)----------------------------------------------->
CPU 2: ----------------------------(Idle Loop: QS)---------------------------->
^
Grace Period Completed Here
The writer interface provides two primary pathways to wait out this interval:
- Synchronous (
synchronize_rcu()): Blocks the calling thread, yielding the processor until the grace period completes. - Asynchronous (
call_rcu()): Enqueues anrcu_headcallback onto a per-CPU list and returns immediately, deferring the invocation of the memory reclamation routine (kfree()or a custom destructor) to an asynchronous software interrupt handler.
Tree RCU Architecture and Quiescent State Propagation
Early Linux implementations maintained a single global bitmask where each bit represented a CPU that had yet to report a quiescent state. As multicore systems scaled past 64 execution contexts, this monolithic mask generated severe cache-line bouncing on cache coherency fabrics. To mitigate this bus contention, modern kernels organize CPUs into a hierarchical balanced n-ary tree known as Tree RCU.
Tree RCU distributes tracking state across levels of struct rcu_node instances, culminating in a single root node. The leaf nodes track individual clusters of CPUs, typically between 16 and 64 cores, determined at compile time by CONFIG_RCU_FANOUT and CONFIG_RCU_FANOUT_LEAF.
+--------------------+
| Root rcu_node |
| qsmask: 0b11 |
+---------+----------+
|
+---------------+---------------+
| |
+---------v----------+ +---------v----------+
| Leaf rcu_node 0 | | Leaf rcu_node 1 |
| qsmask: 0b0011 | | qsmask: 0b1100 |
+----+----+----+-----+ +----+----+----+-----+
| | | | | | | |
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
Each rcu_node structure contains critical fields that drive the state machine:
qsmask: A bitmask of child nodes (or CPUs, for leaf nodes) that must report a quiescent state for the ongoing grace period.qsmaskinit: The initialization mask containing the online CPUs or child entities active when a new grace period launches.gp_seq: The 64-bit sequence counter tracking the current grace period's progression.lock: A raw spinlock protecting the node's local bitmasks.
When a CPU encounters a quiescent state (such as within the timer interrupt handler rcu_core()), it acquires the lock of its immediate leaf rcu_node and clears its bit in that node's qsmask. If other CPUs covered by that leaf still have uncleared bits, the CPU releases the lock and returns to normal execution.
However, if the CPU clears the final set bit in its leaf node's qsmask, it ascends the tree. It acquires the parent node's spinlock, clears the child node's bit in the parent's qsmask, and releases the child lock. This upward propagation continues iteratively. When the final bit in the root rcu_node is cleared, the core that cleared it signals the kernel grace-period kthread (rcu_gp_kthread) that the global grace period is complete.
For systems that implement complex task distribution, similar partitioning concepts emerge in user space. High-performance event frameworks, such as those covered in Architecting Zero-Copy Event Loops: Cache-Line-Aware Memory Arenas, Kernel Bypass, and Lock-Free Ring Buffers, rely on identical spatial cache awareness to prevent contention across socket topologies.
Memory Ordering and the State Machine of Read-Copy-Update Grace Periods
The correctness of Read-Copy-Update Grace Periods relies on strict memory ordering. CPUs and optimizing compilers aggressively reorder independent memory operations. If an RCU write-side memory release reorders around a grace period completion point, a reader on another socket could read freed memory, leading to use-after-free corruption.
Consider an update pattern:
struct data_node {
int value;
struct rcu_head rcu;
};
void update_data(struct data_node **global_ptr, int new_val)
{
struct data_node *new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
struct data_node *old_node;
new_node->value = new_val;
/*
* rcu_assign_pointer enforces store-release semantics.
* Memory writes initializing new_node complete before
* global_ptr publishes the pointer.
*/
old_node = rcu_dereference_protected(*global_ptr, lockdep_is_held(&my_mutex));
rcu_assign_pointer(*global_ptr, new_node);
/* Asynchronously reclaim memory after a grace period */
kfree_rcu(old_node, rcu);
}
The underlying memory-ordering contract requires that all operations inside an RCU read-side critical section happen before the end of the matching grace period. To maintain this contract across weakly ordered architectures (e.g., ARM64, POWER), the kernel inserts memory barriers during every phase of the grace-period state machine:
[GP Initialization]
|
v
smp_mb() <--- Guaranteed full memory barrier
|
(Wait for QS across all CPUs)
|
v
smp_mb() <--- Guaranteed full memory barrier
|
[GP Cleanup & Callback Invocation]
These barriers prevent memory operations initiated inside the grace period from leaking backward before its initialization, and prevent operations executed during callback invocation from leaking forward into the active grace period. The kernel uses an explicit sequence counter (gp_seq) to prevent state corruption:
/* Representation of grace period sequence flags */
#define RCU_SEQ_STATE_MASK 0x03
#define RCU_SEQ_CTR_SHIFT 2
static inline bool rcu_seq_completed(unsigned long snap, unsigned long current_seq)
{
return (long)(current_seq - snap) > 0;
}
The lowest two bits of gp_seq track whether the state machine is idle (00) or running (01). When rcu_gp_kthread launches a grace period, it sets the phase flag and executes an explicit smp_mb().
When tracking memory synchronizations across high-throughput cores, software systems often evaluate store buffering hazards using patterns similar to the ones explored in Designing Lock-Free Shared-Memory Ring Buffers: Cache-Coherence, Memory Barriers, and Kernel-Bypass IPC. The kernel ensures that if a reader sees the pointer update via rcu_dereference() (which introduces an address-dependency barrier or smp_load_acquire()), that reader's subsequent critical-section exit will enforce visibility through the leaf node's memory fences.
Writer CPU: Grace Period Engine: Reader CPU:
------------------------- ----------------------------- ---------------------------
W: write(new_node->data)
W: smp_store_release(ptr)
GP Start: smp_mb() R: ptr = smp_load_acquire()
Loop: Checks QS masks R: read(ptr->data)
R: rcu_read_unlock() -> QS
GP End: smp_mb()
W: kfree(old_node)
Dyntick-Idle Mode and Extended Quiescent State Tracking
In battery-sensitive devices and low-latency bare-metal environments, processors utilize tickless idle features (CONFIG_NO_HZ_IDLE or CONFIG_NO_HZ_FULL). If a CPU halts its periodic timer interrupt (the scheduling tick) to save power or avoid interrupt jitter for dedicated real-time tasks, it cannot process periodic RCU softirqs to clear bits in leaf rcu_nodes.
If the RCU engine waited for an idle or isolated core to wake up and manually acknowledge a quiescent state, grace periods would stall globally. This condition would cause memory allocations awaiting reclamation to back up, leading to out-of-memory (OOM) panic conditions.
To solve this, Linux treats tickless cores as existing in an Extended Quiescent State (EQS). Since an idle core cannot execute kernel code, and a task running in isolated user space (NO_HZ_FULL) cannot enter an RCU-kernel critical section without crossing the kernel boundary, these states are inherently quiescent.
The kernel tracks this state via a per-CPU tracking variable, historically encapsulated in struct rcu_dynticks:
struct rcu_dynticks {
atomic_t dynticks; /* Tracking counter */
long dynticks_nesting; /* Kernel/interrupt nesting level */
long dynticks_nmi_nesting; /* NMI nesting level */
};
The counter variable (dynticks) obeys strict invariant rules:
- An even value indicates that the CPU is in an extended quiescent state (dyntick-idle, guest mode, or isolated user execution).
- An odd value indicates that the CPU is running inside standard kernel context and could be executing an RCU read-side critical section.
User Space / Idle (Even dynticks)
| ^
Syscall / | | Syscall Return /
Interrupt | | Enter Idle
v |
Kernel Execution (Odd dynticks)
When a new grace period starts, rcu_gp_kthread queries the dynticks counter for each core without interrupting it. The kthread reads the counter using an atomic acquire:
static int rcu_dynticks_snap(struct rcu_data *rdp)
{
return atomic_read(&rdp->dynticks);
}
If the snapshot value is even, the engine knows the CPU was already in a quiescent state when the grace period began. The coordinator thread clears that CPU's bit in the hierarchical tree on its behalf.
If the core is running an odd counter value, the core must subsequently step through an atomic transition:
void rcu_dynticks_eqs_enter(void)
{
atomic_inc(&rdp->dynticks); /* Becomes even: entering EQS */
smp_mb__after_atomic();
}
void rcu_dynticks_eqs_exit(void)
{
smp_mb__before_atomic();
atomic_inc(&rdp->dynticks); /* Becomes odd: exiting EQS */
}
If a grace period stalls because an active CPU fails to report a quiescent state within a configured timeout window, RCU triggers a Rescheduling Inter-Processor Interrupt (Resched IPI). This forces the target CPU to enter an interrupt handler, re-evaluate its state, and clear its mask bit.
Deterministic multi-core environments share these latency profile challenges. Engineers deploying low-overhead task handlers often use patterns discussed in High-Throughput Deterministic Actor Runtimes: Cache-Conscious Architectures, Lock-Free Rings, and Kernel-Bypass I/O to minimize context switching overheads similar to those mitigated by dyntick tracking.
Preemptible RCU and Blocked Readers
In non-preemptible kernels, an RCU read-side critical section cannot yield the processor. Thus, context switches naturally serve as quiescent states. However, in kernels compiled with CONFIG_PREEMPT_RCU, a task inside rcu_read_lock() can be preempted by a higher-priority task.
If a task is preempted inside a read-side critical section, treating the subsequent context switch as a quiescent state would cause use-after-free bugs. The grace period would advance and reclaim memory that the preempted task was still accessing.
To handle this, Preemptible RCU tracks preempted readers by decoupling the physical CPU from the active reader task:
Leaf rcu_node
+------------------------------+
| qsmask: 0b0001 (CPU 0 active)|
| blkd_tasks: Task A <-> Task B|
+------------------------------+
When a task executing inside an RCU critical section is preempted:
- The scheduler invokes
rcu_note_context_switch(). - The core inspects
current->rcu_read_lock_nesting. - Finding the nesting counter greater than zero, the core links the preempted
struct task_structonto theblkd_taskslist of the current leafrcu_node. - The CPU then clears its own bit in the leaf's
qsmask, allowing that physical core to enter a quiescent state. - The leaf node's grace period cannot complete until every task on its
blkd_taskslist exits its critical section and clears itself from the list.
If a task blocks inside an RCU read-side critical section and a grace period is actively waiting for it, priority inversion can emerge. A low-priority reader preempted by medium-priority compute workloads can stall the grace period, backing up kernel memory reclamation.
Tree RCU resolves this via RCU Priority Boosting (CONFIG_RCU_BOOST). If an RCU grace period remains blocked by an uncompleted reader for longer than CONFIG_RCU_BOOST_DELAY milliseconds, the kernel invokes priority inheritance, temporarily elevating the stalled task to an SCHED_FIFO real-time priority until it calls rcu_read_unlock().
Callback Invocation and Memory Reclamation Hazards
When a subsystem uses call_rcu(), it queues an asynchronous cleanup callback. The kernel does not execute this callback immediately. It must batch callbacks across distinct grace periods using the segmented callback list (struct rcu_segcblist).
rcu_segcblist
+---------------+---------------+---------------+---------------+
| RCU_DONE_TAIL | RCU_WAIT_TAIL | RCU_NEXT_READY| RCU_NEXT_TAIL |
+---------------+---------------+---------------+---------------+
| | | |
v v v v
Ready for Waiting for Waiting for Queued in
Invocation Current GP Next GP Future GP
The segmented list structure divides pending callbacks into four distinct stages:
RCU_DONE_TAIL: Callbacks whose grace period has completely elapsed. These are ready for execution during the next softirq (RCU_SOFTIRQ).RCU_WAIT_TAIL: Callbacks waiting on the current, active grace period.RCU_NEXT_READY_TAIL: Callbacks enqueued while the current grace period was already running, designated to run in the immediately following grace period.RCU_NEXT_TAIL: Callbacks added since the start of the current cycle, not yet assigned a future grace period sequence ID.
When a grace period ends, rcu_advance_cblist() advances the callbacks across these segments:
/* Pseudocode representation of segmented callback advancement */
static void rcu_advance_cblist_stages(struct rcu_segcblist *rscl, unsigned long current_gp_seq)
{
/* Callbacks that were waiting on current_gp_seq are moved to DONE */
rscl->tails[RCU_DONE_TAIL] = rscl->tails[RCU_WAIT_TAIL];
/* Callbacks ready for next grace period transition to WAIT */
rscl->tails[RCU_WAIT_TAIL] = rscl->tails[RCU_NEXT_READY_TAIL];
/* Newly queued callbacks transition to NEXT_READY */
rscl->tails[RCU_NEXT_READY_TAIL] = rscl->tails[RCU_NEXT_TAIL];
}
Once shifted into RCU_DONE_TAIL, callbacks are invoked in batches by rcu_do_batch(). This function executes each callback's function pointer:
static void rcu_do_batch(struct rcu_data *rdp)
{
struct rcu_head *rhp;
int count = 0;
int max_batch = rdp->blimit; /* Prevents softirq thread starvation */
while ((rhp = rcu_cblist_dequeue(&rdp->cblist)) != NULL) {
rhp->func(rhp);
if (++count >= max_batch)
break;
}
}
Callback Flooding and Expedited Grace Periods
A common risk in RCU-backed systems is callback flooding. If a producer allocates and frees memory faster than the grace-period engine can complete cycles, the segmented callback lists will grow indefinitely, exhausting system memory.
The kernel handles this pressure using adaptive techniques:
- Dynamic batch limits (
blimit): As callback queues grow, RCU scales up the number of callbacks executed per scheduling cycle. - Expedited grace periods (
synchronize_rcu_expedited()): Instead of waiting for voluntary context switches and passive timer interrupts, expedited grace periods broadcast synchronous Inter-Processor Interrupts (IPIs) across all online cores to force immediate quiescent state declarations. - RCU callback offloading (
rcu_nocbs): On systems configured with real-time requirements, callback execution shifts away from softirqs to dedicated per-core kthreads (rcuoc/N), isolating core execution pipelines from cleanup latency.
Conclusion
The implementation of RCU grace periods represents a disciplined balance between memory consistency models, multi-tier tree data structures, and energy-conscious CPU accounting. By separating write publication from dynamic memory destruction, the Linux kernel eliminates read-side synchronization overhead without sacrificing thread safety.
Through the Tree RCU architecture, quiescent state reports ascend lock boundaries without saturating central system buses. In parallel, the extended quiescent state engine monitors isolated and dyntick-idle processors via atomic snapshots, ensuring power-saving measures do not compromise system-wide reclamation.
For developers building high-performance kernel modules or distributed user-space engines, mastering RCU grace periods clarifies the fundamental synchronization contract: memory safety does not require global read coordination, provided writers respect the physical guarantees of the memory architecture and the progression of time.
References
- Linux Kernel Documentation - RCU Concepts:
https://docs.kernel.org/RCU/whatisRCU.html - Hierarchical RCU Architecture and Data Structures:
https://docs.kernel.org/RCU/Design/Data-Structures/Data-Structures.html - Paul E. McKenney - Memory Barriers and the RCU Architecture:
https://kernel.org/pub/linux/kernel/people/paulmck/perfbook/perfbook.html