Linux 内核的 RCU:读不加锁的魔法
读多写少是服务器里最常见的场景。路由表、进程描述符、安全策略——这些数据几乎每纳秒都在被读取,修改却少之又少。经典做法是给读者加读写锁(rwlock),读者拿读锁,写者拿写锁。听起来合理,但问题是:哪怕所有线程都在读、没有任何人在写,读锁本身的开销也不为零。缓存行上的一次 atomic 操作就得几十纳秒,在高频路径上累积起来很可观。
Paul McKenney 在九十年代想了一个看似疯狂的方案:读的时候完全不加锁,连 barrier 都不打。写者修改数据时不是原地覆盖,而是先拷贝一份,改拷贝,再把指针原子地切过去。旧的读者还在看老数据,没关系,让它看完。等所有可能的旧读者都离开了(内核已经有办法检测这一点),再回收老副本。这就是 RCU——Read-Copy-Update。
它的核心想法其实特别朴素:。这个窗口叫做 grace period,在非抢占内核里就是一个完整的调度周期——只要每个 CPU 都经历过一次上下文切换,就说明没有旧读者还卡在临界区里了。
代码写起来非常简单。读者侧就是
不过 RCU 不是银弹。 而且
2010 年以后 Linux 内核里 RCU 的使用量爆炸式增长,网络子系统、VFS、cgroup、sched 几乎全部迁移到了 RCU。原因很简单:在多核机器上,读路径每省一条指令都是几十万 QPS 的差距。
#CS
读多写少是服务器里最常见的场景。路由表、进程描述符、安全策略——这些数据几乎每纳秒都在被读取,修改却少之又少。经典做法是给读者加读写锁(rwlock),读者拿读锁,写者拿写锁。听起来合理,但问题是:哪怕所有线程都在读、没有任何人在写,读锁本身的开销也不为零。缓存行上的一次 atomic 操作就得几十纳秒,在高频路径上累积起来很可观。
Paul McKenney 在九十年代想了一个看似疯狂的方案:读的时候完全不加锁,连 barrier 都不打。写者修改数据时不是原地覆盖,而是先拷贝一份,改拷贝,再把指针原子地切过去。旧的读者还在看老数据,没关系,让它看完。等所有可能的旧读者都离开了(内核已经有办法检测这一点),再回收老副本。这就是 RCU——Read-Copy-Update。
它的核心想法其实特别朴素:。这个窗口叫做 grace period,在非抢占内核里就是一个完整的调度周期——只要每个 CPU 都经历过一次上下文切换,就说明没有旧读者还卡在临界区里了。
synchronize_rcu() 就是等这个周期,call_rcu() 是异步版本,注册一个回调在 grace period 结束后自动释放旧数据。代码写起来非常简单。读者侧就是
rcu_read_lock() / rcu_read_unlock(),在非抢占配置下编译成空操作——零指令。写者侧用 rcu_assign_pointer() 发布新指针,用 kfree_rcu() 延迟释放旧结构。整个 API 面极窄,心智负担几乎没有。不过 RCU 不是银弹。 而且
synchronize_rcu() 本身的等待开销很大,写者多的场景反而不如 rwlock。2010 年以后 Linux 内核里 RCU 的使用量爆炸式增长,网络子系统、VFS、cgroup、sched 几乎全部迁移到了 RCU。原因很简单:在多核机器上,读路径每省一条指令都是几十万 QPS 的差距。
#CS