背景
shmem(tmpfs)启用 huge 后,文件尾部常出现一个超出 i_size 的 huge folio:整个大页被占住,有效数据却只有一小段。shmem unused huge shrinker 负责收编这些 inode,split 掉尾部的浪费。它维护的是 per-superblock 的 inode 列表,对全局回收这没有问题,但列表不感知 memcg:memcg A 内存紧张触发的回收会扫遍 superblock 上的所有 inode,把记账给无关 memcg B 的 huge folio 也 split 掉。
memcg 回收的误伤链
┌───────────────────────────────────────┐
│ memcg A 内存紧张触发回收 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ shrinker 扫描整个 superblock 的 inode │
│ 列表 │
└───────────────────┬───────────────────┘
▼
┌───────────────────────────────────────┐
│ split 掉记账给 memcg B 的 huge folio │ 误伤无关 memcg
└───────────────────────────────────────┘
此前 Usama 的 nr_cached_objects 门控补丁让 memcg 级回收整体跳过这个 shrinker,误伤没有了,但本 memcg 该回收的候选也回收不到,过于保守。本系列把「整体跳过」推进为「按 memcg 精确回收」。
问题
- shrinker 的 inode 列表不感知 memcg,memcg A 的回收会 split 记账给 B 的 huge folio。
- 最坏情况 A 没有可回收的 shmem,回收完全无效还白付延迟。
- 生产环境观测到 split 时页锁竞争造成数百毫秒卡顿。
- 整体跳过 shrinker 的门控前作过于保守,本 memcg 的候选也回收不到。
方案
把 per-superblock 的 inode 列表换成 memcg 感知的 list_lru。
inode 按当前尾部 huge folio 的 memcg 与 NUMA 节点排到 list_lru 的对应子链表,非 root memcg 回收只走记账到本 memcg 的子链表,全局回收、root memcg 回收与 shmem quota 回收保持原有全局语义。
list_lru 按 memcg 分子链表
┌─────────────────────────────────────┐
│ inode 按尾部 huge folio 的 memcg 与 │
│ 节点入 list_lru 子链表 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 非 root memcg 回收只走本 memcg 子链 │
│ 表 │ 全局与 quota 回收保持全局语义
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 扫描时按当前尾部 folio 校验,不匹配 │
│ 则重排 │
└─────────────────────────────────────┘
shmem_inode_info 里存的是 obj_cgroup 引用而不是 mem_cgroup 引用,避免长生命周期的 CSS 把 dying memcg 钉住。list_lru 的增删路径在 RCU 下从 objcg 解析当前 memcg,与 list_lru 自身在 memcg offline 时把条目迁往父 memcg 子链表的行为保持一致,删除时总能找到正确的子链表。
list_lru 记录的是 inode,split 的目标却是 inode 当前的尾部 huge folio,二者可能不同步。扫描时校验该 folio 的 memcg 与节点:不再匹配回收上下文或暂时无法 split 的,按当前尾部 folio 重新排队;inode 已无浪费可收的,从列表摘除。
收益
作者未提供性能数据,从生产观测与功能验证推断的预期收益:
- 消除生产环境观测到的数百毫秒级卡顿:charge 路径陷入该 shrinker 后,split 持页锁曾达 182ms。
- 非 root memcg 回收只处理记账到本 memcg 的候选,不再误伤无关 memcg 的 huge folio。
- shrinker debugfs 验证:两个 memcg 各持 64M tmpfs 尾部 THP,按 A 的 cgroup id 扫描后 A 的 64M 全部回收、B 原样保留,而门控前作下连 A 自己的也回收不到。