引言
在读多写少的并发场景中,传统的锁机制(如互斥锁、读写锁)往往因为读端竞争而导致性能瓶颈。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 正确性的核心保证。其工作原理如下:
- 写者发起更新:
rcu_assign_pointer()将新数据指针发布到全局可见位置。 - 静默状态检测:内核需要确认所有此前已进入
rcu_read_lock()的读者都已退出(即不再持有旧数据的引用)。 - Quiescent State:每个 CPU 在两次上下文切换、或执行用户态代码、或进入 idle 时驻标记自己经历了"静默状态"。
- 完整宽限期:当所有 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.43 | RCU 首次引入 |
| 2.6.32 | Tree RCU 整合,支持 4096 CPU |
| 3.2 | SRCU 支持动态初始化 |
| 4.0 | RCU No-Callback CPU(tickless 系统支持) |
| 4.20 | kfree_rcu() 简化接口 |
| 5.10 | Big CPURCU 优化,千万级 CPU 扩展能力提升 |
| 5.15 | 实时性优化,rcu_read_lock_bh() 参数化 |
| 6.4 | poll_statecnt 优化,减少不必要的宽限期等待 |
| 6.8 | Dynticks 扩展,完整 RT 支持 |
十、总结
RCU 是 Linux 内核皇冠上的明珠之一,它以精妙的"复制-更新-回收"语义解决了读多写少场景的极致性能需求。读完本文后,开发者应掌握:
- R
rcu_read_lock/unlock、rcu_dereference、rcu_assign_pointer三大基石 API 的正确用法 - 宽限期原理 — 理解为什么写端必须等待回收
call_rcu()vssynchronize_rcu()的异步/同步选择- SRCU 的适用场景和与标准 RCU 的区别
- 编写安全、正确 RCU 代码的常见陷阱
在现代云计算、网络、存储系统中,RCU 的优化程度直接影响系统整体吞吐性能。深入理解 RCU 是内核开发的必经之路。

发表评论 取消回复