无锁层级继承: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_counter 的 parent 指针指向上一级,从而形成一棵以 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 | +-------------------+ +-------------------+ |
1 | flowchart TD |
2. memory.current 的读取:实时层级累加
源码片段 src8(mm/memcontrol.c:2434)展示了 mem_find_max_overage() 函数如何遍历层级计算 overage(超限比例):
1 | static u64 mem_find_max_overage(struct mem_cgroup *memcg) |
关键点:
- 从当前 cgroup 向上遍历到 root(但排除 root)。
- 每层独立计算
overage = usage / high。 - 取所有层的最大值,用于决定是否触发 reclaim。
这意味着:即使子组本身未超过 high,但祖先的 high 被突破,reclaim 依然会被触发。这正是层级继承在压力感知上的体现。
类似地,memory.current 的读取(page_counter_read)只需读取本节点的 usage 即可,因为该值已在 charge/uncharge 时被更新为包含子树的总和(通过层级 charge)。
3. 硬限制 memory.max 与 memory.high
memory.max:通过page_counter_set_max()(如src4中mem_cgroup_css_reset调用)设置。当 charge 尝试时,如果本层或任一祖先的usage超过max,page_counter_try_charge()返回 -ENOMEM,调用者(如mem_cgroup_charge())会回收或返回错误。memory.high:软性硬限制。达到后内核不会立即返回错误,而是尝试回收(通过try_charge中的mem_cgroup_reclaim())。src8中的 overage 正是计算 high 的逾限,用于决定回收强度。
两种限制都通过层级传递:对子 cgroup 的 charge 会同时检查所有祖先的 max/high。
4. 软限制 memory.min 与 memory.low
memory.min 和 memory.low 提供内存保护,在全局压力下优先保留给被保护的 cgroup。其实现核心是 mem_cgroup_calculate_protection()(src10 片段):
1 | void mem_cgroup_calculate_protection(struct mem_cgroup *root, |
它调用 page_counter_calculate_protection(),该函数(定义于 mm/page_counter.c)根据每个 cgroup 的 min/low 值和当前层级使用量,按比例分配保护额度。逻辑要点:
- 保护是累积但可抢占的:父 cgroup 设置 min=1G,子组设 min=500M,则整个子树至少保证 1G(但子组额外的 500M 仅在父组未用满时才有效)。
recursive_protection标志控制是否递归:v2 默认递归,即父 min 覆盖整个子树。
效果示意图:
1 | 无保护时,高压力下所有 cgroup 均可能被回收 |
1 | flowchart LR |
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):- 从
counter开始,沿parent链向上遍历。 - 每层原子增加
usage,并检查是否超过max(如果max非无限)。 - 若某一层超过
max,则回滚所有已增加节点的usage,设置*fail为该节点,返回 false。 - 若全部通过,返回 true。
- 从
-
page_counter_uncharge(page_counter *counter, unsigned long nr_pages):- 从
counter开始沿parent链向上遍历。 - 每层原子减去
usage。 - 无错误返回(假设与 charge 配对)。
- 从
mem_cgroup_charge()(位于 mm/memcontrol.c,用户未直接提供)封装了上述调用,并处理 memcg 切换(通过 get_mem_cgroup_from_mm,如 src6)、预充电、KMEM 等细节。其核心路径:
1 | // 简化伪代码,基于公开实现 |
其中 mem_cgroup_try_charge() 调用 page_counter_try_charge 并进行 reclaim 等操作。层级遍历完全交由 page_counter 处理。
6. 软硬限制的协同:一个完整的压力响应流程
当系统内存紧张时,内核通过 try_charge 发起回收,其路径(结合 src8 和 src9):
mem_cgroup_charge()->try_charge()->mem_find_max_overage()计算当前 cgroup 及其祖先的memory.highoverage。- 若 overage 存在,则调用
mem_cgroup_reclaim()从最 overage 的节点开始回收。 - 回收过程中,
mem_cgroup_calculate_protection()确定的 min/low 保护区域会影响 LRU 算法的候选页选择(通过shrink_lruvec中的保护检查)。 - 若超过
memory.max,page_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.c和mm/page_counter.c进一步探索。