4. RCU数据发布:rcu_assign_pointer原理、内存屏障与发布语义、写者更新流程
在RCU(Read-Copy Update)机制中,数据发布(Data Publication)是写者更新共享数据的关键环节。写者通过 rcu_assign_pointer() 宏安全地将新数据结构的指针发布给读者,确保读者在读取时不会看到不一致的中间状态。本章深入解析 rcu_assign_pointer 的实现原理、其背后的内存屏障与发布语义,并完整梳理写者更新流程。
4.1 rcu_assign_pointer 原理
rcu_assign_pointer() 是RCU写者用于发布新指针的核心宏。它的主要职责是:将新指针赋值给共享全局指针,并插入必要的内存屏障,确保所有先前的写操作(如对新数据结构的初始化)在指针发布之前对其他CPU可见。
rcu_dereference() 安全地获取该指针。
其典型实现(简化版)如下:
#define rcu_assign_pointer(p, v) \
({ \
typeof(*p) *_________p1 = (typeof(*p) *)(v); \
smp_wmb(); \
WRITE_ONCE(p, _________p1); \
_________p1; \
})
关键步骤解析:
- 类型转换与临时变量: 将新指针
v转换为与p兼容的类型,并存储在临时变量_________p1中。 - 写内存屏障
smp_wmb(): 这是发布语义的核心。它保证在smp_wmb()之前的所有写操作(如初始化新数据结构的字段)都已完成,并且对其他CPU可见,然后才执行后续的指针赋值。 - 原子指针赋值
WRITE_ONCE(): 确保对全局指针p的写入是原子的,避免编译器优化导致的部分写入(如撕裂写),从而保证读者不会读取到损坏的指针值。
smp_wmb() 与 WRITE_ONCE() 的组合,共同构建了“发布-订阅”模型中的“发布”端。写者必须先完成所有数据初始化,再发布指针。
4.2 内存屏障与发布语义
内存屏障(Memory Barrier)是RCU实现无锁同步的基石。在 rcu_assign_pointer() 中,smp_wmb() 扮演着“发布屏障”的角色。
4.2.1 发布语义的硬件基础
现代CPU为了提升性能,会乱序执行(Out-of-Order Execution)指令。在没有屏障的情况下,写者CPU可能将指针赋值操作提前到数据初始化之前完成,导致其他CPU看到新指针但读取到未初始化的旧数据。发布屏障强制规定了写操作的全局顺序。
下图展示了发布屏障如何防止写操作重排序:
4.2.2 发布语义与获取语义的配对
发布语义(Release Semantics)通常与获取语义(Acquire Semantics)配对使用。在RCU中:
- 发布端(写者):
rcu_assign_pointer()提供发布语义,保证屏障前的写操作不会被重排序到屏障后的写操作之后。 - 获取端(读者):
rcu_dereference()提供获取语义,保证屏障后的读操作不会被重排序到屏障前的读操作之前。
这种配对确保了“happens-before”关系:写者对数据的初始化,必然发生在读者读取该数据之前。
| 语义类型 | 对应宏 | 屏障类型 | 作用 |
|---|---|---|---|
| 发布语义 (Release) | rcu_assign_pointer() |
smp_wmb() 或 smp_store_release() |
防止写-写重排序,确保发布前的写操作全局可见 |
| 获取语义 (Acquire) | rcu_dereference() |
smp_rmb() 或 smp_load_acquire() |
防止读-读/读-写重排序,确保获取后的读操作看到最新数据 |
smp_wmb() 通常被定义为空操作(barrier()),因为单核不存在内存一致性问题的困扰。但在SMP(Symmetric Multi-Processing)系统中,它是必不可少的。
4.3 写者更新流程
一个完整的RCU写者更新流程通常包含三个步骤:分配新数据 → 初始化新数据 → 发布新指针。之后,写者需要等待所有可能持有旧指针的读者完成读端临界区,才能安全地回收旧数据。
下图展示了写者更新流程的完整生命周期:
4.3.1 典型写者代码示例
以下是一个典型的RCU写者更新链表节点的代码片段:
struct my_data {
int key;
int value;
struct rcu_head rcu; // 用于宽限期后的回调
};
static struct my_data __rcu *global_ptr;
void update_data(int new_value) {
struct my_data *old_ptr;
struct my_data *new_ptr;
// 1. 分配新数据
new_ptr = kmalloc(sizeof(*new_ptr), GFP_KERNEL);
if (!new_ptr)
return;
// 2. 初始化新数据
new_ptr->key = 42;
new_ptr->value = new_value;
// 3. 发布新指针(包含smp_wmb())
old_ptr = rcu_assign_pointer(global_ptr, new_ptr);
// 4. 等待宽限期,确保所有读者不再持有旧指针
synchronize_rcu();
// 5. 安全回收旧数据
kfree(old_ptr);
}
4.3.2 宽限期等待的两种方式
写者在发布新指针后,必须等待一个宽限期(Grace Period),才能安全回收旧数据。Linux内核提供了两种主要方式:
| 方式 | 函数/宏 | 行为 | 适用场景 |
|---|---|---|---|
| 同步等待 | synchronize_rcu() |
阻塞当前写者,直到所有CPU都经历一次上下文切换(即所有读者退出读端临界区) | 写者可以睡眠等待的场景,如进程上下文 |
| 异步回调 | call_rcu() |
注册一个回调函数,在宽限期结束后由RCU软中断执行 | 写者不能阻塞的场景,如中断上下文、高性能要求 |
synchronize_rcu() 虽然简单易用,但会导致写者阻塞,可能影响吞吐量。在写操作频繁的场景下,优先考虑使用 call_rcu() 异步回收,将回收工作推迟到宽限期结束后。
4.4 总结
rcu_assign_pointer() 是RCU写者发布数据的核心接口,它通过 smp_wmb() 内存屏障实现了发布语义,确保了数据初始化的全局可见性。写者更新流程遵循“分配-初始化-发布-等待-回收”的严格顺序,其中发布屏障和宽限期等待是保证无锁安全的关键。理解这些原理,是掌握RCU机制和编写正确、高效的无锁代码的基础。