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可见。

核心语义: 发布(Publication)—— 写者完成对新数据的全部写入后,通过一次原子性的指针赋值操作,将新数据“发布”到全局可见域。读者随后通过 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看到新指针但读取到未初始化的旧数据。发布屏障强制规定了写操作的全局顺序。

下图展示了发布屏障如何防止写操作重排序:

发布屏障(smp_wmb)防止写操作重排序 无发布屏障(错误) 写者CPU执行顺序 ① 指针赋值 (p = new) ② 初始化新数据 (new->x = 1) ③ 初始化新数据 (new->y = 2) 读者可能看到未初始化的数据! 有发布屏障(正确) 写者CPU执行顺序 ① 初始化新数据 (new->x = 1) ② 初始化新数据 (new->y = 2) ③ smp_wmb() 发布屏障 ④ 指针赋值 (p = new) 读者保证看到完整初始化的数据!

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() 防止读-读/读-写重排序,确保获取后的读操作看到最新数据
注意事项: 在单核CPU或UP(Uni-Processor)系统中,smp_wmb() 通常被定义为空操作(barrier()),因为单核不存在内存一致性问题的困扰。但在SMP(Symmetric Multi-Processing)系统中,它是必不可少的。

4.3 写者更新流程

一个完整的RCU写者更新流程通常包含三个步骤:分配新数据 → 初始化新数据 → 发布新指针。之后,写者需要等待所有可能持有旧指针的读者完成读端临界区,才能安全地回收旧数据。

下图展示了写者更新流程的完整生命周期:

RCU 写者更新完整流程 步骤1:分配新数据 kmalloc() 分配新结构体 步骤2:初始化新数据 写入新结构体的所有字段 步骤3:发布新指针 rcu_assign_pointer() 步骤4:等待宽限期 synchronize_rcu() 步骤5:回收旧数据 kfree(old_ptr) 关键点 步骤2必须在步骤3之前完成 步骤3通过smp_wmb()保证 步骤4等待所有读者完成

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机制和编写正确、高效的无锁代码的基础。