引言:为什么需要 RCU?
在高性能内核编程中,读写锁(rwlock)是最常见的同步原语之一。但它有一个根本性的缺陷:读写之间互斥——即使读操作远多于写操作,读者之间也会因为写者的存在而产生竞争。当系统扩展到数百个并发读者时,这种竞争会成为性能瓶颈。
RCU(Read-Copy-Update)是 Linux 内核独创的一种同步机制,它在 2002 年被引入 2.5 内核,如今已成为内核中最重要的并发控制手段之一。RCU 的核心洞察是:读取操作可以完全无锁,不需要任何原子指令、内存屏障或自旋锁,而写入操作的代价则由"宽限期"和延迟回收来承担。
在内核中,RCU 的使用无处不在:路由表、文件系统 dentry 缓存、进程描述符、网络协议栈、内存管理子系统、设备驱动模型等关键路径都大量使用 RCU。理解 RCU,是理解现代 Linux 内核并发架构的关键。
一、RCU 核心概念模型
1.1 读取侧临界区(Read-Side Critical Section)
RCU 的读取侧代码被称为"临界区",但它与传统锁的临界区截然不同:
// RCU 读取侧——完全无锁
rcu_read_lock();
// 这里是临界区,可以自由访问 RCU 保护的数据
p = rcu_dereference(ptr); // 发布-订阅语义的指针读取
if (p) {
// 安全地访问 p 指向的数据
printk("value = %d\n", p->value);
}
rcu_read_unlock();
关键点在于:rcu_read_lock() 和 rcu_read_unlock() 在 UP(单核)系统上基本上是空操作(关闭内核抢占)。在 SMP 系统上,它们也只是禁止内核抢占,不涉及任何原子操作或缓存行竞争。因此,成千上万个读者可以同时进入临界区而互不干扰,读取操作的吞吐量可以达到接近缓存访问的速度。
1.2 发布-订阅语义(Publish-Subscribe Semantics)
RCU 的写入侧使用"发布-订阅"模型。写者通过 rcu_assign_pointer() 发布新数据,读者通过 rcu_dereference() 订阅数据。这两个宏不仅仅是赋值和解引用——它们在底层插入了必要的内存屏障,确保:
- 写者:新数据的初始化在指针发布之前完成(防止读者看到未初始化的数据)
- 读者:指针解引用和使用数据的操作不会被乱序执行到指针读取之前(防止基于旧指针的新解引用)
1.3 数据回收与宽限期(Grace Period)
RCU 写入侧最独特之处是延迟回收。当写者需要更新一个被 RCU 保护的数据结构时,它不是直接修改原数据,而是:
- 分配新内存,拷贝旧数据并修改
- 通过
rcu_assign_pointer()原子替换全局指针 - 等待"宽限期"结束——即确保所有在指针替换前进入 RCU 临界区的读者都已经退出
- 宽限期结束后,安全回收旧版本数据
// RCU 写入侧——替换节点
new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
*new_node = *old_node; // 拷贝旧数据
new_node->value = new_value; // 修改
rcu_assign_pointer(global_ptr, new_node); // 原子发布
synchronize_rcu(); // 等待宽限期
kfree(old_node); // 安全回收旧数据
二、宽限期机制深度剖析
2.1 什么是宽限期?
宽限期(Grace Period)是 RCU 中最为核心的概念。它被定义为这样一个时间段:从某个时间点 T 开始,到所有在时间 T 之前已经进入 RCU 读取侧临界区的 CPU 都退出临界区为止。
在这段时间内,任何在 T 之前进入临界区的读者都可能持有旧版本数据的引用。宽限期结束后,可以保证没有任何读者仍在使用旧数据,此时才能安全回收。
Linux 内核实现了一个精妙的宽限期检测机制:
- 使用静止状态(Quiescent State)概念——每个 CPU 在退出 RCU 临界区时报告一次 Q 状态
- 内核维护每个 CPU 的
rcu_data结构,记录该 CPU 的 RCU 状态 - 当一个 CPU 经历了一次上下文切换(或显式调用
rcu_note_context_switch()),就意味着它离开了 RCU 临界区 - 当所有 CPU 都至少经历了一次静止状态,宽限期结束
2.2 宽限期的实现架构
RCU 子系统将 CPU 组织成树状层级结构(rcu_node),每个节点管理一组 CPU。当所有子节点都报告静止状态时,父节点向上级汇报。这种树状结构使得宽限期检测的时间复杂度从 O(N) 降低到 O(log N)。
// 内核中的 RCU 状态节点(简化)
struct rcu_node {
raw_spinlock_t lock;
unsigned long qsmask; // 子节点静止状态位图
struct rcu_node *parent;
};
struct rcu_data {
unsigned long gpnum; // 当前宽限期号
bool qs_pending; // 是否有未报告的静止状态
struct rcu_node *mynode; // 所属节点
};
2.3 同步 vs 异步宽限期
内核提供了两种等待宽限期的方式:
- synchronize_rcu():同步等待,调用者阻塞直到宽限期结束。适用于非热路径(如模块卸载)。
- call_rcu():异步等待,注册回调函数,宽限期结束后在软中断上下文执行回调。适用于热路径(高频更新场景)。
// 异步方式——高频更新场景的首选
void my_callback(struct rcu_head *head) {
struct my_data *old = container_of(head, struct my_data, rcu);
kfree(old);
}
// 更新时替换并注册延迟回收
new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
*new_node = *old_node;
new_node->value = new_value;
rcu_assign_pointer(global_ptr, new_node);
call_rcu(

发表评论 取消回复