背景

写 memory.high、memory.max 或 memory.reclaim 会在 writer 的上下文里同步跑 reclaim,循环到用量降到目标以下(或 memory.reclaim 回收到请求量)。大 cgroup 上这一趟可能很久:swap I/O 时被设备写带宽卡住,thrashing 时几乎无界:每轮回收几页、被 workload 立即 fault 回来,循环一直在"progress"、永不收敛。

┌─────────────────────────────────────────┐
│ 写 memory.high/max/reclaim 同步 reclaim │
│ 持 active ref                           │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│ 并发 rmdir 持 cgroup_mutex 等 kernfs    │
│ drain                                   │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐  thrashing 下 reclaim 持续 progress
│     cgroup_mutex 被持致全系统 stall     │  不收敛
└─────────────────────────────────────────┘

这些写经 cgroup_file_write(),它不持 cgroup_mutex、也不 pin css,而是靠 kernfs 在操作期间持一个 active reference 保活。所以 reclaim 循环跑着的时候,文件上的 active reference 一直握着。此时若有另一任务并发 rmdir 同一个 cgroup:cgroup_rmdir() 拿 cgroup_mutex,然后在 kernfs_drain() 里阻塞等那个 active reference 排干。cgroup_mutex 一路被持,所有要用它的任务都堆在 remover 后面,实测整台机器停摆,连只是读 /proc//cgroup 的无关任务都被 hung task 报出来。

问题

  • 写 memory.high/max/reclaim 同步 reclaim 持 kernfs active reference
  • 并发 rmdir 持 cgroup_mutex 阻塞 kernfs_drain 等 active ref
  • cgroup_mutex 被持致全系统 stall(含无关的 /proc/pid/cgroup 读)
  • thrashing 下 reclaim 持续 progress、MAX_RECLAIM_RETRIES 不触发,stall 无界

方案

cgroup 销毁在 kill_css_sync() 里置 CSS_DYING,早于 css_clear_dir() 触发的 kernfs_drain(),所以 in-flight 的 reclaim 循环保证在下一轮迭代前观测到它。本系列在所有 reclaim 循环里每轮检查 memcg_is_dying(),命中就 bail out,writer 立刻放下 active reference,remover 得以前进。

旧:reclaim 不检查 dying
┌─────────────────────────────────────────┐
│      reclaim 循环只在零进度时退出       │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│    thrashing 下持续 progress 不收敛     │
└────────────────────┬────────────────────┘
                     ▼
┌─────────────────────────────────────────┐
│ 持 active ref 阻塞 rmdir 致全系统 stall │
└─────────────────────────────────────────┘

新:dying 时 bail out
┌───────────────────────────────────────┐
│       dying 标志在 drain 前设置       │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ reclaim 循环每轮检查 memcg 是否 dying │
└───────────────────┬───────────────────┘
                    ▼
┌───────────────────────────────────────┐
│ bail out 释放 active ref 让 rmdir 前  │
│ 进                                    │
└───────────────────────────────────────┘

与只看零进度的 MAX_RECLAIM_RETRIES 不同,dying 检查覆盖慢 swap I/O 与 thrashing:那些场景里 reclaim 一直在"成功一点点"、循环本不会收敛。memory.reclaim 因 dying 而 bail out,意味着请求量没满足,write 返回 -EAGAIN。这套 bail out 与 O_NONBLOCK(c8e6002bd611)正交:O_NONBLOCK 让调用者事先避免同步 reclaim,本系列处理的是 reclaim 已经在跑、cgroup 开始被删的场景。dying 时 reclaim 本就无意义:cgroup 在 teardown,剩余页会 reparent 给父。

收益

作者提供 production hung task 日志(6.6.102 kernel,作者未标具体 CPU/物理机/VM):

指标 修复前 stall 修复后
cgdelete(kernfs_drain 等 active ref) 159s 即时释放,不再 stall
systemd-journal 读 /proc/pid/cgroup(等 cgroup_mutex) 182s 即时,cgroup_mutex 不再被持

修复前系统只在 reclaim 终于跑完、释放 active ref 后才恢复;而那趟 reclaim 在 dying 的 cgroup 上纯属浪费(页将 reparent)。修复后 reclaim 循环观测到 dying 即 bail out,active ref 即时释放、remover 前进、cgroup_mutex 解锁,全系统立刻恢复。