无锁层级继承:memcg v2 性能提升的关键

源码版本声明:本文所有源码引用均基于 linux-next 6e845bcb(与用户提供的 [src*] 片段一致)。不同内核版本/分支的行号可能不同。
特别说明:用户提供的 patch 原文主题为 sched_ext/scx_flatcg,与 memcg 无直接关联。本文聚焦于 memcg v2 统一层级的核心机制,所有技术细节均来自用户给出的 memcg 源码片段及 Linux 内核公开设计,不编造 patch 中未出现的代码。

背景:从扁平层级到统一继承

memcg v2 最关键的改变之一是统一层级(unified hierarchy)。在 v1 中,每个 cgroup 各自独立管理统计、限制和事件,父 cgroup 的配置不自动影响子组,导致监控和资源分配碎片化。v2 的“统一”要求:

  • 所有非根 cgroup 的内存统计自动包含其子组的用量。
  • 软限制(memory.min/low)和硬限制(memory.max/high)在层级上具有明确的递归语义。
  • 父 cgroup 的 memory.current 等于自身直接使用量 + 所有子组的使用量之和(通过 page_counter 层级累加)。

这套机制的核心载体是 page_counter 结构体,它天然支持树形聚合。但问题在于:v2 如何确保统计的原子性?如何避免遍历层级带来的性能开销?如何让软限制在多层 cgroup 中不互相抵消?本文从关键 API 出发,逐步拆解。

核心机制与设计思路

1. 层级统计继承:page_counter 的树形结构

每个 mem_cgroup 都嵌入一个 page_counter memory(定义于 include/linux/memcontrol.h,行号不在用户片段中,但可从通用知识知晓)。page_counterparent 指针指向上一级,从而形成一棵以 root_mem_cgroup 为根的树。

当进程 mmap 或分配 page cache 时,内核调用 page_counter_try_charge()(函数原型位于 mm/page_counter.c,用户未提供片段,但逻辑公开)。它沿着 memcg->memory.parent 链向上遍历,每个节点都尝试增加 usage 计数器,若任何一个父节点超过 max 则回滚。这个过程保证了:

  • 统计自动继承:父 cgroup 的 memory.current 最终等于自身 direct 用量 + 所有子组用量之和(因为每次子组 charge 都会累加到所有祖先)。
  • 限制的层级性:任意一层的 memory.max 被触发,整个子树都会被阻断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
+-------------------+            +-------------------+
| root_mem_cgroup | | memory.current |
| memory.max=10G |<-----------+ = direct + children|
+---------+---------+ +-------------------+
|
v
+---------+---------+ +-------------------+
| cgroup A | | memory.current |
| memory.min=500M |<-----------+ = direct + child B|
+---------+---------+ +-------------------+
|
v
+---------+---------+ +-------------------+
| cgroup B | | memory.current |
| memory.low=200M | | = direct (e.g.100M)|
+-------------------+ +-------------------+
1
2
3
4
5
6
7
flowchart TD
root[Root Memcg<br>max=10G] --> A[Group A<br>min=500M]
A --> B[Group B<br>low=200M]
charge[charge 1 page] --> root
root --> A
A --> B
B --> usage[Update usage at B and all ancestors]

2. memory.current 的读取:实时层级累加

源码片段 src8mm/memcontrol.c:2434)展示了 mem_find_max_overage() 函数如何遍历层级计算 overage(超限比例):

1
2
3
4
5
6
7
8
9
10
11
static u64 mem_find_max_overage(struct mem_cgroup *memcg)
{
u64 overage, max_overage = 0;
do {
overage = calculate_overage(page_counter_read(&memcg->memory),
READ_ONCE(memcg->memory.high));
max_overage = max(overage, max_overage);
} while ((memcg = parent_mem_cgroup(memcg)) &&
!mem_cgroup_is_root(memcg));
return max_overage;
}

关键点:

  • 从当前 cgroup 向上遍历到 root(但排除 root)。
  • 每层独立计算 overage = usage / high
  • 取所有层的最大值,用于决定是否触发 reclaim。

这意味着:即使子组本身未超过 high,但祖先的 high 被突破,reclaim 依然会被触发。这正是层级继承在压力感知上的体现。

类似地,memory.current 的读取(page_counter_read)只需读取本节点的 usage 即可,因为该值已在 charge/uncharge 时被更新为包含子树的总和(通过层级 charge)。

3. 硬限制 memory.maxmemory.high

  • memory.max:通过 page_counter_set_max()(如 src4mem_cgroup_css_reset 调用)设置。当 charge 尝试时,如果本层或任一祖先的 usage 超过 maxpage_counter_try_charge() 返回 -ENOMEM,调用者(如 mem_cgroup_charge())会回收或返回错误。
  • memory.high:软性硬限制。达到后内核不会立即返回错误,而是尝试回收(通过 try_charge 中的 mem_cgroup_reclaim())。src8 中的 overage 正是计算 high 的逾限,用于决定回收强度。

两种限制都通过层级传递:对子 cgroup 的 charge 会同时检查所有祖先的 max/high。

4. 软限制 memory.minmemory.low

memory.minmemory.low 提供内存保护,在全局压力下优先保留给被保护的 cgroup。其实现核心是 mem_cgroup_calculate_protection()src10 片段):

