引言
Read-Copy-Update(RCU)是 Linux 内核中最精妙的同步机制之一。它实现了真正的无锁读取——读取者在访问受 RCU 保护的数据时,不需要获取任何锁、不执行原子操作、不使用内存屏障(在大多数架构上)。这种独特的设计使得 RCU 在读多写少的场景下能够提供近乎完美的读取性能,因此被广泛应用于网络路由表、文件描述符表、内存管理、进程调度等内核核心子系统。
然而,RCU 的理解门槛相当高。它的 API 看似简单(rcu_read_lock / rcu_read_unlock / synchronize_rcu),但其背后的实现机制涉及 grace period(宽限期)、quiescent state(静默状态)、callback 流水线、多 CPU 协同等复杂概念。本文将从基本原理出发,深入剖析 RCU 的设计哲学、内核实现、API 使用模式以及真实性能数据。
1. RCU 设计哲学:以空间换时间,以延迟换并发
1.1 读写锁的困境
传统读写锁(rwlock)允许多个读者并发访问,但一旦写者获取锁,所有读者必须等待。更致命的是,读写锁的读取侧需要获取锁——即使只是读,也要执行原子操作。在现代多核系统中,这意味着:
- 缓存行弹跳(Cache-line bouncing):多个 CPU 核心的缓存行在 Exclusive 和 Shared 状态间频繁转换
- 内存总线争用:原子操作引发总线锁或缓存一致性协议风暴
- 优先级反转:低优先级读者可能阻塞高优先级写者
1.2 RCU 的核心洞察
RCU 的设计基于一个关键洞察:读取者不需要与写入者同步,只需要保证在读取过程中不被置于死地。具体策略是:
- 读取侧:标记一个临界区(
rcu_read_lock()→rcu_read_unlock()),在此期间内核保证不会发生上下文切换或 CPU 空闲 - 写入侧:先创建数据的修改副本(Copy),在副本上更新(Update),然后原子替换指针(Publish),最后等待所有正在退出的 RCU 临界区完成(Grace Period),安全释放旧数据
这个设计的精妙之处在于:读取者的成本极低(仅禁止抢占/中断),而写者的成本被分摊到 grace period 中延迟执行。
2. RCU 核心概念:Grace Period 与 Quiescent State
2.1 Quiescent State(静默状态)
一个 CPU 处于 quiescent state 意味着它不在任何 RCU 临界区中。换句话说,该 CPU 已经通过了 rcu_read_unlock() 或者当前不在 rcu_read_lock()/rcu_read_unlock() 之间。
Linux 内核通过以下方式检测 quiescent state:
- CPU 发生上下文切换(肯定已经退出了 RCU 临界区)
- CPU 进入 idle 状态
- CPU 在用户态执行用户代码(对于 CONFIG_PREEMPT=n 的配置)
2.2 Grace Period(宽限期)
Grace Period 是 RCU 最核心的定义:从一个时刻 T 开始,到所有在 T 之前进入 RCU 临界区的 CPU 都至少经历了一次 quiescent state 为止的时间段。
关键性质:
- 一个 grace period 内,不可能存在在 GP 开始前进入临界区的读取者仍然持有旧数据的引用
- 如果写者在 GP 开始时已经完成了指针的原子替换(Publish),那么 GP 结束后,旧数据可以被安全释放
- 一个 grace period 处理一个批次的写操作,多个写者可以共享同一个 grace period
2.3 状态机视角
内核中为每个 CPU 维护一个 RCU 节点状态机:
ctxsw/idle/user
┌──────────┐ ──────────────→ ┌────────────────┐
│ IN-Critical│ │ QUIESCENT │
│ (lock held)│ ←────────────── │ (passed unlock)│
└──────────┘ rcu_read_lock() └────────────────┘
│
│ all CPUs quiescent
▼
┌──────────────┐
│ Grace Period │
│ COMPLETE │
└──────────────┘
3. RCU API 深度剖析
3.1 基础 API
读取侧(Reader):
// 标记 RCU 临界区开始
rcu_read_lock();
// 安全地访问受 RCU 保护的指针
struct my_data *p = rcu_dereference(g_ptr);
if (p) {
// 使用 p...
printk("value = %d\n", p->value);
}
// 标记 RCU 临界区结束
rcu_read_unlock();
在非抢占内核中,rcu_read_lock()/rcu_read_unlock() 实际上是空操作(no-op)!在可抢占内核中(CONFIG_PREEMPT=y),它们会递增/递减一个 per-CPU 计数器来追踪临界区嵌套深度。
写入侧(Writer):
// 1. 分配并初始化新数据
struct my_data *new_data = kmalloc(sizeof(*new_data), GFP_KERNEL);
new_data->value = 42;
// 2. 原子替换指针(Publish)
struct my_data *old_data = rcu_assign_pointer(g_ptr, new_data);
// 3. 等待所有读者退出临界区
synchronize_rcu();
// 4. 安全释放旧数据
kfree(old_data);
3.2 三种等待 API 的选择
| API | 阻塞 | 上下文 | 延迟 | 适用场景 |
|---|---|---|---|---|
synchronize_rcu() | 是 | 进程上下文 | 中 | 模块卸载、热插拔 |
call_rcu() | 否 | 任意上下文 | 中 | 高频更新、中断上下文 |
| 是 | 进程上下文 | 低 | 少量需要低延迟的场景 |
call_rcu() 是最常用的非阻塞 API,它将释放操作封装为 callback,放入 per-CPU 队列中,在 grace period 结束后异步执行:
void my_callback(struct rcu_head *head) {
struct my_data *old = container_of(head, struct my_data, rcu);
kfree(old);
}
// 写入侧
struct my_data *old = rcu_assign_pointer(g_ptr, new);
call_rcu(

发表评论 取消回复