背景

swap 预读有两条路径(按聚簇预读与按 VMA 预读),两者在预读循环结尾都会调一次 LRU 刷洗,把刚加进 swap cache 的 folio 推上 LRU。这行代码是 2.6.12 时代留下来的,本意是让预读进来的页立刻上 LRU。但逐一读现在的调用方就会发现,没有谁真的需要预读页此刻住在 LRU 上。

┌────────────────────────────────────────┐
│ readahead 把预读页加进 swap cache 后, │
│ 结尾无条件把它们刷上 LRU               │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ 但调用方消费的是它点名要换入的那一页   │
│ 目标 folio,并不要求预读页此刻驻在 LRU │
└───────────────────┬────────────────────┘
                    ▼
┌────────────────────────────────────────┐
│ 这趟刷洗只是把还没填满的批次同步刷掉, │
│ 并多抢一次 LRU 锁                      │  176 CPU 主机每分钟约 2.8 万次
└────────────────────────────────────────┘

问题

  • readahead 结尾无条件把预读页刷上 LRU,是 2.6.12 时代遗留
  • 换入缺页处理忽略预读页是否驻在 LRU,shmem 聚簇换入与 swapoff 全量换回也不依赖
  • 预读页本就会随 per-CPU 批次填满或下次回收自然排走
  • 无条件刷洗只是把未满批次同步刷掉,并多抢一次 LRU 锁

方案

判断这趟刷洗有没有用,只需看每个调用方拿不拿「预读页此刻必须驻在 LRU」当前提。

结论是没有:调用方消费的是它点名要换入的那一页目标 folio,拿到后直接加锁、等写回完成、再操作,全程不查它此刻在不在 LRU 上,shmem 聚簇换入与 swapoff 全量换回都是如此。换入缺页处理更彻底,连目标页之外的预读页都不看一眼。唯一需要 folio 驻在 LRU 的情形,是写缺页路径为了让目标页准确计引用;而那条路径会自行刷洗(正是 #30 那条线改的站点),无须预读代劳。既然每个调用方要么不依赖 LRU、要么自己刷洗,结尾这趟无条件刷洗对所有人都是空操作。

旧:readahead 结尾无条件刷洗
┌───────────────────────────────────────┐
│ readahead 循环把预读页加进 swap cache │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│          结尾无条件刷上 LRU           │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ 未满批次被同步刷掉,每轮多抢一次 LRU  │
│ 锁                                    │
└───────────────────────────────────────┘

新:删除两处刷洗,靠自然排走
┌───────────────────────────────────────┐
│ readahead 循环把预读页加进 swap cache │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│       预读页留在 per-CPU 批次里       │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│     批次填满或下次回收时自然排走      │
└───────────────────────────────────────┘

预读循环新加进 swap cache 的 folio 本来就坐在 per-CPU 批次里,会随批次填满、或下一次回收与压缩触发的全量刷洗自然排走。结尾那行无条件刷洗,实际效果只是把还没填满的批次同步刷掉,并强制在 LRU 锁上多抢一次。直接删掉两条预读路径结尾这两处刷洗,让自然排走接手。这是 commit 1aa43598c03b 那次清理(把同类刷洗从释放 swap cache 的路径上移除)的直接延续。

收益

作者未提供定量吞吐 benchmark,给的是行为观测数据:176-CPU 生产主机在内存压力负载下,这条路径每分钟从预读结尾触发约 28K 次把批次中的 folio 搬上 LRU,是很可观的 LRU 锁流量来源。

从机制推断的预期收益:

  • 消除 readahead 结尾两处无条件刷洗带来的未满批次同步 flush
  • 去掉对应的 LRU 锁强制竞争(原本每轮预读一次)
  • 预读页仍由 per-CPU 批次自然排走(批次填满或下次回收触发),不影响正确性
  • 各调用方本就不依赖预读页驻在 LRU,行为不变

与 #30(写缺页复用路径上的冗余 LRU 刷洗)同族:都消除冗余的 LRU 刷洗、同承 1aa43598c03b 那次前体清理,但落在不同代码路径、出自不同作者。