3. RCU核心思想:RCU全称、设计哲学、读端无锁、写端延迟回收、宽限期概念
RCU(Read-Copy-Update,读-拷贝-更新)是Linux内核中一种高性能的同步机制,专为读多写少的并发场景设计。其核心思想在于:让读操作几乎不产生任何锁开销,而将写操作的同步代价转移到延迟回收阶段。本章将深入剖析RCU的全称含义、设计哲学、读端无锁的实现原理、写端延迟回收机制以及宽限期(Grace Period)这一核心概念。
3.1 RCU全称与设计哲学
| 缩写 | 全称 | 核心含义 |
|---|---|---|
| R | Read | 读取操作,RCU优化的首要目标 |
| C | Copy | 拷贝数据,写操作时先复制一份副本 |
| U | Update | 更新指针,原子性地将旧指针指向新数据 |
RCU的设计哲学可以概括为“读写分离,延迟回收”。它打破了传统锁机制中“读写互斥”的范式,通过以下三条原则实现极致性能:
- 读端无等待:读者在访问共享数据时,不需要获取任何锁,也不需要等待写者完成操作。读者可以随时进入临界区,且不会被阻塞。
- 写端不阻塞读:写者在更新数据时,不会阻塞正在进行的读操作。写者通过创建数据副本并原子性地更新指针来完成修改。
- 延迟回收保证安全:旧数据的回收(释放内存)必须延迟到所有可能还在访问该数据的读者都退出临界区之后。这个“等待所有读者退出”的时间段就是宽限期。
设计哲学类比: 想象图书馆里有一本热门书籍(共享数据)。读者可以随时翻阅(读端无锁)。当需要更新内容时,管理员不直接修改原书,而是复印一本(Copy),在复印本上修改(Update),然后将书架上的书替换为复印本。原书(旧数据)不会立即销毁,而是等到所有正在翻阅原书的读者都离开后才回收(延迟回收)。
3.2 读端无锁:RCU的读端临界区
RCU读端无锁的核心在于rcu_read_lock()和rcu_read_unlock()这一对原语。它们不是传统意义上的锁,而是标记读者进入和退出临界区的“标记点”。
// RCU读端临界区示例
rcu_read_lock();
// 读者在此区域内可以安全地通过指针访问共享数据
struct my_data *p = rcu_dereference(global_ptr);
if (p) {
// 使用p指向的数据
do_something_with(p);
}
rcu_read_unlock();
// 离开临界区后,不能再使用p指针
读端无锁的关键特性:
- 无阻塞:rcu_read_lock()和rcu_read_unlock()通常只涉及内存屏障和抢占控制,不涉及原子操作或自旋锁,因此执行速度极快。
- 可嵌套:RCU读端临界区可以嵌套使用,内核会维护嵌套计数。
- 可抢占:在支持抢占的内核中,RCU读端临界区可以被更高优先级的任务抢占,但不会导致死锁。
- 禁止阻塞:在RCU读端临界区内,禁止调用可能导致睡眠或阻塞的函数(如等待队列、信号量、内存分配等),否则会破坏RCU的宽限期检测机制。
常见陷阱: 在RCU读端临界区内调用可能导致进程调度的函数(如msleep()、wait_event()、kmalloc(GFP_KERNEL))是严重的错误。这会导致宽限期无法结束,进而造成内存泄漏或系统性能下降。
3.3 写端延迟回收:RCU的写端操作
RCU写端操作分为三个步骤:Copy(拷贝)、Update(更新)、Reclaim(回收)。其中“延迟回收”是RCU区别于其他同步机制的核心特征。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1. Copy | 分配新内存,拷贝旧数据 | 写者分配一块新内存,并将旧数据的内容拷贝到新内存中 |
| 2. Update | 修改新数据,发布新指针 | 在新内存上完成修改,然后使用rcu_assign_pointer()原子性地将全局指针指向新数据 |
| 3. Reclaim | 等待宽限期,释放旧数据 | 调用synchronize_rcu()或call_rcu(),等待所有读者退出后,释放旧数据内存 |
// RCU写端操作示例
struct my_data *old, *new;
// 1. Copy: 分配新内存并拷贝
new = kmalloc(sizeof(*new), GFP_KERNEL);
*new = *old_ptr; // 拷贝旧数据
// 2. Update: 修改新数据并发布
new->value = 42;
rcu_assign_pointer(global_ptr, new);
// 3. Reclaim: 等待宽限期后回收旧数据
synchronize_rcu();
kfree(old); // 此时所有读者都已离开临界区,可以安全释放
3.4 宽限期(Grace Period)概念
宽限期是RCU机制中最重要的概念。它定义为:从写者发布新指针开始,到所有在发布之前就已经进入RCU读端临界区的读者都退出临界区为止的时间段。
宽限期的本质: 宽限期是RCU保证“没有读者还在访问旧数据”的一种手段。只有过了宽限期,写者才能安全地释放旧数据。
下图展示了宽限期的时序关系:
宽限期的关键特性:
- 等待所有“老读者”:宽限期只关心在指针更新之前就已经进入读端临界区的读者。在指针更新之后才进入的读者,它们看到的是新数据,因此不需要等待。
- 不阻塞新读者:在宽限期期间,新的读者仍然可以自由地进入和退出读端临界区,它们不会被阻塞。
- 检测机制:内核通过检测每个CPU上是否还有读者处于读端临界区来判断宽限期是否结束。当所有CPU上的读者都退出后,宽限期结束。
3.5 宽限期检测机制:从Quiescent State到Grace Period
RCU内核实现中,宽限期的检测依赖于静止状态(Quiescent State, QS)的概念。每个CPU在特定时刻(如发生上下文切换、进入idle状态、执行用户态代码时)会报告一个QS。当所有CPU都至少报告了一次QS后,内核认为宽限期结束。
宽限期与QS的关系总结:
| 概念 | 定义 | 作用 |
|---|---|---|
| Quiescent State (QS) | CPU上没有任何RCU读端临界区被激活的时刻 | 每个CPU通过报告QS来表明自己已经离开了RCU读端临界区 |
| Grace Period (GP) | 从写者更新指针到所有CPU都至少报告一次QS的时间段 | 宽限期结束后,写者可以安全地回收旧数据 |
核心要点回顾:
- RCU全称:Read-Copy-Update,读-拷贝-更新。
- 设计哲学:读写分离,读端无锁,写端延迟回收。
- 读端无锁:通过
rcu_read_lock()/rcu_read_unlock()标记临界区,无阻塞、无等待。 - 写端延迟回收:写者通过Copy-Update-Reclaim三步完成更新,旧数据的回收必须延迟到宽限期之后。
- 宽限期:等待所有“老读者”退出临界区的时间段,通过检测所有CPU的Quiescent State来判断是否结束。