各位啃 Linux 源码的时候,有没有发现一个细节:很多链表、指针后面有个 __rcu 后缀?比如 struct list_head __rcu *next; 或者 rcu_dereference() 这些 API。
今天这篇文章,主要目的是把 RCU(Read-Copy Update)讲透。
走起!
RCU 全称 Read-Copy Update,直白翻译就是“读-拷贝-更新”。
它的核心思想超级简单:读者可以随意读,几乎零开销;写者想改数据时,先拷贝一份副本,在副本上改完,再一次性把指针指向新数据。
为什么这么设计?因为 Linux 内核里大量场景是读多写少。比如:
传统锁(rwlock)在读多的时候会让所有读者排队,性能差。RCU 直接干掉读端的锁竞争,让多个读者同时读,甚至读者和写者也能并发(写者自己加锁就行)。
读到这,我打赌你现在脑子里肯定冒出了好多问号:RCU怎么这么厉害?写数据的时候读数据居然不用等?有了读写锁为啥还要搞个RCU?别担心,下面这张图能让你恍然大悟!"
官方文档在内核源码 Documentation/RCU/ 目录下非常齐全,主实现者 Paul E. McKenney 也写了大量文章。想深挖的同学可以直接去看源码。
RCU 在设计时重点解决了三个关键问题:
读者正在读的时候,写者删除了节点写者可以把节点从链表移除,但不能立刻释放内存。必须等到所有读者都读完(宽限期 Grace Period)才能销毁。这就是 RCU 的“延迟释放”机制。
读者正在读的时候,写者插入了新节点需要保证读者读到的节点是完整初始化的。这里用到了发布-订阅机制(Publish-Subscribe),靠内存屏障保证可见性。
链表遍历不能因为增删而断链RCU 保证遍历不会从中间断开,但不保证一定能读到最新节点(这也是它和普通锁的区别)。
一句话总结就是RCU 让读者几乎无感,写者承担所有复杂性。
RCU 可以看作 rwlock 的升级版,但更激进:
Grace Period(宽限期) 是 RCU 的灵魂:它指“所有 CPU 都经历一次上下文切换(Quiescent State,安静状态)”的时间。为什么用上下文切换判断?因为 RCU 读端要求读者在临界区内不能被调度(rcu_read_lock 期间关抢占)。一旦切换发生,说明读者已经安全退出。
内核里会维护 per-CPU 变量来标记每个 CPU 是否经历过一次安静状态。写者挂起后:
重置所有 per-CPU 变量为 0;
每个 CPU 切换一次就把自己的变量设为 1;
全部变为 1 后,唤醒写者,执行回调。
我们先看内核文档 whatisRCU.txt 里的经典例子(保护一个全局指针):
struct foo { int a; char b; long c;};DEFINE_SPINLOCK(foo_mutex);struct foo *gbl_foo;voidfoo_read(void){ struct foo *fp = gbl_foo; // 普通读(后续会改成 rcu 版) if (fp != NULL) dosomething(fp->a, fp->b, fp->c);}voidfoo_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 改造后变成这样(注意关键变化):
voidfoo_read(void){ rcu_read_lock(); // 声明读临界区 struct foo *fp = gbl_foo; if (fp != NULL) dosomething(fp->a, fp->b, fp->c); rcu_read_unlock(); // 退出临界区}voidfoo_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)。
rcu_read_lock(); // 进入读临界区(关抢占)rcu_read_unlock(); // 退出读临界区synchronize_rcu(); // 写者等待 Grace Period(核心阻塞点)rcu_assign_pointer(); // 写者安全发布新指针(带内存屏障)rcu_dereference(); // 读者安全解引用(带内存屏障)
后面会逐个讲解它们在链表里的真实用法。
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 Periodkfree(p);
list_del_rcu() 源码很简单:
staticinlinevoidlist_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() 安全替换。
场景一:只有增加/删除(最常见、最容易转换)
原来用 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 引入的重量级同步机制,用好它,内核性能能上一个大台阶。
优点:
缺点: