3. RCU基本API概览
RCU(Read-Copy-Update)机制的核心在于将“读”与“写”解耦,使得读者几乎不受写者影响。Linux内核提供了一组精炼的API,用于实现这种无锁同步。本章将逐一剖析最核心的五个API:rcu_read_lock / rcu_read_unlock、synchronize_rcu、call_rcu、rcu_assign_pointer 和 rcu_dereference。
学习提示: 这五个API构成了RCU编程模型的基石。理解它们各自的作用、配对关系以及数据流,是掌握RCU的关键。建议结合后面的SVG流程图进行记忆。
3.1 读者端:rcu_read_lock / rcu_read_unlock
这两个API标记了一个RCU读端临界区。在临界区内,读者可以安全地通过 rcu_dereference 访问被RCU保护的指针。
- rcu_read_lock():进入RCU读端临界区。在经典实现中,它通常禁止内核抢占(preempt_disable)或跟踪读者状态。
- rcu_read_unlock():退出RCU读端临界区。与
rcu_read_lock严格配对使用。
关键特性: 读端临界区允许嵌套,且开销极低(通常仅涉及内存屏障或计数器操作),这是RCU相比读写锁的巨大优势。
// 典型读者代码模式
rcu_read_lock();
ptr = rcu_dereference(gp); // 获取受保护指针
if (ptr) {
// 安全访问 ptr 指向的数据
do_something_with(ptr->field);
}
rcu_read_unlock();
3.2 写者端:synchronize_rcu 与 call_rcu
写者负责更新数据,但必须等待所有在读端临界区内的旧读者完成,才能安全释放旧数据。这两个API提供了同步和异步两种等待方式。
| API | 类型 | 行为描述 | 适用场景 |
|---|---|---|---|
| synchronize_rcu() | 同步 | 阻塞调用者,直到所有在调用前已存在的RCU读端临界区完成。 | 写者可以睡眠等待,对延迟不敏感。 |
| call_rcu(rcu_head, func) | 异步 | 注册一个回调函数,在未来的某个RCU宽限期(Grace Period)结束后被调用。 | 写者不能睡眠(如中断上下文),或需要批量处理回收。 |
宽限期(Grace Period)概念: 从所有在读端临界区内的旧读者退出,到新读者可以安全看到新数据的时间窗口。synchronize_rcu 和 call_rcu 的核心任务就是等待这个宽限期结束。
// 写者更新示例(同步方式)
struct foo *old, *new;
new = kmalloc(sizeof(*new), GFP_KERNEL);
new->field = 42;
old = rcu_dereference_protected(gp, lockdep_is_held(&my_lock));
rcu_assign_pointer(gp, new); // 发布新指针
synchronize_rcu(); // 等待所有旧读者退出
kfree(old); // 安全释放旧数据
3.3 数据发布与访问:rcu_assign_pointer 与 rcu_dereference
这两个API是RCU数据一致性的“守门员”。它们负责在写者发布新指针和读者获取指针时,插入必要的内存屏障,防止编译器或CPU重排序导致的数据不一致。
- rcu_assign_pointer(p, v):写者使用。将新指针
v赋值给p,并保证在此之前的写操作(初始化新数据)对后续的读者可见。 - rcu_dereference(p):读者使用。从
p读取指针,并保证在此之后的读操作(访问数据)不会在读取指针之前被重排序。
内存屏障语义: rcu_assign_pointer 隐含了一个写屏障(smp_wmb),而 rcu_dereference 隐含了一个读屏障(smp_rmb)或数据依赖屏障。在主流架构上,这些屏障开销极低。
// 写者:发布新数据
struct foo *new_data = alloc_and_init();
rcu_assign_pointer(g_foo, new_data);
// 读者:安全访问
rcu_read_lock();
struct foo *p = rcu_dereference(g_foo);
if (p)
printk("%d\n", p->value);
rcu_read_unlock();
3.4 API协作流程总览
下面的SVG图展示了读者和写者如何通过这五个API协同工作,完成一次无锁更新。
3.5 API配对与使用规则
正确使用RCU API必须遵循严格的配对规则,否则会导致数据竞争或死锁。
| API | 配对对象 | 违反后果 |
|---|---|---|
| rcu_read_lock() | rcu_read_unlock() | 读者可能被错误地视为退出,导致写者提前释放数据。 |
| rcu_dereference() | rcu_assign_pointer() | 读者可能看到未初始化的数据,或写者更新对读者不可见。 |
| synchronize_rcu() | rcu_read_lock() / rcu_read_unlock() | 如果读者不配对,synchronize_rcu 可能永远无法返回。 |
| call_rcu() | rcu_read_lock() / rcu_read_unlock() | 回调函数可能在读者仍在访问数据时被调用。 |
最佳实践: 在写者路径中,
rcu_assign_pointer 必须在 synchronize_rcu 或 call_rcu 之前调用。这确保了新指针先被发布,然后写者才等待旧读者退出,从而让新读者能够立即看到新数据。
3.6 API选择决策树
在实际内核模块开发中,如何选择合适的API组合?下面的决策树可以帮助你快速判断。
3.7 本章小结
本章详细介绍了RCU的五个核心API及其协作模式。读者端API(rcu_read_lock/unlock 和 rcu_dereference)提供了极低开销的读访问;写者端API(rcu_assign_pointer、synchronize_rcu、call_rcu)则通过宽限期机制安全地完成数据更新与回收。理解这些API的语义和配对规则,是后续深入分析RCU内部实现和高级特性的基础。