原作者:Linux教程
原文地址:Linux 内核:RCU机制全解(读多写少场景的性能神器)
各位啃 Linux 源码的时候,有没有发现一个细节:很多链表、指针后面有个 __rcu 后缀?比如 struct list_head __rcu *next; 或者 rcu_dereference() 这些 API。
今天这篇文章,主要目的是把 RCU(Read-Copy Update)讲透。
走起!
Part1 RCU 到底是什么?
RCU 全称 Read-Copy Update ,直白翻译就是**"读-拷贝-更新"**。
它的核心思想超级简单:读者可以随意读,几乎零开销;写者想改数据时,先拷贝一份副本,在副本上改完,再一次性把指针指向新数据。
为什么这么设计?因为 Linux 内核里大量场景是读多写少。比如:
- 文件系统里频繁查找目录(读);
- 路由表查询(读);
- 系统调用审计、SELinux AVC、dcache 等。
传统锁(rwlock)在读多的时候会让所有读者排队,性能差。RCU 直接干掉读端的锁竞争,让多个读者同时读 ,甚至读者和写者也能并发(写者自己加锁就行)。
读到这,我打赌你现在脑子里肯定冒出了好多问号:RCU怎么这么厉害?写数据的时候读数据居然不用等?有了读写锁为啥还要搞个RCU?别担心,下面这张图能让你恍然大悟!"

