背景

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