摘要

RCU(Read-Copy-Update)是 Linux 内核中最精妙的同步机制之一,它允许多个读者同时访问共享数据而无需任何锁,从而在读多写少的场景下实现了卓越的可伸缩性。本文从 RCU 的核心概念出发,深入剖析宽限期(Grace Period)机制、Tree RCU 架构实现、核心 API 使用模式,并结合内核 VFS 路由缓存、网络协议栈等实际案例,展示 RCU 在真实生产环境中的应用技巧与性能调优方法。

1. 为什么需要 RCU

在 SMP(对称多处理)系统中,传统的读写锁(rwlock)和自旋锁(spinlock)在面对高并发读取时存在严重瓶颈。以 Linux 内核的路由缓存(dst_entry)为例,每一帧网络数据包的路由查找都需要读取路由缓存,如果采用读写锁,成千上万个 CPU 核心竞争同一把锁,性能随核心数增加急剧下降。

RCU 的设计哲学是:读者几乎没有任何开销。在非 CONFIG_PREEMPT 内核中,rcu_read_lock() 和 rcu_read_unlock() 不生成任何代码,这意味着 RCU 的读端性能等同于无锁的单一访问。这种特性使 RCU 成为内核中路由表、文件描述符表、PID 映射表等热路径数据结构的首选同步方案。

2. RCU 核心概念

2.1 读者、写者与临界区

RCU 将有数据访问权限的角色分为两类:

  • 读者(Reader):通过 rcu_read_lock()/rcu_read_unlock() 包裹的代码区域称为 RCU 读端临界区。读者不阻塞、不缓存、不修改数据,仅进行读取。
  • 写者(Writer):负责修改共享数据的线程。写者必须确保在释放旧数据引用之前,所有已经进入临界区的读者都已退出。

2.2 宽限期(Grace Period)

这是 RCU 最核心的概念。宽限期是指一个时间段,在此时间段内,所有在宽限期开始前就已经进入 RCU 读端临界区的读者,都已经完成了临界区的执行并退出。

关键性质:宽限期保证,在此之后,没有任何读者持有对旧版本数据的引用,写者可以安全地释放旧数据。

2.3 静止状态(Quiescent State)

当一个 CPU 不在任何 RCU 读端临界区时,称该 CPU 处于静止状态。默认 RCU 实现将所有 CPU 的静止状态检测作为宽限期结束的条件——即所有 CPU 都经历了一次静止状态,说明宽限期结束。

2.4 发布-订阅机制(Publish-Subscribe)

RCU 通过两个原语保证写者发布新数据和读者看到新数据的一致性:

  • rcu_assign_pointer():确保在更新指针之前,所有初始化新数据的操作已完成(带有内存屏障)。
  • rcu_dereference():确保在解引用指针之后,后续的读取操作不会被重排到解引用之前。

3. Core RCU API 详解

3.1 基础 API


// 读者侧 - 标记 RCU 读端临界区开始/结束
void rcu_read_lock(void);    // 禁止内核抢占(可嵌套调用)
void rcu_read_unlock(void);  // 恢复内核抢占

// 写者侧 - 等待所有现有读者退出临界区
void synchronize_rcu(void);  // 同步等待宽限期结束(可能阻塞)

// 发布-订阅原语
#define rcu_assign_pointer(p, v)    // 发布新值
#define rcu_dereference(p)          // 安全读取 RCU 保护指针

3.2 非阻塞回调 API

当写者处于不能阻塞的上下文(如中断处理程序、持有自旋锁时),不能使用 synchronize_rcu(),此时必须使用 call_rcu():


// 在宽限期结束后异步调用回调函数
void call_rcu(struct rcu_head *head, rcu_callback_t func);

// call_rcu() 定义通常在待释放的数据结构中嵌入 rcu_head
struct my_node {
    int data;
    struct rcu_head rcu;
};

static void my_node_free_callback(struct rcu_head *head) {
    struct my_node *node = container_of(head, struct my_node, rcu);
    kfree(node);
}

3.3 新版 API(Linux 4.20+)

modern 内核推荐使用以下更加类型安全的 API:


typeof(*p) *rcu_replace_pointer(typeof(p) __rcu *rp, typeof(*p) *v, bool c);
#define srcu_read_lock(scp)       // 可睡眠版本的 RCU 读锁
#define srcu_read_unlock(scp)
void synchronize_srcu(struct srcu_struct *ssp);

4. 经典 RCU 模式实战

4.1 链表删除模式

这是 RCU 最经典的使用场景——在受 RCU 保护的双向链表中安全删除节点:


