回收 anon folio 的第一步
anon folio
匿名页
写入 swap
这是一次 IO
GFP_NOIO
禁止启动 IO
能收回的 anon folio:只剩 swapcache 里的
内容与 swap 一致,可以直接释放,无须再写 IO
zRAM 系统 · swap 就在内存里
anon LRU
整条列表
swapcache
稀少
整条 anon LRU 扫下来,一个也收不回
dm-verity
校验块读取
dm-bufio
缓冲分配
GFP_NOIO
触发内存回收
Android · 回收撞上 inactive_anon 大、inactive_file 小的 memcg
memcg 的 inactive LRU
inactive_anon
≈ 335 MB
inactive_file
≈ 4 MB
两侧相差约 84 倍
单次 shrink_lruvec()
shrink_folio_list() 调用
≈ 2400 次
扫描 anon folio
≈ 93000 个
这一轮的耗时
150 ms 以上
收回的 anon folio
0 个
整轮扫描,零回收
全部进展来自 file 侧
anon 侧 335MB 占满扫描预算
can_reclaim_anon_pages()
只看 swap 空间余量
swap 余量
> 0 ?
是
放行扫描
否
不扫 anon
只有余量为 0 才拦
能扫的
整条 anon LRU
≈ 93000 个 folio
≠
能收的
只在 swapcache 里
zRAM 下极少
扫的和收的对不上
决定扫不扫时只看了 swap 余量,没看 swapcache 够不够
reclaimable_anon_is_low()
新增:看 swapcache 够不够扫
两个早退分支,一个比较分支
__GFP_IO
放行 · 不算低
回收能力不受 swapcache 限制
MGLRU 启用
放行 · 不算低
它根本不查这个函数
其余情况
比较 swapcache 水位
swapcache 页数 vs anon 总页数 / 64
水位线 = anon 总页数 ÷ 64
1 格 = anon 的 1/64
swapcache 连 1 格都不到 → 判定过低
水位过低
扫了也收不回
memcg 回收
范围就是这一个 memcg
读该 memcg 的 lruvec
anon 与 swapcache 的计数都在这里
两项计数取自同一个 lruvec
全局回收
没有 memcg 上下文
读整 node 的统计
node 级计数覆盖全部 memcg
不能读根 memcg 的 lruvec
它不含子 memcg 的计数
anon 总量算小 → 阈值跟着缩水
anon 实际都住在子 memcg 里
读根 memcg 会让判定形同虚设
所以按回收范围选统计来源
return false
can_reclaim_anon_pages() 说不值得扫
get_scan_count()
anon 份额算成 0
SCAN_FILE
只扫 file 列表
anon LRU · active / inactive
file LRU
整轮不扫
照旧扫描
这条路径不再 aging anon
只是延迟,不是丢失
kswapd 与带 __GFP_IO 的回收器仍会 aging
换回来的是不再空转
上百 ms 留给真正收得回的路径
延迟敏感路径上这笔账划算
swapcache 回到 1/64 之上
anon 扫描自动恢复
MGLRU 启用
自己决定扫 anon 还是 file
不查 can_reclaim_anon_pages()
水位判定跳不过它的 anon
却会拉低 set_initial_priority()
初始 priority 更低 → 可能扫得更多
所以 MGLRU 时直接放行
只覆盖传统 active / inactive LRU
MGLRU 不在这条判定里
MGLRU 场景留作后续处理
其余回收路径行为不变
带 __GFP_IO、水位达标时照旧扫 anon
① 问题
anon 335MB / file 4MB
93000 folio · 150 ms
② 方案
swapcache < anon / 64
→ anon 整轮不扫
③ 收益
150 ms 空转消除
其余路径行为不变
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x