引言:为什么需要 RCU?

在高性能内核编程中,读写锁(rwlock)是最常见的同步原语之一。但它有一个根本性的缺陷:读写之间互斥——即使读操作远多于写操作,读者之间也会因为写者的存在而产生竞争。当系统扩展到数百个并发读者时,这种竞争会成为性能瓶颈。

RCU(Read-Copy-Update)是 Linux 内核独创的一种同步机制,它在 2002 年被引入 2.5 内核,如今已成为内核中最重要的并发控制手段之一。RCU 的核心洞察是:读取操作可以完全无锁,不需要任何原子指令、内存屏障或自旋锁,而写入操作的代价则由"宽限期"和延迟回收来承担。

在内核中,RCU 的使用无处不在:路由表、文件系统 dentry 缓存、进程描述符、网络协议栈、内存管理子系统、设备驱动模型等关键路径都大量使用 RCU。理解 RCU,是理解现代 Linux 内核并发架构的关键。

一、RCU 核心概念模型

1.1 读取侧临界区(Read-Side Critical Section)

RCU 的读取侧代码被称为"临界区",但它与传统锁的临界区截然不同:


// RCU 读取侧——完全无锁
rcu_read_lock();
// 这里是临界区,可以自由访问 RCU 保护的数据
p = rcu_dereference(ptr);  // 发布-订阅语义的指针读取
if (p) {
    // 安全地访问 p 指向的数据
    printk("value = %d\n", p->value);
}
rcu_read_unlock();

关键点在于:rcu_read_lock() 和 rcu_read_unlock() 在 UP(单核)系统上基本上是空操作(关闭内核抢占)。在 SMP 系统上,它们也只是禁止内核抢占,不涉及任何原子操作或缓存行竞争。因此,成千上万个读者可以同时进入临界区而互不干扰,读取操作的吞吐量可以达到接近缓存访问的速度。

1.2 发布-订阅语义(Publish-Subscribe Semantics)

RCU 的写入侧使用"发布-订阅"模型。写者通过 rcu_assign_pointer() 发布新数据,读者通过 rcu_dereference() 订阅数据。这两个宏不仅仅是赋值和解引用——它们在底层插入了必要的内存屏障,确保:

  • 写者:新数据的初始化在指针发布之前完成(防止读者看到未初始化的数据)
  • 读者:指针解引用和使用数据的操作不会被乱序执行到指针读取之前(防止基于旧指针的新解引用)

1.3 数据回收与宽限期(Grace Period)

RCU 写入侧最独特之处是延迟回收。当写者需要更新一个被 RCU 保护的数据结构时,它不是直接修改原数据,而是:

  1. 分配新内存,拷贝旧数据并修改
  2. 通过 rcu_assign_pointer() 原子替换全局指针
  3. 等待"宽限期"结束——即确保所有在指针替换前进入 RCU 临界区的读者都已经退出
  4. 宽限期结束后,安全回收旧版本数据

// RCU 写入侧——替换节点
new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
*new_node = *old_node;        // 拷贝旧数据
new_node->value = new_value;  // 修改
rcu_assign_pointer(global_ptr, new_node); // 原子发布
synchronize_rcu();             // 等待宽限期
kfree(old_node);               // 安全回收旧数据

二、宽限期机制深度剖析

2.1 什么是宽限期?

宽限期(Grace Period)是 RCU 中最为核心的概念。它被定义为这样一个时间段:从某个时间点 T 开始,到所有在时间 T 之前已经进入 RCU 读取侧临界区的 CPU 都退出临界区为止。

在这段时间内,任何在 T 之前进入临界区的读者都可能持有旧版本数据的引用。宽限期结束后,可以保证没有任何读者仍在使用旧数据,此时才能安全回收。

Linux 内核实现了一个精妙的宽限期检测机制:

  • 使用静止状态(Quiescent State)概念——每个 CPU 在退出 RCU 临界区时报告一次 Q 状态
  • 内核维护每个 CPU 的 rcu_data 结构,记录该 CPU 的 RCU 状态
  • 当一个 CPU 经历了一次上下文切换(或显式调用 rcu_note_context_switch()),就意味着它离开了 RCU 临界区
  • 当所有 CPU 都至少经历了一次静止状态,宽限期结束

2.2 宽限期的实现架构

RCU 子系统将 CPU 组织成树状层级结构(rcu_node),每个节点管理一组 CPU。当所有子节点都报告静止状态时,父节点向上级汇报。这种树状结构使得宽限期检测的时间复杂度从 O(N) 降低到 O(log N)。


// 内核中的 RCU 状态节点(简化)
struct rcu_node {
    raw_spinlock_t lock;
    unsigned long qsmask;       // 子节点静止状态位图
    struct rcu_node *parent;
};

struct rcu_data {
    unsigned long gpnum;        // 当前宽限期号
    bool qs_pending;            // 是否有未报告的静止状态
    struct rcu_node *mynode;    // 所属节点
};

2.3 同步 vs 异步宽限期

内核提供了两种等待宽限期的方式:

  • synchronize_rcu():同步等待,调用者阻塞直到宽限期结束。适用于非热路径(如模块卸载)。
  • call_rcu():异步等待,注册回调函数,宽限期结束后在软中断上下文执行回调。适用于热路径(高频更新场景)。

// 异步方式——高频更新场景的首选
void my_callback(struct rcu_head *head) {
    struct my_data *old = container_of(head, struct my_data, rcu);
    kfree(old);
}

// 更新时替换并注册延迟回收
new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
*new_node = *old_node;
new_node->value = new_value;
rcu_assign_pointer(global_ptr, new_node);
call_rcu(                        
                    
点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部