Linux 内核的 RCU:读的时候不加锁,怎么做到的?

多线程编程里,读多写少的场景特别常见——路由表、配置项、进程列表,大部分时间都在读,偶尔才改一次。传统的做法是读写锁,读者拿读锁,写者拿写锁,但读锁本身就有开销:原子操作、缓存行失效、CPU 流水线中断。如果一千个线程同时在读,每个人都要走一遍锁的 acquire/release,这笔账很亏。

RCU(Read-Copy-Update)换了一个思路:读者完全不加锁,连内存屏障都不需要,就是普通的指针读取。写者想修改数据时,不直接改原对象,而是拷贝一份,修改拷贝,然后把指针原子地切换到新对象。旧的副本暂时留着,等所有可能的读者都离开临界区后,再回收旧内存。

关键在于"等所有读者离开"这一步怎么判断。Linux 内核的做法很巧妙:每个 CPU 在调度时会记录一个 qs(quiescent state),内核在宽限期(grace period)里逐个检查每个 CPU 是否经过了 quiescent state。一旦所有 CPU 都经过了,就说明没有读者还在引用旧数据了,这时候回调函数才会真正释放旧内存。整个过程不需要读者做任何事情——最理想的情况下,读者就是一次普通的指针读取,零额外开销。

代价是什么?写者的延迟变高了,因为要等一个完整的宽限期才能回收内存,而这个宽限期可能长达几十毫秒。所以 RCU 只适合写者能容忍延迟的场景。另外,旧数据在宽限期结束前都占着内存,内存压力也会稍大一点。还有一点容易被忽略:RCU 读者的临界区绝不能睡眠,否则这个 CPU 永远不会进入 quiescent state,宽限期就永远结束不了,系统直接死锁。



RCU 从 2002 年进入内核主线,现在已经无处不在——hlist 遍历、fdtable 更新、inode 回收,都依赖它。核心源码在 kernel/rcu/ 目录下,rcu_read_lock()synchronize_rcu() 是两个最关键的入口。

#CS