背景

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 比较)。