4. RCU 数据发布机制:rcu_assign_pointer 与 rcu_dereference 的语义与内存屏障

RCU(Read-Copy-Update)的核心思想是将数据更新操作分解为“发布(Publish)”与“订阅(Dereference)”两个阶段。写入端通过 rcu_assign_pointer() 发布一个新版本的指针,读取端通过 rcu_dereference() 安全地获取该指针。这两个原语背后依赖精确的内存屏障指令来保证多核环境下的可见性与有序性。

4.1 发布-订阅模型概述

在 RCU 机制中,数据更新不是直接修改共享内存,而是分配新版本的数据结构,然后将指向新数据的指针原子性地发布到全局位置。读取端在订阅时,必须确保看到的是完整发布的数据,而不是部分更新的中间状态。

核心原则:写入端必须先完成对数据体的所有写操作,再执行指针发布;读取端必须先获取指针,再通过该指针读取数据体。任何重排都会导致数据损坏。
RCU 发布-订阅模型流程图 写入端(发布者) ① 分配新数据对象 new_data ② 写入 new_data->field1, field2... ③ smp_wmb() 写内存屏障 ④ rcu_assign_pointer(gp, new_data) 读取端(订阅者) ① rcu_dereference(gp) → ptr ② smp_rmb() 读内存屏障 ③ 读取 ptr->field1, field2... ④ 安全使用数据 指针发布

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() 指针赋值操作 防止编译器合并/拆分指针写入,保证原子性
实践要点:在 x86 架构下,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 发布-订阅机制的正确性依赖于写入端和读取端内存屏障的精确配对:

RCU 内存屏障配对关系图 写入端 写数据体 smp_wmb() rcu_assign_pointer 读取端 rcu_dereference smp_rmb() 读数据体 写屏障 → 读屏障 配对 指针发布 → 指针读取 顺序 配对屏障保证:写端数据 → 读端可见

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() 的等待开销。
性能数据(x86-64, 3.0GHz):
- 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 参数的前提。