如果你写过 Linux 内核模块,一定对 RCU(Read-Copy-Update) 不陌生。它让读端几乎零开销,写端优雅延迟回收——代价是"请等一个 grace period"。
但等等,你有没有遇到过这种情况:
你调了 synchronize_rcu_expedited() 来快速推进 GP(Grace Period),可那些用 call_rcu() 注册的回调,根本不管 expedited GP 有没有完成,死等下一个普通 GP。你说急不急?
更头疼的是,在内核编程中,有些上下文不能睡眠、不能拿锁——NMI handler、BPF 程序、某些 RCU 回调。在这些地方你想分配内存?传统 kmalloc() 直接罢工。
今年 7 月初的 2026 Linux Storage, Filesystem, Memory-Management, and BPF Summit 上,两个演讲正好戳中这两个痛点。LWN 的 daroc 把两场合并成一篇报道,因为它们拼起来,刚好是一条从分配到回收的完整无锁内存管线。
下面我们逐一拆解。
一、RCU 回调也能搭 expedited GP 的便车
开发者:Puranjay Mohan补丁系列:[PATCH v1 00/11] RCU: Enable callbacks to benefit from expedited grace periods
痛点
Linux RCU 有两种 grace period:
- 普通 GP
- Expedited GP:通过
synchronize_rcu_expedited() 主动触发,速度快但能耗高。
逻辑上,不管哪种 GP 完成,都意味着"所有读者已经退出临界区",回调可以安全执行了。然而内核之前的实现是:call_rcu() 注册的回调只跟踪普通 GP 的序列号。即使 expedited GP 已经跑完,回调仍然纹丝不动,必须等到下一个普通 GP 到来。
这就好比你要等公交车,明明旁边有一辆直达快车到了,但因为你的票是普通票,只能看着快车开走,继续等下一趟普通车。
方案
Puranjay 的修改很直观:在回调跟踪结构体 struct rcu_segcblist 里同时记录普通 GP 和 expedited GP 的序列号,然后改造推进逻辑——只要任意一种 GP 完成,回调就前进。
涉及的修改路径包括:
rcu_segcblist_advance()rcu_core() 和 rcu_pending():RCU 核心处理与挂起检查
效果立竿见影:synchronize_rcu_expedited() 的调用者不再是唯一受益者,整个系统的回调回收延迟都降低了。
顺便一提:SRCU-fast 在 arm64 上的优化
同一位开发者在今年 3 月还有一个相关优化。他发现 arm64 上 this_cpu_inc() 会插入不必要的 preempt_disable/enable 指令——SRCU-fast 的 per-CPU 计数器最终会被汇总,中途被迁移也没关系,根本不需要迁移保护。
于是引入 srcu_percpu_counter_inc(),在 arm64 上直接使用 atomic_long_fetch_add_relaxed()(编译为 ldadd 指令,L1 cache 内原子完成)。在 72 核 Neoverse-V2 系统上,SRCU-fast 读端耗时从 9.273 ns 降到 8.275 ns,提升约 11%。不换硬件,纯靠优化指令选择,白捡 11% 的性能。
二、kmalloc_nolock():原子世界的 malloc
开发者:Harry Yoo(Oracle)、Alexei Starovoitov合入版本:Linux 7.0
痛点
内核里有些地方绝对不允许睡眠或拿锁:
这些场景下,传统 kmalloc() 指望不上——它可能需要拿 slab 分配器的锁,或者触发 page reclaim 导致睡眠。开发者要么提前预分配缓冲区,要么写丑陋的静态池,要么干脆绕道走。
kmalloc_nolock() 就是为了填这个坑而生的:它使用 try_cmpxchg 原子操作(x86 上是 cmpxchg16b)直接操作 per-CPU freelist,全程不碰任何锁。
关键突破:可以用 kfree_rcu() 回收了
kmalloc_nolock() 不是今年才引入的,但它之前有个很烦的限制:分配出来的对象只能用 kfree_nolock() 释放。
这意味着如果你想把一个对象推迟到所有读者都退出后再回收——RCU 的经典用法——你得手动写 call_rcu() 回调,绕一大圈。BPF 社区对此叫苦连天。
Harry Yoo 的补丁移除了这个限制。现在:
kmalloc_nolock() 分配的对象 可以用 kfree() 或 kfree_rcu() 直接释放- 相关文档已更新到
include/linux/rcupdate.h
这对 BPF 程序员来说是一大福音——BPF 程序经常在原子上下文中分配内存,现在可以无缝对接 RCU 回收机制,无需自建回收逻辑。
另一大变化:Sheaves 简化实现
Vlastimil Babka 的 Sheaves(谷束) 补丁系列(21 个补丁,2026 年 1 月)彻底重写了 slab 分配器的 per-CPU 部分,用更简洁的 sheaves 机制替代了旧的 per-CPU partial slabs。
这个改动对 kmalloc_nolock() 意味着什么?巨大的简化。旧的实现需要一套复杂的无锁快速路径(this_cpu_try_cmpxchg128/64),sheaves 上线后这些全都不要了。代码量缩减,维护负担降低,正确性更容易验证。
架构约束
- 需要硬件支持
__CMPXCHG_DOUBLE(x86 的 cmpxchg16b、arm64 的 CAS 对),否则无法使用 - 在 PREEMPT_RT 内核上,不允许从 hardirq/NMI 上下文调用(因为底层机制依赖的
local_lock 在 RT 内核中是可睡眠锁)
三、交叉映射:两条线拼出无锁内存管线
把上面两件事放在一起看,意义更大:
| | |
|---|
| 分配 | kmalloc() | kmalloc_nolock() |
| 使用 | | |
| 回收 | kfree_rcu() | kfree_rcu() |
Puranjay 的 RCU 改进解决了回收端的问题——回调更快被调度执行;Harry Yoo 的 kmalloc_nolock() + kfree_rcu() 解决了分配端的问题——原子上下文也能分配,还能用 RCU 回收。
两条线汇合,就有了从分配到回收的完整无锁内存管线。
对于 BPF 程序、网络 fast path、存储栈等性能敏感子系统,这意味着:
四、展望:还有哪些拼图?
目前 kmalloc_nolock() 已合入 Linux 7.0,但工作并没有结束:
kvfree_rcu_nolock() 已在 RFC 阶段——Harry Yoo 和团队正在推进更专用的无锁回收路径,进一步减少对 RCU 核心机制的依赖。- KUnit 测试用例已有人提交,用于捕捉
{kmalloc,kfree}_nolock 的微妙 bug。 - Sheaves 重写仍在持续,RCU sheaves 处理中的 batching 优化还在 TODO 列表上。
Linux 内核的并发基础设施在过去二十年里走过了"大内核锁 → spinlock → RCU → 无锁数据结构"的演进路径。2026 年的这两项工作,标志着内核在极端上下文(NMI、BPF)中的内存管理这个最后的堡垒上又攻克了一大块阵地。
以上就是今年 LSM+BPF Summit 上关于 RCU 和 kmalloc_nolock() 的核心内容。如果你在内核模块或 BPF 程序中碰到过"这里不能分配内存"的窘境,kmalloc_nolock() + kfree_rcu() 的组合值得关注。
你对哪个部分最感兴趣?RCU 回调的 GP 感知优化,还是无锁内存分配的工程实现?欢迎留言讨论。
参考来源:
- LWN: Faster RCUs and lockless memory allocation (付费)
- Puranjay Mohan: RCU: Enable callbacks to benefit from expedited grace periods
- Vlastimil Babka: Sheaves v3 — slab: replace cpu (partial) slabs with sheaves
- Harry Yoo: slab updates for 7.0 part 2 (kfree_rcu support)