RCU:Linux 内核的基石性同步机制
RCU(Read-Copy-Update)是 Linux 内核中最重要也最独特的同步原语之一。自 2.5 内核引入以来,RCU 已成为从网络栈、内存管理到文件系统等各子系统的核心基础设施。与传统的读写锁不同,RCU 允许读者在不受任何阻塞、不回写、不使用原子指令的情况下并发访问共享数据结构,这是通过精巧的延迟回收机制实现的。
本文将深入剖析 RCU 的核心原理、内核实现、使用模式、性能调优,并通过多个真实内核场景展示其威力。
1. 为什么需要 RCU
在探讨 RCU 之前,先看看传统同步机制的痛点:
读写锁的问题:读写锁(rwlock)看起来很适合读多写少的场景,但存在严重问题 —— 写者必须等待所有持有锁的读者完成,在高频读取场景下写者可能长时间饥饿。更关键的是,读写锁的获取/释放本身需要原子操作,在 NUMA 架构上跨节点的缓存一致性流量带来显著的开销。
RCU 的核心保证:
- 读者可以无锁、无等待、原子操作-free 地读取数据
- 读者永远不会因为 RCU 而睡眠或阻塞
- 读者可以在几乎所有上下文中运行(包括软中断、硬中断、NMI 等,在 SRCU 中也可以睡眠)
- 写者通过副本修改 + 延迟回收的方式保证读者总能看到一致的数据
2. RCU 的核心抽象:宽限期(Grace Period)
RCU 的全部魔法都围绕"宽限期"这一概念。宽限期是指这样一个时间段 —— 在该时间段开始之前已经开始的所有 RCU 读端临界区都已经结束。
写者的操作流程如下:
- 复制一份现有数据结构的副本
- 在副本上修改(此时读者可能还在读取原数据)
- 用原子指令将全局指针切换到新数据(原子替换,读者要么看到旧数据,要么看到新数据,不会看到中间状态)
- 调用
synchronize_rcu()等待当前所有已过宽限期 - 宽限期过后,安全释放旧数据
3. RCU 的使用模式
3.1 基本 API
这是最经典的使用模式,适用于读多写少的数据结构:
// 读者侧
rcu_read_lock();
p = rcu_dereference(head);
// 安全地访问 p
rcu_read_unlock();
// 写者侧
new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
new_node->value = 42;
old_node = head;
rcu_assign_pointer(head, new_node); // 原子发布
synchronize_rcu(); // 等待宽限期
kfree(old_node); // 安全回收
3.2 链表操作
RCU 为链表提供了丰富的辅助宏:
struct my_node {
int data;
struct my_node __rcu *next;
};
struct my_node __rcu *head;
// RCU 安全的链表遍历
struct my_node *p;
rcu_read_lock();
p = rcu_dereference(head);
while (p) {
printk("data=%d\n", p->data);
p = rcu_dereference(p->next);
}
rcu_read_unlock();
3.3 kfree_rcu延迟回收
Linux 4.0+ 引入了 kfree_rcu(),让你不需要手动调用 synchronize_rcu() + kfree():
struct my_struct {
int value;
struct rcu_head rcu;
};
void update_handler(void)
{
struct my_struct *new = kmalloc(...);
struct my_struct *old = rcu_dereference_protected(global_ptr, ...);
new->value = compute_new_value();
rcu_assign_pointer(global_ptr, new);
// 内核会自动安排宽限期后的释放
kfree_rcu(old, rcu);
}
4. RCU 在内核中的关键应用场景
4.1 路由缓存
Linux 网络栈是 RCU 的重度使用者。路由查找使用 RCU 保护的路由表,数据包转发路径上的路由查找完全无锁。IPv6 的邻居发现协议也依赖 RCU 维护邻居表,每秒数十万次的邻居查询都在 RCU 读端临界区内完成。
4.2 dentry 缓存
虚拟文件系统(VFS)的目录项缓存(dcache)使用 RCU 保护 dentry_hashtable。在 path_lookup() 的 hot path 中,lookup_fast() 通过 rcu_dereference_raw() 直接在无锁状态下比较文件名和 inode 编号。
4.3 进程描述符
task_struct 本身通过引用计数 + RCU 配合的方式管理生命周期。find_task_by_vpid() 使用 tasklist_rcu 遍历进程列表。PID 哈希表使用 RCU 保护,pidfd 等机制依赖其高效查询能力。
4.4 CPU 热插拔
CPU 热插拔时需要迁移 RCU 的等待状态。内核通过 rcu_migrate_enter/exit() 在 CPU offline 过程中安全地将 state 转移到其他 CPU,确保宽限期不会永远等待。
4.5 eBPF
eBPF 的 map 操作大量使用 RCU。bpf_map_lookup_elem() 在 RCU 临界区内执行,而 map 更新通过 RCU 保护的原子指针替换完成。同时 eBPF trampoline 和 program 生命周期管理也依赖 RCU 的延迟回收。
5. RCU 分类与变体
5.1 经典 RCU(Classic RCU)
传统的 rcu_read_lock()/rcu_read_unlock() 对。读者侧禁止调度和睡眠,写者侧使用 synchronize_rcu() 同步等待宽限期。
5.2 可睡眠 RCU(Sleepable RCU, SRCU)
SRCU 允许读者在临界区内睡眠,代价是更复杂的实现和稍高的同步延迟。适用于文件系统操作、内存管理中的可能阻塞读取:
// SRCU 使用
int srcu_idx = srcu_read_lock(&my_srcu);
// 可以在临界区内睡眠
do_something_may_block();
srcu_unlock(&my_srcu, srcu_idx);
// 写者
synchronize_srcu(&my_srcu);
每个 SRCU 结构体是一个独立的域,宽限期只对应该域内的读者。这使得大规模系统中不同子系统的 SRCU 互不干扰。
5.3 Tasks RCU
Linux 4.6+ 引入,专门用于追踪 task_struct 的引用。tasks_rcu_read_lock() 利用内核调度器的天然同步点避免了传统宽限期等待机制的开销。
6. RCU 的实现原理
6.1 读者侧实现
rcu_read_lock() 和 rcu_read_unlock() 在经典 RCU 中本质上是禁止抢占(preempt_disable/enable)。这意味着只要 CPU 在 RCU 读端临界区内,就不会发生上下文切换。
队列式 RCU(Queued RCU)引入了更高效的实现:读者只需要递增一个 per-CPU 计数器,读者侧几乎零开销。
6.2 宽限期检测
内核的宽限期检测分为主动触发(synchronize_rcu())和被动检测(时钟中断等)。每个 CPU 会周期性地报告自己经过了静默状态。当所有在线 CPU 都报告了静默状态后,一个宽限期即宣告结束。
6.3 回调处理
call_rcu() 将回调挂入 per-CPU 的回调队列。宽限期到来时,内核通过软中断调用挂起的回调。回调的处理还可以卸载到专门的 worker(rcuog)上。
6.4 回调批处理与负载均衡
Lazy RCU 延迟触发宽限期,rcu_nocbs 将回调从繁忙 CPU 卸载到专门的 kthread,分层的 RCU 节点树减少全局扫描开销。
7. RCU 的调优参数与实践
7.1 启动参数
# 将 CPU 2,3 从 RCU 回调处理中隔离
rcu_nocbs=2,3
# 启用 RCU 优先级提升
rcu_normal=0
# 启用 Lazy RCU(减少宽限期频率)
rcu_lazy=1
# 让 grace period 尽早尽快完成
rcu_expedited=1
7.2 性能观测
# 查看 per-CPU RCU 统计
cat /sys/kernel/debug/rcu/rcu_preempt/rcudata
7.3 RCU Stall 调试
RCU stall 是最常见的 RCU 问题。成因包括:RCU 读端临界区内阻塞、长时间关抢占、CPU 卡死无法报告静默状态、回调队列积压过多。CONFIG_RCU_CPU_STALL_TIMEOUT 可配置超时。
8. RCU 的编写陷阱
8.1 常见错误
错误1:持有 RCU 锁时睡眠:如果应使用 SRCU。
错误2:rcu_dereference 的结果在 rcu_read_unlock 之后使用:p 可能已经被释放。
错误3:对 rcu_dereference 的结果未提供写者保护地修改:必须持有写者锁。
8.2 必须遵守的规则
rcu_read_lock() 和 rcu_read_unlock() 之间不能阻塞(经典 RCU)、rcu_dereference 返回值的有效范围必须在 RCU 读端临界区内、rcu_assign_pointer 必须与 rcu_dereference 成对使用、rcu_read_lock 不能嵌套超过指定深度(默认 1024)。
9. RCU 与内存序
rcu_assign_pointer() 插入写屏障,确保指针发布之前的所有写操作对读取该指针的读者可见。rcu_dereference() 插入读屏障。在 x86-TSO 上退化为编译器 barrier,但在 ARM、POWER 等上,需要使用 smp_store_release() 和 smp_load_acquire() 保证顺序。
10. RCU 的未来发展
Lazy RCU 的广泛采用、Tasks RCU 的扩展、与新一代同步原语的融合等方向持续演进。RCU 的"读者无等待"特性在可预见的未来仍不可替代。
11. 总结
RCU 是 Linux 内核中"读路径零开销"范式的代表作。其核心洞察是:读取操作远多于写入操作,用时间换空间,用延迟保证安全。这种思想也推动了许多开源组件的设计。当你在写高并发读代码时,不妨自问:"这个场景适合 RCU 吗?你会获得巨大的性能回报。

发表评论 取消回复