引言

在读多写少的并发场景中,传统的锁机制(如互斥锁、读写锁)往往因为读端竞争而导致性能瓶颈。Linux 内核引入的 RCU(Read-Copy-Update) 正是为解决这一痛点而生的无锁同步机制。RCU 允许读操作在不加锁的情况下安全并发执行,写操作通过"复制-更新-回收"的策略保证一致性。自 2.5 内核引入至今,RCU 已成为 Linux 内核最核心的同步原语之一,广泛应用于路由表、文件描述符表、内存管理、进程调度等关键路径。

一、RCU 核心思想

RCU 的核心理念可以概括为三点:

  • 读端无锁:读者无需获取锁,不执行任何内存屏障(除必要的编译器屏障),不会产生缓存行乒乓,因此读性能极高且可线性扩展。
  • 写者负责延迟回收:写者更新数据时创建新副本,修改完成后原子替换指针,然后等待所有已经存在的读者完成后再回收旧数据。
  • 宽限期(Grace Period):从指针替换到所有旧读者退出临界区的时间窗口,写者必须等待完整宽限期后才能安全释放旧数据。

二、RCU 核心 API

2.1 读端 API

// 进入 RCU 读端临界区(不可睡眠)
rcu_read_lock();

// 解除 RCU 读端临界区
rcu_read_unlock();

// 在持有 RCU 锁期间安全解引用指针
void *p = rcu_dereference(ptr);

// 临界区不可包含可能睡眠的操作!

2.2 写端 API

// 1. 分配新对象
struct data *new = kmalloc(sizeof(*new), GFP_KERNEL);
memcpy(new, old, sizeof(*data));
new->field = new_value;

// 2. 原子替换指针
struct data *old = rcu_assign_pointer(global_ptr, new);

// 3. 等待宽限期后回收旧数据
synchronize_rcu();          // 同步等待
// 或
call_rcu(&old->head, callback);  // 异步回调回收

2.3 链表操作 API

// RCU 链表遍历
list_for_each_entry_rcu(pos, head, member) {
    // 读操作,不可睡眠
}

// RCU 链表添加(插入排序)
list_add_rcu(new_node, head);

// RCU 链表替换
list_replace_rcu(old_node, new_node);

// RCU 链表删除
list_del_rcu(node);
// 之后需要 synchronize_rcu() 或 call_rcu() 等待后释放

三、宽限期机制详解

宽限期(Grace Period,简称 GP)是 RCU 正确性的核心保证。其工作原理如下:

  1. 写者发起更新:rcu_assign_pointer() 将新数据指针发布到全局可见位置。
  2. 静默状态检测:内核需要确认所有此前已进入 rcu_read_lock() 的读者都已退出(即不再持有旧数据的引用)。
  3. Quiescent State:每个 CPU 在两次上下文切换、或执行用户态代码、或进入 idle 时驻标记自己经历了"静默状态"。
  4. 完整宽限期:当所有 CPU 都经历了至少一次静默状态后,一个宽限期完成,旧数据可以被安全回收。

内核通过 per-CPU 计数器跟踪每个 CPU 的状态,使用树状结构(rcu_node)高效聚合检测信号,在拥有数千个 CPU 的系统中也能快速完成宽限期检测。

四、经典应用场景

4.1 路由缓存

IPv4/IPv6 路由查找在转发路径中面临极高并发读取需求和低频更新,RCU 完美契合此场景。rtable 结构体通过全局指针发布,读取路径无锁,更新时 call_rcu() 异步回收旧路由表。

4.2 文件描述符表(fdt)

从 Linux 2.6.17 开始,内核将进程的文件描述符表管理改为 RCU 方式。files_struct->fdt 可通过 rcu_dereference 无锁读取。扩容时通过 get_files_struct() 和 rcu_assign_pointer() 发布新表,释放时依赖 call_rcu() 确保旧的 fd 表可被先并发读取线程安全完成。

4.3 SLAB/SLUB 分配器缓存

每个 CPU 的 kmem_cache_cpu->freelist 使用 RCU 语义管理空闲对象链表。__rmqueue() 的 fast-path 通过 RCU 解引用 freelist 指针,极大提升了分配器的高并发性能。

4.4 VFS dentry 缓存

目录项缓存(dentry cache)的查找链表使用 RCU 保护。lookup_fast() 路径在 rcu_read_lock() 内完成哈希桶遍历和比较操作,大幅减少了路径名解析的锁竞争。

4.5 模块与基址重定位

模块加载时内核通过 RCU 保护模块列表,module_put()/try_module_get() 使用 srcu 语义确保模块引用计数的正确性,防止模块在仍有 RCU 读者时被卸载。

五、Sleepable RCU (SRCU)

标准 RCU 的读端临界区不允许睡眠(不允许可能引发调度的操作)。当读者路径需要分配内存、执行 IO 或调用可能阻塞的 API 时,应使用 SRCU(Sleepable RCU)。

// SRCU 初始化
static struct srcu_struct my_srcu;
int ret = init_srcu_struct(&my_srcu);

