背景

透明大页(THP)在某些情形下不能立刻分裂(比如还被映射引用),会被挂到延迟分裂队列(deferred split queue),等 shrinker 在内存回收时处理。这个队列原本是手工维护的(open-coded),按 cgroup 组织:每个 cgroup 一条队列,但不区分 NUMA node。

┌──────────────────────────────┐
│    节点受限的分配进入回收    │
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐
│ 扫描某 cgroup 的延迟分裂队列 │  队列按 cgroup 组织,不区分节点
└──────────────┬───────────────┘
               ▼
┌──────────────────────────────┐  只要该 cgroup 在目标节点有一张大页,所有节点的
│   连带分裂掉其他节点的大页   │  大页都被分裂
└──────────────────────────────┘

这个组织方式在 cgroup 与 NUMA 共存的系统上会出问题。当一次 node 受限的分配触发回收时,回收只针对某些 node,可它扫描的是整个 cgroup 的延迟分裂队列。shrinker bit 本意做护栏,只在 cgroup 于目标 node 有大页时才扫,但这道护栏不严:只要该 cgroup 在目标 node 有一张大页,它名下所有 node 的大页(包括其他 node 上的)都会被扫到并分裂。一次本该局限在少数 node 的回收,就这样越界 split 了其他 node 的 THP。

问题

  • 延迟分裂队列按 cgroup 组织,不区分 node
  • node 受限回收扫的却是整个 cgroup 队列
  • shrinker bit 护栏不严,连带分裂其他 node 的大页
  • 越界 split 损失本不该动的 THP,TLB 收益流失

方案

核心是用 list_lru 取代手工队列。

list_lru 原生就是 per-node × per-cgroup 的交集列表,这恰是延迟分裂队列缺的那个维度。换过去之后,node 受限的回收只扫目标 node 上、属于该 cgroup 的大页,不再越界。

旧:单一 cgroup 队列,不分节点
┌──────────────────────────────────┐
│    延迟分裂队列按 cgroup 组织    │
└────────────────┬─────────────────┘
                 ▼
┌──────────────────────────────────┐
│ 节点受限回收连带分裂其他节点大页 │
└──────────────────────────────────┘

新:节点与 cgroup 的交集列表
┌───────────────────────────┐
│ 延迟分裂队列切到 list_lru │
└─────────────┬─────────────┘
              ▼
┌───────────────────────────┐  队列头按需在可分裂页创建时一次建好所有节点;跨节
│ 回收只扫目标节点上的大页  │  点迁移无需特殊处理
└───────────────────────────┘

per-memcg 的 list_lru 头按需实例化:在首次为某 cgroup 创建可分裂页时(匿名缺页、swapin 缺页、khugepaged 折叠),一次性建好该 cgroup 在所有 node 上的头。因为一次建全,大页后来跨 node 迁移也无需特殊处理,目的 node 的头早就备好了。

换用 list_lru 还顺带接管了原先手工维护队列的大量 list 操作与回收遍历。系列前段(1-8/9)为此铺路:整理 list_lru 锁路径、补按需分配头与匿名缺页路径的接口,并修一个 list_lru 里 pre-existing 的 shrinker bit 竞态。

收益

作者未提供性能数据。从机制与代码逻辑推断的预期收益:

  • node 受限回收不再越界 split 其他 node 的 THP,更多大页得以保留,TLB 未命中更低
  • 回收遍历精确到 node × cgroup 交集,扫描工作量更聚焦,NUMA + cgroup 共存场景的可扩展性更好
  • 借力已被其他主要 shrinker 广泛使用的 list_lru 基础设施,后续 shrinker 改造也更简单