1
2
3
4
5
6
7
8
9
void mem_cgroup_calculate_protection(struct mem_cgroup *root,
struct mem_cgroup *memcg)
{
bool recursive_protection =
cgrp_dfl_root.flags & CGRP_ROOT_MEMORY_RECURSIVE_PROT;
// ...
page_counter_calculate_protection(&root->memory, &memcg->memory,
recursive_protection);
}

它调用 page_counter_calculate_protection(),该函数(定义于 mm/page_counter.c)根据每个 cgroup 的 min/low 值和当前层级使用量,按比例分配保护额度。逻辑要点:

  • 保护是累积但可抢占的:父 cgroup 设置 min=1G,子组设 min=500M,则整个子树至少保证 1G(但子组额外的 500M 仅在父组未用满时才有效)。
  • recursive_protection 标志控制是否递归:v2 默认递归,即父 min 覆盖整个子树。

效果示意图:

1
2
3
4
5
6
7
8
无保护时,高压力下所有 cgroup 均可能被回收
+-------------------+
| Root (总计 4G) |
+--+-------------+--+
| |
A(min=1G) B(min=500M)
| 压力下若A用1.2G,保证A至少1G,回收0.2G
| B只保证500M,超出部分可回收
1
2
3
4
5
6
7
flowchart LR
root[Root 4G] --> A[Group A min=1G]
root --> B[Group B min=500M]
reclaim[Reclaim under global pressure] --> A
reclaim --> B
A --> |protected| keepA[Keep at least 1G]
B --> |protected| keepB[Keep at least 500M]

5. page_counter_try_charge()page_counter_uncharge() 的层级计费路径

这两个函数定义在 mm/page_counter.c(用户未提供片段,但逻辑是内核标准实现)。我们总结其行为:

  • page_counter_try_charge(page_counter *counter, unsigned long nr_pages, struct page_counter **fail)

    1. counter 开始,沿 parent 链向上遍历。
    2. 每层原子增加 usage,并检查是否超过 max(如果 max 非无限)。
    3. 若某一层超过 max,则回滚所有已增加节点的 usage,设置 *fail 为该节点,返回 false。
    4. 若全部通过,返回 true。
  • page_counter_uncharge(page_counter *counter, unsigned long nr_pages)

    1. counter 开始沿 parent 链向上遍历。
    2. 每层原子减去 usage
    3. 无错误返回(假设与 charge 配对)。

mem_cgroup_charge()(位于 mm/memcontrol.c,用户未直接提供)封装了上述调用,并处理 memcg 切换(通过 get_mem_cgroup_from_mm,如 src6)、预充电、KMEM 等细节。其核心路径:

1
2
3
4
5
6
7
8
9
10
// 简化伪代码,基于公开实现
int mem_cgroup_charge(struct page *page, struct mm_struct *mm, gfp_t gfp)
{
struct mem_cgroup *memcg = get_mem_cgroup_from_mm(mm);
if (!mem_cgroup_try_charge(memcg, nr_pages, &memcg, gfp))
return -ENOMEM;
// commit charge: 设置 page->mem_cgroup,更新统计
commit_charge(page, memcg);
return 0;
}

其中 mem_cgroup_try_charge() 调用 page_counter_try_charge 并进行 reclaim 等操作。层级遍历完全交由 page_counter 处理。

6. 软硬限制的协同:一个完整的压力响应流程

当系统内存紧张时,内核通过 try_charge 发起回收,其路径(结合 src8src9):

  1. mem_cgroup_charge() -> try_charge() -> mem_find_max_overage() 计算当前 cgroup 及其祖先的 memory.high overage。
  2. 若 overage 存在,则调用 mem_cgroup_reclaim() 从最 overage 的节点开始回收。
  3. 回收过程中,mem_cgroup_calculate_protection() 确定的 min/low 保护区域会影响 LRU 算法的候选页选择(通过 shrink_lruvec 中的保护检查)。
  4. 若超过 memory.maxpage_counter_try_charge 直接返回失败,try_charge 返回 -ENOMEM,导致 mem_cgroup_charge 失败。

关键代码路径总结

功能 关键函数(来源) 层级行为
统计累加 page_counter_try_charge (public) 自底向上原子加
统计扣减 page_counter_uncharge (public) 自底向上原子减
硬限制检测 page_counter_try_charge (public) 每个节点检查 max
high 压力检测 mem_find_max_overage (src8:2434) 向上遍历取最大 overage
软保护计算 mem_cgroup_calculate_protection (src10:5101) 递归或非递归计算保护
cgroup 重置 mem_cgroup_css_reset (src4:4358) 设 memory.max 为无穷大

延伸阅读

  • 用户提供的 src8 (mm/memcontrol.c:2434) 和 src10 (mm/memcontrol.c:5101) 是理解层级 overage 和保护的直接入口。
  • Linux kernel docs: cgroup-v2.rst 官方文档中“Memory Interface Files”章节详细描述了所有文件的语义。
  • 第1部分(已发布)介绍了 v2 统一层级的背景与设计哲学。本部分深入统计与限制的实现。后续第3部分将讨论 memory.reclaim 与 proactive reclaim。

注意:由于用户提供的 patch 原文并不涉及 memcg,本文所有技术细节均基于 Linux 内核公开实现以及用户提供的 [src8][src10] 等片段。读者应结合内核源码 mm/memcontrol.cmm/page_counter.c 进一步探索。