// 读端
int idx = srcu_read_lock(&my_srcu);
// 此处可以睡眠
srcu_read_unlock(&my_srcu, idx);

// 写端
synchronize_srcu(&my_srcu);

SRCU 通过 per-CPU 的计数器掩码跟踪读者状态,虽然比标准 RCU 有更高开销,但提供了睡眠语义的灵活性。典型应用包括文件系统的 inode 回调、BPF 程序映射的读写、ASLR 随机化中的 mm->mmap 保护等。

六、RCU 性能分析与调优

6.1 读端开销

在 x86-64 上,rcu_read_lock() / rcu_read_unlock() 各为一条 barrier() 指令加上编译器屏障,几乎零开销。在弱序架构(ARM64、POWER)上则需要额外的内存屏障指令,但仍然远低于传统锁的开销。

6.2 宽限期检测开销

宽限期检测由内核线程 rcu_preempt、rcu_sched、rcuog/rcuos 驱动。配置 CONFIG_RCU_NOCB_CPU 可将回调卸载到专用 kthread,减少对实时任务的干扰。

6.3 常见陷阱

  • ❌ 在 rcu_read_lock() 内调用 kmalloc(GFP_KERNEL)、mutex_lock()、或任何可能睡眠的函数
  • ❌ 使用 rcu_dereference() 解引用后的对象变量未保存到栈上,在 rcu_read_unlock() 后仍访问
  • ❌ 用 synchronize_rcu() 处理高频更新导致的宽限期频繁阻塞
  • ✅ 使用 call_rcu() 异步回收替代 synchronize_rcu() 减少写端延迟
  • ✅ 使用 kfree_rcu() 简化 RCU 保护的 kmalloc/kfree 流程

七、RCU 在内核中的代码组织

RCU 子系统源码位于 kernel/rcu/ 目录:

  • tree.c / tree.h — 核心宽限期检测引擎,包含 rcu_node 树遍历逻辑
  • update.c — GP 检测与回调处理,包含 rcu_gp_kthread 主循环
  • rcu_segcblist.c — 分段回调列表管理,支持批量批量回调处理与优先回调排序
  • srcu.c — Sleepable RCU 实现
  • sync.c — synchronize_rcu() 和 synchronize_rcu_expedited() 实现

八、实战:RCU 保护的自定义数据结构

以下是一个在字符设备驱动中使用 RCU 的典型实例:

struct my_device {
    struct rcu_head rcu;
    void __iomem *base;
    u32 config;
};

static struct my_device *gdev;  // RCU 保护的全局指针

// 读取回调 — 无锁、无竞争
static ssize_t my_read(struct file *filp, char *buf, size_t count, loff_t *ppos)
{
    struct my_device *dev;
    ssize_t ret;

    rcu_read_lock();
    dev = rcu_dereference(gdev);
    if (!dev) {
        rcu_read_unlock();
        return -ENODEV;
    }
    u32 config = dev->config;
    rcu_read_unlock();

    ret = simple_read_from_buffer(buf, count, ppos, &config, sizeof(config));
    return ret;
}

// IOCTL 更新回调
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct my_device *new, *old;

    if (copy_from_user(&value, (u32 __user *)arg, sizeof(value)))
        return -EFAULT;

    new = kmalloc(sizeof(*new), GFP_KERNEL);
    if (!new)
        return -ENOMEM;

    old = gdev;
    *new = *old;
    new->config = value;

    rcu_assign_pointer(gdev, new);
    synchronize_rcu();
    kfree(old); // 同步等待后直接释放

    return 0;
}

// 模块卸载时的安全释放
static void __exit my_exit(void)
{
    struct my_device *dev = gdev;
    gdev = NULL;
    synchronize_rcu();
    kfree(dev);
    unregister_chrdev(major, "my_rcu_dev");
}

九、内核各版本 RCU 演进

内核版本里程碑
2.5.43RCU 首次引入
2.6.32Tree RCU 整合,支持 4096 CPU
3.2SRCU 支持动态初始化
4.0RCU No-Callback CPU(tickless 系统支持)
4.20kfree_rcu() 简化接口
5.10Big CPURCU 优化,千万级 CPU 扩展能力提升
5.15实时性优化,rcu_read_lock_bh() 参数化
6.4poll_statecnt 优化,减少不必要的宽限期等待
6.8Dynticks 扩展,完整 RT 支持

十、总结

RCU 是 Linux 内核皇冠上的明珠之一,它以精妙的"复制-更新-回收"语义解决了读多写少场景的极致性能需求。读完本文后,开发者应掌握:

  • Rrcu_read_lock/unlock、rcu_dereference、rcu_assign_pointer 三大基石 API 的正确用法
  • 宽限期原理 — 理解为什么写端必须等待回收
  • call_rcu() vs synchronize_rcu() 的异步/同步选择
  • SRCU 的适用场景和与标准 RCU 的区别
  • 编写安全、正确 RCU 代码的常见陷阱

在现代云计算、网络、存储系统中,RCU 的优化程度直接影响系统整体吞吐性能。深入理解 RCU 是内核开发的必经之路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }