背景

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 自己的也回收不到。