官方文档在内核源码 Documentation/RCU/ 目录下非常齐全,主实现者 Paul E. McKenney 也写了大量文章。想深挖的同学可以直接去看源码。
Part2 RCU 解决了哪些难题?
RCU 在设计时重点解决了三个关键问题:
- 读者正在读的时候,写者删除了节点 写者可以把节点从链表移除,但不能立刻释放内存。必须等到所有读者都读完(宽限期 Grace Period)才能销毁。这就是 RCU 的"延迟释放"机制。
- 读者正在读的时候,写者插入了新节点 需要保证读者读到的节点是完整初始化 的。这里用到了发布-订阅机制(Publish-Subscribe),靠内存屏障保证可见性。
- 链表遍历不能因为增删而断链 RCU 保证遍历不会从中间断开,但不保证一定能读到最新节点(这也是它和普通锁的区别)。
一句话总结就是RCU 让读者几乎无感,写者承担所有复杂性。
Part3 RCU 原理
RCU 可以看作 rwlock 的升级版,但更激进:
- 读者:几乎零开销。不加锁、不用原子指令(除 Alpha 外无需内存屏障),也不会导致死锁。
- 写者:先拷贝副本 → 修改副本 → 注册回调 → 等待 Grace Period → 真正替换/释放。
Grace Period(宽限期) 是 RCU 的灵魂:它指"所有 CPU 都经历一次上下文切换(Quiescent State,安静状态)"的时间。为什么用上下文切换判断?因为 RCU 读端要求读者在临界区内不能被调度(rcu_read_lock 期间关抢占)。一旦切换发生,说明读者已经安全退出。
内核里会维护 per-CPU 变量来标记每个 CPU 是否经历过一次安静状态。写者挂起后:
- 重置所有 per-CPU 变量为 0;
- 每个 CPU 切换一次就把自己的变量设为 1;
- 全部变为 1 后,唤醒写者,执行回调。
Part4 一个简单的例子看穿本质
我们先看内核文档 whatisRCU.txt 里的经典例子(保护一个全局指针):
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo *gbl_foo;
void foo_read(void)
{
struct foo *fp = gbl_foo; // 普通读(后续会改成 rcu 版)
if (fp != NULL)
dosomething(fp->a, fp->b, fp->c);
}
void foo_update(struct foo *new_fp)
{
spin_lock(&foo_mutex);
struct foo *old_fp = gbl_foo;
gbl_foo = new_fp; // 指针切换
spin_unlock(&foo_mutex);
kfree(old_fp); // 危险!可能被读者还在用
}
如果去掉 spinlock,直接并发更新,会出现**释放后使用(use-after-free)**的 bug。RCU 改造后变成这样(注意关键变化):
void foo_read(void)
{
rcu_read_lock(); // 声明读临界区
struct foo *fp = gbl_foo;
if (fp != NULL)
dosomething(fp->a, fp->b, fp->c);
rcu_read_unlock(); // 退出临界区
}
void foo_update(struct foo *new_fp)
{
spin_lock(&foo_mutex);
struct foo *old_fp = gbl_foo;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
synchronize_rcu(); // 等待所有读者退出!
kfree(old_fp);
}
RCU 允许多个读者并发,也允许读者和写者并发,但多个写者之间仍需锁同步(这里用了 spinlock)。
Part5 RCU 核心 API
rcu_read_lock(); // 进入读临界区(关抢占)
rcu_read_unlock(); // 退出读临界区
synchronize_rcu(); // 写者等待 Grace Period(核心阻塞点)
rcu_assign_pointer(); // 写者安全发布新指针(带内存屏障)
rcu_dereference(); // 读者安全解引用(带内存屏障)
后面会逐个讲解它们在链表里的真实用法。
Part6 链表上的实例
6.1 增加链表项
内核里增加 RCU 链表项的经典代码:
#define list_next_rcu(list) (*((struct list_head __rcu **)(&(list)->next)))
static inline void __list_add_rcu(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
new->next = next;
new->prev = prev;
rcu_assign_pointer(list_next_rcu(prev), new); // 关键发布
next->prev = new;
}
__rcu 后缀是 Sparse 工具的标注,强制开发者必须用 rcu_dereference() 访问。
重点看 rcu_assign_pointer():
#define __rcu_assign_pointer(p, v, space) \
({ \
smp_wmb(); // 写内存屏障
(p) = (typeof(*v) __force space *)(v); \
})
为什么需要内存屏障?
CPU 乱序执行可能导致:新节点还没初始化完,就被读者看到!内存屏障保证 new->next、new->prev 先写完,再把指针发布出去。
注意:如果多个线程同时 add,仍需额外 spinlock 保护。
6.2 访问链表项
标准读模式:
rcu_read_lock();
list_for_each_entry_rcu(pos, head, member) {
// do something with pos
}
rcu_read_unlock();
list_for_each_entry_rcu 内部最终调用 rcu_dereference():
#define __rcu_dereference_check(p, c, space) \
({ \
typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); \
rcu_lockdep_assert(c, "suspicious rcu_dereference_check() usage"); \
rcu_dereference_sparse(p, space); \
smp_read_barrier_depends(); // 读依赖屏障
((typeof(*p) __force __kernel *)(_________p1)); \
})
在 Alpha 架构上,这条屏障能防止编译器/CPU 猜测优化导致的乱序;在 x86/arm 上则是空实现(性能无损)。
读临界区全局可见:只要有任何一个读者还在临界区,synchronize_rcu() 就会阻塞,直到所有读者退出。这就是 Grace Period 的直观体现。
6.3 删除链表项
p = search_the_entry_to_delete();
list_del_rcu(p->list); // 只移除,不释放
synchronize_rcu(); // 等待 Grace Period
kfree(p);
list_del_rcu() 源码很简单:
static inline void list_del_rcu(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->prev = LIST_POISON2; // 毒化,防止误用
}
6.4 更新链表项
p = search_the_entry_to_update();
q = kmalloc(sizeof(*p), GFP_KERNEL);
*q = *p; // 拷贝
q->field = new_value; // 修改副本
list_replace_rcu(&p->list, &q->list);
synchronize_rcu();
kfree(p); // 老版本延迟释放
list_replace_rcu() 内部同样使用 rcu_assign_pointer() 安全替换。
Part7 实战:RCU 在系统调用审计中的应用
场景一:只有增加/删除(最常见、最容易转换)
原来用 rwlock 的读端:
read_lock(&auditsc_lock);
list_for_each_entry(e, &audit_tsklist, list) { ... }
read_unlock(&auditsc_lock);
改成 RCU 后:
rcu_read_lock();
list_for_each_entry_rcu(e, &audit_tsklist, list) { ... }
rcu_read_unlock();
写端原来用 write_lock,现在只需把 list_add/list_del 换成 _rcu 版本,并用 call_rcu() 异步释放(代替 synchronize_rcu())。
场景二:需要修改链表条目
必须先拷贝 → 修改副本 → list_replace_rcu() → call_rcu() 释放旧条目。
场景三:不能容忍旧数据(立即可见)
在每个条目里加 deleted 标志 + 每个条目自己的 spinlock:
读端检查 if (e->deleted) 立即跳过;写端删除时先标记 deleted = 1,再 list_del_rcu() + call_rcu()。
总结
RCU 是 Linux 2.6 引入的重量级同步机制,用好它,内核性能能上一个大台阶。
优点:
- 读端几乎零开销,完美适配"读多写少"场景;
- 在路由表、dcache、SELinux AVC、IPC 等地方已大规模替换 rwlock,性能提升明显。
缺点:
- 写端延迟释放会短暂占用内存(嵌入式系统需谨慎);
- 写者较多或不能容忍旧数据时,仍需额外锁,收益会打折扣。