KSM 一页多映射:rmap 要找到每个映射
KSM folio
内容相同的页合并成一页
同一页被很多 VMA 映射
VMA
VMA
…
换出 / 迁移
每个映射都要反向遍历
rmap_walk_ksm
每个 rmap 项都挂在一个 anon_vma 上
walk 要逐个加锁,再在锁里找目标 VMA
现状 · 锁着 anon_vma 扫全地址空间
取一个 rmap 项
folio + anon_vma
锁 anon_vma
anon_vma->lock
遍历区间树
anon_vma_interval_tree_foreach
匹配目标 VMA
地址落在它的区间
根因:walk 不知道目标页在哪
只能拿 (0, ULONG_MAX) 去套,命中的只有一个 VMA
锁必须握到整个遍历结束
无关 VMA 越多,持锁越久,纯属空转
病态 · VMA 被切成上万小片
mprotect(PROT_NONE)
JVM / Go 的 GC barrier、guard page
把原本的 VMA 反复切开
madvise(MADV_DONTNEED)
MySQL / PostgreSQL 释放特定页
同样留下大量小 VMA
同一个 anon_vma 上挂约
20000
个 VMA(实测规模)
遍历 20000 个 VMA:命中一个,其余全跳过
99.9% 空转
空转的迭代也在锁内,锁一直握着
地址不在该 VMA 范围,就直接跳过
代价 · 这把锁不只 KSM 用
page fault
reclaim
migration
compaction
mlock
exit_mmap
还有 cgroup accounting,也要拿这把锁
一个线程持锁最久(实测 worst case)
705.12
ms
期间其它路径全部阻塞在锁上
应用线程 stall、延迟尖峰、container timeout
根因在 KSM,受影响的是整台机器
方案 · 去重时把线性位置记下来
rmap walk 缺的不是遍历,是目标页的位置
KSM 去重时本来就知道:把 linear_page_index 记进 ksm_rmap_item
旧:(0, ULONG_MAX)
整个地址空间逐个试
99.9% 的迭代被跳过
新:(index, index)
直接定位 vm_pgoff 区间含该 index
跳到目标 VMA,跳过无关项
KSM folio 恒为 order-0(单页)
所以一个 page index 就够描述位置
对没有病态 VMA 分布的系统:只多 1 次 pgoff 比较
开销可忽略
实测 · anon_vma 锁持有(QEMU benchmark)
max
705.12
ms
→
1.67
ms
约 422x(-99.8%)
avg
532.04
ms
→
1.44
ms
约 370x(-99.7%)
生产 Java 应用 + KSM
锁持有 max 228 ms(compaction + migration 触发)
作者未标 CPU 型号 / 物理机或 VM
病态负载:锁持有下降 100x 到 500x
非病态负载不受影响,仅多 1 次 pgoff 比较
收束 · 症结是缺一个位置
旧:不知道页的线性位置
只能从 0 扫到 ULONG_MAX
每个 rmap 项都白扫一遍
锁持有数百毫秒
新:去重时记下 index
interval tree 范围收窄到一点
直接定位目标 VMA
锁只握在一次精确查找上
改动只有一个字段、一个范围
却把「全空间扫」换成了「按位置找」
20000 个 VMA 的病态负载:705.12 ms → 1.67 ms
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x