4. RCU 数据发布机制:rcu_assign_pointer 与 rcu_dereference 的语义与内存屏障
RCU(Read-Copy-Update)的核心思想是将数据更新操作分解为“发布(Publish)”与“订阅(Dereference)”两个阶段。写入端通过 rcu_assign_pointer() 发布一个新版本的指针,读取端通过 rcu_dereference() 安全地获取该指针。这两个原语背后依赖精确的内存屏障指令来保证多核环境下的可见性与有序性。
4.1 发布-订阅模型概述
在 RCU 机制中,数据更新不是直接修改共享内存,而是分配新版本的数据结构,然后将指向新数据的指针原子性地发布到全局位置。读取端在订阅时,必须确保看到的是完整发布的数据,而不是部分更新的中间状态。
4.2 rcu_assign_pointer 语义与实现
rcu_assign_pointer() 用于写入端,将一个新指针安全地赋值给 RCU 保护的全局指针变量。其核心语义是:确保在指针赋值之前,所有对数据体的写操作对其他 CPU 可见。
4.2.1 函数原型与宏定义
#define rcu_assign_pointer(p, v) \
do { \
uintptr_t _r_a_p__v = (uintptr_t)(v); \
rcu_assign_pointer_check((p), _r_a_p__v); \
} while (0)
/* 核心实现(简化) */
#define rcu_assign_pointer(p, v) \
do { \
smp_wmb(); /* 写内存屏障:确保之前所有写操作完成 */ \
WRITE_ONCE(p, v); /* 原子写入指针 */ \
} while (0)
4.2.2 内存屏障作用
| 屏障类型 | 位置 | 作用 |
|---|---|---|
smp_wmb() |
数据体写入之后,指针赋值之前 | 防止编译器/CPU 将数据体写操作重排到指针赋值之后 |
WRITE_ONCE() |
指针赋值操作 | 防止编译器合并/拆分指针写入,保证原子性 |
smp_wmb() 退化为 barrier() 编译器屏障,因为 x86 的强内存模型已经保证了写写有序。但在 ARM/PowerPC 等弱内存模型架构上,smp_wmb() 会生成真正的硬件屏障指令(如 dmb)。
4.3 rcu_dereference 语义与实现
rcu_dereference() 用于读取端,从 RCU 保护的指针中安全地获取值。其核心语义是:确保在通过返回指针读取数据体之前,指针本身的读取操作已完成,且后续的读操作不会被重排到指针读取之前。
4.3.1 函数原型与宏定义
#define rcu_dereference(p) \
({ \
typeof(p) _________p1 = READ_ONCE(p); \
rcu_dereference_check(_________p1); \
(_________p1); \
})
/* 核心实现(简化) */
#define rcu_dereference(p) \
({ \
typeof(p) _p = READ_ONCE(p); /* 原子读取指针 */ \
smp_read_barrier_depends(); /* 读内存屏障(依赖屏障) */ \
_p; \
})
4.3.2 依赖屏障(Dependency Barrier)
smp_read_barrier_depends() 是一种轻量级的读屏障,仅阻止后续数据依赖读操作被重排到指针读取之前。在大多数架构上,数据依赖关系天然保证了有序性,因此该屏障退化为空操作(nop)。但在 Alpha 架构上,CPU 可能会推测性地执行依赖读取,因此需要显式屏障。
| 架构 | smp_read_barrier_depends 实现 | 说明 |
|---|---|---|
| x86 / x86-64 | 空操作(nop) | 硬件保证读读有序,数据依赖天然成立 |
| ARM / ARM64 | 空操作(nop) | ARM 架构保证依赖读取有序 |
| PowerPC | 空操作(nop) | PowerPC 保证依赖读取有序 |
| Alpha | mb(完整内存屏障) | Alpha 可能推测执行依赖读取,需要显式屏障 |
4.4 内存屏障的完整配对关系
RCU 发布-订阅机制的正确性依赖于写入端和读取端内存屏障的精确配对:
4.5 典型使用模式
以下代码展示了 RCU 发布-订阅机制的完整使用模式:
/* 定义 RCU 保护的数据结构 */
struct foo {
int a;
int b;
int c;
};
/* 全局指针,由 RCU 保护 */
struct foo __rcu *gp;
/* 写入端:更新数据 */
void update_foo(int new_a, int new_b, int new_c)
{
struct foo *new_fp, *old_fp;
/* 1. 分配新数据对象 */
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
new_fp->a = new_a;
new_fp->b = new_b;
new_fp->c = new_c;
/* 2. 发布新指针(内部包含 smp_wmb()) */
old_fp = rcu_replace_pointer(gp, new_fp, lockdep_is_held(&mutex));
/* 3. 等待所有读取端完成旧指针的使用 */
synchronize_rcu();
/* 4. 安全释放旧数据 */
kfree(old_fp);
}
/* 读取端:读取数据 */
void read_foo(void)
{
struct foo *fp;
rcu_read_lock();
/* 1. 订阅指针(内部包含 smp_rmb()) */
fp = rcu_dereference(gp);
/* 2. 安全使用数据 */
if (fp) {
printk("a=%d, b=%d, c=%d\n", fp->a, fp->b, fp->c);
}
rcu_read_unlock();
}
4.6 常见错误与避坑指南
gp = new_fp;)代替 rcu_assign_pointer(),会导致读取端看到部分初始化的数据,造成数据损坏或崩溃。
| 错误模式 | 后果 | 正确做法 |
|---|---|---|
不使用 rcu_assign_pointer |
读取端看到未完全初始化的数据 | 始终使用 rcu_assign_pointer 发布指针 |
不使用 rcu_dereference |
读取端可能读取到过时或不一致的指针 | 始终使用 rcu_dereference 获取指针 |
在 rcu_read_lock 外使用 rcu_dereference |
读取端可能被抢占,导致引用已释放的数据 | 确保在 RCU 读侧临界区内使用 |
在写入端使用 smp_rmb() 代替 smp_wmb() |
屏障方向错误,无法保证写写有序 | 写入端使用 smp_wmb(),读取端使用 smp_rmb() |
4.7 性能考量
RCU 发布-订阅机制在读取端几乎没有开销:
- 读取端:
rcu_dereference()在大多数架构上仅生成一个READ_ONCE()原子读取指令,无锁、无总线事务、无缓存行失效。 - 写入端:
rcu_assign_pointer()包含一个smp_wmb()写屏障,在 x86 上退化为编译器屏障,在 ARM 上生成dmb ishst指令,开销约 10-40 纳秒。 - 更新频率:RCU 适用于读多写少的场景。如果每秒更新超过 10,000 次,建议评估
synchronize_rcu()的等待开销。
- rcu_dereference:约 1-2 纳秒(单条 MOV 指令)
- rcu_assign_pointer:约 2-5 纳秒(含编译器屏障)
- synchronize_rcu:约 10-100 微秒(取决于 CPU 数量和 grace period 状态)
4.8 小结
rcu_assign_pointer 与 rcu_dereference 是 RCU 数据发布机制的核心原语,它们通过精确配对的内存屏障保证了多核环境下数据的一致性和可见性。理解这两个原语的语义和屏障行为,是正确使用 RCU 锁机制的基础,也是后续优化 sysctl 参数的前提。