背景
hugetlb 支持一种页表级共享:多个进程把同一 hugetlb 文件按 PUD 边界(x86 上 1G)对齐共享映射时,可以共用同一张 PMD 页表,各自省下一整层页表的内存。
共享带来一个失效语义上的约束:拆除共享时要清掉 PUD 项,而清 PUD 项会让整个 PUD 范围的映射随之失效,所以发给 MMU notifier 的失效范围必须覆盖整个 PUD 跨度,否则共享这张页表的其他进程会留下过期映射。MMU notifier 的典型消费者是 KVM,它靠这些回调把 guest 的二级页表(如 EPT)与宿主保持同步。
问题出在判断条件上。现状的 adjust_range_if_pmd_sharing_possible() 只检查「共享是否可能存在」:VMA 带 VM_MAYSHARE 标志,且失效范围落在某个对齐的 PUD 区域内。满足就把范围向外扩到 PUD 边界。至于这个 hugetlb 文件此刻是否真的存在共享的 PMD 页表,它一概不问。
可能共享就按 PUD 边界扩大失效范围
┌─────────────────────────────────────────┐
│ 多个进程按 PUD 对齐共享映射同一 hugetlb │
│ 文件时,可以共用一张 PMD 页表 │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 拆除共享要清 PUD 项,整个 PUD 范围的 │
│ 映射随之失效 │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐
│ 于是只要共享可能存在,MMU notifier │
│ 范围就保守扩大到 PUD 边界 │
└────────────────────┬────────────────────┘
▼
┌─────────────────────────────────────────┐
│ KVM 对一次 2M 打洞收到 1G 失效,误删 │
│ 无关二级映射,guest 反复缺页 │
└─────────────────────────────────────────┘
问题
- 「共享可能存在」只看 VMA 标志与对齐,文件当前无共享也命中。
- 一次 2M 打洞被扩大成 1G 失效,KVM 误删无关的二级映射。
- guest 要为被误删的映射反复缺页,拖累应用性能。
方案
核心思路是把「可能存在」换成「确实存在」。为每个 hugetlbfs inode 维护一个活跃 PMD 共享的计数:huge_pmd_share() 成功把共享 PMD 页表安装进 PUD 之后加一,__huge_pmd_unshare() 真正拆下共享 PMD 页表时减一。计数为零,意味着这个文件当下没有任何共享页表,失效范围保持原样即可。
if (hugetlbfs_pmd_sharing_active(file_inode(vma->vm_file)))
adjust_range_if_pmd_sharing_possible(vma, start, end);
判定的时机是正确性的关键。zap 与 unmap 路径不再进门前就扩大范围,而是先拿到映射的写锁再读计数:huge_pmd_share() 在同一把锁的读侧运行,读写互斥,所以写锁下读到零,就说明此刻既没有活跃共享,也不会有并发的新共享插进来。计数本身用 atomic64,因为多个进程可以在读锁下并发安装共享页表,增量会互相竞争,64 位则避免 32 位计数回绕到零的极端情况。
按 inode 计数活跃的 PMD 共享
┌───────────────────────────────────────┐
│ huge_pmd_share 成功安装共享 PMD 页表 │
│ 后,inode 计数加一 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ __huge_pmd_unshare 真正拆下共享 PMD │
│ 页表时,计数减一 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ zap 与 unmap 路径在映射写锁下读计数, │
│ 为零说明没有也不会并发开始共享 │
└───────────────────────────────────────┘
效果很直接:对从未或已经不再共享 PMD 的文件做 2M 打洞,KVM 收到的就是 2M 失效,不再误伤同一 PUD 里的其他映射。
计数为零时失效范围不再扩大
┌──────────────┐ ┌─────────────────┐ ┌──────────────────────┐
│ 对 hugetlb │ │ 计数为零,跳过 │ │ KVM 只收到 2M 失效, │
│ 文件做 2M 打 ├──▶│ 向 PUD 边界的扩 ├──▶│ guest 无需为无关映射 │
│ 洞 │ │ 大 │ │ 反复缺页 │
└──────────────┘ └─────────────────┘ └──────────────────────┘
收益
Redis 跑在虚拟机里、对 2M hugetlb 冷页做打洞的场景:
| 指标 | Before | After | 改善 |
|---|---|---|---|
| P99 QPS 劣化率 | 7.09% | 1.45% | 降 5.64 个百分点 |