背景
mmap_miss 是 readahead 启发式的输入:它会在"需要同步 mmap readahead"时递增,在 filemap_map_pages 把已在 page cache 的 folio 映射上时递减;递减代表一次"命中"。当 mmap_miss 累积超过 MMAP_LOTSAMISS,内核判定这是随机访问、不再值得预读。所以这个计数准不准,直接决定 readahead 该不该继续干活。
┌──────────────────────────────┐
│ 未命中计数在映射命中页时抵扣 │
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 抵扣被两类来源虚高 │ 预填周围页表项都算命中;重试抵消同一未命中
└──────────────┬───────────────┘
▼
┌──────────────────────────────┐
│ 计数虚低误导预读启发式 │ 随机与大步长访问被当成顺序,触发大量无用预读
└──────────────────────────────┘
问题出在递减那一侧,它有两种过度 credit:
- fault-around:缺页处理会把 faulting address 附近的 PTE 一并装上,可这次 fault 只能证明 faulting address 被访问过,附近那些 PTE 并没有被真正访问,把它们都算成命中,等于凭空压低了 mmap_miss
- retry:一次启动同步 readahead 的 fault 可能在释放 mmap_lock 后以 VM_FAULT_RETRY 返回,retry 时直接映射上那次 miss 刚带入的 folio,又把同一个 miss 抵消掉
两类虚低加起来,mmap_miss 长期低于真实未命中量,readahead 启发式被误导,把本该判定为随机的访问当成顺序,持续触发大量无用预读。
问题
- mmap_miss 递减侧有两类过度 credit
- fault-around 把附近 PTE 都算命中
- retry 抵消同一未命中
- 计数虚低误导 readahead,随机访问触发大量无用预读
方案
修复分两处收紧递减那一侧:
- fault-around:只有真正映射了故障地址时才递减 mmap_miss,fault-around 顺手装上的附近 PTE 不再算作命中
- retry:缺页重试不再计为命中。重试时映射的 folio 虽来自 page cache,却正是触发那次同步 readahead 的 miss 所带入的页,若算作命中会把同一个 miss 抵消掉
旧:命中被虚高,启发式被误导
┌────────────────────────────────┐
│ 预填周围页表项与重试都算命中 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 计数虚低,随机访问触发无用预读 │
└────────────────────────────────┘
新:只算真正命中,组合才生效
┌──────────────────────────────────┐
│ 只计故障地址,不计缺页重试 │
└────────────────┬─────────────────┘
▼
┌──────────────────────────────────┐
│ 计数反映真实未命中,无用预读消失 │ 两处单独改都不显著,组合才把计数扳回
└──────────────────────────────────┘
关键在两处必须一起改。作者用 ablation 拆开测:只收紧 fault-around、或只收紧 retry,random 和大步长访问的 pgpgin 都和 baseline 几乎一样;只有两处一起改,计数才被真正扳回真实未命中量。
收益
作者用 mmap_miss_probe 实测(KVM/data-disk,8 GiB guest,2 vCPUs,8192 KiB read_ahead_kb,每次 cold page cache,访问文件 1%,3 次中位数;指标 pgpgin GiB / elapsed s)。
20 GiB larger-than-memory(完整修复前后):
| 访问模式 | Before (pgpgin/时间) | After (pgpgin/时间) |
|---|---|---|
| random | 223.377 GiB/101.293s | 1.010 GiB/4.790s |
| stride2053 | 409.584 GiB/193.700s | 0.970 GiB/3.685s |
| stride4099 | 406.452 GiB/134.241s | 0.975 GiB/3.499s |
| stride1021 | 204.214 GiB/97.557s | 204.208 GiB/108.086s |
| sequential | 0.212 GiB/0.050s | 0.212 GiB/0.057s |
ablation(20 GiB,random 与 stride2053,pgpgin):
| 访问模式 | baseline | 仅 fault-around | 仅 retry | 完整修复 |
|---|---|---|---|---|
| random | 223.377 GiB | 223.268 GiB | 223.257 GiB | 1.010 GiB |
| stride2053 | 409.584 GiB | 409.584 GiB | 15.722 GiB | 0.970 GiB |
random 和大步长访问的 pgpgin 从数百 GiB 降到约 1 GiB,无用 readahead 几乎被消除。
4 GiB fit-in-memory 场景趋势一致(random 3.987→0.980 GiB、stride2053 3.991→0.811 GiB)。