背景
KSM 把内容相同的页合并,一个 KSM 页可能被很多 VMA 映射。换出或迁移这种页时,内核用 rmap_walk_ksm 反向遍历它的所有映射。麻烦在于:对每个 rmap 项,它先锁住对应的 anon_vma,再用 anon_vma_interval_tree_foreach 从页索引 0 一路扫到 ULONG_MAX(即整个地址空间)去匹配目标 VMA。
当一个 anon_vma 上挂着大量互不相干的 VMA 时,这就糟了。VMA 分割会累积出这种情况:JVM/Go 用 mprotect(PROT_NONE) 做 GC barrier 或 guard page,MySQL/PostgreSQL 用 madvise(MADV_DONTNEED) 释放特定页,都会把原本的 VMA 切成上万个挂在同一个 anon_vma 上的小片。实测里一个 anon_vma 挂约 20000 个 VMA,rmap walk 的循环中 99.9% 的迭代都因地址不在该 VMA 范围被跳过,纯属空转,但 anon_vma 锁一直握着,worst case 705ms。
┌──────────────────────────────────────┐
│ KSM rmap walk 反向遍历页的所有映射 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 在 anon_vma 锁下扫整个地址空间找目标 │
│ VMA │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐ 该锁被 fault 与 reclaim 与 migration
│ 大量无关 VMA 挂同一 anon_vma 时空转 │ 等共享长持有致 stall
│ 持锁数百毫秒 │
└──────────────────────────────────────┘
更要命的是 anon_vma 锁不只 KSM 用:page fault、reclaim、migration、compaction、mlock、exit_mmap、cgroup accounting 都要拿它。一个线程因低效的 KSM rmap walk 持锁数百毫秒,其它所有要这把锁的路径都跟着阻塞,表现为应用线程 stall、延迟尖峰、甚至 container timeout。
问题
- rmap_walk_ksm 在 anon_vma 锁下扫整个地址空间(0 到 ULONG_MAX)找目标 VMA
- 一个 anon_vma 挂大量无关 VMA 时 99.9% 迭代空转
- anon_vma 锁持有达数百毫秒(实测 max 705ms)
- 该锁被 page fault/reclaim/migration/compaction/mlock/exit_mmap 共享,长持有致 stall
方案
根因是 rmap walk 不知道目标页的线性位置,只能拿全地址空间范围去套。
既然 KSM 在去重时就知道这个页原来在哪,把它的 linear_page_index(页在线性地址空间的索引)记进 ksm_rmap_item;rmap_walk_ksm 直接用这个 index 把 anon_vma_interval_tree_foreach 的范围从 (0, ULONG_MAX) 收窄到 (index, index),直接定位到 vm_pgoff 区间含该 index 的那个 VMA,跳过所有无关 VMA。
旧:扫整个地址空间
┌────────────────────────────────┐
│ interval tree 遍历整个地址空间 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 大量无关 VMA 有 99.9% 空转 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ anon_vma 锁持有数百毫秒 │
└────────────────────────────────┘
新:按 page index 精确定位
┌────────────────────────────────┐
│ 去重时记下页的线性索引 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ interval tree 范围收窄到该索引 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 直接定位目标 VMA 跳过无关项 │
└────────────────────────────────┘
KSM folio 恒为 order-0(单页),所以一个 page index 就够。对没有这种病态 VMA 分布的系统,改动只多 1 次 pgoff 比较,开销可忽略。
收益
平台:QEMU benchmark(20000 个 VMA 共享一个 anon_vma,配 OOT rmap testbench);生产 Java 应用 + KSM 实测。作者未标 CPU 型号/物理机或 VM。
| 指标 | Before | After | 改善 |
|---|---|---|---|
| anon_vma 锁持有 max(QEMU benchmark) | 705.12 ms | 1.67 ms | 约 422x(-99.8%) |
| anon_vma 锁持有 avg(QEMU benchmark) | 532.04 ms | 1.44 ms | 约 370x(-99.7%) |
| anon_vma 锁持有 max(生产 Java,compaction+migration 触发) | 228 ms | — | 真实环境印证严重性 |
作者总结病态负载下锁持有时间下降 100x 到 500x,直接减少 page fault、reclaim、migration、compaction、mlock、exit_mmap 等并行操作的阻塞;非病态负载不受影响(仅多 1 次 pgoff 比较)。