// 链表节点定义
struct route_entry {
    __be32 dst_addr;
    __be32 next_hop;
    struct list_head list;
};

static LIST_HEAD(route_list);
static DEFINE_RWLOCK(route_lock);  // 仅写者之间互斥

// 读者:无锁查找
struct route_entry *route_lookup_rcu(__be32 dst) {
    struct route_entry *entry;
    
    rcu_read_lock();
    list_for_each_entry_rcu(entry, &route_list, list) {
        if (entry->dst_addr == dst) {
            rcu_read_unlock();
            return entry;
        }
    }
    rcu_read_unlock();
    return NULL;
}

// 写者:安全删除
void route_delete(__be32 dst) {
    struct route_entry *entry, *tmp;
    
    write_lock(&route_lock);
    list_for_each_entry_safe(entry, tmp, &route_list, list) {
        if (entry->dst_addr == dst) {
            list_del_rcu(&entry->list);  // 使用 RCU 版本删除
            write_unlock(&route_lock);
            synchronize_rcu();            // 等待宽限期结束
            kfree(entry);                 // 安全释放内存
            return;
        }
    }
    write_unlock(&route_lock);
}

// 写者:安全插入
int route_add(__be32 dst, __be32 hop) {
    struct route_entry *new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL);
    if (!new_entry) return -ENOMEM;
    
    new_entry->dst_addr = dst;
    new_entry->next_hop = hop;
    
    write_lock(&route_lock);
    list_add_rcu(&new_entry->list, &route_list);
    write_unlock(&route_lock);
    
    return 0;
}

4.2 指针替换模式(单全局指针)


struct global_config {
    int timeout;
    int max_retries;
    bool debug_mode;
};

struct __rcu *global_config;  // RCU 保护的全局指针

// 读者:无锁读取配置
void read_config(void) {
    struct global_config *cfg;
    
    rcu_read_lock();
    cfg = rcu_dereference(global_config);
    printk("timeout=%d, retries=%d, debug=%d\n",
           cfg->timeout, cfg->max_retries, cfg->debug_mode);
    rcu_read_unlock();
}

// 写者:原子替换全局配置
int update_config(int timeout, int max) {
    struct global_config *new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
    struct global_config *old_cfg;
    if (!new_cfg) return -ENOMEM;
    
    new_cfg->timeout = timeout;
    new_cfg->max_retries = max;
    new_cfg->debug_mode = true;
    
    old_cfg = rcu_dereference_protected(global_config, 
                                         lockdep_is_held(&config_lock));
    rcu_assign_pointer(global_config, new_cfg);
    synchronize_rcu();
    kfree(old_cfg);
    
    return 0;
}

5. Tree RCU 架构解析

5.1 为什么需要 Tree RCU

早期经典 RCU(Classic RCU)在检测宽限期结束时需要遍历所有 CPU 的静止状态,当 CPU 数量达到数千颗时,每次宽限期检测的开销变得不可接受。Tree RCU 通过将 CPU 组织为树状层级结构,将检测复杂度从 O(N) 降低到 O(log N)。

5.2 Tree RCU 节点层级


                    root rcu_node (覆盖所有 CPU)
                   /                              \
          Node [0-31]                     Node [32-63]  
         /          \                    /           \
    Leaf[0-15]  Leaf[16-31]      Leaf[32-47]   Leaf[48-63]
    CPU 0-15    CPU 16-31         CPU 32-47    CPU 48-63

每个 rcu_node 通过一个位图(bitmap)追踪其管辖的 CPU 是否都报告了静止状态。当一个 leaf 节点确认其所有 CPU 都静止后,该 leaf 向上级节点报告。根节点收到所有子节点确认后,宣告宽限期结束。

5.3 宽限期检测流程

  1. 写者发起 synchronize_rcu(),设置新的宽限期序列号。
  2. 每个 CPU 在上下文切换、用户态返回或空闲循环中检测是否需要报告静止状态。
  3. 静止状态信息从 leaf 节点逐级向上传播。
  4. 根节点确认所有 CPU 均已静止,通知等待的写者宽限期结束。

5.4 SRCU — 可睡眠 RCU

标准 RCU 要求读者不可睡眠(不可阻塞),否则会无限期延长宽限期。对于需要睡眠的读者场景(如访问可能触发缺页的内核内存),内核提供了 Sleepable RCU(SRCU):


// SRCU API(读者侧允许睡眠)
int srcu_read_lock(struct srcu_struct *ssp);
void srcu_read_unlock(struct srcu_struct *ssp, int idx);
void synchronize_srcu(struct srcu_struct *ssp);
void call_srcu(struct srcu_struct *ssp, struct rcu_head *rhp, rcu_callback_t func);

