背景
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 回收的优势。