2026-06-11 内核社区日报
今日亮点:per-VMA locks 走向无条件启用,KSM rmap 性能优化带来近百倍加速,arm64 开始引入 ROX cache 执行内存分配器。
🧠 重点 · 内存
1. per-VMA locks 取消编译开关,全面启用
发生了什么:
Dave Hansen 提交 v2 系列(5 补丁),使 per-VMA lock 机制从 CONFIG_PER_VMA_LOCK 编译选项变为无条件可用。该机制自 2022 年引入,用于在 mmap lock 之外提供更细粒度的 VMA 并发保护,但此前需要架构支持且依赖显式配置。v2 系列做了三件事:
- 移除 CONFIG_PER_VMA_LOCK,所有架构默认启用。当前所有主流架构(x86、arm64、riscv、s390 等)均已实现必要的
pte_offset_map_lock等原语,条件已经成熟。 - 让 binder 的 shrinker(内存回收回调)完全依赖 per-VMA lock,不再回退到 mmap read lock——binder 是 Android 的核心 IPC 机制,其 shrinker 路径之前会因持锁问题导致性能损失或死锁风险。
- 增加 RCU-based 的 VMA lookup 路径:当
lock_vma_under_rcu()返回 NULL 时,先尝试验证 RCU 指针,若验证失败可立即退出而无需取 mmap lock,降低慢路径代价。
为什么重要:
per-VMA lock 从“可选优化”变成“默认行为”,直接受益者是高频遍历 VMA 的场景,如 /proc/pid/smaps_rollup(lmkd 高频率读取该文件进行内存压力判断)、page reclaim 时遍历进程 VMA、以及 madvise/mbind 等系统调用。取消编译开关意味着所有 arm64 手机(Android GKI 已经包含该机制)将无条件获得更低的 mmap lock 竞争,减少 lmkd 被其他线程持有 mmap write lock 时的阻塞。binder shrinker 的改进则直接减少 Android 服务进程被杀时的锁冲突。
2. KSM 反向映射性能优化 v9:rmap_walk_ksm 加速百倍
发生了什么:
xu xin 提交 v9 系列(3 补丁),解决 KSM(Kernel Same-page Merging)在内存紧张时反向映射遍历(rmap_walk_ksm)性能灾难。问题本质:KSM 合并后,一个物理 page 对应多个 VMA 中的地址,遍历其所有映射(反向映射)需要遍历所有 ksm_rmap_item。当单 page 被大量进程共享时(例如数据库 buffer cache),rmap walk 可能遍历数万个 item,而每个 item 需要调用 linear_page_index() 重新计算页在内存的虚拟地址索引。
优化方法:
- (1)在 ksm_rmap_item 中缓存
linear_page_index()的结果,避免每次 rmap walk 重复计算。计算该索引需要mapping -> host -> i_mmap树的查找,开销很大。 - (2)利用缓存的索引直接进行 radix tree 搜索,替代原本的逐 item 遍历,复杂度从 O(N) 降为 O(log N),N 为共享该 page 的 VMA 数量。
性能数据(来自 v9 commit 描述):当单个 KSM page 被 100 个进程共享时,rmap walk 耗时从 ~10 秒降至 ~0.1 秒(即 100 倍加速),且随共享进程数增多,改善接近线性。在虚拟机场景(如 KVM 使用 KSM),大内存密度合并时该优化能显著减少内存压力导致的 CPU 飙升。
为什么重要:
KSM 在内存超分(overcommit)和虚拟化场景仍然有广泛应用,但之前的 rmap 性能瓶颈会导致系统接近 OOM 时陷入长时间的 rmap walk 循环,表现为整机卡顿。该优化意味着即使用户层合并了数万个 VMA,rmap walk 仍能快速完成,降低系统锁死风险。
3. arm64: 引入 ROX cache execmem 分配器
发生了什么:
Adrian Barnaś 提交 RFC v1 系列(6 补丁),为 arm64 系统添加 ROX cache 执行内存分配器。背景:arm64 上 module_alloc() 和 ftrace_alloc() 的动态分配内存需要后续 set_memory_ro() / set_memory_x() / set_memory_rw() 等权限调整,而每次调整都需要 TLB flush,频繁运行时开销大。ROX cache 将一小块区域预先分配为只读可执行(ROX),然后由 execmem 分配器从中划拨,不需要额外页表权限修改。
具体改动:
- (1) 显式声明 module 和 ftrace 的 execmem 区域类型(EXECMEM_DEFAULT 不再是回退)。
- (2) 允许在调整 execmem cache(如清理时)使用 huge vmap(2M/1G),减少 TLB 压力。
- (3) 修复线性映射恢复权限时的错误:执行 execmem cache 清理后需要恢复线性映射的读属性,但之前的实现错误地移除了
PAGE_KERNEL中的读权限,导致后续访问出错。
为什么重要:
arm64 模块加载、BPF JIT、ftrace 等场景都会频繁分配小块可执行内存。ROX cache 直接消除权限调整的 TLB 开销,尤其对**端侧 AI 推理(频繁 JIT 编译 BPF 或加载内核模块)**有明显收益。对于 Android GKI,如果后续回推,可以减少系统更新后的内核模块加载延迟。当前是 RFC,但设计方向已被社区关注。
4. 减少匿名 folio 重用路径中的 lru_add_drain 冗余调用
发生了什么:
Barry Song(Xiaomi)提交 3 补丁 RFC 系列,核心发现:在 do_swap_page() 和 wp_page_reuse() 中,存在大量不必要的 lru_add_drain() 调用。lru_add_drain() 原本用于将临时缓存的 folio 排入 LRU 链表,但在以下路径中实际不需要:
wp_can_reuse_anon_folio():该函数判断写时复制(COW)后能否重用原 folio,之前无条件调用lru_add_drain()以确保之后判断 folio refcount 时不会被临时缓存影响。但分析表明,refcount==1 的判断对于刚分配的匿名 folio 是正确的,drain 并非必要。do_swap_page()中的冗余 drain:对于同步 I/O 的 swap 设备(如 zram),swapin 完成后 folio 已经稳定,再 drain 是浪费。在测试程序中,do_swap_page()100% 的时间都做了 drain,但实际 <1% 时 drain 有意义(异步 I/O 场景)。
为什么重要:
lru_add_drain() 内部要持有 per-CPU lru batch lock 并遍历所有 CPU,在大规模并发页错误时是明显的锁热点。减少冗余调用可以降低 do_swap_page() 延迟,在 zram 场景(Android 标配)下每次 page fault 都有一个 drain 被消除,可能带来百分之十几的微基准改善。需要注意的是,该系列仍为 RFC,因为完全移除 drain 可能影响 refcount==1 的准确性(其他 CPU 可能正在向 batch 中添加同一 folio),review 中还需要确认 safety。
5. mm/slab: 引入 alloc_flags 和 slab_alloc_context 重构
发生了什么:
Vlastimil Babka 提交 v2 系列(16 补丁),为 slab 分配器(重点在 SLUB)引入 alloc_flags 和 slab_alloc_context 两个核心数据结构,目标是统一目前分散在多个参数(gfp flags, orig_size, node, addr, caller 等)中的分配控制信息。
alloc_flags是 slab 内部的位掩码,类似 page allocator 的ALLOC_*flags,用于控制诸如“是否尝试从 partial list 慢路径”等行为。新引入SLAB_ALLOC_TRYLOCK等 flag。slab_alloc_context是一个完整上下文结构,打包当前调用路径的状态,避免函数参数爆炸。- 基于此重构,系列还做了以下具体工作:
- 优化 kfence 对象初始化:当需要 zeroing 时,不再对 kfence 对象进行初始化(kfence 有自己独立的初始化 path),避免不必要的 memset。
- 将 alloc_flags 传递给 slab 新页分配函数(
allocate_slab()),使慢路径能更好地控制 GFP 行为。 - 为后续“local slab sheave(滑动批分配器)”等性能优化铺路(该方向已在 06-10 日报道)。
为什么重要:
slab 是内核最核心的内存分配器之一,任何内部路径优化都有全局影响。alloc_flags 与 alloc_context 的重构是 SLUB 代码现代化的重要一步,使后续引入复杂决策(如 NUMA-aware caching、dead folio 检测等)更加干净,同时减少分支开销。该系列的 v2 已收到 review 反馈,有望合入 mm-unstable。
6. mm/shrinker: 修复 debugfs count 在 RCU 临界区内睡眠的 bug
发生了什么:
Shakeel Butt 提交单补丁,修复 shrinker_debugfs_count_show() 在 RCU 读锁内调用 ->count_objects() 回调时可能睡眠的问题。内核报告如下调用链:
1 | BUG: sleeping function called from invalid context at kernel/cgroup/rstat.c:421 |
zswap 回调通过 css_rstat_flush() 刷新 memcg 统计,该函数可能睡眠,因此不应在 RCU 读锁下执行。补丁直接移除 rcu_read_lock()/rcu_read_unlock(),因为 mem_cgroup_iter() 内部已处理 RCU,且 shrinker 的生命周期由打开的 debugfs 文件保证(shrinker_free() 通过 debugfs_remove_recursive() 等待所有读者耗尽后才释放)。同一文件的 “scan” 处理程序已无 RCU 保护下调用 ->scan_objects()。
为什么重要:
该 bug 在启用 shrinker debugfs 接口(CONFIG_SHRINKER_DEBUG)时会触发内核警告,修复后 debugfs “count” 文件可在不违反 RCU 规则的前提下安全读取。
7. mm: 清理 clear_not_present_full_ptes() 并统一命名
发生了什么:
David Hildenbrand 提交 3 补丁,清理几乎没人注意的 clear_not_present_full_ptes() 接口:
- (1)sparc64 删除自定义
pte_clear_not_present_full():sparc64 的实现现在等同于通用__set_pte_at(),属于无用重写,删除。 - (2)完全删除
pte_clear_not_present_full()宏:因为架构不再需要特殊处理清除非 present PTE。之前该宏用于在某些硬件上刷新 TLB(因为 sparc TLB 可能缓存非 present 页表项),现在不再需要。 - (3)全局清理
clear_not_present_full_ptes()函数:删除full参数(已无用),将最终实现简化为set_ptes(mm, addr, ptep, nudef_none_pte(), nr),统一命名为clear_non_present_ptes()。
为什么重要:
代码清理虽不改变行为,但减少架构差异化代码,使 mm/mprotect.c 和 mm/mempolicy.c 中涉及非 present PTE 清除的路径更易维护。消除 full 参数(该参数最初是为“是否全量刷 TLB”设计)避免混淆,并为将来引入更统一的 PTE 操作基元扫清障碍。
🔧 其它子系统
eBPF
- BPF signed loader 验证落地:Daniel Borkmann 提交 5 补丁系列,将 BPF 签名加载器的验证从 BPF 程序内部移到内核加载阶段——之前签名的 loader 需要自己通过硬编码的
struct bpf_map偏移读取内核缓存的 map SHA256 并比较(in-loader 检查),现在内核在BPF_PROG_LOAD时自动 folding loader 的 fd_array maps 进入签名覆盖范围,安全性更强。bpftool 的bpftool prog sign命令也同步更新,覆盖 metadata 签名。来源 - skmsg 四连修:Jiayuan Chen 提交 4 补丁系列修复
sk_msg路径中的多个 bug:bpf_msg_push_data()中拒绝溢出的copy+len、修复错误的 rsge 偏移、将分配的内核页零初始化、以及保持sk_msg拷贝状态同步。来源
文件系统 / VFS
- shmem 交换 I/O 重构:
shmem_write_folio()封装:Christoph Hellwig 为 shmem 提供 wrapper 函数供 DRM 驱动(i915)使用,目标是完全将 swap_iocb 处理放在 shmem 内部,为后续 swap I/O 路径的通用化铺路。来源(注意:该系列是 6/1 的,今天有回复讨论) - writeout 函数合并入 pageout:同一系列的另一个补丁,将回收路径中的
writeout()(只被pageout()调用)直接内联合并,消除一个不必要的函数调用层。来源
锁
- x86_64 spin_unlock 内联性能测试:i-cache 成本普遍存在:Dmitry Ilvokhin 应 Peter Zijlstra 的建议,在 x86 上测试了内联
_raw_spin_unlock()的效果(设置ARCH_INLINE_SPIN_UNLOCK*),测量了 RocksDB、内核编译和 nginx 等工作负载。结果在所有负载中均观察到内核 L1i 缓存缺失增加;在 i-cache 受限的 nginx 连接处理中,吞吐量下降约 1.27%(指令数仅减少 0.5%),表明内联带来的 i-cache 污染抵消了指令数优势。作者指出未发现任何工作负载因内联而受益。来源
👀 值得追的讨论 / Patch
- mm/kmemleak 栈扫描导致软锁问题:Breno Leitao 提交 RFC,指出
kmemleak_scan()在遍历所有任务栈时会触发软锁(soft lockup),因为每个栈需要释放内存,遍历到大量线程时长时间不调度。当前方案是在遍历循环中加入cond_resched()或need_resched()检查,但 reviewer 指出扫描不应该可中断(会导致误报或漏报),需要更细致的设计。来源 - mm: split file’s i_mmap tree for NUMA:Huang Shijie 提交 v2 系列(4 补丁),将文件对应的 i_mmap 树按 NUMA node 拆分并拆分锁,以减少多节点并发访问竞争。在 Hygon 12 节点服务器上,UnixBench execl 测试显示超过 400% 的性能提升。来源
- mm/vmalloc: 添加 decrypted 分配器:Catalin Marinas 提交 RFC,为 CCA 机密计算场景引入显式解密内存分配接口。该路径需要与
set_memory_decrypted配合,社区对接口命名(vzalloc_decryptedvsvmalloc_decrypted_encrypted)和是否需要独立新函数存在分歧。来源
⚡ 一句话速览
- mm/lruvec trace:JP Kobryn 提交 v4,新增
mm_lru_add_drain和mm_lru_add_drain_alltracepoint,用于追踪 LRU 添加批量被排空时的调用栈和 CPU 信息,帮助定位 LRU 锁竞争来源。来源 - kasan:x86 5 级页表假阳性修复:Ihor Solodrai 修复 kasan 在 x86 5-level paging 下的 wild-memory-access 误报,与
pgd_bit计算溢出有关。来源 - page_owner_sort 内存泄漏:Yichong Chen 修复
add_list()不释放 per-record comm 和 txt 的问题。来源 - swap.h 清理:Ritesh Harjani 移除未使用的
SWAP_BATCH宏和多项 forward 声明。来源 - selftests/mm hmm 修复:Aboorva Devarajan v3 系列修复 hmm-tests 在 hugepage 配置非默认(1G 等)时的崩溃问题。来源
- mm/memfd_luo 验证:Tarun Sahu 修复 memfd_luo 中未验证序列化数据即做类型转换的潜在安全问题。来源
- mm/damon 栈使用减缩:Arnd Bergmann 调整 DAMON 工作线程,将过大的内核栈变量移出堆栈或使用动态分配。来源
- mm: remove NODE_RECLAIM_xxx macros:Petr Tesarik 将
node_reclaim()返回值改为 bool,同时移除未用的宏常量。来源 - MM: drop pte_clear_not_present_full:David Hildenbrand 提交了 v1 补丁系列,清理
clear_not_present_full_ptes(),移除了pte_clear_not_present_full()并将前者重命名为clear_non_present_ptes()。来源 - khugepaged collapse hint for MGLRU:Luka Bai 添加 collapse hint 机制,允许 MGLRU 指示 khugepaged 优先折叠哪些 folio,提高 mTHP 命中率。来源