背景

传统 active/inactive LRU 的回收分两拨人干活。回收侧由 shrink_inactive_list() 从 inactive LRU 尾部隔离一批 folio,shrink_folio_list() 逐个处理,处理完后 move_folios_to_lru() 把没回收掉的放回链表头部。对 swap-backed 且设备支持异步写的 folio,回收只负责把它排进写回队列,就继续处理下一个。写回侧独立成批地冲刷队列,每写完一个 folio 就调 folio_rotate_reclaimable(),试图把它轮转到 LRU 尾部,标记为优先回收候选。

这个轮转有一个前提:folio 得已经被放回 LRU 才轮得动。两拨人各干各的,中间没有任何同步。

异步 swap 足够快时轮转落空
┌─────────────────────┐   ┌───────────────────────┐   ┌──────────────────────┐
│ 回收从 LRU 尾部隔离 │   │ swap folio 排队写回, │   │ 设备太快写完,rotate │
│ 一批 folio,逐个处  ├──▶│ 同批其余 folio 仍     ├──▶│ 调用落空,folio 被放 │
│ 理                  │   │ 在处理                │   │ 回头部               │
└─────────────────────┘   └───────────────────────┘   └──────────────────────┘

设备足够快时,写回在回收还在处理同批次其余 folio 时就完成了,rotate 落空。等回收批次结束,这个 folio 按默认逻辑被放回头部。它明明已经干净、写过盘,是最该被优先回收的候选,却挡在更热的 folio 前面。

冷热反转的后果
┌─────────────┐   ┌──────────────┐   ┌────────────────────┐
│ 已写回的干  │   │ 更热的 folio │   │ 回收要绕一整圈才再 │
│ 净 folio 留 ├──▶│ 反而排在它后 ├──▶│ 访问它,回收效率下 │
│ 在 LRU 头部 │   │ 面           │   │ 降                 │
└─────────────┘   └──────────────┘   └────────────────────┘

MGLRU 已经解决过同类问题,commit 359a5e1416ca 让它对隔离期间被写回的 folio 做一次重试。传统 LRU 一直没有对应处理。

问题

  • 异步 swap 设备够快时,写回先于回收批次完成,rotate 调用落空。
  • 已写回的干净 folio 被放回 LRU 头部,错过轮转。
  • 冷热反转,优先回收候选挡在更热的 folio 前面。
  • 干净 folio 滞留内存,内存与 swap 占用双双虚高。

方案

不重新回收这些 folio,那样会丢掉 swap cache,将来命中时还有用。正确做法是把错过的轮转补回来:move_folios_to_lru() 新增 do_rotate 参数,放回时甄别出疑似错过轮转的 folio,用 lruvec_add_folio_tail() 放到 LRU 尾部而不是头部。

放回 LRU 时的去向判断
                      ┌────────────────────────────────────────────────────┐
┌────────────┐   ┌───▶│ 干净,未映射,未引用且无意外 pin,放到尾部补上轮转 │
│ 回收批次结 │   │    └────────────────────────────────────────────────────┘
│ 束,逐一放 ├───┤
│ 回 folio   │   │    ┌────────────────────────────────────────────────────┐
└────────────┘   └───▶│          被引用或有意外 pin,留在头部不动          │
                      └────────────────────────────────────────────────────┘

甄别条件全部偏保守,只有确凿无疑的候选才放尾部:在 inactive 上、未映射、不脏、不在写回、未被引用,并且引用计数等于预期值。后两条各护住一类特殊情况:被引用的 folio 留在头部,照样拿二次机会;有意外 pin(比如 GUP)的 folio 反正现在回收不了,也留在头部。

do_rotate 只在 shrink_inactive_list() 的放回路径开启。shrink_active_list() 的链表顺序本身是刻意安排的降级与升级决策,传 false 不动;MGLRU 的 evict_folios() 已有自己的 clean-list 重试机制,同样传 false 保持不变。后续可以用同一套 do_rotate 取代 MGLRU 的重试通道,统一两种 LRU 的做法。

收益

作者在 1.6Gi 内存、1Gi swap 的 cgroup 环境用复现器做了对比测试:

指标 Before After 改善
空闲内存(free -h Mem) 299Mi 322Mi +7.7%
Swap 已用(free -h Swap) 678Mi 328Mi -51.6%
memory.usage_in_bytes 1073700864 1071140864 -0.24%
memsw 与 memory 差值对 swap 已用 324Mi 对 678Mi,严重偏离 326Mi 对 328Mi,基本吻合 记账恢复一致