workingset shadow node
folio 被驱逐
离开内存
留下 shadow node
记录驱逐位置
refault 算驱逐距离
判断是否 thrashing
既是 thrashing 检测依据,也是 MGLRU 恢复热页状态的凭据
shadow shrinker 的预算
count_shadow_nodes()
算出 shadow node 预算
lruvec_lru_size()
读可驱逐页数
memcg lru_zone_size
数组
预算 = 可驱逐 LRU 页数折成 1/8 + slab 部分
两种 LRU 的记账路径
classical LRU
lruvec_add/del_folio()
更新 lru_zone_size
MGLRU
lru_gen_add/del_folio() 走 __update_lru_size()
只写 lrugen->nr_pages[]
MGLRU 从不碰 lru_zone_size 数组
lruvec_lru_size() 读到的数
1 GiB page cache + 64 MiB slab 的 memcg
正常预算
约 3.5 万
MGLRU 下
约 2 千
可驱逐页被算成 0,预算只剩 slab 与 unevictable 零头
预算跌到零头之后
shadow entry 随建随清
shrinker 一直回收
refault 找不到记录
驱逐距离算不出来
无法恢复 workingset
thrashing 保护失效
波及每次 memcg reclaim 与全局 reclaim
换回两种 LRU 都维护的统计源
classical LRU
更新 state_local
MGLRU
更新 state_local
lruvec_page_state_local()
统计源换回后,两种 LRU 下预算都完整
统计源不再是 MGLRU 不维护的 lru_zone_size
关闭 state_local 的 reparenting 竞态
state_local 非层级
offline 时统计先 reparent 到父级
v2 更新仍记 dying memcg
父级账与自身账对不上
get_non_dying_memcg_start()
重定向扩到所有层级
v1 早就有,v2 也补上
统计源换回后 v2 也读 state_local,重定向必须扩到所有层级
把 dying 检查移出 RCU 锁
dying 检查
只读调用方已持有引用的 CSS_DYING
不需要锁保护
RCU 锁只保护上行遍历
向最近的存活祖先走
fast path 不拿锁直接返回
offline 是稀有操作,fast path 上只剩一次 dying 检查
修复的功能收益
Before
After
shadow node 预算
只剩零头
→
恢复完整
shadow entry
随建随清
→
不再被提前清掉
refault
找不到记录
→
命中并恢复热页状态
thrashing 保护
memcg 与全局都失效
→
两条回收路径恢复
refault 重新命中后,lru_gen_refault() 恢复 workingset 状态
10-vCPU QEMU · 8 GiB RAM · cgroup v2 · 7 轮取中位数
负载
LRU
base
patches 1-3
变化
w1-anon-churn · pages/s
classical
4393028
4381832
-0.3%
w1-anon-churn · pages/s
MGLRU
4377122
4377053
+0.0%
w2-file-churn · MB/s
classical
8277
8157
-1.4%
w2-file-churn · MB/s
MGLRU
8226
8294
+0.8%
patch 2 的无条件 RCU 锁
w1-anon-churn · MGLRU · patches 1-2
-0.6%
w2-file-churn · MGLRU · patches 1-2
-1.3%
patch 3 把 dying 检查移出锁外
回落到 base 水平
残余差异在轮次噪声内,reparent 记账健全性检查全部通过
空格
播放/暂停
←
→
逐动作
R
重播
⚠
1x