背景

MGLRU(multi-gen LRU)按 generation 组织 folio 的回收顺序,越年轻的 generation 越受保护。它给映射在页表里的 folio 高优先级,因此 page fault 期间读到的 folio 都会被 folio_set_active 设为活跃,起步于靠前的活跃 generation。

┌──────────────────────────────────┐
│  缺页读入的 folio 都被设为活跃   │
└────────────────┬─────────────────┘
                 ▼
┌──────────────────────────────────┐
│ 预读带入许多不经页表访问的 folio │  它们也享受高优先级,被过度保护
└────────────────┬─────────────────┘
                 ▼
┌──────────────────────────────────┐
│  挤占内存,refault 与 swap 升高  │
└──────────────────────────────────┘

问题在于,page fault 读入的 folio 里,有相当一部分是 readahead 顺带带进来的,它们进了页缓存却从不经页表被访问。folio_set_active 不加区分地把这些 folio 也设为活跃,让它们和真正被访问的 folio 一样享受高优先级保护。结果是这些本该尽快回收的 cold folio 被过度保护,挤占内存,推高 refault 与 swap。

这段历史有来龙去脉。在 commit 4d5d14a01e2c 之前,带 PG_active 的 folio 一律放进最年轻 generation,过度保护、refault 居高不下;那个 commit 把它们改放到第二年轻 generation,有所收敛,但在有 readahead 的场景里仍然偏乐观。对比之下,经典的 active/inactive 双链表方案反而更保守。曾有人提议为 readahead 单独建一条 LRU 让这些页更易回收,但被认为过度设计。

问题

  • page fault 读入的 folio 被一律设为活跃
  • readahead 带入的 cold folio 也享受高优先级保护
  • cold folio 挤占内存,推高 refault 与 swap
  • 即便改到第二年轻 generation,有 readahead 时仍偏乐观

方案

核心是把「设活跃」换成「标记被访问」。

改用 folio_mark_accessed 后,预读带入的 file folio 不再被推到活跃 generation,而是从次旧 generation 起步,与经典 active/inactive 方案的保守度对齐。

旧:预读 folio 起步于活跃 generation
┌──────────────────────────────────────┐
│       缺页 folio 一律设为活跃        │
└──────────────────┬───────────────────┘
                   ▼
┌──────────────────────────────────────┐
│ 预读带入的 folio 也进活跃 generation │  过度保护,refault 与 swap 升高
└──────────────────────────────────────┘

新:预读 folio 起步于次旧 generation
┌──────────────────────────┐
│ 预读 folio 不再被设活跃  │
└────────────┬─────────────┘
             ▼
┌──────────────────────────┐
│ 改从次旧 generation 起步 │  不再被过度保护,回收更精准,接近经典方案保守度
└──────────────────────────┘

切换函数之所以牵动几处配套,是因为原先有几处逻辑假设「首次扫描会设 PG_referenced」,而 folio_mark_accessed 把这个标志的设置时机提前了。于是预读 file folio 进入 generation 的位置、workingset 的激活与二次扫描后的提升判定,都要跟着新的标志时机对齐。

收益

作者在 x86 上、memcg 1GB 内存限制下、用 20 线程做内核构建,对比无补丁、打补丁、以及经典 active/inactive LRU 三组数据:

指标 w/o patch w/ patch active/inactive(参照)
real 1m50.764s 1m48.879s 1m49.928s
user 25m32.305s 25m29.224s 25m28.196s
sys 4m0.012s 3m37.421s 3m40.740s
pswpin 1,333,245 568,480 463,452
pswpout 4,366,443 2,322,657 2,309,119
pgpgin 6,962,592 4,073,416 4,438,856
pgpgout 17,780,712 9,613,408 9,568,628
swpout_zero 1,019,603 593,275 743,704
swpin_zero 14,764 9,118 7,244
refault_file 287,794 262,505 562,555
refault_anon 1,347,963 577,550 470,694

打补丁后 swap in/out 与 refault(尤其匿名页)大幅下降:pswpin 减约 57%、pswpout 减约 47%、refault_anon 减约 57%,sys 时间降约 10%(4m0s → 3m37s)。与经典 active/inactive 参照相比,打补丁的 MGLRU 在多数指标上接近甚至更优(refault_file 更低),说明这次修正让 MGLRU 收敛到了经典方案的保守度,同时保留了多 generation 回收的优势。