6. RCU 性能分析与锁对比

6.1 读端开销基准测试

同步机制单读者耗时64核并发读总吞吐可扩展性
无同步(理论上限)1x64x线性
RCU(非抢占内核)1x63.8x近线性
原子读(atomic)1.05x58x良好
读写锁(rwlock)1.3x8x(写时0)极差
序列锁(seqlock)1.1x45x良好

6.2 写端代价

RCU 的写者代价主要来源于 synchronize_rcu() 的同步等待。典型的宽限期持续时间在 1-10ms 之间。因此 RCU 不适合更新频率极高(大于1000次/秒)的场景,而更适合读频率极高但写频率较低的场景。

7. RCU CPU Stall 检测与调试

当某个 CPU 长时间停留在 RCU 读端临界区,宽限期无法结束,导致写者无限等待。内核的 RCU Stall Detector 会打印详细诊断信息。


INFO: rcu_sched self-detected stall on CPU
 rcu_sched: 
    0: c1767891 ..... g11000486 f0000001 03 .....
    (t=2500 jiffies g=11000486 c=11000485 q=87)
 NMI backtrace for cpu 0
 CPU: 0 PID: 0 Comm: swapper/0
 Call Trace:
  <IRQ>
  dump_stack
  rcu_check_callbacks
  rcu_cpu_stall_timeout

7.1 常见 Stall 原因

  • 读者在 RCU 临界区内调用了可能睡眠的函数(如 copy_from_user())
  • 内核抢占被关闭时间过长且处于 RCU 临界区内
  • CPU 频率不稳定或 TSC 不同步导致计时错误

7.2 调优参数


# 调整烧检测超时时间(默认21秒,可缩短到5秒)
sysctl -w kernel.rcu_cpu_stall_timeout=5

# 启用实验性快速检测
CONFIG_RCU_EXP_CPU_STALL_TIMEOUT=5000

# 禁用烧检测(生产环境谨慎使用)
sysctl -w kernel.rcu_cpu_stall_suppress=1

8. 生产级 RCU 使用最佳实践

8.1 设计原则

  • 确保读多写少:如果写频率超过每秒1000次,考虑使用 seqlock 或读写锁。
  • 控制临界区大小:RCU 读者临界区内不做任何 I/O、内存分配或睡眠操作。
  • 写者使用适当序列化:RCU 只保护读者和写者之间的同步,写者之间的互斥需要额外使用 spinlock 或 mutex。

8.2 避免的陷阱

  • 在 rcu_read_lock 内睡眠:导致宽限期无法结束,最终触发 CPU Stall 或 OOM。
  • 忘记 rcu_assign_pointer:直接赋值可能导致读者看到半初始化的数据结构。
  • 在 call_rcu 回调中再次进入 RCU 临界区:形成嵌套可能导致死锁或无限延迟。

8.3 性能优化技巧

  • 使用 call_rcu() 批量操作替代频繁的 synchronize_rcu(),降低宽限期等待频率。
  • 利用 kfree_rcu() 替代 call_rcu(kfree) 以获得批量释放的优化路径。
  • 在读路径使用 rcu_dereference_bh() 或 rcu_dereference_sched() 获得更轻量的屏障。

9. 内核子系统中的 RCU 应用实例

9.1 网络协议栈 — 路由缓存

Linux 内核的路由表(FIB)使用 RCU 保护,每一帧数据包的路由查找都可以无锁进行。这是 RCU 在内核中最核心的应用场景之一。

9.2 VFS 层 — 文件描述符表

files_struct->fdt 是 RCU 保护的文件描述符表。当进程扩展其文件描述符表时,新表通过 rcu_assign_pointer() 原子替换旧表,旧表通过 call_rcu() 异步释放。

9.3 进程调度 — PID 哈希表

PID 到 struct task_struct 的映射通过 RCU 保护的哈希表实现,find_task_by_pid() 等 API 可以无锁高效运行。

10. 总结

RCU 是 Linux 内核中最优雅且最强大的同步原语,其核心思想是以空间换时间、以写者代价换读者自由。掌握 RCU 不仅需要 API 层面的技能,更需要从宽限期机制、Tree 层级架构、内存屏障语义等多个维度理解其本质。在多核时代,热路径的并发数据结构设计几乎都离不开 RCU。对于系统软件开发者来说,理解 RCU 是通往高性能内核编程的必经之路。

参考资料

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部