2. RCU 核心 API:语义与使用规范
RCU(Read-Copy-Update)同步机制的核心在于其轻量级的读侧与高效的写侧配合。本章深入剖析五大核心 API 的语义、使用规范及常见陷阱,帮助你在内核工程中正确运用 RCU。
2.1 RCU 读侧:rcu_read_lock / rcu_read_unlock
读侧临界区由 rcu_read_lock() 和 rcu_read_unlock() 界定。在这对调用之间,读者可以安全地通过 rcu_dereference() 访问被 RCU 保护的数据结构。
- 语义:标记一个 RCU 读侧临界区。在此期间,禁止抢占(非抢占内核)或允许抢占但禁止阻塞(可抢占内核)。
- 核心规则:临界区内不允许调用可能导致睡眠的函数(如
kmalloc(GFP_KERNEL)、mutex_lock、copy_to_user)。 - 嵌套:允许嵌套使用,但必须严格配对。
工程提示:在可抢占内核(CONFIG_PREEMPT_RCU)中,
rcu_read_lock 会禁用抢占;在非抢占内核中,它仅是一个屏障。始终将读侧临界区视为“不可阻塞”区域。
// 典型读侧模式
rcu_read_lock();
ptr = rcu_dereference(global_ptr);
if (ptr)
do_something(ptr->data);
rcu_read_unlock();
2.2 RCU 写侧:synchronize_rcu
synchronize_rcu() 是写侧最常用的同步原语,用于等待所有正在进行的 RCU 读侧临界区完成。
- 语义:阻塞调用者,直到所有在调用时刻之前已开始的 RCU 读侧临界区都已完成。
- 使用场景:在释放被 RCU 保护的内存之前,必须调用
synchronize_rcu()以确保没有读者仍在访问旧数据。 - 性能代价:这是一个阻塞调用,可能休眠数毫秒(取决于 CPU 数量和读侧临界区长度)。
死锁风险:绝不能在
rcu_read_lock 临界区内调用 synchronize_rcu()。这会导致自我死锁,因为 synchronize_rcu() 需要等待当前 CPU 上的读侧临界区完成,而它自己正阻塞在临界区内。
// 写侧更新与回收模式
old = rcu_dereference_protected(global_ptr, lockdep_is_held(&update_lock));
new = alloc_new_data();
rcu_assign_pointer(global_ptr, new);
synchronize_rcu(); // 等待所有读者离开旧指针
kfree(old); // 安全释放
2.3 异步回收:call_rcu
当写侧不能阻塞时,call_rcu() 提供异步回调机制,将回收操作推迟到所有读者完成之后。
- 语义:注册一个回调函数,在未来的某个 RCU 宽限期(grace period)结束后被调用。
- 使用场景:中断上下文、高吞吐路径、或任何不允许睡眠的写侧代码。
- 回调执行环境:回调在软中断(softirq)上下文中执行,不能睡眠。
| 特性 | synchronize_rcu | call_rcu |
|---|---|---|
| 阻塞性 | 阻塞调用者 | 非阻塞,立即返回 |
| 执行上下文 | 进程上下文 | 任何上下文(包括中断) |
| 回调上下文 | N/A(同步等待) | 软中断上下文 |
| 延迟 | 确定,但可能较长 | 不确定,取决于宽限期 |
| 典型用途 | 模块卸载、慢速路径 | 高频率更新、中断路径 |
// 异步回收示例
struct rcu_head my_rcu;
void my_callback(struct rcu_head *head) {
struct my_data *p = container_of(head, struct my_data, rcu);
kfree(p); // 在软中断中执行
}
// 更新路径
old = rcu_dereference_protected(gp, 1);
new = alloc_new();
rcu_assign_pointer(gp, new);
call_rcu(&old->rcu, my_callback); // 不阻塞
2.4 安全解引用:rcu_dereference
rcu_dereference() 是读者访问 RCU 保护指针的唯一合法入口。它确保在读取指针时,编译器不会进行重排序优化,且硬件能观察到一致的值。
- 语义:返回被 RCU 保护的指针的值,并插入必要的内存屏障(memory barrier)和编译器屏障。
- 变体:
rcu_dereference_protected()用于写侧已知持有锁的场景;rcu_dereference_check()用于附加条件检查。 - 规范:每次解引用 RCU 指针都必须使用此宏,否则可能导致读取到未完成更新的中间状态。
关键区别:
rcu_dereference 与普通指针读取的区别在于它强制了“依赖顺序”(address dependency),确保在读取指针指向的内容之前,指针本身的值是完整的。
// 正确用法
rcu_read_lock();
p = rcu_dereference(gp);
if (p)
val = p->field; // 安全
rcu_read_unlock();
// 错误用法(可能导致崩溃)
rcu_read_lock();
p = gp; // 缺少 rcu_dereference!
if (p)
val = p->field; // 可能读到垃圾值
rcu_read_unlock();
2.5 原子更新:rcu_assign_pointer
rcu_assign_pointer() 是写侧更新 RCU 保护指针的标准方式。它确保新指针的赋值操作对读者是原子的,并插入必要的写屏障。
- 语义:将新值安全地赋值给 RCU 指针,保证所有在此之前的内存写入(如新数据结构的初始化)对后续的读者可见。
- 配对使用:写侧用
rcu_assign_pointer发布新指针,读侧用rcu_dereference获取。 - 零开销:在大多数架构上,如果指针赋值本身就是原子的,此宏仅充当编译器屏障。
// 写侧发布模式
struct my_data *new, *old;
new = kmalloc(sizeof(*new), GFP_KERNEL);
new->field = 42; // 初始化数据
smp_wmb(); // 确保初始化完成
rcu_assign_pointer(gp, new); // 发布指针
// 等价于:smp_store_release(&gp, new);
2.6 API 协作流程
下图展示了 RCU 读侧与写侧 API 的完整协作流程,以及宽限期(Grace Period)的概念。
2.7 使用规范与常见陷阱
| API | 必须遵守的规范 | 常见错误 |
|---|---|---|
| rcu_read_lock/unlock | 临界区内禁止睡眠、禁止调用 synchronize_rcu |
在临界区内调用 kmalloc(GFP_KERNEL) |
| synchronize_rcu | 必须在进程上下文调用;不能持有自旋锁 | 在 RCU 读侧临界区内调用导致死锁 |
| call_rcu | 回调函数不能睡眠;不能调用任何阻塞函数 | 回调中调用 mutex_lock |
| rcu_dereference | 必须在 RCU 读侧临界区内使用;每次解引用都必须使用 | 直接读取 gp 而非 rcu_dereference(gp) |
| rcu_assign_pointer | 确保新数据完全初始化后再赋值;写侧需持有更新锁 | 赋值后未使用 smp_wmb() 或 rcu_assign_pointer |
死锁场景复现:假设线程 A 持有自旋锁并在 RCU 读侧临界区内调用
synchronize_rcu(),线程 B 在另一个 CPU 上尝试获取同一自旋锁。由于 synchronize_rcu() 需要等待线程 A 离开临界区,而线程 A 又在等待 synchronize_rcu() 返回,形成循环等待——经典死锁。
2.8 总结:API 选择决策树
在实际工程中,根据上下文选择合适的 RCU API 至关重要。以下决策树帮助你快速判断: