Linux内核分析之进程管理-04

This language version is unavailable; showing the other language.

12.1 cgroups 演进与 v2 设计哲学

cgroups (Control Groups) 是 Linux 内核中最重要的进程资源管理基础设施之一。它将系统中的进程组织成层次化的分组,并对每个分组施加资源限制、统计和隔离策略。本节深入分析 cgroups 从 v1 到 v2 的演进历程,剖析 v2 设计中的核心哲学,并结合 Linux 7.0.10 内核源码进行详尽解读。


12.1.1 cgroups 演进史

起源:cpuset 与 cgroups v1

cgroups 的历史可以追溯到 2003 年 BULL SA 的 Simon Derr 和 Silicon Graphics 的 Paul Jackson 开发的 cpuset 系统。cpuset 的目标是在大型 NUMA 系统上将进程限制在特定的 CPU 和内存节点上运行。2006 年,Google 的 Paul Menage 将 cpuset 的思想泛化,提取出一套通用的进程分组框架,即 cgroups v1。

内核源码 kernel/cgroup/cgroup.c:1-24 的版权声明完整记录了这一历史:

// SPDX-License-Identifier: GPL-2.0
/*
 *  Generic process-grouping system.
 *
 *  Based originally on the cpuset system, extracted by Paul Menage
 *  Copyright (C) 2006 Google, Inc
 *
 *  Notifications support
 *  Copyright (C) 2009 Nokia Corporation
 *  Author: Kirill A. Shutemov
 *
 *  Copyright notices from the original cpuset code:
 *  --------------------------------------------------
 *  Copyright (C) 2003 BULL SA.
 *  Copyright (C) 2004-2006 Silicon Graphics, Inc.
 *  ...
 */

cgroups v1 的核心设计是"每个控制器一个层次结构"。每个控制器 (cpu、memory、blkio 等) 可以独立挂载到不同的目录树中,进程在每个层次结构中可以位于不同的 cgroup。这种设计给予了极大的灵活性,但也带来了根本性问题。

v1 的核心问题

1. 进程状态不一致

在 v1 中,同一个进程 PID 42 可以在 cpu 层次中位于 /cpu-intensive/,同时在 memory 层次中位于 /memory-limited/。当管理员试图理解"这个进程到底在哪个组"时,答案取决于谈论的是哪个控制器。这种不一致性使得系统行为难以预测和调试。

2. 层次结构间的竞争条件

多个控制器独立操作同一组进程时,缺乏全局一致的同步机制。例如,在一个控制器中迁移进程时,另一个控制器可能正在对同一进程执行完全不同的操作。

3. 委托困难

由于控制器间没有共享的层次结构,将一组一致的权限委托给非特权用户或容器几乎不可能。容器运行时需要手动协调多个独立挂载的控制器,容易出现配置错误。

4. 管理复杂度

每个控制器需要独立挂载、独立配置,系统管理员需要理解多个层次结构之间的交互关系,运维成本高昂。

v2 的诞生

2016 年,Tejun Heo 主导设计的 cgroups v2 合入 Linux 4.5 主线内核。v2 的设计哲学是:单一层次结构、一致的进程归属、简化的管理接口。v2 不是对 v1 的简单修补,而是基于 v1 十年生产经验的重新设计。

内核通过 cgrp_dfl_root 变量 (kernel/cgroup/cgroup.c:193-196) 定义默认层次结构的根:

struct cgroup_root cgrp_dfl_root = {
    .cgrp.self.rstat_cpu = &root_rstat_cpu,
    .cgrp.rstat_base_cpu = &root_rstat_base_cpu,
};

与之配套的可见性标志 cgrp_dfl_visible (kernel/cgroup/cgroup.c:203) 控制默认层次是否对用户空间可见。默认层次始终存在于内核中,但在首次挂载 cgroup2 文件系统之前保持隐藏,这是为了向后兼容 v1 系统。

bool cgrp_dfl_visible;

内核还维护了若干全局掩码来管理控制器的行为 (kernel/cgroup/cgroup.c:206-212):

static u32 cgrp_dfl_inhibit_ss_mask;    /* 不支持 v2 的控制器掩码 */
static u32 cgrp_dfl_implicit_ss_mask;   /* 隐式启用的控制器掩码 */
static u32 cgrp_dfl_threaded_ss_mask;   /* 支持线程化的控制器掩码 */

12.1.2 v2 核心设计原则

统一层次结构 (Unified Hierarchy)

v2 最根本的设计决策是所有控制器共享同一棵 cgroup 树。系统中只有一棵默认层次树,由 cgrp_dfl_root 表示。当一个进程被放入某个 cgroup 时,它在该层次的所有控制器中都属于同一个 cgroup。

子系统声明的展开机制 (kernel/cgroup/cgroup.c:154-166) 确保了所有控制器在统一的数组中被管理:

#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys,
struct cgroup_subsys *cgroup_subsys[] = {
#include <linux/cgroup_subsys.h>
};
#undef SUBSYS

#define SUBSYS(_x) [_x ## _cgrp_id] = #_x,
static const char *cgroup_subsys_name[] = {
#include <linux/cgroup_subsys.h>
};
#undef SUBSYS

include/linux/cgroup_subsys.h 中列出了所有 16 个子系统 (cpuset、cpu、cpuacct、io、memory、devices、freezer、net_cls、perf_event、net_prio、hugetlb、pids、rdma、misc、dmem、debug),通过宏展开在编译时生成子系统数组和名称数组。

no-internal-process 约束

v2 引入了一个关键约束:一个 cgroup 如果启用了控制器 (即 subtree_control 非空),则不能直接包含进程;反之,如果要包含进程,则不能启用控制器。这一约束的核心实现位于 cgroup_migrate_vet_dst() (kernel/cgroup/cgroup.c:2822-2844):

int cgroup_migrate_vet_dst(struct cgroup *dst_cgrp)
{
    /* v1 doesn't have any restriction */
    if (!cgroup_on_dfl(dst_cgrp))
        return 0;

    /* verify @dst_cgrp can host resources */
    if (!cgroup_is_valid_domain(dst_cgrp->dom_cgrp))
        return -EOPNOTSUPP;

    if (cgroup_can_be_thread_root(dst_cgrp) || cgroup_is_threaded(dst_cgrp))
        return 0;

    /* apply no-internal-process constraint */
    if (dst_cgrp->subtree_control)
        return -EBUSY;

    return 0;
}

这一约束的目的是避免叶子 cgroup 中的进程与子 cgroup 之间的资源竞争。当管理员在某个 cgroup 上启用控制器时,意味着该 cgroup 的子节点将进行资源竞争分配,此时该 cgroup 本身不应再直接承载进程。

同样的约束在启用控制器时也会被检查。cgroup_vet_subtree_control_enable() (kernel/cgroup/cgroup.c:3524-3562) 确保如果 cgroup 已经包含任务,则不能启用 domain 控制器:

static int cgroup_vet_subtree_control_enable(struct cgroup *cgrp, u32 enable)
{
    u32 domain_enable = enable & ~cgrp_dfl_threaded_ss_mask;

    if (!enable)
        return 0;

    if (!cgroup_is_valid_domain(cgrp->dom_cgrp))
        return -EOPNOTSUPP;

    if (cgroup_is_mixable(cgrp))
        return 0;

    if (domain_enable) {
        if (cgroup_is_thread_root(cgrp) || cgroup_is_threaded(cgrp))
            return -EOPNOTSUPP;
    } else {
        if (cgroup_can_be_thread_root(cgrp) || cgroup_is_threaded(cgrp))
            return 0;
    }

    /* 控制器不能在有任务的 cgroup 上启用 */
    if (cgroup_has_tasks(cgrp))
        return -EBUSY;

    return 0;
}

委托 (Delegation)

v2 的委托机制允许非特权用户或容器管理 cgroup 子树。关键标志 CGRP_ROOT_NS_DELEGATE (include/linux/cgroup-defs.h:86) 定义了当挂载 cgroup2 文件系统时的委托边界:

/*
 * Consider namespaces as delegation boundaries.  If this flag is
 * set, controller specific interface files in a namespace root
 * aren't writeable from inside the namespace.
 */
CGRP_ROOT_NS_DELEGATE = (1 << 3),

在委托模型中,子树的拥有者可以自由管理其下的 cgroup 创建、任务迁移和控制器配置,但不能跨越 namespace 边界修改父级或兄弟 cgroup 的配置。

接口文件的 CFTYPE_NS_DELEGATABLE 标志 (include/linux/cgroup-defs.h:137) 标记了可以跨越委托边界写入的文件:

CFTYPE_NS_DELEGATABLE = (1 << 2),  /* writeable beyond delegation boundaries */

线程化模式 (Threaded Mode)

v2 引入了线程级别的 cgroup 管理。在默认的 domain 模式下,cgroup 以进程 (thread group) 为粒度管理任务。线程化模式允许以单个线程为粒度进行管理,这对某些场景(如线程池中不同线程需要不同的 CPU 亲和性)非常重要。

cgroup_enable_threaded() (kernel/cgroup/cgroup.c:3669-3714) 处理从 domain 到 threaded 的转换:

static int cgroup_enable_threaded(struct cgroup *cgrp)
{
    struct cgroup *parent = cgroup_parent(cgrp);
    struct cgroup *dom_cgrp = parent->dom_cgrp;
    struct cgroup *dsct;
    struct cgroup_subsys_state *d_css;
    int ret;

    /* noop if already threaded */
    if (cgroup_is_threaded(cgrp))
        return 0;

    if (cgroup_is_populated(cgrp) ||
        cgrp->subtree_control & ~cgrp_dfl_threaded_ss_mask)
        return -EOPNOTSUPP;

    if (!cgroup_is_valid_domain(dom_cgrp) ||
        !cgroup_can_be_thread_root(dom_cgrp))
        return -EOPNOTSUPP;

    cgroup_save_control(cgrp);

    cgroup_for_each_live_descendant_pre(dsct, d_css, cgrp)
        if (dsct == cgrp || cgroup_is_threaded(dsct))
            dsct->dom_cgrp = dom_cgrp;

    ret = cgroup_apply_control(cgrp);
    if (!ret)
        parent->nr_threaded_children++;

    cgroup_finalize_control(cgrp, ret);
    return ret;
}

转换为 threaded 模式后,cgroup 的 dom_cgrp 指针将指向最近的 domain 祖先。domain 级别的资源消耗(不属于特定任务的消耗)将记入 dom_cgrp。

在 css_set 结构中 (include/linux/cgroup-defs.h:289),dom_cset 字段实现了类似的映射:

/*
 * For a domain cgroup, the following points to self.  If threaded,
 * to the matching cset of the nearest domain ancestor.
 */
struct css_set *dom_cset;

隐式控制器

某些控制器对用户空间是透明的,它们在默认层次上自动启用,不出现在 cgroup.controllers 或 cgroup.subtree_control 中,并且不受 no-internal-process 约束的限制。implicit_on_dfl 标志 (include/linux/cgroup-defs.h:810) 标记了这类控制器:

/*
 * If %true, the controller, on the default hierarchy, doesn't show
 * up in "cgroup.controllers" or "cgroup.subtree_control", is
 * implicitly enabled on all cgroups on the default hierarchy, and
 * bypasses the "no internal process" constraint.
 */
bool implicit_on_dfl:1;

典型的隐式控制器包括 perf_event 和 debug (仅在特定配置下)。

favordynmods 优化

CGRP_ROOT_FAVOR_DYNMODS 标志 (include/linux/cgroup-defs.h:105) 提供了一种优化,通过使用每线程组的 rwsem 替代全局 cgroup_threadgroup_rwsem,降低了 fork/exit 热路径与 cgroup.procs 写操作之间的竞争:

/*
 * Reduce latencies on dynamic cgroup modifications such as task
 * migrations and controller on/offs by disabling percpu operation on
 * cgroup_threadgroup_rwsem. This makes hot path operations such as
 * forks and exits into the slow path and more expensive.
 */
CGRP_ROOT_FAVOR_DYNMODS = (1 << 4),

当启用此标志时,写 cgroup.procs 只需获取目标线程组的 rwsem 而非全局锁,减少了不同线程组之间的不必要竞争。


12.1.3 cgroup v2 文件系统接口

kernfs 基础

cgroup 使用 kernfs 作为其文件系统后端。每个 cgroup 在 kernfs 中对应一个目录,控制文件是 kernfs 中的文件节点。kernfs 提供了高效的目录缓存、事件通知和用户空间接口。

在 cgroup_create() (kernel/cgroup/cgroup.c:5885-5892) 中,cgroup 目录的创建通过 kernfs API 完成:

kn = kernfs_create_dir_ns(parent->kn, name, mode,
                          current_fsuid(), current_fsgid(),
                          cgrp, NULL);
if (IS_ERR(kn)) {
    ret = PTR_ERR(kn);
    goto out_cancel_ref;
}
cgrp->kn = kn;

核心接口文件

cgroup v2 提供了一组标准化的接口文件,定义在 cgroup_base_files[] 数组 (kernel/cgroup/cgroup.c:5460-5541) 中。以下逐一分析每个文件的功能和实现。

cgroup.type

{
    .name = "cgroup.type",
    .flags = CFTYPE_NOT_ON_ROOT,
    .seq_show = cgroup_type_show,
    .write = cgroup_type_write,
},

显示和设置 cgroup 的类型。可能的值包括: - domain: 普通 domain cgroup (默认) - domain threaded: 作为线程化子树的根 - domain invalid: 无效的 domain (有混合的子节点类型) - threaded: 线程化 cgroup

cgroup_type_show() (kernel/cgroup/cgroup.c:3716-3729) 的实现直接反映 cgroup 的当前状态:

static int cgroup_type_show(struct seq_file *seq, void *v)
{
    struct cgroup *cgrp = seq_css(seq)->cgroup;

    if (cgroup_is_threaded(cgrp))
        seq_puts(seq, "threaded\n");
    else if (!cgroup_is_valid_domain(cgrp))
        seq_puts(seq, "domain invalid\n");
    else if (cgroup_is_thread_root(cgrp))
        seq_puts(seq, "domain threaded\n");
    else
        seq_puts(seq, "domain\n");

    return 0;
}

cgroup.procs

{
    .name = "cgroup.procs",
    .flags = CFTYPE_NS_DELEGATABLE,
    .file_offset = offsetof(struct cgroup, procs_file),
    .release = cgroup_procs_release,
    .seq_start = cgroup_procs_start,
    .seq_next = cgroup_procs_next,
    .seq_show = cgroup_procs_show,
    .write = cgroup_procs_write,
},

这是最核心的接口文件。读取时显示 cgroup 中所有进程的 PID (线程组领导者的 TID)。写入时将指定进程 (及其整个线程组) 迁移到当前 cgroup。

CFTYPE_NS_DELEGATABLE 标志表明此文件可以跨越 cgroup namespace 边界写入,支持委托场景。file_offset 将文件句柄记录到 cgroup->procs_file 字段中,以便内核内部发送文件变更通知。

写入操作经过以下路径:cgroup_procs_write() -> __cgroup_procs_write() -> 验证目标 cgroup -> 执行迁移。

cgroup.threads

{
    .name = "cgroup.threads",
    .flags = CFTYPE_NS_DELEGATABLE,
    .release = cgroup_procs_release,
    .seq_start = cgroup_threads_start,
    .seq_next = cgroup_procs_next,
    .seq_show = cgroup_procs_show,
    .write = cgroup_threads_write,
},

与 cgroup.procs 类似,但以线程 (而非进程) 粒度操作。写入单个线程的 TID 时只迁移该线程而非整个线程组。这使得在线程化子树中可以精细控制每个线程的 cgroup 归属。

cgroup.controllers

{
    .name = "cgroup.controllers",
    .seq_show = cgroup_controllers_show,
},

只读文件,显示当前 cgroup 可用的控制器列表。一个控制器在某个 cgroup 上"可用"意味着它已经在父 cgroup 的 subtree_control 中被启用 (或通过依赖关系隐式启用)。

cgroup.subtree_control

{
    .name = "cgroup.subtree_control",
    .flags = CFTYPE_NS_DELEGATABLE,
    .seq_show = cgroup_subtree_control_show,
    .write = cgroup_subtree_control_write,
},

这是 v2 控制器管理的核心接口。通过向此文件写入 +controller 或 -controller 来启用或禁用子树中的控制器。写入操作触发一系列复杂的状态转换,详见 12.1.4 节的完整流程分析。

cgroup.events

{
    .name = "cgroup.events",
    .flags = CFTYPE_NOT_ON_ROOT,
    .file_offset = offsetof(struct cgroup, events_file),
    .seq_show = cgroup_events_show,
},

只读文件,显示以下事件: - populated: 1 表示 cgroup 及其子树中有活着的任务,0 表示没有 - frozen: 1 表示 cgroup 被冻结,0 表示未冻结

此文件支持 poll 操作,当状态变化时会产生事件通知。

cgroup.max.descendants 和 cgroup.max.depth

{
    .name = "cgroup.max.descendants",
    .seq_show = cgroup_max_descendants_show,
    .write = cgroup_max_descendants_write,
},
{
    .name = "cgroup.max.depth",
    .seq_show = cgroup_max_depth_show,
    .write = cgroup_max_depth_write,
},

这两个文件限制 cgroup 子树的大小。cgroup.max.descendants 限制后代 cgroup 的总数,cgroup.max.depth 限制子树的最大深度。默认值为 "max" (无限制)。在创建新 cgroup 时,cgroup_check_hierarchy_limits() 会检查这些限制。

cgroup.stat

{
    .name = "cgroup.stat",
    .seq_show = cgroup_stat_show,
},

显示 cgroup 的核心统计信息,包括后代数量和在线状态等基本元数据。

cgroup.stat.local

{
    .name = "cgroup.stat.local",
    .flags = CFTYPE_NOT_ON_ROOT,
    .seq_show = cgroup_core_local_stat_show,
},

显示 cgroup 自身 (不含子树) 的局部统计。

cgroup.freeze

{
    .name = "cgroup.freeze",
    .flags = CFTYPE_NOT_ON_ROOT,
    .seq_show = cgroup_freeze_show,
    .write = cgroup_freeze_write,
},

写入 1 冻结 cgroup 中的所有进程,写入 0 解冻。冻结通过设置 CGRP_FREEZE 标志并通知 freezer 控制器实现。被冻结的进程不会被调度执行,但保持其在内存中的状态完整。

cgroup.kill

{
    .name = "cgroup.kill",
    .flags = CFTYPE_NOT_ON_ROOT,
    .write = cgroup_kill_write,
},

只写文件。写入任何值将立即向 cgroup 中的所有进程发送 SIGKILL 信号。这是一个不可逆的破坏性操作,用于紧急终止失控的 cgroup 中的所有任务。cgroup 结构体中的 kill_seq 字段 (include/linux/cgroup-defs.h:522) 提供了递增的序列号来追踪 kill 操作。

cpu.stat

{
    .name = "cpu.stat",
    .seq_show = cpu_stat_show,
},

显示 CPU 使用统计,包括基础使用时间和带宽限流统计。基础统计通过 rstat 框架递归聚合,额外统计由 css_extra_stat_show 回调提供。

cpu.stat.local

{
    .name = "cpu.stat.local",
    .seq_show = cpu_local_stat_show,
},

显示 cgroup 自身 (不含子树) 的 CPU 统计。

PSI (Pressure Stall Information) 文件

PSI 文件定义在单独的 cgroup_psi_files[] 数组 (kernel/cgroup/cgroup.c:5543-5586) 中:

static struct cftype cgroup_psi_files[] = {
#ifdef CONFIG_PSI
    {
        .name = "io.pressure",
        .file_offset = offsetof(struct cgroup, psi_files[PSI_IO]),
        .seq_show = cgroup_io_pressure_show,
        .write = cgroup_io_pressure_write,
        .poll = cgroup_pressure_poll,
        .release = cgroup_pressure_release,
    },
    {
        .name = "memory.pressure",
        .file_offset = offsetof(struct cgroup, psi_files[PSI_MEM]),
        .seq_show = cgroup_memory_pressure_show,
        .write = cgroup_memory_pressure_write,
        .poll = cgroup_pressure_poll,
        .release = cgroup_pressure_release,
    },
    {
        .name = "cpu.pressure",
        .file_offset = offsetof(struct cgroup, psi_files[PSI_CPU]),
        .seq_show = cgroup_cpu_pressure_show,
        .write = cgroup_cpu_pressure_write,
        .poll = cgroup_pressure_poll,
        .release = cgroup_pressure_release,
    },
#ifdef CONFIG_IRQ_TIME_ACCOUNTING
    {
        .name = "irq.pressure",
        .file_offset = offsetof(struct cgroup, psi_files[PSI_IRQ]),
        .seq_show = cgroup_irq_pressure_show,
        .write = cgroup_irq_pressure_write,
        .poll = cgroup_pressure_poll,
        .release = cgroup_pressure_release,
    },
#endif
    {
        .name = "cgroup.pressure",
        .seq_show = cgroup_pressure_show,
        .write = cgroup_pressure_write,
    },
#endif /* CONFIG_PSI */
    { }    /* terminate */
};

每个 PSI 文件显示对应资源的压力指标: - io.pressure: I/O 资源压力 - memory.pressure: 内存资源压力 - cpu.pressure: CPU 资源压力 - irq.pressure: 中断处理压力 (需要 CONFIG_IRQ_TIME_ACCOUNTING) - cgroup.pressure: cgroup 级别的全局压力控制

PSI 数据格式为 some avg10=... avg60=... avg300=... total=... 和 full avg10=... avg60=... avg300=... total=...,分别表示部分和全部任务因资源不足而停顿的时间比例。

所有 PSI 文件都支持 poll 操作,可以用于实时监控资源压力变化。


12.1.4 控制器启用流程详解

向 cgroup.subtree_control 写入 +cpu +memory 等命令时,内核执行一系列复杂的状态转换。入口函数是 cgroup_subtree_control_write() (kernel/cgroup/cgroup.c:3565-3658)。

第一阶段:解析用户输入

static ssize_t cgroup_subtree_control_write(struct kernfs_open_file *of,
                                            char *buf, size_t nbytes,
                                            loff_t off)
{
    u32 enable = 0, disable = 0;
    struct cgroup *cgrp, *child;
    struct cgroup_subsys *ss;
    char *tok;
    int ssid, ret;

    buf = strstrip(buf);
    while ((tok = strsep(&buf, " "))) {
        if (tok[0] == '\0')
            continue;
        do_each_subsys_mask(ss, ssid, ~cgrp_dfl_inhibit_ss_mask) {
            if (!cgroup_ssid_enabled(ssid) ||
                strcmp(tok + 1, ss->name))
                continue;

            if (*tok == '+') {
                enable |= 1 << ssid;
                disable &= ~(1 << ssid);
            } else if (*tok == '-') {
                disable |= 1 << ssid;
                enable &= ~(1 << ssid);
            } else {
                return -EINVAL;
            }
            break;
        } while_each_subsys_mask();
        if (ssid == CGROUP_SUBSYS_COUNT)
            return -EINVAL;
    }

解析逻辑非常直观:以空格分隔输入字符串,每个令牌以 + 或 - 开头,后跟控制器名称。遍历所有已注册的子系统,匹配名称后将对应位设置到 enable 或 disable 掩码中。跳过被 cgrp_dfl_inhibit_ss_mask 排除的控制器 (如仅 v1 的 debug 控制器)。

第二阶段:验证

    cgrp = cgroup_kn_lock_live(of->kn, true);
    if (!cgrp)
        return -ENODEV;

    for_each_subsys(ss, ssid) {
        if (enable & (1 << ssid)) {
            if (cgrp->subtree_control & (1 << ssid)) {
                enable &= ~(1 << ssid);  /* 已启用,跳过 */
                continue;
            }
            /* 控制器在 cgroup.controllers 中是否可用? */
            if (!(cgroup_control(cgrp) & (1 << ssid))) {
                ret = -ENOENT;
                goto out_unlock;
            }
        } else if (disable & (1 << ssid)) {
            if (!(cgrp->subtree_control & (1 << ssid))) {
                disable &= ~(1 << ssid);  /* 未启用,跳过 */
                continue;
            }
            /* 子 cgroup 是否仍在使用此控制器? */
            cgroup_for_each_live_child(child, cgrp) {
                if (child->subtree_control & (1 << ssid)) {
                    ret = -EBUSY;
                    goto out_unlock;
                }
            }
        }
    }

验证包括两方面: 1. 启用检查: 控制器必须在 cgroup_control(cgrp) 中存在 (即父 cgroup 已为当前 cgroup 启用了该控制器);调用 cgroup_vet_subtree_control_enable() 检查 no-internal-process 等约束 2. 禁用检查: 子 cgroup 不能仍在使用该控制器;否则返回 -EBUSY

第三阶段:保存当前状态并修改

    if (!enable && !disable) {
        ret = 0;
        goto out_unlock;
    }

    ret = cgroup_vet_subtree_control_enable(cgrp, enable);
    if (ret)
        goto out_unlock;

    /* 保存当前控制掩码状态 */
    cgroup_save_control(cgrp);

    /* 修改 subtree_control 掩码 */
    cgrp->subtree_control |= enable;
    cgrp->subtree_control &= ~disable;

cgroup_save_control() 遍历 cgroup 子树,将当前的 subtree_control 和 subtree_ss_mask 保存到 old_subtree_control 和 old_subtree_ss_mask 中,以便在失败时回滚。

第四阶段:应用变更

    ret = cgroup_apply_control(cgrp);
    cgroup_finalize_control(cgrp, ret);
    if (ret)
        goto out_unlock;

    kernfs_activate(cgrp->kn);

cgroup_apply_control() (kernel/cgroup/cgroup.c:3489-3505) 是变更应用的核心函数:

static int cgroup_apply_control(struct cgroup *cgrp)
{
    int ret;

    cgroup_propagate_control(cgrp);

    ret = cgroup_apply_control_enable(cgrp);
    if (ret)
        return ret;

    return cgroup_update_dfl_csses(cgrp);
}

它执行三个步骤:

1. cgroup_propagate_control(cgrp): 将 subtree_control 的变更传播到整个子树,更新每个后代 cgroup 的有效控制器掩码 (subtree_ss_mask)。传播算法会处理控制器间的依赖关系。

2. cgroup_apply_control_enable(cgrp) (kernel/cgroup/cgroup.c:3397-3428): 遍历子树,为需要新启用控制器的 cgroup 创建 css:

static int cgroup_apply_control_enable(struct cgroup *cgrp)
{
    struct cgroup *dsct;
    struct cgroup_subsys_state *d_css;
    struct cgroup_subsys *ss;
    int ssid, ret;

    cgroup_for_each_live_descendant_pre(dsct, d_css, cgrp) {
        for_each_subsys(ss, ssid) {
            struct cgroup_subsys_state *css = cgroup_css(dsct, ss);

            if (!(cgroup_ss_mask(dsct) & (1 << ss->id)))
                continue;

            if (!css) {
                css = css_create(dsct, ss);
                if (IS_ERR(css))
                    return PTR_ERR(css);
            }

            if (css_visible(css)) {
                ret = css_populate_dir(css);
                if (ret)
                    return ret;
            }
        }
    }

    return 0;
}

对于子树中每个 cgroup,检查其有效控制器掩码。如果某个控制器应该启用但对应的 css 尚不存在,则调用 css_create() 创建新的 css。css_create() (kernel/cgroup/cgroup.c:5813-5860) 执行以下步骤:

  1. 调用 ss->css_alloc() 让控制器分配自己的状态结构 (如 task_group)
  2. init_and_link_css(): 初始化 css 字段、链接到父 css、分配 serial_nr
  3. percpu_ref_init(): 初始化引用计数
  4. cgroup_idr_alloc(): 分配 css ID
  5. css_rstat_init(): 初始化递归统计结构
  6. list_add_tail_rcu(): 将 css 加入父的 children 链表
  7. online_css(): 调用 ss->css_online() 回调完成上线

如果 css 已存在但不可见,css_populate_dir() 会在 kernfs 中创建对应的接口文件。

3. cgroup_update_dfl_csses(cgrp): 更新子树中所有任务的 css_set 关联。当控制器启用/禁用后,任务的 css_set->subsys[] 数组需要更新以反映新的 css 布局。这个函数会为需要变更的 css_set 集合创建新的 css_set (或复用已有的),然后将任务迁移过去。

第五阶段:完成

static void cgroup_finalize_control(struct cgroup *cgrp, int ret)
{
    if (ret) {
        cgroup_restore_control(cgrp);
        cgroup_propagate_control(cgrp);
    }

    cgroup_apply_control_disable(cgrp);
}

cgroup_finalize_control() (kernel/cgroup/cgroup.c:3514-3522) 处理收尾工作: - 如果前面的步骤失败,cgroup_restore_control() 恢复之前保存的掩码状态 - cgroup_apply_control_disable() (kernel/cgroup/cgroup.c:3443-3470) 处理需要禁用的控制器:对于不再需要的 css 调用 kill_css_sync() + kill_css_finish();对于需要隐藏但仍有依赖的 css,调用 css_clear_dir() 移除接口文件,并调用 ss->css_reset() 重置控制器状态

禁用控制器时的处理

cgroup_apply_control_disable() 的实现 (kernel/cgroup/cgroup.c:3443-3470):

static void cgroup_apply_control_disable(struct cgroup *cgrp)
{
    struct cgroup *dsct;
    struct cgroup_subsys_state *d_css;
    struct cgroup_subsys *ss;
    int ssid;

    cgroup_for_each_live_descendant_post(dsct, d_css, cgrp) {
        for_each_subsys(ss, ssid) {
            struct cgroup_subsys_state *css = cgroup_css(dsct, ss);

            if (!css)
                continue;

            if (css->parent &&
                !(cgroup_ss_mask(dsct) & (1 << ss->id))) {
                kill_css_sync(css);
                kill_css_finish(css);
            } else if (!css_visible(css)) {
                css_clear_dir(css);
                if (ss->css_reset)
                    ss->css_reset(css);
            }
        }
    }
}

注意遍历使用的是后序遍历 (cgroup_for_each_live_descendant_post),确保子节点在父节点之前被处理。这与启用时的前序遍历 (cgroup_for_each_live_descendant_pre) 形成对称:启用时先创建父 css 再创建子 css,禁用时先销毁子 css 再销毁父 css。

完整流程图

用户写入 "+cpu +memory" 到 cgroup.subtree_control
          |
          v
    cgroup_subtree_control_write()
          |
          v
    1. 解析输入:enable=CPU位|MEMORY位, disable=0
          |
          v
    2. 验证:控制器可用性、子 cgroup 约束
          |
          v
    3. cgroup_vet_subtree_control_enable(): no-internal-process 检查
          |
          v
    4. cgroup_save_control(): 保存当前掩码到 old_*
          |
          v
    5. 修改 cgrp->subtree_control |= enable
          |
          v
    6. cgroup_apply_control():
       |
       +-- cgroup_propagate_control(): 传播掩码到子树
       |
       +-- cgroup_apply_control_enable(): 创建/显示 css
       |   |
       |   +-- css_create() -> ss->css_alloc -> init_and_link_css
       |   |               -> percpu_ref_init -> css_rstat_init
       |   |               -> online_css -> ss->css_online
       |   |
       |   +-- css_populate_dir(): 创建接口文件
       |
       +-- cgroup_update_dfl_csses(): 迁移任务到新 css_set
          |
          v
    7. cgroup_finalize_control():
       |
       +-- 失败时: cgroup_restore_control() 回滚
       |
       +-- cgroup_apply_control_disable(): 清理不再需要的 css
          |
          v
    8. kernfs_activate(): 激活新创建的 kernfs 节点

控制器间的依赖关系

cgroup_subsys 结构中的 depends_on 字段 (include/linux/cgroup-defs.h:857) 声明了控制器间的依赖关系:

/*
 * A subsystem may depend on other subsystems.  When such subsystem
 * is enabled on a cgroup, the depended-upon subsystems are enabled
 * together if available.
 */
unsigned int depends_on;

例如,如果控制器 A 声明 depends_on = (1 << memory_cgrp_id),则当 A 被启用时,memory 控制器也会自动启用 (如果可用)。通过依赖关系隐式启用的控制器不会出现在 cgroup.controllers 中,直到被显式启用。

这种机制确保了控制器间的语义一致性,同时保持了用户空间接口的简洁性。

12.2 cgroup 核心数据结构与生命周期

cgroup 子系统的实现建立在几个精巧的数据结构之上,它们共同构成了进程到资源控制器的映射桥梁。本节深入分析每个核心数据结构的字段语义、引用计数机制、生命周期管理,以及任务迁移和锁层次等关键机制。


12.2.1 cgroup_subsys_state (css)

cgroup_subsys_state (简称 css) 是 cgroup 子系统中最基础的构建单元,它代表一个 cgroup 与一个特定控制器之间的关联。每个 (cgroup, controller) 对应一个 css 实例。完整定义位于 include/linux/cgroup-defs.h:179-263。

字段详解

struct cgroup_subsys_state {
    /* PI: the cgroup that this css is attached to */
    struct cgroup *cgroup;                              /* 行 181 */

    /* PI: the cgroup subsystem that this css is attached to */
    struct cgroup_subsys *ss;                           /* 行 184 */

    /* reference count - access via css_[try]get() and css_put() */
    struct percpu_ref refcnt;                           /* 行 187 */

    struct css_rstat_cpu __percpu *rstat_cpu;           /* 行 204 */

    struct list_head sibling;                           /* 行 211 */
    struct list_head children;                          /* 行 212 */

    int id;                                             /* 行 218 */
    unsigned int flags;                                 /* 行 220 */

    u64 serial_nr;                                      /* 行 228 */

    atomic_t online_cnt;                                /* 行 234 */

    struct work_struct destroy_work;                    /* 行 237 */
    struct rcu_work destroy_rwork;                      /* 行 238 */

    struct cgroup_subsys_state *parent;                 /* 行 244 */

    int nr_descendants;                                 /* 行 252 */

    struct cgroup_subsys_state *rstat_flush_next;       /* 行 262 */
};

cgroup (PI, Public and Immutable): 指向此 css 所属的 cgroup。标记为 PI 表示创建后不变,可无锁读取。

ss (PI): 指向此 css 关联的控制器 (cgroup_subsys)。对于 cgroup 的自引用 css (cgroup->self),此字段为 NULL。

refcnt: 基于 percpu_ref 的引用计数。percpu_ref 在热路径 (如调度器 tick) 上使用每 CPU 计数器避免缓存行竞争,在需要精确计数时切换到原子模式。引用计数的操作接口包括: - css_get(): 增加引用计数,要求调用者已持有有效引用 - css_tryget_online(): 尝试增加引用计数,仅当 css 处于 online 状态时成功 - css_put(): 减少引用计数,当计数降为零时触发释放流程

rstat_cpu: per-CPU 递归统计数据。对于 cgroup::self css,它存储基础的 CPU 时间统计;对于控制器 css,仅当控制器定义了 css_rstat_flush 回调时才使用。rstat_cpu 的初始化时机取决于上下文 (参见 include/linux/cgroup-defs.h:192-203 的注释)。

sibling / children: 链接到父 css 的 children 链表和自身的子 css 链表。通过这两个链表,同一控制器的所有 css 构成与 cgroup 层次树完全同构的一棵树。保护方式为 cgroup_mutex 或 RCU 读锁。

id: 子系统唯一的 css ID。0 未使用,根 css 的 ID 固定为 1。通过 css_from_id() 可以反向查找 css。ID 通过 IDR (ss->css_idr) 分配和管理。

flags: css 的状态标志位,定义在 include/linux/cgroup-defs.h:50-56:

enum {
    CSS_NO_REF    = (1 << 0),  /* 不使用引用计数 */
    CSS_ONLINE    = (1 << 1),  /* 在 css_online 和 css_offline 之间 */
    CSS_RELEASED  = (1 << 2),  /* 引用计数已归零,已释放 */
    CSS_VISIBLE   = (1 << 3),  /* css 对用户空间可见 */
    CSS_DYING     = (1 << 4),  /* css 正在消亡 */
};

各标志的语义: - CSS_NO_REF: 用于 cgroup::self css,跳过引用计数操作 - CSS_ONLINE: css 已完成 css_online() 回调但尚未调用 css_offline(),表示 css 处于活跃可用状态 - CSS_RELEASED: 引用计数已归零,css 已进入释放流程 - CSS_VISIBLE: css 的接口文件已在 kernfs 中创建,对用户空间可见 - CSS_DYING: css 正在被销毁

serial_nr: 单调递增的唯一序列号。由全局变量 css_serial_nr_next (kernel/cgroup/cgroup.c:228) 分配:

static u64 css_serial_nr_next = 1;

serial_nr 保证所有 css 的 children 链表按递增顺序排列,从而支持中断和恢复遍历。在 init_and_link_css() (kernel/cgroup/cgroup.c:5750) 中分配:

css->serial_nr = css_serial_nr_next++;

online_cnt: 跟踪在线的自身和后代 css 数量。在 online_css() 中递增父 css 的 online_cnt,在 offline_css() 中递减。其核心目的是保证父 css 不会在其任何子 css 之前被 offline。具体来说,offline_css() 会检查 atomic_read(&css->online_cnt) > 1 (至少有自身在线),如果子 css 仍在线则拒绝 offline 请求。

destroy_work / destroy_rwork: 用于 css 销毁流程的工作队列和 RCU 延迟工作。css 的释放需要经过多个阶段 (详见 12.2.5 节),这两个字段在阶段间传递工作。

parent (PI): 指向父 css。在 init_and_link_css() (kernel/cgroup/cgroup.c:5753-5756) 中设置并增加父 css 的引用:

if (cgroup_parent(cgrp)) {
    css->parent = cgroup_css(cgroup_parent(cgrp), ss);
    css_get(css->parent);
}

nr_descendants: 可见后代 css 的总数。在 css 创建时递增所有祖先的计数,销毁时递减。与 cgroup->nr_dying_subsys[] 配合使用,后者跟踪正在消亡的 css 数量。

rstat_flush_next: rstat flush 过程中使用的临时链表指针。在 css_rstat_push_children() (kernel/cgroup/rstat.c:198) 中用于构建待刷新的 css 链表。受 rstat_base_lock (base css) 或 ss->rstat_ss_lock (子系统 css) 保护。

cgroup::self -- 特殊的 css

每个 cgroup 都内嵌一个 self css (include/linux/cgroup-defs.h:474),其 ss 字段为 NULL。这个自引用 css 的用途包括: 1. 作为 rstat 框架的节点,跟踪 cgroup 的基础统计 (CPU 时间、nice 时间等) 2. 管理 cgroup 自身的引用计数和生命周期 3. 作为层次树遍历的锚点

css_is_self() 宏检查 css 是否为 cgroup::self,这在 rstat 和其他需要区分 base css 与 controller css 的逻辑中广泛使用。


12.2.2 css_set:进程到 cgroup 的桥梁

css_set 是连接进程与 cgroup 子系统的关键数据结构。它保存了一组 css 指针,使得 task_struct 只需一个指针就能引用其在所有控制器中的 cgroup 归属。完整定义位于 include/linux/cgroup-defs.h:272-360。

设计动机

如结构体上方注释 (include/linux/cgroup-defs.h:266-271) 所述:

/*
 * A css_set is a structure holding pointers to a set of
 * cgroup_subsys_state objects. This saves space in the task struct
 * object and speeds up fork()/exit(), since a single inc/dec and a
 * list_add()/del() can bump the reference count on the entire cgroup
 * set for a task.
 */

如果每个 task_struct 都直接存储各控制器的 css 指针,fork/exit 时需要逐个增减引用计数。css_set 将这些指针聚合为一个集合,fork 时只需增加 css_set 的引用计数,exit 时减少一次即可,显著降低了热路径开销。

字段详解

struct css_set {
    struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; /* 行 278 */
    refcount_t refcount;                                      /* 行 281 */
    struct css_set *dom_cset;                                 /* 行 289 */
    struct cgroup *dfl_cgrp;                                  /* 行 292 */
    int nr_tasks;                                             /* 行 295 */
    struct list_head tasks;                                   /* 行 304 */
    struct list_head mg_tasks;                                /* 行 305 */
    struct list_head dying_tasks;                             /* 行 306 */
    struct list_head task_iters;                              /* 行 309 */
    struct list_head e_cset_node[CGROUP_SUBSYS_COUNT];        /* 行 318 */
    struct list_head threaded_csets;                          /* 行 321 */
    struct list_head threaded_csets_node;                     /* 行 322 */
    struct hlist_node hlist;                                  /* 行 328 */
    struct list_head cgrp_links;                              /* 行 334 */
    struct list_head mg_src_preload_node;                     /* 行 340 */
    struct list_head mg_dst_preload_node;                     /* 行 341 */
    struct list_head mg_node;                                 /* 行 342 */
    struct cgroup *mg_src_cgrp;                               /* 行 351 */
    struct cgroup *mg_dst_cgrp;                               /* 行 352 */
    struct css_set *mg_dst_cset;                              /* 行 353 */
    bool dead;                                                /* 行 356 */
    struct rcu_head rcu_head;                                 /* 行 359 */
};

subsys[]: 核心字段,包含每个控制器的 css 指针。数组大小为 CGROUP_SUBSYS_COUNT (16)。在 v2 中,subsys[ssid] 可能指向祖先 cgroup 的 css (因为不是所有控制器都在每个 cgroup 上启用)。注释说明 (include/linux/cgroup-defs.h:276) 此数组在创建后不可变 (init_css_set 例外)。

refcount: css_set 的引用计数。每增加一个使用此 css_set 的 task,refcount 增加 1;此外,哈希表和临时引用也会增加计数。操作接口为 get_css_set() / put_css_set() / get_css_set_locked()。

dom_cset: 在 domain 模式下指向自身 (dom_cset = self);在 threaded 模式下指向最近 domain 祖先的匹配 css_set。这使得 threaded cgroup 中的任务可以将 domain 级别的资源消耗正确地记入 domain 祖先。

dfl_cgrp: 指向此 css_set 关联的默认层次 cgroup。这是任务通过 task_dfl_cgroup() 获取的 cgroup。

nr_tasks: 使用此 css_set 的任务计数。受 css_set_lock 保护。通过 cgroup_has_tasks() 宏间接使用,检查 cset->nr_tasks > 0。

tasks / mg_tasks / dying_tasks: 三个任务链表,分别存放: - tasks: 正常关联到此 css_set 的活任务 - mg_tasks: 正在迁移中 (源或目的地) 的任务。迁移期间任务暂时从 tasks 移到 mg_tasks - dying_tasks: 正在退出过程中的任务。已脱离正常调度但尚未完成清理

任务通过 task_struct->cg_list 链接到这些链表。

task_iters: 当前正在遍历此 css_set 的 css_task_iter 迭代器链表。当任务在迭代过程中被迁移或退出时,迭代器需要特殊处理以避免遗漏或重复。

e_cset_node[]: 有效 css_set 链的节点数组。在 v2 中,一个 cgroup 的 subsys[ssid] 可能指向祖先的 css。e_cset_node[ssid] 将此 css_set 链接到 subsys[ssid]->cgroup->e_csets[ssid] 链表上,使得可以遍历所有指向同一 cgroup css 的 css_set。

threaded_csets / threaded_csets_node: 对于 domain css_set,threaded_csets 是所有 dom_cset 指向自身的 threaded css_set 链表头;对于 threaded css_set,threaded_csets_node 将其链接到 domain css_set 的链表上。

hlist: 哈希表节点。所有 css_set 通过哈希表管理,哈希键为 subsys[] 指针的集合。find_existing_css_set() 通过哈希查找来复用已存在的 css_set,避免重复创建。

cgrp_links: cgrp_cset_link 链表头。每个 css_set 与其关联的所有 cgroup 之间通过 cgrp_cset_link 结构建立双向链接。cgrp_cset_link 将 css_set 链接到 cgroup 的 cset_links 链表,同时也将 cgroup 链接到 css_set 的 cgrp_links 链表。

mg_* 字段: 迁移过程中使用的临时字段: - mg_src_preload_node / mg_dst_preload_node: 迁移预备阶段将 css_set 链接到预备链表 - mg_node: 迁移执行阶段将 css_set 链接到迁移源或目的地集合 - mg_src_cgrp / mg_dst_cgrp / mg_dst_cset: 记录迁移的源/目的地 cgroup 和目标 css_set

dead: 标记 css_set 已死亡并正在被排空。死亡的 css_set 在迁移时应被忽略。

rcu_head: 用于 RCU 延迟释放 css_set。

哈希表管理

css_set 通过全局哈希表 css_set_table 管理。哈希函数基于 subsys[] 数组中的指针值:

#define CSS_SET_HASH_BITS    7
static DEFINE_HASHTABLE(css_set_table, CSS_SET_HASH_BITS);

当需要为任务找到或创建新的 css_set 时 (如迁移任务后),内核首先通过 find_existing_css_set() 在哈希表中查找是否已存在匹配的 css_set (即 subsys[] 指针完全相同)。如果找到,直接复用;否则创建新的 css_set。

find_existing_css_set() 的核心逻辑是: 1. 基于目标 cgroup 和当前 css_set 的 subsys[] 计算新的 subsys[] 指针集合 2. 计算哈希值 3. 在哈希桶中遍历比较每个 css_set 的 subsys[] 是否匹配


12.2.3 cgroup 结构体

cgroup 是 cgroup 层次树中节点的完整表示。每个 cgroup 目录对应一个 cgroup 实例。完整定义位于 include/linux/cgroup-defs.h:472-639。

字段详解

struct cgroup {
    struct cgroup_subsys_state self;           /* 行 474 */
    unsigned long flags;                       /* 行 476 */
    int level;                                 /* 行 484 */
    int max_depth;                             /* 行 487 */
    int nr_descendants;                        /* 行 500 */
    int nr_dying_descendants;                  /* 行 501 */
    int max_descendants;                       /* 行 502 */
    int nr_populated_csets;                    /* 行 515 */
    int nr_populated_domain_children;          /* 行 516 */
    int nr_populated_threaded_children;        /* 行 517 */
    int nr_threaded_children;                  /* 行 519 */
    unsigned int kill_seq;                     /* 行 522 */
    struct kernfs_node *kn;                    /* 行 524 */
    struct cgroup_file procs_file;             /* 行 525 */
    struct cgroup_file events_file;            /* 行 526 */
    struct cgroup_file psi_files[NR_PSI_RESOURCES]; /* 行 529 */
    u32 subtree_control;                       /* 行 538 */
    u32 subtree_ss_mask;                       /* 行 539 */
    u32 old_subtree_control;                   /* 行 540 */
    u32 old_subtree_ss_mask;                   /* 行 541 */
    struct cgroup_subsys_state __rcu *subsys[CGROUP_SUBSYS_COUNT]; /* 行 544 */
    int nr_dying_subsys[CGROUP_SUBSYS_COUNT];  /* 行 550 */
    struct cgroup_root *root;                  /* 行 552 */
    struct list_head cset_links;               /* 行 558 */
    struct list_head e_csets[CGROUP_SUBSYS_COUNT]; /* 行 567 */
    struct cgroup *dom_cgrp;                   /* 行 576 */
    struct cgroup *old_dom_cgrp;               /* 行 577 */
    struct cgroup_rstat_base_cpu __percpu *rstat_base_cpu; /* 行 588 */
    CACHELINE_PADDING(_pad_);                  /* 行 595 */
    struct cgroup_base_stat last_bstat;        /* 行 598 */
    struct cgroup_base_stat bstat;             /* 行 599 */
    struct prev_cputime prev_cputime;          /* 行 600 */
    struct list_head pidlists;                 /* 行 606 */
    struct mutex pidlist_mutex;                /* 行 607 */
    wait_queue_head_t offline_waitq;           /* 行 610 */
    struct work_struct finish_destroy_work;     /* 行 613 */
    struct work_struct release_agent_work;     /* 行 616 */
    struct psi_group *psi;                     /* 行 619 */
    struct cgroup_bpf bpf;                     /* 行 622 */
    struct cgroup_freezer_state freezer;       /* 行 625 */
    struct bpf_local_storage __rcu *bpf_cgrp_storage; /* 行 628 */
    union {
        DECLARE_FLEX_ARRAY(struct cgroup *, ancestors); /* 行 633 */
        struct {
            struct cgroup *_root_ancestor;
            DECLARE_FLEX_ARRAY(struct cgroup *, _low_ancestors);
        };
    };
};

self: 内嵌的 css,ss 字段为 NULL。这是 cgroup 自身的引用计数和层次树遍历的锚点。通过 cgroup->self 可以在 css 和 cgroup 之间双向导航。

flags: cgroup 级别的标志位 (include/linux/cgroup-defs.h:59-74): - CGRP_NOTIFY_ON_RELEASE: cgroup 变空时通知用户空间 (v1 遗留功能) - CGRP_CPUSET_CLONE_CHILDREN: 继承父 cpuset 配置 (v1 遗留) - CGRP_FREEZE: cgroup 应被冻结 - CGRP_FROZEN: cgroup 当前处于冻结状态

level: cgroup 在层次树中的深度。根 cgroup 的 level 为 0,每向下一层加 1。level 与 ancestors[] 配合使用,可以在 O(1) 时间内判断两个 cgroup 的祖孙关系。

max_depth: 允许的最大子树深度。通过 cgroup.max.depth 接口文件配置。在 cgroup_check_hierarchy_limits() (kernel/cgroup/cgroup.c:5992) 中检查。

nr_descendants / nr_dying_descendants / max_descendants: 后代 cgroup 的计数和限制。nr_descendants 跟踪活跃后代数量,nr_dying_descendants 跟踪正在销毁中的后代。两个计数器受 cgroup_mutex 和 css_set_lock 双重保护 (读取只需其一)。max_descendants 通过 cgroup.max.descendants 配置。

nr_populated_csets: 具有非零 nr_tasks 的 css_set 计数。当此值为 0 时表示 cgroup 中没有任务。

nr_populated_domain_children / nr_populated_threaded_children: 分别跟踪有任务的 domain 和 threaded 子 cgroup 数量。这些计数器用于 cgroup.events 中的 populated 标志。

nr_threaded_children: 活着的 threaded 子 cgroup 数量。

subtree_control: 用户通过 cgroup.subtree_control 配置的控制器掩码。只有在此掩码中的控制器才会在子 cgroup 中启用。

subtree_ss_mask: 实际生效的子树控制器掩码。可能包含比 subtree_control 更多的控制器 (通过依赖关系隐式启用的)。

old_subtree_control / old_subtree_ss_mask: 保存之前的掩码值,用于控制器启用/禁用失败时的回滚。

subsys[]: 每个 cgroup 的每控制器 css 指针数组。使用 RCU 保护 (__rcu),读取时需要 RCU 读锁或 cgroup_mutex。注意在 v2 中,未启用的控制器的 subsys[ssid] 为 NULL,任务通过 css_set 的 e_csets 机制找到最近启用了该控制器的祖先。

nr_dying_subsys[]: 每个控制器正在消亡的 css 数量。当控制器被禁用但 css 尚未完全释放时递增。

root: 指向所属的 cgroup_root。

cset_links: 指向 cgrp_cset_link 结构的链表,每个 link 连接一个在此 cgroup 中有任务的 css_set。

e_csets[]: 有效 css_set 链表数组。e_csets[ssid] 链接所有 subsys[ssid] 指向此 cgroup css 的 css_set。

dom_cgrp: 在 domain 模式下指向自身;在 threaded 模式下指向最近的 domain 祖先。注释 (include/linux/cgroup-defs.h:569-576) 解释了 threaded 子树中 domain 级资源消耗如何通过此指针正确记账。

old_dom_cgrp: 在启用 threaded 模式过程中保存之前的 dom_cgrp 值,用于失败回滚。

rstat_base_cpu: per-CPU 基础统计数据。cgroup_rstat_base_cpu 结构包含 bsync (seqcount 保护)、bstat (当前统计)、last_bstat (上次快照)、subtree_bstat (子树累计) 等字段。CACHELINE_PADDING 将 rstat 指针与频繁更新的 bstat 字段分离到不同缓存行。

bstat / last_bstat / prev_cputime: 全局基础统计及其辅助字段。bstat 存储聚合后的统计值,last_bstat 用于计算增量,prev_cputime 用于 cputime_adjust (处理 utime/stime 的精度调整)。

pidlists / pidlist_mutex: 用于 cgroup.procs 和 cgroup.threads 读取时的 PID 列表缓存。

offline_waitq: 等待 css 完成离线操作的等待队列。

finish_destroy_work: 延迟销毁工作。当 cgroup 被删除但仍有活跃引用时,此工作项在 cgroup 变为空后完成销毁。

psi: PSI (Pressure Stall Information) 组指针,用于跟踪资源压力。

bpf: eBPF 程序存储,支持 cgroup 级别的 BPF 程序附加。

freezer: cgroup freezer 状态,包含冻结标志、冻结计数、冻结时间等。

ancestors[]: 弹性数组,存储从根到自身的所有祖先 cgroup 指针。ancestors[i] 指向 level 为 i 的祖先。此数组在 cgroup 创建时 (cgroup_create(), kernel/cgroup/cgroup.c:5912-5913) 填充:

for (tcgrp = cgrp; tcgrp; tcgrp = cgroup_parent(tcgrp))
    cgrp->ancestors[tcgrp->level] = tcgrp;

通过 ancestors 数组,判断 cgroup A 是否为 cgroup B 的祖先只需比较 A->level < B->level 且 B->ancestors[A->level] == A,时间复杂度为 O(1)。


12.2.4 cgroup_root

cgroup_root 代表一棵完整的 cgroup 层次树。在 v2 中通常只有一个活跃的 cgroup_root (即 cgrp_dfl_root),但 v1 允许多个独立挂载的层次结构。定义位于 include/linux/cgroup-defs.h:646-677。

struct cgroup_root {
    struct kernfs_root *kf_root;              /* 行 647 */
    unsigned int subsys_mask;                  /* 行 650 */
    int hierarchy_id;                          /* 行 653 */
    struct list_head root_list;                /* 行 655 */
    struct rcu_head rcu;                       /* 行 657 */
    atomic_t nr_cgrps;                         /* 行 660 */
    unsigned int flags;                        /* 行 663 */
    char release_agent_path[PATH_MAX];         /* 行 666 */
    char name[MAX_CGROUP_ROOT_NAMELEN];        /* 行 669 */
    struct cgroup cgrp;                        /* 行 676 */
};

kf_root: 关联的 kernfs 根。kernfs 管理 cgroup 文件系统的目录和文件节点。

subsys_mask: 附加到此层次结构的控制器位掩码。

hierarchy_id: 全局唯一的层次 ID,通过 cgroup_hierarchy_idr IDR 分配。

root_list: 链接到全局 cgroup_roots 链表 (kernel/cgroup/cgroup.c:215),用于遍历所有活跃的层次结构。

nr_cgrps: 层次中的 cgroup 总数。原子操作,用于 /proc/cgroups 报告。

flags: 层次级别的标志位 (include/linux/cgroup-defs.h:77-131),包括 CGRP_ROOT_NS_DELEGATE、CGRP_ROOT_FAVOR_DYNMODS、CGRP_ROOT_CPUSET_V2_MODE 等。

cgrp: 内嵌的根 cgroup。此结构必须放在最后,因为 cgroup 的 ancestors[] 是弹性数组。cgroup_root 的分配和释放实际上就是围绕这个内嵌 cgroup 的生命周期管理。


12.2.5 cgroup 生命周期

cgroup 创建

cgroup 的创建由 cgroup_create() (kernel/cgroup/cgroup.c:5866-5990) 完成。以下是完整的创建流程分析:

static struct cgroup *cgroup_create(struct cgroup *parent, const char *name,
                                    umode_t mode)
{
    struct cgroup_root *root = parent->root;
    struct cgroup *cgrp, *tcgrp;
    struct kernfs_node *kn;
    int i, level = parent->level + 1;
    int ret;

步骤 1: 分配内存

    cgrp = kzalloc_flex(*cgrp, _low_ancestors, level);
    if (!cgrp)
        return ERR_PTR(-ENOMEM);

使用 kzalloc_flex 分配 cgroup 结构加上 level-1 个 ancestors 指针的空间。根 cgroup 的 level=0 只有一个 _root_ancestor 指针 (指向自身),level=1 的子 cgroup 有一个 _root_ancestor 和一个 _low_ancestors[0] 指针,以此类推。

步骤 2: 初始化引用计数

    ret = percpu_ref_init(&cgrp->self.refcnt, css_release, 0, GFP_KERNEL);
    if (ret)
        goto out_free_cgrp;

初始化自引用 css 的 percpu_ref,释放回调为 css_release。

步骤 3: 创建 kernfs 目录

    kn = kernfs_create_dir_ns(parent->kn, name, mode,
                              current_fsuid(), current_fsgid(),
                              cgrp, NULL);
    if (IS_ERR(kn)) {
        ret = PTR_ERR(kn);
        goto out_cancel_ref;
    }
    cgrp->kn = kn;

使用当前进程的 uid/gid 创建 kernfs 目录。

步骤 4: 初始化内部状态

    init_cgroup_housekeeping(cgrp);

    cgrp->self.parent = &parent->self;
    cgrp->root = root;
    cgrp->level = level;

init_cgroup_housekeeping() 初始化链表、锁、等待队列等基础设施字段。

步骤 5: 初始化 rstat

    ret = css_rstat_init(&cgrp->self);
    if (ret)
        goto out_kernfs_remove;

为 self css 分配 per-CPU rstat 数据。对于 cgroup::self,这同时分配 cgrp->rstat_base_cpu。

步骤 6: 分配 PSI

    ret = psi_cgroup_alloc(cgrp);
    if (ret)
        goto out_stat_exit;

步骤 7: 初始化 ancestors 数组

    for (tcgrp = cgrp; tcgrp; tcgrp = cgroup_parent(tcgrp))
        cgrp->ancestors[tcgrp->level] = tcgrp;

从自身向上遍历到根,填充 ancestors 数组。

步骤 8: 继承 freezer 状态

    cgrp->freezer.e_freeze = parent->freezer.e_freeze;
    seqcount_spinlock_init(&cgrp->freezer.freeze_seq, &css_set_lock);
    if (cgrp->freezer.e_freeze) {
        set_bit(CGRP_FREEZE, &cgrp->flags);
        cgrp->freezer.freeze_start_nsec = ktime_get_ns();
        set_bit(CGRP_FROZEN, &cgrp->flags);
    }

如果父 cgroup 处于冻结状态,子 cgroup 也继承冻结状态。由于新 cgroup 没有任务,可以立即标记为 FROZEN。

步骤 9: 分配 serial number

    cgrp->self.serial_nr = css_serial_nr_next++;

步骤 10: 提交创建

    spin_lock_irq(&css_set_lock);
    for (i = 0; i < level; i++) {
        tcgrp = cgrp->ancestors[i];
        tcgrp->nr_descendants++;
        if (cgrp->freezer.e_freeze)
            tcgrp->freezer.nr_frozen_descendants++;
    }
    spin_unlock_irq(&css_set_lock);

    list_add_tail_rcu(&cgrp->self.sibling, &cgroup_parent(cgrp)->self.children);
    atomic_inc(&root->nr_cgrps);
    cgroup_get_live(parent);

更新所有祖先的后代计数,将 cgroup 加入父的 children 链表,增加根的 cgroup 计数。

步骤 11: 配置控制器

    if (!cgroup_on_dfl(cgrp))
        cgrp->subtree_control = cgroup_control(cgrp);

    cgroup_propagate_control(cgrp);

在 v1 中,子 cgroup 自动继承父的控制器配置。在 v2 中不自动继承。

css 销毁四阶段

css 的销毁是一个精心设计的异步四阶段过程。kernel/cgroup/cgroup.c:5588-5609 的注释完整描述了这一过程:

/*
 * css destruction is four-stage process.
 *
 * 1. Destruction starts.  Killing of the percpu_ref is initiated.
 *    Implemented in kill_css_finish().
 *
 * 2. When the percpu_ref is confirmed to be killed on all CPUs
 *    and thus css_tryget_online() is guaranteed to fail, the css can be
 *    offlined by invoking offline_css().  After offlining, the base ref
 *    is put.  Implemented in css_killed_work_fn().
 *
 * 3. When the percpu_ref reaches zero, the only possible remaining
 *    accessors are inside RCU read sections.  css_release() schedules the
 *    RCU callback.
 *
 * 4. After the grace period, the css can be freed.  Implemented in
 *    css_free_rwork_fn().
 */

阶段 1: kill_css_finish -- 启动 percpu_ref kill

static void kill_css_finish(struct cgroup_subsys_state *css)
{
    percpu_ref_kill(&css->refcnt);
    css->flags |= CSS_DYING;
    INIT_WORK(&css->destroy_work, css_killed_work_fn);
    queue_work(cgroup_offline_wq, &css->destroy_work);
}

调用 percpu_ref_kill() 将 refcnt 切换到原子模式,此后 css_tryget_online() 保证失败。然后在 cgroup_offline_wq 工作队列上调度阶段 2 的工作。

阶段 2: css_killed_work_fn -- offline

当 percpu_ref 确认在所有 CPU 上都可见为 killed 状态后,执行 offline:

static void css_killed_work_fn(struct work_struct *work)
{
    struct cgroup_subsys_state *css = ...;

    offline_css(css);
    css_put(css);
}

offline_css() 调用 ss->css_offline() 回调,清除 CSS_ONLINE 标志,然后通知等待离线完成的阻塞者。

阶段 3: css_release -- 引用计数归零

当所有引用都释放后,percpu_ref 的回调 css_release() 被调用:

static void css_release(struct percpu_ref *ref)
{
    struct cgroup_subsys_state *css =
        container_of(ref, struct cgroup_subsys_state, refcnt);

    INIT_WORK(&css->destroy_work, css_release_work_fn);
    queue_work(cgroup_release_wq, &css->destroy_work);
}

在 cgroup_release_wq 工作队列上调度 css_release_work_fn()。

阶段 4: css_release_work_fn -> css_free_rwork_fn -- RCU 延迟释放

css_release_work_fn() (kernel/cgroup/cgroup.c:5661-5726) 执行最终清理:

static void css_release_work_fn(struct work_struct *work)
{
    struct cgroup_subsys_state *css = ...;

    cgroup_lock();

    css->flags |= CSS_RELEASED;
    list_del_rcu(&css->sibling);

    if (!css_is_self(css)) {
        css_rstat_flush(css);
        cgroup_idr_replace(&ss->css_idr, NULL, css->id);
        if (ss->css_released)
            ss->css_released(css);
        /* 更新 dying_subsys 计数 */
    } else {
        css_rstat_flush(&cgrp->self);
        /* 更新 dying_descendants 计数 */
        /* 清除 kn->priv */
    }

    cgroup_unlock();

    INIT_RCU_WORK(&css->destroy_rwork, css_free_rwork_fn);
    queue_rcu_work(cgroup_free_wq, &css->destroy_rwork);
}

最后通过 queue_rcu_work() 等待 RCU 宽限期结束后执行 css_free_rwork_fn() (kernel/cgroup/cgroup.c:5610-5659):

static void css_free_rwork_fn(struct work_struct *work)
{
    struct cgroup_subsys_state *css = ...;

    percpu_ref_exit(&css->refcnt);
    css_rstat_exit(css);

    if (!css_is_self(css)) {
        ss->css_free(css);
        cgroup_idr_remove(&ss->css_idr, id);
        cgroup_put(cgrp);
        if (parent)
            css_put(parent);
    } else {
        /* cgroup 释放路径 */
        atomic_dec(&cgrp->root->nr_cgrps);
        bpf_cgrp_storage_free(cgrp);
        if (cgroup_parent(cgrp)) {
            cgroup_put(cgroup_parent(cgrp));
            kernfs_put(cgrp->kn);
            psi_cgroup_free(cgrp);
            kfree(cgrp);
        } else {
            cgroup_destroy_root(cgrp->root);
        }
    }
}

三个独立工作队列

如注释 (kernel/cgroup/cgroup.c:123-152) 所述,css 销毁使用三个独立的工作队列:

static struct workqueue_struct *cgroup_offline_wq;    /* 行 150 */
static struct workqueue_struct *cgroup_release_wq;    /* 行 151 */
static struct workqueue_struct *cgroup_free_wq;       /* 行 152 */

分离工作队列的原因是防止死锁。如果使用单一工作队列,可能出现以下死锁场景:

  1. umount net_prio 控制器,net_prio 根销毁将工作加入队列
  2. perf_event CSS A 的 offline 工作也加入同一队列
  3. net_prio 根销毁等待 perf_event CSS A offline 完成
  4. 但 CSS A 的 offline 工作排在 net_prio 根销毁工作之后
  5. 由于 workqueue 的 max_active 限制,形成死锁

使用三个独立工作队列确保不同阶段的销毁工作不会互相阻塞。


12.2.6 任务迁移机制

任务从一个 cgroup 迁移到另一个 cgroup 是 cgroup 的核心操作之一。写入 cgroup.procs 或 cgroup.threads 触发迁移。

迁移执行:cgroup_migrate_execute()

cgroup_migrate_execute() (kernel/cgroup/cgroup.c:2715-2811) 是迁移的执行函数:

static int cgroup_migrate_execute(struct cgroup_mgctx *mgctx)
{
    struct cgroup_taskset *tset = &mgctx->tset;
    struct cgroup_subsys *ss;
    struct task_struct *task, *tmp_task;
    struct css_set *cset, *tmp_cset;
    int ssid, failed_ssid, ret;

步骤 1: can_attach 回调

    if (tset->nr_tasks) {
        do_each_subsys_mask(ss, ssid, mgctx->ss_mask) {
            if (ss->can_attach) {
                tset->ssid = ssid;
                ret = ss->can_attach(tset);
                if (ret) {
                    failed_ssid = ssid;
                    goto out_cancel_attach;
                }
            }
        } while_each_subsys_mask();
    }

遍历所有受影响的控制器,调用 can_attach 回调。如果任一控制器拒绝迁移,跳转到 cancel_attach 回滚路径,对已成功通过 can_attach 的控制器调用 cancel_attach。

步骤 2: 实际迁移

    spin_lock_irq(&css_set_lock);
    list_for_each_entry(cset, &tset->src_csets, mg_node) {
        list_for_each_entry_safe(task, tmp_task, &cset->mg_tasks, cg_list) {
            struct css_set *from_cset = task_css_set(task);
            struct css_set *to_cset = cset->mg_dst_cset;

            get_css_set(to_cset);
            to_cset->nr_tasks++;
            css_set_move_task(task, from_cset, to_cset, true);
            from_cset->nr_tasks--;
            cgroup_freezer_migrate_task(task, from_cset->dfl_cgrp,
                                        to_cset->dfl_cgrp);
            put_css_set_locked(from_cset);
        }
    }
    spin_unlock_irq(&css_set_lock);

这是迁移的提交点 (commit point)。在 css_set_lock 的保护下: 1. 增加目标 css_set 的引用计数和任务计数 2. css_set_move_task() 将任务从源 css_set 移到目标 css_set 3. 更新源 css_set 的任务计数 4. cgroup_freezer_migrate_task() 处理冻结状态:如果源或目标 cgroup 被冻结,需要相应地冻结或解冻任务 5. 减少源 css_set 的引用计数

步骤 3: attach 回调

    if (tset->nr_tasks) {
        do_each_subsys_mask(ss, ssid, mgctx->ss_mask) {
            if (ss->attach) {
                tset->ssid = ssid;
                ss->attach(tset);
            }
        } while_each_subsys_mask();
    }

通知所有控制器迁移已完成。控制器在 attach 回调中执行实际的状态更新 (如 CPU 控制器的 sched_move_task())。

css_set_move_task()

css_set_move_task() 是实际移动任务的核心函数。它执行以下操作: 1. 从源 css_set 的 tasks/mg_tasks/dying_tasks 链表中移除任务 2. 更新任务的所有迭代器引用 3. 将任务添加到目标 css_set 的相应链表 4. 更新 task->cgroups 指针指向新的 css_set

迁移失败回滚

如果 can_attach 回调失败,cancel_attach 机制确保所有控制器回滚:

out_cancel_attach:
    if (tset->nr_tasks) {
        do_each_subsys_mask(ss, ssid, mgctx->ss_mask) {
            if (ssid == failed_ssid)
                break;
            if (ss->cancel_attach) {
                tset->ssid = ssid;
                ss->cancel_attach(tset);
            }
        } while_each_subsys_mask();
    }

只对在失败 ssid 之前通过 can_attach 的控制器调用 cancel_attach。


12.2.7 锁层次与并发

cgroup 子系统使用多个层次的锁来保护不同的数据。

cgroup_mutex

DEFINE_MUTEX(cgroup_mutex);   /* kernel/cgroup/cgroup.c:89 */

主锁,保护 cgroup 层次结构的所有修改操作。注释 (kernel/cgroup/cgroup.c:79-84) 说明:

cgroup_mutex is the master lock.  Any modification to cgroup or its
hierarchy must be performed while holding it.

持有 cgroup_mutex 的操作包括: - 创建/销毁 cgroup - 启用/禁用控制器 - 任务迁移的准备和提交 - css 的 online/offline - 挂载/卸载 cgroup 文件系统

css_set_lock

DEFINE_SPINLOCK(css_set_lock);   /* kernel/cgroup/cgroup.c:90 */

自旋锁,保护 css_set 的内容和任务链表。注释说明:

css_set_lock protects task->cgroups pointer, the list of css_set
objects, and the chain of tasks off each css_set.

由于可能在中断上下文中持有 (如 scheduler tick),它是一个自旋锁而非互斥锁。

cgroup_threadgroup_rwsem

DEFINE_PERCPU_RWSEM(cgroup_threadgroup_rwsem);  /* kernel/cgroup/cgroup.c:116 */

percpu 读写信号量,保证 fork/exit 与 cgroup.procs 写操作的互斥。fork 和 exit 作为读端 (percpu_down_read),cgroup.procs 写作为写端 (percpu_down_write)。由于 fork/exit 是极其频繁的操作,使用 percpu_rwsem 将读端开销降到最低。

当 CGRP_ROOT_FAVOR_DYNMODS 启用时,系统切换为每线程组 rwsem (task->signal->cgroup_threadgroup_rwsem),减少不同线程组之间的不必要竞争。此行为由 cgroup_enable_per_threadgroup_rwsem 全局变量控制 (kernel/cgroup/cgroup.c:247)。

percpu_ref

css 的引用计数使用 percpu_ref,在热路径上使用每 CPU 计数器避免缓存行竞争。percpu_ref 有两种模式: - percpu 模式: 正常操作,每个 CPU 有独立的计数器,增加/减少无需原子操作 - atomic 模式: 在 kill 后切换,所有操作使用单一的原子变量

模式切换由 percpu_ref_kill() 发起,在所有 CPU 确认后完成。此机制保证了 css_tryget_online() 在 kill 后可靠地失败。

lockdep 断言

内核提供了 cgroup_assert_mutex_or_rcu_locked() 宏 (kernel/cgroup/cgroup.c:118-121) 用于 lockdep 验证:

#define cgroup_assert_mutex_or_rcu_locked()            \
    RCU_LOCKDEP_WARN(!rcu_read_lock_held() &&          \
                       !lockdep_is_held(&cgroup_mutex), \
                       "cgroup_mutex or RCU read lock required");

锁获取顺序

为避免死锁,锁的获取遵循以下层次:

  1. cgroup_mutex (最外层)
  2. cgroup_threadgroup_rwsem (写端)
  3. css_set_lock (自旋锁,禁用中断)
  4. 控制器内部锁 (如调度器的 rq->lock)

fork/exit 路径中,锁的获取顺序为: cgroup_threadgroup_rwsem (读) -> css_set_lock -> 控制器锁

迁移路径中,锁的获取顺序为: cgroup_mutex -> cgroup_threadgroup_rwsem (写) -> css_set_lock


12.2.8 cftype 与控制器接口文件

cftype (cgroup file type) 结构定义了 cgroup 中控制文件的接口。控制器通过定义 cftype 数组来声明自己提供的接口文件。完整定义位于 include/linux/cgroup-defs.h:686-766。

结构字段

struct cftype {
    char name[MAX_CFTYPE_NAME];                /* 行 691 */
    unsigned long private;                      /* 行 692 */
    size_t max_write_len;                       /* 行 698 */
    unsigned int flags;                         /* 行 701 */
    unsigned int file_offset;                   /* 行 709 */
    struct cgroup_subsys *ss;                   /* 行 715 */
    struct list_head node;                      /* 行 716 */
    struct kernfs_ops *kf_ops;                  /* 行 717 */

    int (*open)(struct kernfs_open_file *of);   /* 行 719 */
    void (*release)(struct kernfs_open_file *of); /* 行 720 */

    u64 (*read_u64)(struct css *, struct cftype *);  /* 行 726 */
    s64 (*read_s64)(struct css *, struct cftype *);  /* 行 730 */
    int (*seq_show)(struct seq_file *, void *v);     /* 行 733 */
    void *(*seq_start)(struct seq_file *, loff_t *); /* 行 736 */
    void *(*seq_next)(struct seq_file *, void *, loff_t *); /* 行 737 */
    void (*seq_stop)(struct seq_file *, void *);     /* 行 738 */
    int (*write_u64)(struct css *, struct cftype *, u64); /* 行 745 */
    int (*write_s64)(struct css *, struct cftype *, s64); /* 行 750 */
    ssize_t (*write)(struct kernfs_open_file *, char *, size_t, loff_t); /* 行 759 */
    __poll_t (*poll)(struct kernfs_open_file *, struct poll_table_struct *); /* 行 762 */

    struct lock_class_key lockdep_key;          /* 行 765 */
};

name: 文件名。在 cgroup_file_name() 中自动添加子系统名称前缀 (除非设置了 CFTYPE_NO_PREFIX)。

private: 控制器可自由使用的私有数据字段。

max_write_len: 允许的最大写入长度。默认为 PAGE_SIZE-1。

flags: 文件标志位 (include/linux/cgroup-defs.h:134-147):

enum {
    CFTYPE_ONLY_ON_ROOT     = (1 << 0),  /* 仅在根 cgroup 创建 */
    CFTYPE_NOT_ON_ROOT      = (1 << 1),  /* 不在根 cgroup 创建 */
    CFTYPE_NS_DELEGATABLE   = (1 << 2),  /* 可跨越委托边界写入 */
    CFTYPE_NO_PREFIX        = (1 << 3),  /* 不添加子系统前缀 */
    CFTYPE_WORLD_WRITABLE   = (1 << 4),  /* 所有人可写 */
    CFTYPE_DEBUG            = (1 << 5),  /* 仅在 cgroup_debug 时创建 */
    __CFTYPE_ONLY_ON_DFL    = (1 << 16), /* 仅在默认层次 */
    __CFTYPE_NOT_ON_DFL     = (1 << 17), /* 不在默认层次 */
    __CFTYPE_ADDED          = (1 << 18), /* 已添加标记 */
};

file_offset: 如果非零,表示从 css 开始到 struct cgroup_file 字段的偏移量。cgroup 会将创建的文件句柄记录到对应字段中,便于内核内部发送文件变更通知。

回调函数组: cftype 提供了多组读写回调: - read_u64 / write_u64: 单个无符号整数的读写快捷方式 - read_s64 / write_s64: 单个有符号整数的读写快捷方式 - seq_show / seq_start / seq_next / seq_stop: seq_file 接口,用于多行输出 - write: 原始写入接口,覆盖所有其他写入回调 - poll: 轮询接口,用于 PSI 等需要实时通知的文件

dfl_cftypes 与 legacy_cftypes

cgroup_subsys 结构中的两个 cftype 数组指针 (include/linux/cgroup-defs.h:847-848):

struct cftype *dfl_cftypes;    /* for the default hierarchy */
struct cftype *legacy_cftypes;  /* for the legacy hierarchies */
  • dfl_cftypes: 在 v2 默认层次上注册的接口文件
  • legacy_cftypes: 在 v1 legacy 层次上注册的接口文件
  • 两者可以指向同一个数组

以 CPU 控制器为例 (kernel/sched/core.c:10221-10236):

struct cgroup_subsys cpu_cgrp_subsys = {
    .css_alloc      = cpu_cgroup_css_alloc,
    .css_online     = cpu_cgroup_css_online,
    .css_offline    = cpu_cgroup_css_offline,
    .css_released   = cpu_cgroup_css_released,
    .css_free       = cpu_cgroup_css_free,
    .css_extra_stat_show = cpu_extra_stat_show,
    .css_local_stat_show = cpu_local_stat_show,
    .can_attach     = cpu_cgroup_can_attach,
    .attach         = cpu_cgroup_attach,
    .cancel_attach  = cpu_cgroup_cancel_attach,
    .legacy_cftypes = cpu_legacy_files,
    .dfl_cftypes    = cpu_files,
    .early_init     = true,
    .threaded       = true,
};

v2 使用 cpu_files[] (kernel/sched/core.c:10169),提供 cpu.weight、cpu.max、cpu.max.burst、cpu.idle、cpu.uclamp.min、cpu.uclamp.max 等接口。v1 使用 cpu_legacy_files[] (kernel/sched/core.c:9899),提供 cpu.shares、cpu.cfs_quota_us、cpu.cfs_period_us、cpu.cfs_burst_us、cpu.stat 等接口。

文件注册

控制器通过 cgroup_add_dfl_cftypes() 和 cgroup_add_legacy_cftypes() 注册接口文件。这两个函数最终调用 cgroup_addrm_files(),遍历 cftype 数组并在每个 cgroup 的 kernfs 目录中创建对应的文件节点。

当控制器通过 cgroup_apply_control_enable() 在子树中启用时,css_populate_dir() 为新创建或变为可见的 css 创建接口文件。反之,cgroup_apply_control_disable() 中的 css_clear_dir() 移除不再需要的接口文件。

12.3 CPU 控制器

CPU 控制器 (cpu) 是 cgroup 中最复杂的子系统之一,它直接与内核调度器集成,提供 CPU 时间分配的精细控制。在 cgroups v2 中,CPU 控制器合并了 v1 中独立的 cpuacct 子系统功能,通过统一的接口提供带宽控制、权重分配和 CPU 使用统计。本节深入分析 CPU 控制器的数据结构、带宽控制机制、权重实现以及与 CFS 调度器的集成细节。


12.3.1 CPU 控制器概述

配置与注册

CPU 控制器通过 CONFIG_CGROUP_SCHED 配置选项启用。在 include/linux/cgroup_subsys.h:17-18 中声明:

#if IS_ENABLED(CONFIG_CGROUP_SCHED)
SUBSYS(cpu)
#endif

CPU 控制器的核心定义位于 kernel/sched/core.c:10221-10236:

struct cgroup_subsys cpu_cgrp_subsys = {
    .css_alloc      = cpu_cgroup_css_alloc,
    .css_online     = cpu_cgroup_css_online,
    .css_offline    = cpu_cgroup_css_offline,
    .css_released   = cpu_cgroup_css_released,
    .css_free       = cpu_cgroup_css_free,
    .css_extra_stat_show = cpu_extra_stat_show,
    .css_local_stat_show = cpu_local_stat_show,
    .can_attach     = cpu_cgroup_can_attach,
    .attach         = cpu_cgroup_attach,
    .cancel_attach  = cpu_cgroup_cancel_attach,
    .legacy_cftypes = cpu_legacy_files,
    .dfl_cftypes    = cpu_files,
    .early_init     = true,
    .threaded       = true,
};

关键字段分析: - early_init = true: CPU 控制器在系统启动的早期阶段初始化,早于大多数其他子系统,因为调度器需要尽早使用 task_group - threaded = true: CPU 控制器支持线程化模式,可以在 threaded 子树中使用 - css_alloc/css_online/css_offline/css_free: 标准的 css 生命周期回调 - can_attach/attach/cancel_attach: 任务迁移回调,确保调度器状态与 cgroup 归属同步 - css_extra_stat_show: 提供额外的统计信息 (throttling 数据) - css_local_stat_show: 提供局部 (不含子树) 统计信息

v1 到 v2 的接口变化

在 v1 中,CPU 相关功能分散在两个独立控制器中: - cpu: 提供 cpu.shares、cpu.cfs_quota_us、cpu.cfs_period_us、cpu.cfs_burst_us、cpu.stat - cpuacct: 提供 cpuacct.usage、cpuacct.stat、cpuacct.usage_percpu 等统计文件

在 v2 中,cpu 和 cpuacct 的功能合并到单一的 cpu 控制器中,统计功能通过 cgroup 核心的 cpu.stat 文件和 rstat 框架提供。cpuacct 的 charge 逻辑 (cpuacct_charge()) 和 account_field 逻辑 (cpuacct_account_field()) 仍然存在 (include/linux/cgroup.h:724-731),但直接与 cgroup 基础统计集成:

#ifdef CONFIG_CGROUP_CPUACCT
void cpuacct_charge(struct task_struct *tsk, u64 cputime);
void cpuacct_account_field(struct task_struct *tsk, int index, u64 val);
#else
static inline void cpuacct_charge(struct task_struct *tsk, u64 cputime) {}
static inline void cpuacct_account_field(struct task_struct *tsk, int index,
                                         u64 val) {}
#endif

v2 接口文件

v2 的 CPU 控制器接口文件定义在 cpu_files[] 数组 (kernel/sched/core.c:10169-10218) 中:

static struct cftype cpu_files[] = {
#ifdef CONFIG_GROUP_SCHED_WEIGHT
    {
        .name = "weight",
        .flags = CFTYPE_NOT_ON_ROOT,
        .read_u64 = cpu_weight_read_u64,
        .write_u64 = cpu_weight_write_u64,
    },
    {
        .name = "weight.nice",
        .flags = CFTYPE_NOT_ON_ROOT,
        .read_s64 = cpu_weight_nice_read_s64,
        .write_s64 = cpu_weight_nice_write_s64,
    },
    {
        .name = "idle",
        .flags = CFTYPE_NOT_ON_ROOT,
        .read_s64 = cpu_idle_read_s64,
        .write_s64 = cpu_idle_write_s64,
    },
#endif
#ifdef CONFIG_GROUP_SCHED_BANDWIDTH
    {
        .name = "max",
        .flags = CFTYPE_NOT_ON_ROOT,
        .seq_show = cpu_max_show,
        .write = cpu_max_write,
    },
    {
        .name = "max.burst",
        .flags = CFTYPE_NOT_ON_ROOT,
        .read_u64 = cpu_burst_read_u64,
        .write_u64 = cpu_burst_write_u64,
    },
#endif
#ifdef CONFIG_UCLAMP_TASK_GROUP
    {
        .name = "uclamp.min",
        .flags = CFTYPE_NOT_ON_ROOT,
        .seq_show = cpu_uclamp_min_show,
        .write = cpu_uclamp_min_write,
    },
    {
        .name = "uclamp.max",
        .flags = CFTYPE_NOT_ON_ROOT,
        .seq_show = cpu_uclamp_max_show,
        .write = cpu_uclamp_max_write,
    },
#endif
    { }    /* terminate */
};

所有文件都标记为 CFTYPE_NOT_ON_ROOT,因为根 cgroup 不受限制。


12.3.2 CPU 带宽控制

CPU 带宽控制 (CFS bandwidth) 是 v2 中最重要的 CPU 资源限制机制。它允许管理员精确控制一个 cgroup 在每个时间周期内可以使用的 CPU 时间上限。

核心数据结构:cfs_bandwidth

struct cfs_bandwidth 定义在 kernel/sched/sched.h:447-471,是带宽控制的核心数据结构:

struct cfs_bandwidth {
#ifdef CONFIG_CFS_BANDWIDTH
    raw_spinlock_t      lock;
    ktime_t             period;
    u64                 quota;
    u64                 runtime;
    u64                 burst;
    u64                 runtime_snap;
    s64                 hierarchical_quota;

    u8                  idle;
    u8                  period_active;
    u8                  slack_started;
    struct hrtimer      period_timer;
    struct hrtimer      slack_timer;
    struct list_head    throttled_cfs_rq;

    /* Statistics: */
    int                 nr_periods;
    int                 nr_throttled;
    int                 nr_burst;
    u64                 throttled_time;
    u64                 burst_time;
#endif
};

lock: 保护 cfs_bandwidth 结构的原始自旋锁。由于 bandwidth 操作可能在调度器热路径上执行 (如 assign_cfs_rq_runtime),使用 raw_spinlock 以避免在原子上下文中睡眠。

period: 带宽周期,以 ktime_t 表示。默认值为 100ms。在一个周期内,cgroup 最多可以使用 quota 数量的 CPU 时间。

quota: 每周期允许的最大 CPU 运行时间 (纳秒)。RUNTIME_INF 表示无限制。在 v2 中,写入 cpu.max 文件的格式为 "quota period" (如 "50000 100000" 表示每 100ms 可用 50ms CPU 时间)。

runtime: 当前周期内剩余的可用时间。每个周期开始时被重置为 quota (加 burst)。当 runtime 降为 0 时,对应的 cfs_rq 会被 throttle。

burst: 允许的突发时间。cgroup 可以累计未使用的 quota 用于后续周期的突发需求。这在 v2 中通过 cpu.max.burst 接口控制。

runtime_snap: runtime 的快照,用于统计目的。

hierarchical_quota: 层次化的有效 quota,考虑了所有祖先 cgroup 的限制。在 v2 层次结构中,子 cgroup 的实际 quota 不能超过父 cgroup 的 quota。

idle: 标记 bandwidth 是否处于空闲状态 (没有活跃的 throttled cfs_rq)。

period_active: 标记周期定时器是否活跃。

slack_started: 标记 slack 定时器是否已启动。

period_timer: 高精度周期定时器。每个周期到期时触发,执行 sched_cfs_period_timer() 回调,重置 runtime 并 unthrottle 被限流的 cfs_rq。

slack_timer: 松弛时间定时器。当 cfs_rq 有未使用的 runtime 时,通过 slack 机制将部分 runtime 归还给父级,提高全局的 CPU 利用率。

throttled_cfs_rq: 被限流的 cfs_rq 链表。当一个 cfs_rq 用尽其分配的 runtime 时,它会被添加到此链表并在下一个周期开始时被重新激活。

统计字段: nr_periods (经历的总周期数)、nr_throttled (被限流的周期数)、nr_burst (发生突发的次数)、throttled_time (累计被限流的时间)、burst_time (累计突发使用的时间)。

task_group 与 cfs_bandwidth 的关系

struct task_group (kernel/sched/sched.h:474-527) 是 CPU 控制器的核心数据结构,它内嵌了 cgroup_subsys_state 并包含调度器特定的状态:

struct task_group {
    struct cgroup_subsys_state css;

#ifdef CONFIG_GROUP_SCHED_WEIGHT
    int             idle;
#endif

#ifdef CONFIG_FAIR_GROUP_SCHED
    struct sched_entity    **se;
    struct cfs_rq          **cfs_rq;
    unsigned long           shares;
    atomic_long_t           load_avg ____cacheline_aligned;
#endif

#ifdef CONFIG_RT_GROUP_SCHED
    struct sched_rt_entity **rt_se;
    struct rt_rq           **rt_rq;
    struct rt_bandwidth     rt_bandwidth;
#endif

    struct scx_task_group   scx;

    struct rcu_head         rcu;
    struct list_head        list;

    struct task_group      *parent;
    struct list_head        siblings;
    struct list_head        children;

#ifdef CONFIG_SCHED_AUTOGROUP
    struct autogroup       *autogroup;
#endif

    struct cfs_bandwidth    cfs_bandwidth;

#ifdef CONFIG_UCLAMP_TASK_GROUP
    unsigned int            uclamp_pct[UCLAMP_CNT];
    struct uclamp_se        uclamp_req[UCLAMP_CNT];
    struct uclamp_se        uclamp[UCLAMP_CNT];
#endif
};

每个 task_group 包含: - se[]: 每个CPU上的调度实体。task_group 在每个 CPU 的 cfs_rq 中以 sched_entity 的形式参与调度 - cfs_rq[]: 每个CPU上该 task_group 拥有的 CFS 运行队列。属于该 cgroup 的任务被放入此 cfs_rq - shares: CPU 份额/权重值 - cfs_bandwidth: CFS 带宽控制参数 - parent/siblings/children: task_group 层次树,与 cgroup 层次树一一对应

task_group 层次与 cgroup 层次的对应关系通过 css 嵌入实现:css_tg() 宏从 cgroup_subsys_state 指针获取包含它的 task_group。

tg_set_cfs_bandwidth() 流程

写入 cpu.max 最终调用 tg_set_cfs_bandwidth() (kernel/sched/core.c:9481-9548):

static int tg_set_cfs_bandwidth(struct task_group *tg,
                                u64 period_us, u64 quota_us, u64 burst_us)
{
    int i, ret = 0, runtime_enabled, runtime_was_enabled;
    struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
    u64 period, quota, burst;

    period = (u64)period_us * NSEC_PER_USEC;

    if (quota_us == RUNTIME_INF)
        quota = RUNTIME_INF;
    else
        quota = (u64)quota_us * NSEC_PER_USEC;

    burst = (u64)burst_us * NSEC_PER_USEC;

    guard(cpus_read_lock)();
    guard(mutex)(&cfs_constraints_mutex);

    ret = __cfs_schedulable(tg, period, quota);
    if (ret)
        return ret;

    runtime_enabled = quota != RUNTIME_INF;
    runtime_was_enabled = cfs_b->quota != RUNTIME_INF;

    if (runtime_enabled && !runtime_was_enabled)
        cfs_bandwidth_usage_inc();

步骤 1: 参数转换: 将微秒转换为纳秒。

步骤 2: 可调度性检查: __cfs_schedulable() 验证设置不会导致子 cgroup 的 quota 总和超过父 cgroup 的 quota。这个检查通过递归遍历 task_group 层次树完成。

步骤 3: 更新全局状态: 如果从未限制切换到限制模式,增加 cfs_bandwidth_used 静态键。

    scoped_guard (raw_spinlock_irq, &cfs_b->lock) {
        cfs_b->period = ns_to_ktime(period);
        cfs_b->quota = quota;
        cfs_b->burst = burst;

        __refill_cfs_bandwidth_runtime(cfs_b);

        if (runtime_enabled)
            start_cfs_bandwidth(cfs_b);
    }

步骤 4: 更新 bandwidth 参数: 在 cfs_b->lock 保护下设置新的 period、quota 和 burst。__refill_cfs_bandwidth_runtime() 重置 runtime (设置为 quota + burst 的较小值)。如果启用限制,启动周期定时器。

    for_each_online_cpu(i) {
        struct cfs_rq *cfs_rq = tg->cfs_rq[i];
        struct rq *rq = cfs_rq->rq;

        guard(rq_lock_irq)(rq);
        cfs_rq->runtime_enabled = runtime_enabled;
        cfs_rq->runtime_remaining = 1;

        if (cfs_rq->throttled)
            unthrottle_cfs_rq(cfs_rq);
    }

    if (runtime_was_enabled && !runtime_enabled)
        cfs_bandwidth_usage_dec();

    return 0;
}

步骤 5: 更新每 CPU 状态: 遍历所有在线 CPU,在每个 CPU 的 rq 锁保护下更新 cfs_rq 的 runtime_enabled 和 runtime_remaining。如果 cfs_rq 当前处于 throttled 状态且限制被取消,立即 unthrottle。

v2 接口:cpu.max

在 v2 中,cpu.max 文件提供了统一的带宽控制接口 (kernel/sched/core.c:10144)。读取格式为 "quota period":

static int cpu_max_show(struct seq_file *sf, void *v)
{
    struct task_group *tg = css_tg(seq_css(sf));
    u64 period_us, quota_us;

    tg_bandwidth(tg, &period_us, &quota_us, NULL);
    ...
}

写入格式同样为 "quota period" (如 "max 100000" 或 "50000 100000")。解析逻辑由 cpu_period_quota_parse() (kernel/sched/core.c:10125) 处理,支持字符串 "max" 表示 RUNTIME_INF。

v2 通过 tg_set_bandwidth() (kernel/sched/core.c:9752-9800) 统一了 CFS 和 sched_ext (scx) 的带宽设置:

static int tg_set_bandwidth(struct task_group *tg,
                            u64 period_us, u64 quota_us, u64 burst_us)
{
    ...
#ifdef CONFIG_CFS_BANDWIDTH
    ret = tg_set_cfs_bandwidth(tg, period_us, quota_us, burst_us);
#endif
    if (!ret)
        scx_group_set_bandwidth(tg, period_us, quota_us, burst_us);
    return ret;
}

quota 分配:throttle/unthrottle 机制

当 cfs_rq 用尽其分配的 runtime 时,会被 throttle。throttle 操作在 __throttle_cfs_rq() 中执行: 1. 从调度器运行队列中移除 cfs_rq 的 sched_entity 2. 设置 cfs_rq->throttled = 1 3. 将 cfs_rq 加入 cfs_b->throttled_cfs_rq 链表 4. 更新 throttled 统计

在每个周期开始时 (period_timer 触发),sched_cfs_period_timer() 回调执行: 1. __refill_cfs_bandwidth_runtime() 重置全局 runtime 2. distribute_cfs_runtime() 将 runtime 分配给被 throttled 的 cfs_rq 3. 分配到足够 runtime 的 cfs_rq 被 unthrottle

unthrottle 在 unthrottle_cfs_rq() 中执行: 1. 从 cfs_b->throttled_cfs_rq 链表移除 2. 将 cfs_rq 的 sched_entity 重新加入调度器运行队列 3. 触发重新调度

此外,slack 机制 (kernel/sched/fair.c:6308) 在 cfs_rq 有未使用的 runtime 时通过高精度定时器将部分时间归还给全局池,提高利用率。


12.3.3 CPU 权重 (cpu.weight)

CPU 权重决定了在同一层级中不同 cgroup 之间的 CPU 时间比例分配。

权重常量

cgroup v2 使用统一的权重范围,定义在 include/linux/cgroup.h:39-41:

#define CGROUP_WEIGHT_MIN       1
#define CGROUP_WEIGHT_DFL       100
#define CGROUP_WEIGHT_MAX       10000

默认权重 100 是最小值 1 和最大值 10000 的对数中心,允许在两个方向上表达 100 倍的差异。这种设计确保了权重的对称性:权重 10000 的 cgroup 获得的 CPU 时间是权重 100 的 100 倍,权重 1 的 cgroup 获得的 CPU 时间是权重 100 的 1/100。

v2 接口实现

cpu.weight 文件的读写实现 (kernel/sched/core.c:10051-10072):

static u64 cpu_weight_read_u64(struct cgroup_subsys_state *css,
                               struct cftype *cft)
{
    return sched_weight_to_cgroup(tg_weight(css_tg(css)));
}

static int cpu_weight_write_u64(struct cgroup_subsys_state *css,
                                struct cftype *cft, u64 cgrp_weight)
{
    unsigned long weight;
    int ret;

    if (cgrp_weight < CGROUP_WEIGHT_MIN || cgrp_weight > CGROUP_WEIGHT_MAX)
        return -ERANGE;

    weight = sched_weight_from_cgroup(cgrp_weight);

    ret = sched_group_set_shares(css_tg(css), scale_load(weight));
    if (!ret)
        scx_group_set_weight(css_tg(css), cgrp_weight);
    return ret;
}

写入流程: 1. 验证权重在 [CGROUP_WEIGHT_MIN, CGROUP_WEIGHT_MAX] 范围内 2. 通过 sched_weight_from_cgroup() 将 cgroup 权重转换为调度器内部权重 3. sched_group_set_shares() 更新 task_group 的 shares 值并传播到调度器 4. 同时更新 sched_ext 的权重

cgroup 权重到调度器权重的转换

cgroup 使用 1-10000 的权重范围,而调度器内部使用 0-1024 (NICE_0_LOAD) 的范围。转换函数 sched_weight_from_cgroup() 和 sched_weight_to_cgroup() 实现了双向映射。

ROOT_TASK_GROUP_LOAD (kernel/sched/sched.h:530) 定义了根任务组的默认负载:

#define ROOT_TASK_GROUP_LOAD    NICE_0_LOAD

当 cgroup 权重为 100 (默认) 时,对应的调度器权重为 NICE_0_LOAD (1024)。

sched_group_set_shares 的实现

sched_group_set_shares() 是权重设置的核心函数。它执行以下操作:

  1. 更新 tg->shares 字段
  2. 对于每个 CPU,更新 task_group 的 sched_entity 权重 (reweight_entity())
  3. 触发负载跟踪更新,确保调度器在下次调度决策时使用新权重

由于权重影响调度决策但不涉及硬限制,权重变更可以即时生效而无需复杂的状态转换。

cpu.weight.nice 接口

除了直接写入权重值,v2 还提供了 cpu.weight.nice 接口 (kernel/sched/core.c:10074-10110),使用 nice 值 (-20 到 19) 替代权重值:

static s64 cpu_weight_nice_read_s64(struct cgroup_subsys_state *css,
                                    struct cftype *cft)
{
    unsigned long weight = tg_weight(css_tg(css));
    int last_delta = INT_MAX;
    int prio, delta;

    for (prio = 0; prio < ARRAY_SIZE(sched_prio_to_weight); prio++) {
        delta = abs(sched_prio_to_weight[prio] - weight);
        if (delta >= last_delta)
            break;
        last_delta = delta;
    }

    return PRIO_TO_NICE(prio - 1 + MAX_RT_PRIO);
}

读取时,找到最接近当前权重的 nice 值对应的调度优先级权重表条目。写入时直接使用 nice 值对应的 sched_prio_to_weight[] 表项。

cpu.idle 接口

cpu.idle 接口 (kernel/sched/core.c:9880-9897) 允许将 cgroup 标记为 SCHED_IDLE:

static s64 cpu_idle_read_s64(struct cgroup_subsys_state *css,
                               struct cftype *cft)
{
    return css_tg(css)->idle;
}

static int cpu_idle_write_s64(struct cgroup_subsys_state *css,
                                struct cftype *cft, s64 idle)
{
    int ret;

    ret = sched_group_set_idle(css_tg(css), idle);
    if (!ret)
        scx_group_set_idle(css_tg(css), idle);
    return ret;
}

设置为 1 的 cgroup 获得的调度优先级极低,只在系统几乎空闲时才会被执行。这对后台批处理任务非常有用。


12.3.4 cpu.stat 统计

cgroup 核心的 cpu.stat

cgroup 核心提供了 cpu.stat 文件 (kernel/cgroup/cgroup.c:5533),通过 rstat 框架显示 CPU 使用统计。cgroup_base_stat_cputime_show() (kernel/cgroup/rstat.c:713-744) 负责输出:

void cgroup_base_stat_cputime_show(struct seq_file *seq)
{
    struct cgroup *cgrp = seq_css(seq)->cgroup;
    struct cgroup_base_stat bstat;

    if (cgroup_parent(cgrp)) {
        css_rstat_flush(&cgrp->self);
        __css_rstat_lock(&cgrp->self, -1);
        bstat = cgrp->bstat;
        cputime_adjust(&cgrp->bstat.cputime, &cgrp->prev_cputime,
                       &bstat.cputime.utime, &bstat.cputime.stime);
        __css_rstat_unlock(&cgrp->self, -1);
    } else {
        root_cgroup_cputime(&bstat);
    }

    do_div(bstat.cputime.sum_exec_runtime, NSEC_PER_USEC);
    do_div(bstat.cputime.utime, NSEC_PER_USEC);
    do_div(bstat.cputime.stime, NSEC_PER_USEC);
    do_div(bstat.ntime, NSEC_PER_USEC);

    seq_printf(seq, "usage_usec %llu\n"
                    "user_usec %llu\n"
                    "system_usec %llu\n"
                    "nice_usec %llu\n",
                    bstat.cputime.sum_exec_runtime,
                    bstat.cputime.utime,
                    bstat.cputime.stime,
                    bstat.ntime);

    cgroup_force_idle_show(seq, &bstat);
}

输出的字段包括: - usage_usec: 总 CPU 使用时间 (用户 + 系统) - user_usec: 用户态 CPU 时间 - system_usec: 内核态 CPU 时间 - nice_usec: 以非零 nice 值运行的用户态时间 - core_sched.force_idle_usec: 核心调度强制空闲时间 (需要 CONFIG_SCHED_CORE)

对于非根 cgroup,首先调用 css_rstat_flush() 刷新 per-CPU 统计,然后在 rstat 锁保护下读取 bstat。cputime_adjust() 处理 utime/stime 的精度调整。对于根 cgroup,直接从全局 CPU 统计 (kcpustat_cpu) 读取数据。

cgroup_account_cputime -- 调度器 tick 时的记账

CPU 时间的记账通过 cgroup_account_cputime() (include/linux/cgroup.h:737-747) 在调度器 tick 时触发:

static inline void cgroup_account_cputime(struct task_struct *task,
                                          u64 delta_exec)
{
    struct cgroup *cgrp;

    cpuacct_charge(task, delta_exec);

    cgrp = task_dfl_cgroup(task);
    if (cgroup_parent(cgrp))
        __cgroup_account_cputime(cgrp, delta_exec);
}

此函数执行两个操作: 1. cpuacct_charge(): 如果启用了 cpuacct,进行额外的 CPU 使用记账 2. __cgroup_account_cputime(): 更新 cgroup 的基础统计

__cgroup_account_cputime() (kernel/cgroup/rstat.c:621-629) 更新 per-CPU 统计:

void __cgroup_account_cputime(struct cgroup *cgrp, u64 delta_exec)
{
    struct cgroup_rstat_base_cpu *rstatbc;
    unsigned long flags;

    rstatbc = cgroup_base_stat_cputime_account_begin(cgrp, &flags);
    rstatbc->bstat.cputime.sum_exec_runtime += delta_exec;
    cgroup_base_stat_cputime_account_end(cgrp, rstatbc, flags);
}

更新流程: 1. cgroup_base_stat_cputime_account_begin(): 获取当前 CPU 的 per-CPU 数据,禁用本地中断,开始 seqcount 写入 2. 累加 delta_exec 到 bstat.cputime.sum_exec_runtime 3. cgroup_base_stat_cputime_account_end(): 结束 seqcount 写入,调用 css_rstat_updated() 将 cgroup 标记为需要刷新,启用本地中断

css_rstat_updated() 将 cgroup 的 self css 添加到 per-CPU 的更新列表中,确保下次读取 cpu.stat 时会触发 flush 将 per-CPU 数据聚合到全局。

__cgroup_account_cputime_field -- 按 CPU usage stat 类型记账

__cgroup_account_cputime_field() (kernel/cgroup/rstat.c:631-661) 根据不同的 CPU 使用类型更新不同的统计字段:

void __cgroup_account_cputime_field(struct cgroup *cgrp,
                                    enum cpu_usage_stat index, u64 delta_exec)
{
    struct cgroup_rstat_base_cpu *rstatbc;
    unsigned long flags;

    rstatbc = cgroup_base_stat_cputime_account_begin(cgrp, &flags);

    switch (index) {
    case CPUTIME_NICE:
        rstatbc->bstat.ntime += delta_exec;
        fallthrough;
    case CPUTIME_USER:
        rstatbc->bstat.cputime.utime += delta_exec;
        break;
    case CPUTIME_SYSTEM:
    case CPUTIME_IRQ:
    case CPUTIME_SOFTIRQ:
        rstatbc->bstat.cputime.stime += delta_exec;
        break;
#ifdef CONFIG_SCHED_CORE
    case CPUTIME_FORCEIDLE:
        rstatbc->bstat.forceidle_sum += delta_exec;
        break;
#endif
    default:
        break;
    }

    cgroup_base_stat_cputime_account_end(cgrp, rstatbc, flags);
}

注意 CPUTIME_NICE 使用 fallthrough 同时更新 ntime 和 utime,因为 nice 时间是用户时间的一个子集。

rstat 递归聚合

读取 cpu.stat 时,css_rstat_flush() 触发 rstat 框架的递归聚合。聚合过程 (cgroup_base_stat_flush(), kernel/cgroup/rstat.c:563-600):

  1. 读取 cgroup 的 per-CPU 统计 (使用 seqcount 保护)
  2. 计算与上次快照的增量
  3. 将增量加到 cgroup 的全局 bstat
  4. 将增量传播到父 cgroup 的 bstat (如果父不是根)

这种延迟聚合的设计使得更新操作 (在调度器 tick 中) 只需写 per-CPU 变量加一次无锁链表插入 (css_rstat_updated()),而聚合操作推迟到读取时执行。这避免了每个 tick 都要在 cgroup 层次树中传播统计,显著降低了开销。

CPU 控制器的额外统计

cpu_extra_stat_show() (kernel/sched/core.c:10005-10029) 提供了 CFS bandwidth 的额外统计:

static int cpu_extra_stat_show(struct seq_file *sf,
                               struct cgroup_subsys_state *css)
{
#ifdef CONFIG_CFS_BANDWIDTH
    {
        struct task_group *tg = css_tg(css);
        struct cfs_bandwidth *cfs_b = &tg->cfs_bandwidth;
        u64 throttled_usec, burst_usec;

        throttled_usec = cfs_b->throttled_time;
        do_div(throttled_usec, NSEC_PER_USEC);
        burst_usec = cfs_b->burst_time;
        do_div(burst_usec, NSEC_PER_USEC);

        seq_printf(sf, "nr_periods %d\n"
                   "nr_throttled %d\n"
                   "throttled_usec %llu\n"
                   "nr_bursts %d\n"
                   "burst_usec %llu\n",
                   cfs_b->nr_periods, cfs_b->nr_throttled,
                   throttled_usec, cfs_b->nr_burst, burst_usec);
    }
#endif
    return 0;
}

输出字段: - nr_periods: 经历的带宽周期总数 - nr_throttled: 发生限流的周期数 - throttled_usec: 累计被限流的时间 (微秒) - nr_bursts: 发生突发使用的次数 - burst_usec: 累计突发使用的时间 (微秒)

这些统计通过 css_extra_stat_show 回调集成到 cpu.stat 的输出中,位于基础统计之后。

cpu_local_stat_show() (kernel/sched/core.c:10031-10047) 提供 cgroup 自身 (不含子树) 的 throttled 统计:

static int cpu_local_stat_show(struct seq_file *sf,
                               struct cgroup_subsys_state *css)
{
#ifdef CONFIG_CFS_BANDWIDTH
    {
        struct task_group *tg = css_tg(css);
        u64 throttled_self_usec;

        throttled_self_usec = throttled_time_self(tg);
        do_div(throttled_self_usec, NSEC_PER_USEC);

        seq_printf(sf, "throttled_usec %llu\n",
                   throttled_self_usec);
    }
#endif
    return 0;
}

throttled_time_self() (kernel/sched/core.c:9697-9705) 通过累加所有 CPU 上 cfs_rq 的 throttled_clock_self_time 计算自身的限流时间:

static u64 throttled_time_self(struct task_group *tg)
{
    int i;
    u64 total = 0;

    for_each_possible_cpu(i)
        total += READ_ONCE(tg->cfs_rq[i]->throttled_clock_self_time);

    return total;
}

12.3.5 cpuacct 子系统

v1 独立存在,v2 合并到 cpu

在 v1 中,cpuacct 是独立于 cpu 的控制器,提供 CPU 使用统计功能。在 v2 中,cpuacct 的功能合并到 cpu 控制器中,通过 cgroup 核心的 cpu.stat 文件提供统计。

cpuacct 的两个核心记账函数仍然存在于 v2 中 (include/linux/cgroup.h:724-731),但直接被 cgroup 基础统计框架调用。

cpuacct_charge()

cpuacct_charge() 在每次调度器 tick 更新任务 CPU 时间时被调用。它将 delta_exec 累加到 cgroup 的 cputime.sum_exec_runtime 字段。在 v2 中,此函数直接调用 __cgroup_account_cputime(),无需独立的 cpuacct css。

cpuacct_account_field()

cpuacct_account_field() 根据 CPU usage stat 的类型 (CPUTIME_USER、CPUTIME_SYSTEM 等) 分别记账。在 v2 中,此函数调用 __cgroup_account_cputime_field(),将不同类型的 CPU 时间分别累加到 cgroup 的 bstat 中。

统一的记账入口

在 v2 中,所有 CPU 时间的记账通过两个入口函数完成:

static inline void cgroup_account_cputime(struct task_struct *task,
                                          u64 delta_exec)
{
    struct cgroup *cgrp;

    cpuacct_charge(task, delta_exec);

    cgrp = task_dfl_cgroup(task);
    if (cgroup_parent(cgrp))
        __cgroup_account_cputime(cgrp, delta_exec);
}

static inline void cgroup_account_cputime_field(struct task_struct *task,
                                                enum cpu_usage_stat index,
                                                u64 delta_exec)
{
    struct cgroup *cgrp;

    cpuacct_account_field(task, index, delta_exec);

    cgrp = task_dfl_cgroup(task);
    if (cgroup_parent(cgrp))
        __cgroup_account_cputime_field(cgrp, index, delta_exec);
}

两个函数都首先调用 cpuacct 函数 (兼容 v1),然后更新 v2 的 cgroup 基础统计。在 v2-only 环境中,cpuacct 函数为空操作,实际工作由 __cgroup_account_cputime* 完成。


12.3.6 与调度器的集成

CPU 控制器与 CFS 调度器的集成是通过 task_group 层次与 cgroup 层次的一一对应实现的。每个 cgroup 在启用 cpu 控制器后,都会创建一个对应的 task_group。

css_alloc: sched_create_group

当在 cgroup 中启用 cpu 控制器时,css_create() 调用 cpu_cgroup_css_alloc() (kernel/sched/core.c:9173-9189):

static struct cgroup_subsys_state *
cpu_cgroup_css_alloc(struct cgroup_subsys_state *parent_css)
{
    struct task_group *parent = css_tg(parent_css);
    struct task_group *tg;

    if (!parent) {
        /* This is early initialization for the top cgroup */
        return &root_task_group.css;
    }

    tg = sched_create_group(parent);
    if (IS_ERR(tg))
        return ERR_PTR(-ENOMEM);

    return &tg->css;
}

对于根 cgroup (parent_css 为 NULL),返回预分配的 root_task_group.css。对于子 cgroup,调用 sched_create_group() (kernel/sched/core.c:9039-9061):

struct task_group *sched_create_group(struct task_group *parent)
{
    struct task_group *tg;

    tg = kmem_cache_alloc(task_group_cache, GFP_KERNEL | __GFP_ZERO);
    if (!tg)
        return ERR_PTR(-ENOMEM);

    if (!alloc_fair_sched_group(tg, parent))
        goto err;

    if (!alloc_rt_sched_group(tg, parent))
        goto err;

    scx_tg_init(tg);
    alloc_uclamp_sched_group(tg, parent);

    return tg;
err:
    sched_free_group(tg);
    return ERR_PTR(-ENOMEM);
}

sched_create_group() 分配 task_group 并初始化其 CFS 和 RT 调度组件: - alloc_fair_sched_group(): 为每个 CPU 分配 cfs_rq 和 sched_entity,初始化 shares 和 load_avg - alloc_rt_sched_group(): 为每个 CPU 分配 rt_rq 和 sched_rt_entity - scx_tg_init(): 初始化 sched_ext 组件 - alloc_uclamp_sched_group(): 初始化 util clamp 参数

css_online: sched_online_group

cpu_cgroup_css_online() (kernel/sched/core.c:9192-9213) 在 css 上线时调用 sched_online_group():

static int cpu_cgroup_css_online(struct cgroup_subsys_state *css)
{
    struct task_group *tg = css_tg(css);
    struct task_group *parent = css_tg(css->parent);
    int ret;

    ret = scx_tg_online(tg);
    if (ret)
        return ret;

    if (parent)
        sched_online_group(tg, parent);
    ...
    return 0;
}

sched_online_group() (kernel/sched/core.c:9063-9079) 将 task_group 注册到调度器:

void sched_online_group(struct task_group *tg, struct task_group *parent)
{
    unsigned long flags;

    spin_lock_irqsave(&task_group_lock, flags);
    list_add_tail_rcu(&tg->list, &task_groups);

    WARN_ON(!parent);

    tg->parent = parent;
    INIT_LIST_HEAD(&tg->children);
    list_add_rcu(&tg->siblings, &parent->children);
    spin_unlock_irqrestore(&task_group_lock, flags);

    online_fair_sched_group(tg);
}
  1. 将 task_group 加入全局 task_groups 链表
  2. 设置 parent 指针并加入 parent 的 children 链表
  3. 调用 online_fair_sched_group() 完成 CFS 层面的上线

cgroup 层次与 task_group 层次的对应

cgroup 层次和 task_group 层次是两棵完全同构的树: - cgroup 的 self.sibling/children 对应 css 的层次 - task_group 的 siblings/children 对应调度器的层次 - 两棵树通过 task_group 内嵌的 css (tg->css) 相关联

当 cgroup 被创建并启用 cpu 控制器时,对应的 task_group 也被创建。当任务迁移到新 cgroup 时,调度器通过 sched_move_task() 将任务移到新 task_group 的 cfs_rq。

cfs_rq->tg 指针

每个 cfs_rq 都有一个 tg 指针指向其所属的 task_group。这使得调度器在处理 cfs_rq 时能快速获取 task_group 的配置 (如 bandwidth、shares)。

任务迁移时的调度器更新

当任务从一个 cgroup 迁移到另一个时,cpu_cgroup_attach() 回调调用 sched_move_task() (kernel/sched/core.c:9146-9171):

void sched_move_task(struct task_struct *tsk, bool for_autogroup)
{
    unsigned int queue_flags = DEQUEUE_SAVE | DEQUEUE_MOVE;
    bool resched = false;
    bool queued = false;
    struct rq *rq;

    CLASS(task_rq_lock, rq_guard)(tsk);
    rq = rq_guard.rq;

    scoped_guard (sched_change, tsk, queue_flags) {
        sched_change_group(tsk);
        if (!for_autogroup)
            scx_cgroup_move_task(tsk);
        if (scope->running)
            resched = true;
        queued = scope->queued;
    }

    if (resched)
        resched_curr(rq);
    else if (queued)
        wakeup_preempt(rq, tsk, 0);

    __balance_callbacks(rq, &rq_guard.rf);
}

sched_change_group() (kernel/sched/core.c:9117-9137) 更新任务的调度组:

static void sched_change_group(struct task_struct *tsk)
{
    struct task_group *tg;

    tg = container_of(task_css_check(tsk, cpu_cgrp_id, true),
                      struct task_group, css);
    tg = autogroup_task_group(tsk, tg);
    tsk->sched_task_group = tg;
    ...
}

从任务的 cpu css 获取新的 task_group,更新 tsk->sched_task_group,然后通过 set_task_rq() 更新任务的 se.cfs_rq 和 se.parent 指针到新 cgroup 的 cfs_rq。

CPU 带宽限流对 pick_next_task_fair 的影响

当 cfs_rq 被 throttle 时,其 sched_entity 从父 cfs_rq 的运行队列中被移除。这意味着 pick_next_task_fair() 在遍历调度实体时不会选择被 throttle 的 cfs_rq 中的任务。

具体来说,在 pick_next_task_fair() 的遍历中: 1. 从根 cfs_rq 开始,选择 vruntime 最小的 sched_entity 2. 如果选中的 entity 是一个 task_group 的 se,向下进入该 task_group 的 cfs_rq 3. 被 throttle 的 cfs_rq 的 se 不在父 cfs_rq 的运行队列中,因此不会被选中

这种设计确保了带宽限制的严格性:一旦 runtime 耗尽,整个 cgroup 中的所有任务都不会被调度,直到下一个周期到来或通过 burst 机制获得额外的 runtime。

cpu.max 和层次化配额

在 v2 中,cpu.max 的配额是层次化的。hierarchical_quota 字段跟踪考虑了所有祖先限制后的有效配额。当在 cpu_cgroup_css_alloc() 中设置子 task_group 的配额时 (kernel/sched/core.c:9627-9639):

if (cgroup_subsys_on_dfl(cpu_cgrp_subsys)) {
    if (quota == RUNTIME_INF)
        quota = parent_quota;
    else if (parent_quota != RUNTIME_INF)
        quota = min(quota, parent_quota);
}

子 cgroup 的配额不能超过父 cgroup 的配额。如果子 cgroup 请求的配额大于父配额,实际生效的是父配额。这保证了层次结构中资源限制的单调性。

can_attach 回调

CPU 控制器的 can_attach 回调 cpu_cgroup_can_attach() 检查迁移的有效性。对于 RT 调度类,它验证任务的实时优先级不会违反目标 cgroup 的限制。如果验证失败,返回错误码阻止迁移。cancel_attach 回调在迁移被取消时执行清理操作。

12.4 内存控制器 -- memory cgroup

内存控制器是 cgroups v2 中最复杂、最重要的资源控制器之一。它负责追踪和控制 cgroup 中进程的内存使用量,包括页面缓存 (page cache)、匿名内存 (anonymous memory)、内核内存 (kernel memory) 以及交换空间 (swap)。本节将深入分析 Linux 7.0.10 内核中内存控制器的核心数据结构、计数机制、限制执行策略以及统计框架。

12.4.1 mem_cgroup 结构体详解

内存控制器的核心数据结构定义在 include/linux/memcontrol.h:190-325。struct mem_cgroup 是每个 cgroup 中内存子系统的实例对象,通过嵌入 cgroup_subsys_state 与 cgroup 核心框架关联。

12.4.1.1 结构体完整分析

// include/linux/memcontrol.h:190-325
struct mem_cgroup {
    struct cgroup_subsys_state css;                    // 行191

    /* Private memcg ID. Used to ID objects that outlive the cgroup */
    struct mem_cgroup_private_id id;                   // 行194

    /* Accounted resources */
    struct page_counter memory;                        // 行197

    union {
        struct page_counter swap;                      // 行200, v2 only
        struct page_counter memsw;                     // 行201, v1 only
    };

    /* registered local peak watchers */
    struct list_head memory_peaks;                     // 行205
    struct list_head swap_peaks;                       // 行206
    spinlock_t          peaks_lock;                    // 行207

    /* Range enforcement for interrupt charges */
    struct work_struct high_work;                      // 行210
    ...
};

css 嵌入 (行191):css 字段是 cgroup 子系统标准模式的核心。每个 cgroup 子系统都通过在自身的控制器结构体中嵌入 struct cgroup_subsys_state 来与 cgroup 核心框架建立关联。通过 container_of 宏,内核可以从 css 指针反向获取到 mem_cgroup 结构体。在 mm/memcontrol.c:80 中,内核定义了全局的 memory_cgrp_subsys:

// mm/memcontrol.c:80
struct cgroup_subsys memory_cgrp_subsys __read_mostly;

id 字段 (行194):mem_cgroup_private_id 结构(定义于 include/linux/memcontrol.h:68-71)包含一个整型 id 和一个引用计数 ref。该 ID 用于标识可能比 cgroup 本身存活更久的对象(例如已经迁移到其他 cgroup 但仍被旧 cgroup 的缓存引用的页面)。

page_counter memory (行197):主内存计数器,追踪该 cgroup 的总内存使用量。该字段在 cgroup v1 和 v2 中均存在,是整个内存控制机制的核心。关于 page_counter 的详细分析见 12.4.2 节。

swap/memsw 联合体 (行199-202):这是一个关键的设计细节。在 cgroup v2 中,swap 字段作为独立的交换空间计数器使用;而在 cgroup v1 中,同一个内存位置被用作 memsw(memory+swap 合并计数器)。这种联合体设计使得两套接口可以共存于同一内核中,通过 CONFIG_MEMCG_V1 编译选项和运行时检查区分。

memory_peaks/swap_peaks (行205-206):用于 memory.peak 和 memory.swap.peak 接口,记录内存和交换空间的历史峰值。peaks_lock 自旋锁保护峰值观察者列表的并发访问。

high_work (行210):struct work_struct 类型,用于 memory.high 软限制的异步回收。当内存使用超过 memory.high 阈值时,内核不会立即触发 OOM killer,而是将回收工作排队到工作队列中异步执行。这一机制避免在 charge 路径(可能在中断上下文中)进行复杂的回收操作。

12.4.1.2 配置与状态字段

继续分析 mem_cgroup 结构体的配置和状态相关字段:

// include/linux/memcontrol.h:212-230
#ifdef CONFIG_ZSWAP
    unsigned long zswap_max;                          // 行213
    bool zswap_writeback;                             // 行219
#endif

    struct vmpressure vmpressure;                     // 行223

    bool oom_group;                                   // 行228
    int swappiness;                                   // 行230

zswap_max (行213):当启用 CONFIG_ZSWAP 时,该字段限制 cgroup 可以使用的压缩交换空间 (zswap) 的最大值。zswap_writeback (行219) 控制是否允许将 zswap 中的页面写回到普通 swap 设备。

vmpressure (行223):struct vmpressure 用于向用户空间报告内存压力事件。当 cgroup 内的内存回收压力增大时,通过 eventfd 机制通知用户空间的监听者。

oom_group (行228):当设为 true 时,如果 OOM killer 被触发,内核会杀死该 cgroup 中的所有进程,而不仅仅是触发 OOM 的单个进程。这是通过 memory.oom.group 接口配置的。

swappiness (行230):控制该 cgroup 的 swap 倾向性。值越高,内核越倾向于回收匿名页面(将其交换到磁盘);值越低,越倾向于回收文件页面。

12.4.1.3 事件与统计字段

// include/linux/memcontrol.h:232-270
    struct cgroup_file events_file;                   // 行233
    struct cgroup_file events_local_file;             // 行234
    struct cgroup_file swap_events_file;              // 行237

    struct memcg_vmstats     *vmstats;                // 行240

    atomic_long_t     memory_events[MEMCG_NR_MEMORY_EVENTS];  // 行243
    atomic_long_t     memory_events_local[MEMCG_NR_MEMORY_EVENTS]; // 行244

events_file/events_local_file (行233-234):这两个 cgroup_file 分别对应 memory.events 和 memory.events.local 文件。前者记录该 cgroup 及其所有子 cgroup 的累计事件计数;后者仅记录该 cgroup 自身的事件。

memory_events[] (行243):事件类型定义在 include/linux/memcontrol.h:45-57 的 enum memcg_memory_event 中:

// include/linux/memcontrol.h:45-57
enum memcg_memory_event {
    MEMCG_LOW,           // 触发 low 保护回收
    MEMCG_HIGH,          // 触发 high 回收
    MEMCG_MAX,           // 达到 max 限制
    MEMCG_OOM,           // OOM 触发
    MEMCG_OOM_KILL,      // OOM 杀进程
    MEMCG_OOM_GROUP_KILL, // OOM 杀整组
    MEMCG_SWAP_HIGH,     // swap high 触发
    MEMCG_SWAP_MAX,      // swap max 触发
    MEMCG_SWAP_FAIL,     // swap 分配失败
    MEMCG_SOCK_THROTTLED, // socket 被限流
    MEMCG_NR_MEMORY_EVENTS,
};

每个事件类型在数组中对应一个 atomic_long_t 计数器,保证在无锁条件下的原子更新。

vmstats (行240):指向 struct memcg_vmstats 的指针,该结构体管理 per-CPU 统计数据的聚合。在 mm/memcontrol.c:270 中,对应的 per-CPU 版本为 vmstats_percpu:

// include/linux/memcontrol.h:270
struct memcg_vmstats_percpu __percpu *vmstats_percpu;

12.4.1.4 对象级记账与节点信息

// include/linux/memcontrol.h:264-268
    struct obj_cgroup __rcu   *objcg;                // 行265
    struct obj_cgroup         *orig_objcg;           // 行266
    struct list_head objcg_list;                     // 行268

objcg (行265):当前活跃的 obj_cgroup 指针,用于子对象级记账。关于对象级记账的详细分析见 12.4.7 节。orig_objcg (行266) 在 cgroup 被销毁时保留对原始 objcg 的引用。objcg_list (行268) 维护了继承的 objcg 链表。

nodeinfo[] (行324):柔性数组成员,每个 NUMA 节点对应一个 struct mem_cgroup_per_node 实例:

// include/linux/memcontrol.h:88-123
struct mem_cgroup_per_node {
    struct mem_cgroup     *memcg;                    // 行90, 反向指针

    struct lruvec_stats_percpu __percpu *lruvec_stats_percpu; // 行93
    struct lruvec_stats              *lruvec_stats;  // 行94
    struct shrinker_info __rcu *shrinker_info;       // 行95

    struct lruvec        lruvec;                     // 行113
    unsigned long        lru_zone_size[MAX_NR_ZONES][NR_LRU_LISTS]; // 行115
    struct mem_cgroup_reclaim_iter iter;             // 行116
};

mem_cgroup_per_node 的 lruvec (行113) 是该 cgroup 在该 NUMA 节点上的 LRU 链表向量,管理匿名页面和文件页面的活跃/不活跃链表。lru_zone_size (行115) 追踪每个 zone 和每种 LRU 类型的页面数量。iter (行116) 用于内存回收时的迭代游标。

12.4.1.5 根内存 cgroup

在 mm/memcontrol.c:83 中定义了全局的根内存 cgroup:

// mm/memcontrol.c:83
struct mem_cgroup *root_mem_cgroup __read_mostly;

所有不属于任何非根 cgroup 的内存使用都归属于 root_mem_cgroup。根 cgroup 没有内存限制,其 page_counter 的 max 字段初始化为 PAGE_COUNTER_MAX。

12.4.2 page_counter 层次化计数

page_counter 是内存控制器的基础设施,实现了层次化的页面计数和限制机制。其定义位于 include/linux/page_counter.h:10-44。

12.4.2.1 page_counter 结构体分析

// include/linux/page_counter.h:10-44
struct page_counter {
    atomic_long_t usage;                              // 行15
    unsigned long failcnt;                            // 行16, v1-only

    CACHELINE_PADDING(_pad1_);                        // 行18

    /* effective memory.min and memory.min usage tracking */
    unsigned long emin;                               // 行21
    atomic_long_t min_usage;                          // 行22
    atomic_long_t children_min_usage;                 // 行23

    /* effective memory.low and memory.low usage tracking */
    unsigned long elow;                               // 行26
    atomic_long_t low_usage;                          // 行27
    atomic_long_t children_low_usage;                 // 行28

    unsigned long watermark;                          // 行30
    unsigned long local_watermark;                    // 行32

    CACHELINE_PADDING(_pad2_);                        // 行35

    bool protection_support;                          // 行37
    bool track_failcnt;                               // 行38
    unsigned long min;                                // 行39
    unsigned long low;                                // 行40
    unsigned long high;                               // 行41
    unsigned long max;                                // 行42
    struct page_counter *parent;                      // 行43
};

usage (行15):核心的原子计数器,追踪当前的页面使用量。atomic_long_t 类型确保了无锁的原子读写操作。该字段被单独放在一个缓存行上(通过行18的 CACHELINE_PADDING 确认),因为它是整个结构体中被最频繁访问的字段 -- 每次 charge/uncharge 操作都会更新它。注释明确说明了这一点:

// include/linux/page_counter.h:13-14
 * Make sure 'usage' does not share cacheline with any other field in
 * v2. The memcg->memory.usage is a hot member of struct mem_cgroup.

failcnt (行16):仅在 cgroup v1 中使用,记录 charge 失败的次数。在 v2 中通过 track_failcnt 标志 (行38) 禁用。

emin/elow (行21, 26):有效的 min/low 保护值。这些值不是用户直接配置的,而是由 page_counter_calculate_protection() 函数递归计算得出的。详见 12.4.2.4 节。

min_usage/children_min_usage (行22-23):min_usage 追踪当前 cgroup 的实际被保护使用量(min(usage, min)),children_min_usage 是所有子 cgroup 的 min_usage 之和。这些值用于保护值的递归计算。

watermark/local_watermark (行30, 32):追踪使用量的历史峰值。watermark 是全局峰值,local_watermark 是可以通过 memory.peak 接口独立重置的局部峰值。代码保证 watermark >= local_watermark。

min/low/high/max (行39-42):四级限制值,分别对应 memory.min、memory.low、memory.high 和 memory.max 接口。

parent (行43):指向父 cgroup 的 page_counter,构成层次化的计数链。charge/uncharge 操作会沿着 parent 链向上传播。

protection_support (行37):布尔标志,指示该计数器是否支持保护机制(min/low)。只有主内存计数器 (memory) 支持保护机制,swap 计数器不支持。该标志在 page_counter_init() 中设置:

// include/linux/page_counter.h:55-64
static inline void page_counter_init(struct page_counter *counter,
                     struct page_counter *parent,
                     bool protection_support)
{
    counter->usage = (atomic_long_t)ATOMIC_LONG_INIT(0);
    counter->max = PAGE_COUNTER_MAX;
    counter->parent = parent;
    counter->protection_support = protection_support;
    counter->track_failcnt = false;
}

12.4.2.2 page_counter_charge -- 无条件充电

page_counter_charge() 定义在 mm/page_counter.c:76-107,执行无条件的层次化页面充电。它不考虑任何限制,直接沿 parent 链向上增加 usage:

// mm/page_counter.c:76-107
void page_counter_charge(struct page_counter *counter, unsigned long nr_pages)
{
    struct page_counter *c;
    bool protection = track_protection(counter);

    for (c = counter; c; c = c->parent) {
        long new;
        new = atomic_long_add_return(nr_pages, &c->usage);
        if (protection)
            propagate_protected_usage(c, new);
        if (new > READ_ONCE(c->local_watermark)) {
            WRITE_ONCE(c->local_watermark, new);
            if (new > READ_ONCE(c->watermark))
                WRITE_ONCE(c->watermark, new);
        }
    }
}

关键设计点:

  1. 使用 atomic_long_add_return() 进行原子增加,同时获取新值。该操作隐含完整的内存屏障。
  2. 如果支持保护机制,调用 propagate_protected_usage() 更新 min_usage 和 low_usage。
  3. 更新 watermark 时有轻微的竞争,但内核接受这种不精确性。代码注释说明 "This is indeed racy, but we can live with some inaccuracy"。

12.4.2.3 page_counter_try_charge -- 带限制检查的充电

page_counter_try_charge() 是限制执行的核心函数,定义在 mm/page_counter.c:118-172:

// mm/page_counter.c:118-172
bool page_counter_try_charge(struct page_counter *counter,
                 unsigned long nr_pages,
                 struct page_counter **fail)
{
    struct page_counter *c;
    bool protection = track_protection(counter);
    bool track_failcnt = counter->track_failcnt;

    for (c = counter; c; c = c->parent) {
        long new;
        new = atomic_long_add_return(nr_pages, &c->usage);
        if (new > c->max) {
            atomic_long_sub(nr_pages, &c->usage);
            if (track_failcnt)
                data_race(c->failcnt++);
            *fail = c;
            goto failed;
        }
        if (protection)
            propagate_protected_usage(c, new);
        if (new > READ_ONCE(c->local_watermark)) {
            WRITE_ONCE(c->local_watermark, new);
            if (new > READ_ONCE(c->watermark))
                WRITE_ONCE(c->watermark, new);
        }
    }
    return true;

failed:
    for (c = counter; c != *fail; c = c->parent)
        page_counter_cancel(c, nr_pages);
    return false;
}

该函数的设计蕴含多个关键决策:

投机性充电 (Speculative Charging):代码注释详细解释了这一策略 -- 先无条件增加 usage,然后检查是否超过 max。如果超限,再回滚。这种方式避免了昂贵的 CAS (Compare-And-Swap) 操作。注释指出:"Charge speculatively to avoid an expensive CAS. If a bigger charge fails, it might falsely lock out a racing smaller charge and send it into reclaim early, but the error is limited to the difference between the two sizes."

内存屏障保证:atomic_long_add_return() 隐含完整的内存屏障,确保在增加 usage 和读取 limit 之间的顺序性。代码注释说明:"The atomic_long_add_return() implies a full memory barrier between incrementing the count and reading the limit."

失败回滚:failed 标签处的回滚循环使用 page_counter_cancel() 撤销所有已成功充电的层级。注意回滚的范围是从 counter 到 *fail(不含),因为 *fail 层级已在前面直接用 atomic_long_sub() 回滚了。

12.4.2.4 page_counter_calculate_protection -- 递归保护值计算

page_counter_calculate_protection() 定义在 mm/page_counter.c:424-464,实现了保护值的递归计算。这是内存保护机制中最复杂的部分:

// mm/page_counter.c:424-464
void page_counter_calculate_protection(struct page_counter *root,
                       struct page_counter *counter,
                       bool recursive_protection)
{
    unsigned long usage, parent_usage;
    struct page_counter *parent = counter->parent;

    if (root == counter)
        return;

    usage = page_counter_read(counter);
    if (!usage)
        return;

    if (parent == root) {
        counter->emin = READ_ONCE(counter->min);
        counter->elow = READ_ONCE(counter->low);
        return;
    }

    parent_usage = page_counter_read(parent);

    WRITE_ONCE(counter->emin, effective_protection(usage, parent_usage,
            READ_ONCE(counter->min),
            READ_ONCE(parent->emin),
            atomic_long_read(&parent->children_min_usage),
            recursive_protection));

    WRITE_ONCE(counter->elow, effective_protection(usage, parent_usage,
            READ_ONCE(counter->low),
            READ_ONCE(parent->parent->elow),
            atomic_long_read(&parent->children_low_usage),
            recursive_protection));
}

该函数必须作为自顶向下的树遍历的一部分使用,不能用于独立查询。对于根节点的直接子节点,保护值直接等于配置值;对于更深层的节点,调用 effective_protection() 进行计算。

effective_protection() 函数(mm/page_counter.c:337-410)实现了以下五条规则:

规则 1:在第一级回收中,有效保护等于声明的保护值。

规则 2:在后续层级中,有效保护被限制在父 cgroup 的有效保护值之内,实现安全的委托。

规则 3:如果兄弟 cgroup 声明的保护总量超过了父 cgroup 的保护预算,则按实际使用比例分配。这是工作保持 (work-conserving) 的:声明但未使用的保护可供兄弟 cgroup 使用。

// mm/page_counter.c:358-359
if (siblings_protected > parent_effective)
    return protected * parent_effective / siblings_protected;

规则 4:当保护声明不足时(子 cgroup 声明的总保护小于父 cgroup 的保护预算),不按比例分配,而是将每个子 cgroup 的保护限制在其自身声明值。

规则 5:如果启用了 recursive_protection,未使用的保护预算按未保护内存的使用量比例分配给子 cgroup。这使得保护对于兄弟 cgroup 是中性的,允许它们自由竞争共享的保护预算,同时将整个子树与相邻子树隔离开来。

12.4.2.5 propagate_protected_usage -- 保护使用量传播

propagate_protected_usage() 定义在 mm/page_counter.c:21-47,在每次 usage 变化时更新保护使用量追踪:

// mm/page_counter.c:21-47
static void propagate_protected_usage(struct page_counter *c,
                      unsigned long usage)
{
    unsigned long protected, old_protected;
    long delta;

    if (!c->parent)
        return;

    protected = min(usage, READ_ONCE(c->min));
    old_protected = atomic_long_read(&c->min_usage);
    if (protected != old_protected) {
        old_protected = atomic_long_xchg(&c->min_usage, protected);
        delta = protected - old_protected;
        if (delta)
            atomic_long_add(delta, &c->parent->children_min_usage);
    }

    protected = min(usage, READ_ONCE(c->low));
    old_protected = atomic_long_read(&c->low_usage);
    if (protected != old_protected) {
        old_protected = atomic_long_xchg(&c->low_usage, protected);
        delta = protected - old_protected;
        if (delta)
            atomic_long_add(delta, &c->parent->children_low_usage);
    }
}

该函数确保 children_min_usage 和 children_low_usage 始终反映所有子 cgroup 的实际保护使用量之和。atomic_long_xchg() 保证了在更新 min_usage/low_usage 和调整父 cgroup 的 children_*_usage 之间的原子性。

12.4.3 内存限制执行

12.4.3.1 memory.max 硬限制

memory.max 是内存使用的硬限制。当 cgroup 的内存使用量达到该限制时,内核会阻止进一步的内存分配,并可能触发 OOM killer。

限制执行的核心在 try_charge_memcg() 函数(mm/memcontrol.c:2355 开始):

// mm/memcontrol.c:2355-2399 (简化)
static int try_charge_memcg(struct mem_cgroup *memcg, gfp_t gfp_mask,
                unsigned int nr_pages)
{
    unsigned int batch = max(MEMCG_CHARGE_BATCH, nr_pages);
    int nr_retries = MAX_RECLAIM_RETRIES;
    ...
retry:
    if (consume_stock(memcg, nr_pages))
        return 0;

    if (!do_memsw_account() ||
        page_counter_try_charge(&memcg->memsw, batch, &counter)) {
        if (page_counter_try_charge(&memcg->memory, batch, &counter))
            goto done_restock;
        ...
        mem_over_limit = mem_cgroup_from_counter(counter, memory);
    }

充电过程使用批量化策略(MEMCG_CHARGE_BATCH = 64,定义于 include/linux/memcontrol.h:332),以减少对 page_counter 的原子操作频率。首先尝试从 per-CPU 缓存 (consume_stock) 中满足请求;如果缓存不足,则通过 page_counter_try_charge() 尝试向层次化的 page_counter 充电。

当 page_counter_try_charge() 返回 false(即超限),内核进入回收和 OOM 处理路径:

  1. 首先尝试回收内存 (try_to_free_mem_cgroup_pages())
  2. 如果回收后仍超限,经过 MAX_RECLAIM_RETRIES 次重试后
  3. 触发 OOM killer (mem_cgroup_out_of_memory())

12.4.3.2 memory.high 软限制

memory.high 是软限制,当内存使用超过该值时,内核会异步地回收内存并限流分配进程。与 memory.max 不同,memory.high 不会触发 OOM killer。

当 charge 导致 usage 超过 high 阈值时,try_charge_memcg() 中的逻辑会:

  1. 递增 MEMCG_HIGH 事件计数器
  2. 将超出部分的页面数记录在 current->memcg_nr_pages_over_high
  3. 在返回用户空间时调用 __mem_cgroup_handle_over_high()

__mem_cgroup_handle_over_high() 函数(mm/memcontrol.c:2265-2353)实现了多阶段的回收和限流策略:

// mm/memcontrol.c:2265-2353 (关键部分)
void __mem_cgroup_handle_over_high(gfp_t gfp_mask)
{
    ...
retry_reclaim:
    if (task_is_dying())
        goto out;

    nr_reclaimed = reclaim_high(memcg,
                    in_retry ? SWAP_CLUSTER_MAX : nr_pages,
                    gfp_mask);

    penalty_jiffies = calculate_high_delay(memcg, nr_pages,
                           mem_find_max_overage(memcg));
    penalty_jiffies += calculate_high_delay(memcg, nr_pages,
                           swap_find_max_overage(memcg));

    penalty_jiffies = min(penalty_jiffies, MEMCG_MAX_HIGH_DELAY_JIFFIES);

    if (penalty_jiffies <= HZ / 100)
        goto out;

    if (nr_reclaimed || nr_retries--) {
        in_retry = true;
        goto retry_reclaim;
    }

    psi_memstall_enter(&pflags);
    schedule_timeout_killable(penalty_jiffies);
    psi_memstall_leave(&pflags);
}

惩罚时间 penalty_jiffies 通过 calculate_high_delay() 计算(mm/memcontrol.c:2228-2258),采用二次方增长策略:

// mm/memcontrol.c:2245
penalty_jiffies = max_overage * max_overage * HZ;

超出量 overage 越大,惩罚时间呈指数级增长。同时考虑了当前 charge 的页面数,使得多次小分配和一次大分配的惩罚大致相当。

12.4.3.3 memory.min/memory.low 保护

memory.min 和 memory.low 提供了内存回收保护机制。在内存紧张时,内核的回收器会优先回收没有保护的 cgroup 的内存,只有在必要时才会回收受保护 cgroup 的内存。

保护值通过 page_counter_set_min() 和 page_counter_set_low() 设置(mm/page_counter.c:236-261):

// mm/page_counter.c:236-244
void page_counter_set_min(struct page_counter *counter, unsigned long nr_pages)
{
    struct page_counter *c;
    WRITE_ONCE(counter->min, nr_pages);
    for (c = counter; c; c = c->parent)
        propagate_protected_usage(c, atomic_long_read(&c->usage));
}

在回收路径中,内核通过 mem_cgroup_protection() 函数查询 cgroup 的有效保护值(emin 和 elow),并据此决定是否跳过该 cgroup 的回收。

memory.min:硬保护。在该保护范围内的内存不会被回收,除非没有其他可回收的内存(包括没有保护的 cgroup)。

memory.low:软保护。在该保护范围内的内存尽量不被回收,但如果系统整体内存紧张,可能会被部分回收。

12.4.3.4 OOM Killer 集成

当 memory.max 硬限制被突破且内存回收无法将使用量降到限制以下时,内核会调用 mem_cgroup_out_of_memory() 触发 OOM killer。

如果 cgroup 设置了 oom_group(include/linux/memcontrol.h:228),OOM killer 会杀死该 cgroup 中的所有进程,而不仅仅是选定的单个进程。这对应 MEMCG_OOM_GROUP_KILL 事件。

12.4.4 内存事件 (memory.events)

内存控制器通过 memory.events 和 memory.events.local 文件向用户空间报告重要的事件。事件类型定义在 include/linux/memcontrol.h:45-57。

12.4.4.1 MEMCG_LOW

当回收器开始回收受 memory.low 保护的内存时触发。这表明系统内存压力已经大到需要侵犯配置的保护边界。

12.4.4.2 MEMCG_HIGH

当内存使用超过 memory.high 时触发。该事件标志着内核开始限流分配进程并进行异步回收。

12.4.4.3 MEMCG_MAX

当 page_counter_try_charge() 因为达到 memory.max 而失败时触发。这是 charge 路径中对硬限制违反的直接指示。

12.4.4.4 MEMCG_OOM / MEMCG_OOM_KILL / MEMCG_OOM_GROUP_KILL

  • MEMCG_OOM:OOM 条件被检测到时触发
  • MEMCG_OOM_KILL:OOM killer 实际杀死一个进程时触发
  • MEMCG_OOM_GROUP_KILL:当 oom_group 启用且整个 cgroup 被杀死时触发

12.4.4.5 Swap 相关事件

  • MEMCG_SWAP_HIGH:swap 使用超过 memory.swap.high
  • MEMCG_SWAP_MAX:swap 使用达到 memory.swap.max
  • MEMCG_SWAP_FAIL:swap 分配在达到限制后失败

12.4.4.6 memory.events.local

memory_events_local[] 数组(include/linux/memcontrol.h:244)仅记录该 cgroup 自身直接产生的事件,不包括子 cgroup 的事件。这对于区分 "是本 cgroup 导致的问题还是子 cgroup 导致的问题" 至关重要。

12.4.5 交换空间控制

12.4.5.1 memory.swap.max 和 memory.swap.current

交换空间通过 struct page_counter swap(include/linux/memcontrol.h:200)进行独立追踪和控制。memory.swap.max 设置 swap 使用的硬限制,memory.swap.current 显示当前 swap 使用量。

在 cgroup v2 中,swap 控制与 v1 的 memsw(memory+swap 合并计数器)有本质区别。v2 中 swap 有独立的计数器,使得用户可以分别控制内存和 swap 的限制。

12.4.5.2 zswap 控制

当启用 CONFIG_ZSWAP 时,内存控制器提供额外的 zswap 控制:

// include/linux/memcontrol.h:212-220
#ifdef CONFIG_ZSWAP
    unsigned long zswap_max;
    bool zswap_writeback;
#endif

zswap_max 限制该 cgroup 可以使用的压缩交换空间的最大值。zswap_writeback 控制是否允许将 zswap 中的页面写回到普通 swap 设备,以及 swap store 失败时是否允许 swap out。

12.4.6 per-CPU 统计与 rstat

12.4.6.1 memcg_rstat_updated -- 统计更新通知

memcg_rstat_updated() 定义在 mm/memcontrol.c:567-597,在每次统计更新时被调用,通知 rstat 框架需要进行刷新:

// mm/memcontrol.c:567-597
static inline void memcg_rstat_updated(struct mem_cgroup *memcg, int val,
                       int cpu)
{
    struct memcg_vmstats_percpu __percpu *statc_pcpu;
    struct memcg_vmstats_percpu *statc;
    unsigned int stats_updates;

    if (!val)
        return;

    css_rstat_updated(&memcg->css, cpu);
    statc_pcpu = memcg->vmstats_percpu;
    for (; statc_pcpu; statc_pcpu = statc->parent_pcpu) {
        statc = this_cpu_ptr(statc_pcpu);
        if (memcg_vmstats_needs_flush(statc->vmstats))
            break;

        stats_updates = this_cpu_add_return(statc_pcpu->stats_updates,
                            abs(val));
        if (stats_updates < MEMCG_CHARGE_BATCH)
            continue;

        stats_updates = this_cpu_xchg(statc_pcpu->stats_updates, 0);
        atomic_add(stats_updates, &statc->vmstats->stats_updates);
    }
}

该函数沿 cgroup 层次向上遍历,直到遇到一个已经标记为需要刷新的祖先 cgroup(因为如果祖先已经需要刷新,那么所有更上层的祖先也都需要刷新,无需重复通知)。

12.4.6.2 mem_cgroup_flush_stats -- 读取时刷新

mem_cgroup_flush_stats() 定义在 mm/memcontrol.c:624-633,在读取统计数据时被调用:

// mm/memcontrol.c:624-633
void mem_cgroup_flush_stats(struct mem_cgroup *memcg)
{
    if (mem_cgroup_disabled())
        return;

    if (!memcg)
        memcg = root_mem_cgroup;

    __mem_cgroup_flush_stats(memcg, false);
}

内部函数 __mem_cgroup_flush_stats() 首先检查是否确实需要刷新(通过 memcg_vmstats_needs_flush() 检查更新事件数是否超过阈值),如果需要则调用 css_rstat_flush() 执行实际的刷新操作。

统计刷新的优化策略包括:

  1. 定期异步刷新:每 2 秒通过延迟工作队列 (stats_flush_dwork, mm/memcontrol.c:556) 刷新全局统计,防止 rstat 更新树无限增长。
  2. 条件同步刷新:仅当更新事件数超过 MEMCG_CHARGE_BATCH * nr_cpus 时才在读取侧执行同步刷新。

12.4.6.3 统计项

内存控制器的统计项涵盖多种内存类型和状态:

// mm/memcontrol.c:296-313 (部分)
static const unsigned int memcg_node_stat_items[] = {
    NR_INACTIVE_ANON,
    NR_ACTIVE_ANON,
    NR_INACTIVE_FILE,
    NR_ACTIVE_FILE,
    NR_UNEVICTABLE,
    NR_SLAB_RECLAIMABLE,
    NR_SLAB_UNRECLAIMABLE,
    WORKINGSET_REFAULT,
    WORKINGSET_ACTIVATE,
    WORKINGSET_RESTORE,
    WORKINGSET_NODERECLAIM,
    NR_SHMEM,
    ...
};

此外还有 cgroup 特有的统计项(include/linux/memcontrol.h:34-43):

enum memcg_stat_item {
    MEMCG_SWAP = NR_VM_NODE_STAT_ITEMS,
    MEMCG_SOCK,
    MEMCG_PERCPU_B,
    MEMCG_VMALLOC,
    MEMCG_KMEM,
    MEMCG_ZSWAP_B,
    MEMCG_ZSWAPPED,
    MEMCG_NR_STAT,
};

12.4.6.4 LRU 管理

每个 mem_cgroup_per_node 包含一个 lruvec(include/linux/memcontrol.h:113),管理该 cgroup 在该 NUMA 节点上的 LRU 链表。LRU 链表按类型(匿名/文件)和活跃度(活跃/不活跃)分类:

  • Inactive Anonymous:最近未被访问的匿名页面
  • Active Anonymous:最近被访问的匿名页面
  • Inactive File:最近未被访问的文件缓存页面
  • Active File:最近被访问的文件缓存页面
  • Unevictable:不可回收的页面(如 mlock 页面)

lru_zone_size[MAX_NR_ZONES][NR_LRU_LISTS](行115)按 zone 和 LRU 类型追踪页面数量,用于回收时的精确控制。

12.4.7 对象级记账 (obj_cgroup)

12.4.7.1 obj_cgroup 结构

obj_cgroup 定义在 include/linux/memcontrol.h:174-182:

// include/linux/memcontrol.h:174-182
struct obj_cgroup {
    struct percpu_ref refcnt;                        // 行175
    struct mem_cgroup *memcg;                        // 行176
    atomic_t nr_charged_bytes;                       // 行177
    union {
        struct list_head list;                       // 行179, protected by objcg_lock
        struct rcu_head rcu;                         // 行180
    };
};

12.4.7.2 为什么需要对象级记账

传统的内存控制器以页面为粒度进行记账。但 slab 分配器 (slab allocator) 中的对象(通过 kmalloc/kmem_cache_alloc 分配)可能远小于一个页面(最小可至 8 字节),而一个 slab 页面中可能包含来自不同 cgroup 的对象。如果按页面粒度记账,会导致:

  1. 精度损失:一个 8 字节的对象被计为 4KB(一个页面)
  2. cgroup 间不公平:一个 slab 页面只被计入拥有该页面中最多对象的 cgroup
  3. 无法精确回收:无法知道 slab 页面中哪些对象属于哪个 cgroup

obj_cgroup 通过引入更细粒度的记账解决了这些问题。每个 slab 对象都与一个 obj_cgroup 关联,而 obj_cgroup 又与一个 mem_cgroup 关联。对象的字节级记账通过 nr_charged_bytes 追踪。

12.4.7.3 obj_cgroup_release -- 对象释放回调

当 obj_cgroup 的最后一个对象被释放时,percpu_ref 的引用计数归零,触发 obj_cgroup_release() 回调(mm/memcontrol.c:140-188):

// mm/memcontrol.c:140-188 (关键部分)
static void obj_cgroup_release(struct percpu_ref *ref)
{
    struct obj_cgroup *objcg = container_of(ref, struct obj_cgroup, refcnt);
    unsigned int nr_bytes;
    unsigned int nr_pages;

    nr_bytes = atomic_read(&objcg->nr_charged_bytes);
    WARN_ON_ONCE(nr_bytes & (PAGE_SIZE - 1));
    nr_pages = nr_bytes >> PAGE_SHIFT;

    if (nr_pages) {
        struct mem_cgroup *memcg;
        memcg = get_mem_cgroup_from_objcg(objcg);
        mod_memcg_state(memcg, MEMCG_KMEM, -nr_pages);
        if (!mem_cgroup_is_root(memcg))
            memcg_uncharge(memcg, nr_pages);
        mem_cgroup_put(memcg);
    }

    spin_lock_irqsave(&objcg_lock, flags);
    list_del(&objcg->list);
    spin_unlock_irqrestore(&objcg_lock, flags);

    percpu_ref_exit(ref);
    kfree_rcu(objcg, rcu);
}

该函数处理了 nr_charged_bytes 可能积累到整页面的情况(由于 slab 对象的字节级记账与页面级记账之间的舍入),将这些整页面从 mem_cgroup 中解除记账。

12.4.7.4 objcg 的 reparenting

当 cgroup 被删除时,其 obj_cgroup 需要被重新归属 (reparent) 到父 cgroup。这一过程由 memcg_reparent_objcgs()(mm/memcontrol.c:209-229)完成:

// mm/memcontrol.c:209-229
static void memcg_reparent_objcgs(struct mem_cgroup *memcg,
                  struct mem_cgroup *parent)
{
    struct obj_cgroup *objcg, *iter;

    objcg = rcu_replace_pointer(memcg->objcg, NULL, true);

    spin_lock(&objcg_lock);
    list_add(&objcg->list, &memcg->objcg_list);
    list_for_each_entry(iter, &memcg->objcg_list, list)
        WRITE_ONCE(iter->memcg, parent);
    list_splice(&memcg->objcg_list, &parent->objcg_list);
    spin_unlock(&objcg_lock);

    percpu_ref_kill(&objcg->refcnt);
}

这一过程将所有已 reparent 的 obj_cgroup 原子性地移动到父 cgroup 的 objcg_list 中。通过将 iter->memcg 修改为指向父 cgroup,确保后续的对象释放回调将页面记账到正确的父 cgroup。

12.4.8 全局变量与初始化

内存控制器的关键全局变量:

// mm/memcontrol.c:80-100
struct cgroup_subsys memory_cgrp_subsys __read_mostly;     // 行80
struct mem_cgroup *root_mem_cgroup __read_mostly;          // 行83
DEFINE_PER_CPU(struct mem_cgroup *, int_active_memcg);     // 行87
static bool cgroup_memory_nosocket __ro_after_init;        // 行91
static bool cgroup_memory_nokmem __ro_after_init;          // 行94
static bool cgroup_memory_nobpf __ro_after_init;           // 行97
static struct workqueue_struct *memcg_wq __ro_after_init;  // 行99
  • int_active_memcg:中断上下文中使用的活跃 memcg 指针。由于中断上下文无法安全地获取当前进程的 memcg,内核使用该 per-CPU 变量作为替代。
  • cgroup_memory_nokmem:启动参数控制是否禁用内核内存记账。禁用后可减少开销。
  • memcg_wq:用于异步回收操作(如 high_work)的工作队列。

内存控制器通过 DEFINE_STATIC_KEY_FALSE(memcg_kmem_online_key)(mm/memcontrol.c:237)提供静态分支优化。当没有 cgroup 使用内核内存记账时,相关的代码路径通过静态分支被完全跳过,消除了性能开销。

12.5 IO 控制器 -- io cost 与 io weight

IO 控制器是 cgroups v2 中管理块设备 IO 资源分配的子系统。与内存控制器不同,IO 控制器面临着一个独特的挑战:缺乏简单可观测的成本度量。CPU 时间和内存字节数可以直接作为资源分配的依据,但 IO 操作的成本因设备类型、访问模式等因素而有数量级的差异。Linux 内核通过三种互补的机制来解决这一问题:基于带宽/iops 的限速 (blk-throttle)、基于 IO cost 权重的比例分配 (blk-iocost) 和基于延迟目标的反馈控制 (blk-iolatency)。

12.5.1 blkcg 结构与 blkg 关联

12.5.1.1 struct blkcg -- block cgroup 核心

struct blkcg 是 IO cgroup 子系统的核心数据结构,定义在 block/blk-cgroup.h:94-120:

// block/blk-cgroup.h:94-120
struct blkcg {
    struct cgroup_subsys_state css;                   // 行95
    spinlock_t               lock;                   // 行96
    refcount_t               online_pin;             // 行97
    atomic_t                 congestion_count;       // 行99

    struct radix_tree_root   blkg_tree;              // 行101
    struct blkcg_gq  __rcu  *blkg_hint;              // 行102
    struct hlist_head        blkg_list;              // 行103

    struct blkcg_policy_data *cpd[BLKCG_MAX_POLS];   // 行105

    struct list_head         all_blkcgs_node;        // 行107

    struct llist_head __percpu *lhead;               // 行112

#ifdef CONFIG_BLK_CGROUP_FC_APPID
    char                     fc_app_id[FC_APPID_LEN]; // 行115
#endif
#ifdef CONFIG_CGROUP_WRITEBACK
    struct list_head         cgwb_list;              // 行118
#endif
};

css (行95):与内存控制器类似,blkcg 通过嵌入 cgroup_subsys_state 与 cgroup 核心框架关联。通过 css_to_blkcg() 内联函数(block/blk-cgroup.h:122)可以从 css 指针反向获取 blkcg。

lock (行96):保护 blkcg 内部数据结构的自旋锁,主要用于保护 blkg 的创建和查找。

congestion_count (行99):原子计数器,追踪该 cgroup 中有多少请求队列处于拥塞状态。当该值非零时,该 cgroup 被视为拥塞的。

blkg_tree (行101):基数树 (radix tree),以请求队列的 ID 为键,存储与该 blkcg 关联的所有 blkcg_gq 结构。这是查找特定块设备上该 cgroup 的 blkg 的主要数据结构。

blkg_hint (行102):RCU 保护的提示指针,缓存最近使用的 blkg,用于加速查找。

lhead (行112):per-CPU 的无锁链表头,用于 rstat 框架追踪哪些 blkg 有 IO 统计更新。这一设计在 block/blk-cgroup.c:66-83 中有详细注释:

There are multiple blkg's (one for each block device) attached to each
blkcg. The rstat code keeps track of which cpu has IO stats updated,
but it doesn't know which blkg has the updated stats. If there are many
block devices in a system, the cost of iterating all the blkg's to flush
out the IO stats can be high. To reduce such overhead, a set of percpu
lockless lists (lhead) per blkcg are used to track the set of recently
updated iostat_cpu's since the last flush.

全局的根 blkcg 定义在 block/blk-cgroup.c:50:

// block/blk-cgroup.c:50
struct blkcg blkcg_root;

12.5.1.2 struct blkcg_gq -- blkcg 与 request_queue 的关联

struct blkcg_gq(简称 blkg)是 IO cgroup 体系中最核心的关联结构,它将一个 blkcg 和一个 request_queue 连接起来。定义在 block/blk-cgroup.h:56-92:

// block/blk-cgroup.h:56-92
struct blkcg_gq {
    struct request_queue     *q;                     // 行58
    struct list_head         q_node;                 // 行59
    struct hlist_node        blkcg_node;             // 行60
    struct blkcg             *blkcg;                 // 行61

    struct blkcg_gq          *parent;                // 行64

    struct percpu_ref        refcnt;                 // 行67

    bool                     online;                 // 行70

    struct blkg_iostat_set __percpu *iostat_cpu;     // 行72
    struct blkg_iostat_set  iostat;                  // 行73

    struct blkg_policy_data  *pd[BLKCG_MAX_POLS];    // 行75

    atomic_t                 use_delay;              // 行85
    atomic64_t               delay_nsec;             // 行86
    atomic64_t               delay_start;            // 行87
    u64                      last_delay;             // 行88
    int                      last_use;               // 行89

    struct rcu_head          rcu_head;               // 行91
};

q (行58):指向关联的请求队列。每个块设备有一个 request_queue。

blkcg (行61):指向关联的 blkcg。

parent (行64):指向父 blkg(即父 blkcg 与同一 request_queue 的关联)。所有非根 blkg 都保证有 parent。

refcnt (行67):per-CPU 引用计数,使用 percpu_ref 实现。当引用计数归零时,通过 RCU 延迟释放 blkg。

iostat_cpu (行72):per-CPU 的 IO 统计数据。IO 统计首先更新到 per-CPU 缓存中,然后通过 rstat 框架聚合到全局统计。

pd[BLKCG_MAX_POLS] (行75):per-policy 数据数组。每个 IO 控制策略(如 throttle、iocost、iolatency)可以在 blkg 中存储自己的策略数据。

use_delay/delay_nsec (行85-86):延迟限流机制。use_delay 控制是否启用延迟,delay_nsec 累积需要延迟的纳秒数。这种机制允许 IO 控制器在 bio 提交路径上引入人为延迟来限流。

12.5.1.3 blkg 的创建

blkg_alloc() 函数(block/blk-cgroup.c:299-364)负责分配和初始化 blkg:

// block/blk-cgroup.c:299-364 (关键部分)
static struct blkcg_gq *blkg_alloc(struct blkcg *blkcg, struct gendisk *disk,
                   gfp_t gfp_mask)
{
    struct blkcg_gq *blkg;
    int i, cpu;

    blkg = kzalloc_node(sizeof(*blkg), gfp_mask, disk->queue->node);
    if (!blkg)
        return NULL;
    if (percpu_ref_init(&blkg->refcnt, blkg_release, 0, gfp_mask))
        goto out_free_blkg;
    blkg->iostat_cpu = alloc_percpu_gfp(struct blkg_iostat_set, gfp_mask);
    ...

    blkg->q = disk->queue;
    INIT_LIST_HEAD(&blkg->q_node);
    blkg->blkcg = blkcg;
    blkg->iostat.blkg = blkg;

    for_each_possible_cpu(cpu) {
        u64_stats_init(&per_cpu_ptr(blkg->iostat_cpu, cpu)->sync);
        per_cpu_ptr(blkg->iostat_cpu, cpu)->blkg = blkg;
    }

    for (i = 0; i < BLKCG_MAX_POLS; i++) {
        struct blkcg_policy *pol = blkcg_policy[i];
        if (!blkcg_policy_enabled(disk->queue, pol))
            continue;
        pd = pol->pd_alloc_fn(disk, blkcg, gfp_mask);
        ...
        blkg->pd[i] = pd;
        pd->blkg = blkg;
        pd->plid = i;
    }

    return blkg;
}

blkg 的分配过程包括:分配内存、初始化 per-CPU 引用计数、分配 per-CPU IO 统计、为每个已激活的策略分配策略数据。

blkg_create() 函数(block/blk-cgroup.c:370 开始)在持有队列锁的条件下将 blkg 注册到 blkcg 和 request_queue 中:

// block/blk-cgroup.c:370-399 (简化)
static struct blkcg_gq *blkg_create(struct blkcg *blkcg, struct gendisk *disk,
                    struct blkcg_gq *new_blkg)
{
    ...
    lockdep_assert_held(&disk->queue->queue_lock);

    if (!css_tryget_online(&blkcg->css))
        ...

    /* Insert blkg into the blkcg's blkg_tree */
    ret = radix_tree_insert(&blkcg->blkg_tree, q->id, blkg);
    ...

    /* Insert blkg into the request_queue's blkg_list */
    hlist_add_head_rcu(&blkg->blkcg_node, &blkcg->blkg_list);
    list_add(&blkg->q_node, &q->blkg_list);
    ...
}

blkg_lookup_create() 是查找或创建 blkg 的高层接口。它首先尝试在 radix tree 中查找,如果不存在则创建新的 blkg。这一路径使用 GFP_NOWAIT 分配以避免在持有队列锁时睡眠。

12.5.1.4 __blkcg_rstat_flush -- IO 统计刷新

__blkcg_rstat_flush() 函数(block/blk-cgroup.c:38 声明)负责将 per-CPU 的 IO 统计数据聚合到全局统计中。在 blkg 释放时(__blkg_release(), block/blk-cgroup.c:164-185),内核会刷新所有 CPU 上的统计:

// block/blk-cgroup.c:164-185
static void __blkg_release(struct rcu_head *rcu)
{
    struct blkcg_gq *blkg = container_of(rcu, struct blkcg_gq, rcu_head);
    struct blkcg *blkcg = blkg->blkcg;
    int cpu;

    for_each_possible_cpu(cpu)
        __blkcg_rstat_flush(blkcg, cpu);

    css_put(&blkg->blkcg->css);
    blkg_free(blkg);
}

IO 统计通过 blk_cgroup_bio_start() 在 bio 提交时更新 per-CPU 计数器,然后通过 rstat 框架在读取时聚合。

12.5.2 io.max (blk-throttle)

blk-throttle 是 IO 控制器中最直观的限速机制,通过 io.max 接口提供基于带宽 (BPS) 和 IO 操作速率 (iops) 的限制。

12.5.2.1 BPS/iops 限制

blk-throttle 支持四种独立的限制维度:

  • read_bps:读带宽限制(字节/秒)
  • write_bps:写带宽限制(字节/秒)
  • read_iops:读 IOPS 限制(操作/秒)
  • write_iops:写 IOPS 限制(操作/秒)

这些限制存储在 struct throtl_grp 中,通过 tg_bps_limit() 和 tg_iops_limit() 函数读取(block/blk-throttle.c:84-100):

// block/blk-throttle.c:84-92
static uint64_t tg_bps_limit(struct throtl_grp *tg, int rw)
{
    struct blkcg_gq *blkg = tg_to_blkg(tg);

    if (cgroup_subsys_on_dfl(io_cgrp_subsys) && !blkg->parent)
        return U64_MAX;    // 根 cgroup 无限制

    return tg->bps[rw];
}

根 cgroup 始终返回 U64_MAX(无限),确保根 cgroup 不受限制。

12.5.2.2 throttle 机制

blk-throttle 的核心数据结构是 struct throtl_data(block/blk-throttle.c:31-43):

// block/blk-throttle.c:31-43
struct throtl_data
{
    struct throtl_service_queue service_queue;    // 行34
    struct request_queue *queue;                  // 行36
    unsigned int nr_queued[2];                    // 行39
    struct work_struct dispatch_work;             // 行41
};

service_queue 管理被限速的 IO 请求队列,使用红黑树按时间排序。dispatch_work 在定时器触发时从队列中分发已到期的 IO。

12.5.2.3 令牌桶算法实现

blk-throttle 使用令牌桶算法 (Token Bucket Filter) 来实现带宽和 IOPS 限制。每个 throtl_grp 维护独立的令牌桶状态:

  • bytes_disp/iops_disp:当前时间片内已分发的字节/IO 数
  • last_finish_time:上次 IO 完成的时间戳
  • slice_start:当前时间片的起始时间

时间片长度由 DFL_THROTL_SLICE 定义(block/blk-throttle.c:24),默认为 HZ/10(100ms):

// block/blk-throttle.c:24
#define DFL_THROTL_SLICE (HZ / 10)

在每个时间片开始时,令牌配额被重置。当 bio 提交时,throttle 层检查当前时间片内的剩余配额;如果不足,bio 被延迟到下一个配额可用的时间点。

12.5.2.4 层次化限速传播

blk-throttle 支持层次化的限速。子 cgroup 的 IO 受到自身限制和所有祖先 cgroup 限制的双重约束。在层次化模式下,IO 请求从叶子 cgroup 向上传播,每个层级都独立地应用限速检查。

THROTL_GRP_QUANTUM 定义为 8(block/blk-throttle.c:18),限制一轮中从一个组分发的最大 IO 数量。THROTL_QUANTUM 定义为 32(行19),限制所有组一轮的总分发数量。这些参数防止某一组在单轮中独占分发机会。

12.5.3 io.weight (blk-iocost)

blk-iocost 是基于 IO cost 模型的比例分配控制器,通过 io.weight 和 io.cost 接口配置。与 blk-throttle 的硬限速不同,iocost 旨在工作保持 (work-conserving) 的前提下按权重比例分配 IO 带宽。

12.5.3.1 IO cost 模型

iocost 的设计理念在源码注释(block/blk-iocost.c:9-47)中有详细说明。核心思想是:

IO 成本模型通过估计给定 IO 的 "成本"(以设备时间衡量)来解决缺乏简单成本度量的问题。如果一个 IO 被估计成本为 10ms,那么设备应该能够在一秒内处理约 100 个这样的 IO。

当前实现了唯一的内建成本模型 -- 线性模型。每个 IO 被分类为顺序或随机,并给定相应的基础成本。在此基础上,加上与 IO 长度成正比的大小成本:

// block/blk-iocost.c:38-44 (简化)
 * Currently, there's only one builtin cost model - linear. Each IO is
 * classified as sequential or random and given a base cost accordingly.
 * On top of that, a size cost proportional to the length of the IO is
 * added.

12.5.3.2 控制策略 -- 虚拟时间分布

iocost 使用设备虚拟时间 (vtime) 作为主要控制度量。控制策略包含三个部分:

Vtime 分布:当 cgroup 在 IO 方面变为活跃时,计算其层次化份额。vtime 以与 hweight 成反比的速度运行。例如,12.5% 权重的 cgroup,其时间运行速度比设备 vtime 慢 8 倍。

默认权重:CGROUP_WEIGHT_DFL = 100,权重范围为 [1, 10000]。

权重分配:以源码中的层次结构为例(block/blk-iocost.c:57-68):

          root
        /       \
     A (w:100)  B (w:300)
     /       \
 A0 (w:100)  A1 (w:100)

如果 B 空闲且只有 A0 和 A1 活跃地发出 IO,两者权重相等,各获得 50% 的份额。如果 B 开始发出 IO,B 获得 300/(100+300) = 75% 的份额,A0 和 A1 各获得 12.5%。

12.5.3.3 vrate 调节机制

iocost 包含一个 vrate 调节机制来补偿或惩罚 cgroup:

  • 当设备利用率低于目标时,vrate 加速(>100%),所有 cgroup 的 vtime 债务更快地偿还
  • 当设备利用率超过目标时,vrate 减速(<100%),限制 IO 提交速率

这种反馈机制确保了 IO 带宽的公平分配,同时保持设备在高利用率下的工作保持特性。

12.5.4 io.latency (blk-iolatency)

blk-iolatency 是基于延迟目标的反馈控制器,通过 io.latency 接口设定 IO 延迟阈值。

12.5.4.1 延迟目标设定

iolatency 允许用户为 cgroup 设定 IO 完成延迟的目标值。当监控到 IO 延迟超过目标时,控制器会限制上层 cgroup 的 IO 提交速率,从而为被保护的 cgroup 保留足够的 IO 带宽。

源码注释(block/blk-iolatency.c:17-64)详细描述了层次化的延迟控制机制:

The hierarchy works like the cpu controller does, we track the latency at
every configured node, and each configured node has its own independent
queue depth.

以源码中的示例(block/blk-iolatency.c:27-34):

               root blkg
         /                     \
    fast (target=5ms)     slow (target=10ms)
     /     \                  /        \
   a        b          normal(15ms)   unloved

"a" 和 "b" 没有自己的延迟目标,但它们在 "fast" 下的组合 IO 不能超过 5ms 的平均延迟。如果超过,"slow" 组将被限流。

12.5.4.2 监测与反压

iolatency 使用 100ms 窗口内的平均延迟进行监控(block/blk-iolatency.c:10-12)。使用平均延迟而非瞬时延迟是因为写入操作可能特别快,容易产生对其他工作负载影响的错误判断。

当超过延迟目标时,iolatency 提供两种限流机制:

1. 队列深度限流:随着延迟增加,逐步降低允许的最大并发 IO 数。从初始的无限制逐步降低到 1。如果组只为自己提交 IO,这是唯一的限流方式。

2. 诱导延迟限流:当组产生的 IO 必须由根 cgroup 发出以避免优先级反转(如 REQ_META 或 REQ_SWAP 请求)时使用。累积惩罚时间:

total_time += min_lat_nsec - actual_io_completion

在限流时应用:

throttle_time = min(total_time, NSEC_PER_SEC)

这种诱导延迟在用户空间返回时执行,限流产生根 cgroup IO 的活动(无论是元数据密集型操作还是大量内存使用导致 swap 的操作)。

12.5.5 IO 统计 (io.stat)

12.5.5.1 统计指标

io.stat 文件提供以下 IO 统计指标:

  • rbytes/wbytes:读/写总字节数
  • rios/wios:读/写 IO 操作数
  • dbytes/dios:丢弃 (discard) 总字节数/操作数
  • rbytes/wbytes 还包括更细粒度的统计

这些统计存储在 struct blkg_iostat_set 中,per-CPU 版本为 iostat_cpu(block/blk-cgroup.h:72)。

12.5.5.2 per-CPU 统计与 rstat 聚合

IO 统计使用 cgroup rstat 框架进行聚合。在 bio 提交时,blk_cgroup_bio_start() 将 IO 统计更新到 per-CPU 的 blkg_iostat_set 中,并将 blkg 添加到 blkcg 的 per-CPU lhead 链表中:

// block/blk-cgroup.c:66-83 (注释)
 * New IO stats are stored in the percpu iostat_cpu within blkcg_gq (blkg).
 * There are multiple blkg's (one for each block device) attached to each
 * blkcg. The rstat code keeps track of which cpu has IO stats updated,
 * but it doesn't know which blkg has the updated stats. If there are many
 * block devices in a system, the cost of iterating all the blkg's to flush
 * out the IO stats can be high. To reduce such overhead, a set of percpu
 * lockless lists (lhead) per blkcg are used to track the set of recently
 * updated iostat_cpu's since the last flush.

当用户读取 io.stat 时,rstat 框架遍历 lhead 链表中的 blkg,仅刷新有更新的 blkg 的统计,避免遍历所有 blkg。

12.5.5.3 IO 延迟直方图

IO 控制器还提供延迟直方图统计,追踪 IO 完成延迟的分布。直方图数据通过 io.lat 接口暴露,帮助管理员了解 IO 延迟的百分位分布,而不仅仅是平均值。

12.5.5.4 初始化与 per-CPU 链表

blkcg 的 per-CPU lhead 链表在 init_blkcg_llists() 中初始化(block/blk-cgroup.c:84-95):

// block/blk-cgroup.c:84-95
static int init_blkcg_llists(struct blkcg *blkcg)
{
    int cpu;

    blkcg->lhead = alloc_percpu_gfp(struct llist_head, GFP_KERNEL);
    if (!blkcg->lhead)
        return -ENOMEM;

    for_each_possible_cpu(cpu)
        init_llist_head(per_cpu_ptr(blkcg->lhead, cpu));
    return 0;
}

每个 CPU 的 lhead 链表追踪该 CPU 上有 IO 统计更新的 blkg 集合。由于使用无锁链表 (lockless list),更新操作(在 bio 提交路径中)不需要获取任何锁,确保了最小化的性能开销。

12.5.5.5 策略注册框架

IO 控制器使用策略注册框架来管理不同的控制策略。全局策略数组定义在 block/blk-cgroup.c:56:

// block/blk-cgroup.c:56
static struct blkcg_policy *blkcg_policy[BLKCG_MAX_POLS];

策略通过 blkcg_policy_register() 和 blkcg_policy_unregister() 进行注册和注销。每个策略通过 struct blkcg_policy 定义自己的操作集,包括 pd_alloc_fn、pd_free_fn、pd_init_fn、pd_online_fn 等回调函数。

两个互斥锁保护策略注册的并发访问:

// block/blk-cgroup.c:47-48
static DEFINE_MUTEX(blkcg_pol_register_mutex);   // 保护策略注册/注销
static DEFINE_MUTEX(blkcg_pol_mutex);             // 保护策略激活/停用

blkcg_pol_register_mutex 嵌套在 blkcg_pol_mutex 外部,同步整个策略注册/注销操作(包括 cgroup 文件的添加/删除)。

12.5.6 三种 IO 控制机制的比较

特性 blk-throttle (io.max) blk-iocost (io.weight) blk-iolatency (io.latency)
控制维度 BPS + IOPS 硬限制 权重比例分配 延迟目标
工作保持 否(硬限制) 是 是
配置复杂度 低(直接设限速值) 中(需要设权重和 cost 模型) 中(设延迟阈值)
适用场景 绝对限速 公平分配 QoS 保证
控制粒度 全局 BPS/IOPS 按 vtime 比例 按延迟反馈

这三种机制可以组合使用。例如,可以使用 io.max 设置硬上限,同时用 io.weight 在上限内进行公平分配,再用 io.latency 保证关键工作负载的延迟目标。

12.6 cpuset 与设备控制器

cpuset 控制器管理进程的 CPU 和内存节点亲和性,将进程限制在指定的 CPU 子集和内存节点子集上运行。cpuset 是 cgroups v2 中唯一同时涉及 CPU 调度和内存放置两个维度的控制器。本节将深入分析 cpuset 的数据结构、分区调度机制、层次传播算法、热插拔处理,以及 freezer、pids 等其他辅助控制器。

12.6.1 cpuset 结构与核心概念

12.6.1.1 struct cpuset

struct cpuset 定义在 kernel/cgroup/cpuset-internal.h:75-193,是 cpuset 控制器的核心数据结构:

// kernel/cgroup/cpuset-internal.h:75-193
struct cpuset {
    struct cgroup_subsys_state css;                   // 行76

    unsigned long flags;                              // 行78

    /* user-configured CPUs and Memory Nodes allow to tasks */
    cpumask_var_t cpus_allowed;                       // 行101
    nodemask_t mems_allowed;                          // 行102

    /* effective CPUs and Memory Nodes allow to tasks */
    cpumask_var_t effective_cpus;                     // 行105
    nodemask_t effective_mems;                        // 行106

    /* exclusive CPUs for partition */
    cpumask_var_t effective_xcpus;                    // 行120
    cpumask_var_t exclusive_cpus;                     // 行134

    nodemask_t old_mems_allowed;                      // 行146

    int attach_in_progress;                           // 行152

    /* partition root state */
    int partition_root_state;                         // 行155

    bool remote_partition;                            // 行162

    int nr_deadline_tasks;                            // 行168
    int nr_migrate_dl_tasks;                          // 行169
    u64 sum_migrate_dl_bw;                            // 行171
    int dl_bw_cpu;                                    // 行176

    enum prs_errcode prs_err;                         // 行179

    struct cgroup_file partition_file;                // 行182
    ...
};

css (行76):标准的 cgroup 子系统状态嵌入。

flags (行78):位图,存储 cpuset 的各种标志。标志位定义在 kernel/cgroup/cpuset-internal.h:41-49:

// kernel/cgroup/cpuset-internal.h:41-49
typedef enum {
    CS_CPU_EXCLUSIVE,        // CPU 独占
    CS_MEM_EXCLUSIVE,        // 内存节点独占
    CS_MEM_HARDWALL,         // 内存硬墙
    CS_MEMORY_MIGRATE,       // 任务迁移时迁移内存页面
    CS_SCHED_LOAD_BALANCE,   // 启用负载均衡
    CS_SPREAD_PAGE,          // 页面均匀分布
    CS_SPREAD_SLAB,          // slab 均匀分布
} cpuset_flagbits_t;

12.6.1.2 配置掩码 vs 有效掩码

cpuset 的掩码管理遵循一个关键的设计原则:用户配置的掩码与实际生效的掩码分离。

在 cgroup v2 中(kernel/cgroup/cpuset-internal.h:80-98 注释):

  • cpus_allowed/mems_allowed:用户通过 cpuset.cpus 和 cpuset.mems 配置的掩码。只能通过写入控制文件修改,不受父 cgroup 掩码的限制。
  • effective_cpus/effective_mems:实际生效的掩码,等于 configured_mask & parent's effective_mask。如果结果为空,则继承父 cgroup 的掩码。

这一设计使得用户可以为子 cgroup 配置比当前父 cgroup 范围更大的掩码,当父 cgroup 的掩码扩大时,子 cgroup 的有效掩码会自动扩展。

12.6.1.3 top_cpuset

top_cpuset 是 cpuset 层次结构的根节点,定义在 kernel/cgroup/cpuset.c:287-292:

// kernel/cgroup/cpuset.c:287-292
struct cpuset top_cpuset = {
    .flags = BIT(CS_CPU_EXCLUSIVE) |
             BIT(CS_MEM_EXCLUSIVE) | BIT(CS_SCHED_LOAD_BALANCE),
    .partition_root_state = PRS_ROOT,
    .dl_bw_cpu = -1,
};

top_cpuset 始终与 cpu_active_mask 同步。注释强调(kernel/cgroup/cpuset.c:273-286):

The top_cpuset is always synchronized to cpu_active_mask and we should
avoid using cpu_online_mask as much as possible. An active CPU is always
an online CPU, but not vice versa. cpu_active_mask and cpu_online_mask
can differ during hotplug operations.

CPU 被标记为 active 是在 CPU 上线的最后阶段 (CPUHP_AP_ACTIVE),也是 cpuset 热插拔代码被调用以更新调度域的阶段。

12.6.1.4 分区根状态

分区根状态 (partition_root_state) 是 cpuset v2 中的关键概念,定义在 kernel/cgroup/cpuset.c:206-210:

// kernel/cgroup/cpuset.c:206-210
#define PRS_MEMBER           0     // 普通成员(非分区根)
#define PRS_ROOT             1     // 分区根
#define PRS_ISOLATED         2     // 隔离分区根(无负载均衡)
#define PRS_INVALID_ROOT    -1     // 无效分区根
#define PRS_INVALID_ISOLATED -2    // 无效隔离分区根

通过 is_partition_valid() 和 is_partition_invalid() 检查状态(kernel/cgroup/cpuset.c:235-243):

// kernel/cgroup/cpuset.c:235-243
static inline bool is_partition_valid(const struct cpuset *cs)
{
    return cs->partition_root_state > 0;
}

static inline bool is_partition_invalid(const struct cpuset *cs)
{
    return cs->partition_root_state < 0;
}

无效分区错误码定义在 kernel/cgroup/cpuset-internal.h:26-38:

// kernel/cgroup/cpuset-internal.h:26-38
enum prs_errcode {
    PERR_NONE = 0,
    PERR_INVCPUS,       // cpuset.cpus.exclusive 中的无效 CPU 列表
    PERR_INVPARENT,     // 父 cpuset 是无效分区根
    PERR_NOTPART,       // 父 cpuset 不是分区根
    PERR_NOTEXCL,       // cpuset.cpus 中的 CPU 不独占
    PERR_NOCPUS,        // 父 cpuset 无法向下游分发 CPU
    PERR_HOTPLUG,       // 热插拔导致无可用 CPU
    PERR_CPUSEMPTY,     // cpuset.cpus 和 cpuset.cpus.exclusive 都为空
    PERR_HKEEPING,      // 分区配置与 housekeeping 设置冲突
    PERR_ACCESS,        // 无权限启用分区
    PERR_REMOTE,        // 底层有远程分区
};

12.6.1.5 锁层次

cpuset 使用四层锁来保护其数据结构(kernel/cgroup/cpuset.c:64-115 注释),按获取顺序为:

  1. cpuset_top_mutex:防止常规 cpuset 控制文件写入与 housekeeping_update() 之间的干扰
  2. cpu_hotplug_lock:cpus_read_lock()/cpus_write_lock(),保护 CPU 热插拔操作
  3. cpuset_mutex:全局 cpuset 互斥锁,保护 cpuset 层次结构的修改
  4. callback_lock:原始自旋锁,保护 cpuset 中对外可见的字段(如掩码)的快速读写

对于可靠的只读访问,只需持有 cpuset_mutex 或 callback_lock 中的一个即可。对于修改操作,需要同时持有 cpuset_mutex 和 callback_lock。

12.6.2 cpuset 分区调度

12.6.2.1 分区概念

分区调度是 cpuset v2 的核心特性。分区将一组 CPU 独占地分配给一个 cgroup,形成独立的调度域。这意味着分区中的 CPU 不会参与其他 cgroup 的负载均衡,从而实现 CPU 资源的硬隔离。

分区有两种类型:

本地分区 (Local Partition):父 cpuset 本身就是分区根。设置 cpuset.cpus.exclusive 是可选的。

远程分区 (Remote Partition):父 cpuset 不是分区根。必须通过设置 cpuset.cpus.exclusive 沿祖先节点传递独占 CPU。由 cpuset->remote_partition 标志 (行162) 标识。

12.6.2.2 update_partition_exclusive_flag

update_partition_exclusive_flag() 函数(kernel/cgroup/cpuset.c:1116-1128)在分区状态变更时更新独占标志:

// kernel/cgroup/cpuset.c:1116-1128
static int update_partition_exclusive_flag(struct cpuset *cs, int new_prs)
{
    bool exclusive = (new_prs > PRS_MEMBER);

    if (exclusive && !is_cpu_exclusive(cs)) {
        if (cpuset_update_flag(CS_CPU_EXCLUSIVE, cs, 1))
            return PERR_NOTEXCL;
    } else if (!exclusive && is_cpu_exclusive(cs)) {
        cpuset_update_flag(CS_CPU_EXCLUSIVE, cs, 0);
    }
    return 0;
}

当启用分区时,自动设置 CS_CPU_EXCLUSIVE 标志;禁用分区时,自动清除该标志。

12.6.2.3 分区负载均衡控制

update_partition_sd_lb() 函数(kernel/cgroup/cpuset.c:1137-1162)管理分区的负载均衡状态:

// kernel/cgroup/cpuset.c:1137-1162
static void update_partition_sd_lb(struct cpuset *cs, int old_prs)
{
    int new_prs = cs->partition_root_state;
    bool rebuild_domains = (new_prs > 0) || (old_prs > 0);
    bool new_lb;

    if (new_prs > 0) {
        new_lb = (new_prs != PRS_ISOLATED);  // 隔离分区无负载均衡
    } else {
        new_lb = is_sched_load_balance(parent_cs(cs));  // 继承父 cpuset
    }
    if (new_lb != !!is_sched_load_balance(cs)) {
        rebuild_domains = true;
        if (new_lb)
            set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);
        else
            clear_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);
    }

    if (rebuild_domains)
        cpuset_force_rebuild();
}

PRS_ISOLATED 状态的分区不参与负载均衡,适用于需要极低延迟抖动的工作负载。

12.6.2.4 validate_change -- 验证 cpuset 修改

validate_change() 函数(kernel/cgroup/cpuset.c:707-768)验证对 cpuset 的修改是否遵循结构规则:

// kernel/cgroup/cpuset.c:707-768
static int validate_change(struct cpuset *cur, struct cpuset *trial)
{
    struct cgroup_subsys_state *css;
    struct cpuset *c, *par;
    bool xcpus_changed;
    int ret = 0;

    rcu_read_lock();

    if (!is_in_v2_mode())
        ret = cpuset1_validate_change(cur, trial);
    if (ret)
        goto out;

    if (cur == &top_cpuset)
        goto out;

    par = parent_cs(cur);

    /* 不能缩减到无法容纳 SCHED_DEADLINE 任务 */
    ret = -EBUSY;
    if (is_cpu_exclusive(cur) && is_sched_load_balance(cur) &&
        !cpuset_cpumask_can_shrink(cur->effective_cpus, user_xcpus(trial)))
        goto out;

    /* 检查兄弟 cpuset 之间的独占 CPU 重叠 */
    ret = -EINVAL;
    xcpus_changed = !cpumask_equal(cur->exclusive_cpus, trial->exclusive_cpus);
    cpuset_for_each_child(c, css, par) {
        if (c == cur)
            continue;
        if (cpus_excl_conflict(trial, c, xcpus_changed))
            goto out;
        if (mems_excl_conflict(trial, c))
            goto out;
    }

    ret = 0;
out:
    rcu_read_unlock();
    return ret;
}

验证步骤包括:

  1. 对于 v1 模式,调用 cpuset1_validate_change() 进行额外验证
  2. 对于根 cpuset,跳过验证
  3. 如果 cpuset 是 CPU 独占且启用负载均衡的,检查 CPU 掩码是否可以缩减而不影响 SCHED_DEADLINE 任务
  4. 遍历兄弟 cpuset,检查独占 CPU 和独占内存节点是否重叠

12.6.2.5 独占 CPU 管理

独占 CPU 通过 effective_xcpus(kernel/cgroup/cpuset-internal.h:120)和 exclusive_cpus(行134)管理:

  • exclusive_cpus:用户通过 cpuset.cpus.exclusive 设置的请求独占 CPU
  • effective_xcpus:实际被授予的独占 CPU,可能比请求的少

partition_xcpus_add() 和 partition_xcpus_del() 函数管理独占 CPU 的添加和移除。当添加独占 CPU 时,父 cpuset 的 effective_cpus 会减去这些 CPU:

// kernel/cgroup/cpuset.c:1228-1245 (简化)
static void partition_xcpus_add(int new_prs, struct cpuset *parent,
                struct cpumask *xcpus)
{
    ...
    if (parent == &top_cpuset)
        cpumask_or(subpartitions_cpus, subpartitions_cpus, xcpus);
    ...
    cpumask_andnot(parent->effective_cpus, parent->effective_cpus, xcpus);
}

全局的 subpartitions_cpus 掩码追踪所有被分发到子分区的独占 CPU。当该掩码为空时,系统使用单一的全局调度域(kernel/cgroup/cpuset.c:830-834),覆盖 99% 的系统配置。

12.6.3 cpuset 层次传播

12.6.3.1 update_cpumasks_hier -- CPU 掩码传播

update_cpumasks_hier() 函数(kernel/cgroup/cpuset.c:2086-2250)是 CPU 掩码层次传播的核心。当某个 cpuset 的 cpus_allowed 或分区状态发生变化时,该函数自顶向下更新所有后代 cpuset 的 effective_cpus:

// kernel/cgroup/cpuset.c:2086-2250 (关键部分)
static void update_cpumasks_hier(struct cpuset *cs, struct tmpmasks *tmp,
                 bool force)
{
    struct cpuset *cp;
    struct cgroup_subsys_state *pos_css;
    int old_prs, new_prs;

    rcu_read_lock();
    cpuset_for_each_descendant_pre(cp, pos_css, cs) {
        struct cpuset *parent = parent_cs(cp);
        bool remote = is_remote_partition(cp);

        old_prs = new_prs = cp->partition_root_state;

        /* 处理远程分区 */
        if (remote && (cp != cs)) {
            compute_excpus(cp, tmp->new_cpus);
            if (cpumask_equal(cp->effective_xcpus, tmp->new_cpus)) {
                pos_css = css_rightmost_descendant(pos_css);
                continue;
            }
            rcu_read_unlock();
            remote_cpus_update(cp, NULL, tmp->new_cpus, tmp);
            rcu_read_lock();
        }

        /* 计算新的 effective_cpus */
        if (remote || (is_partition_valid(parent) && is_partition_valid(cp)))
            compute_partition_effective_cpumask(cp, tmp->new_cpus);
        else
            compute_effective_cpumask(tmp->new_cpus, cp, parent);

        /* 空掩码继承父 cpuset */
        if (is_in_v2_mode() && !remote && cpumask_empty(tmp->new_cpus))
            cpumask_copy(tmp->new_cpus, parent->effective_cpus);

        /* 跳过无变化的子树 */
        if (!cp->partition_root_state && !force &&
            cpumask_equal(tmp->new_cpus, cp->effective_cpus) && ...) {
            pos_css = css_rightmost_descendant(pos_css);
            continue;
        }

        /* 更新 cpuset 数据 */
        rcu_read_unlock();
        spin_lock_irq(&callback_lock);
        cpumask_copy(cp->effective_cpus, tmp->new_cpus);
        cp->partition_root_state = new_prs;
        ...
        spin_unlock_irq(&callback_lock);

        notify_partition_change(cp, old_prs);

        /* 更新该 cpuset 中所有任务的 CPU 亲和性 */
        cpuset_update_tasks_cpumask(cp, tmp->new_cpus);
        ...
    }
}

传播算法的关键特征:

  1. 先序遍历:使用 cpuset_for_each_descendant_pre 进行先序遍历,确保父 cpuset 在子 cpuset 之前被处理
  2. 子树剪枝:如果某个 cpuset 的 effective_cpus 没有变化(且没有分区状态变化),使用 css_rightmost_descendant() 跳过整个子树
  3. RCU 保护:在 RCU 读锁内进行遍历,在需要睡眠的操作前释放锁
  4. callback_lock:在修改 effective_cpus 等字段时获取 callback_lock 自旋锁

12.6.3.2 update_nodemasks_hier -- 内存节点掩码传播

update_nodemasks_hier() 函数(kernel/cgroup/cpuset.c:2682-2723)实现了类似的层次传播机制,但针对内存节点掩码:

// kernel/cgroup/cpuset.c:2682-2723 (关键部分)
static void update_nodemasks_hier(struct cpuset *cs, nodemask_t *new_mems)
{
    struct cpuset *cp;
    struct cgroup_subsys_state *pos_css;

    rcu_read_lock();
    cpuset_for_each_descendant_pre(cp, pos_css, cs) {
        struct cpuset *parent = parent_cs(cp);

        bool has_mems = nodes_and(*new_mems, cp->mems_allowed,
                      parent->effective_mems);

        if (is_in_v2_mode() && !has_mems)
            *new_mems = parent->effective_mems;

        if (nodes_equal(*new_mems, cp->effective_mems)) {
            pos_css = css_rightmost_descendant(pos_css);
            continue;
        }

        if (!css_tryget_online(&cp->css))
            continue;
        rcu_read_unlock();

        spin_lock_irq(&callback_lock);
        cp->effective_mems = *new_mems;
        spin_unlock_irq(&callback_lock);

        cpuset_update_tasks_nodemask(cp);

        rcu_read_lock();
        css_put(&cp->css);
    }
    rcu_read_unlock();
}

与 CPU 掩码传播的主要区别:在更新 effective_mems 后,调用 cpuset_update_tasks_nodemask() 来更新所有任务的内存策略和页面迁移。

12.6.3.3 guarantee_online_cpus/mems

guarantee_online_mems() 函数(kernel/cgroup/cpuset.c:499-503)确保任务至少有一个在线的内存节点可用:

// kernel/cgroup/cpuset.c:499-503
static void guarantee_online_mems(struct cpuset *cs, nodemask_t *pmask)
{
    while (!nodes_and(*pmask, cs->effective_mems, node_states[N_MEMORY]))
        cs = parent_cs(cs);
}

如果当前 cpuset 的 effective_mems 与在线内存节点没有交集,该函数沿层次向上查找,直到找到一个有在线内存节点的祖先 cpuset。这保证了即使在极端配置下,任务也不会被放置到完全没有内存可用的状态。

12.6.4 任务迁移与 cpuset

12.6.4.1 cpuset_can_attach -- 验证任务可迁移

当任务被迁移到新的 cpuset 时,内核首先调用 cpuset_can_attach() 验证迁移是否可行。该函数检查:

  1. 目标 cpuset 是否有可用的 CPU 和内存节点
  2. SCHED_DEADLINE 任务的带宽是否可以在目标 cpuset 的 CPU 上分配

如果验证通过,attach_in_progress 计数器递增(kernel/cgroup/cpuset.c:3073),防止在 can_attach 和 attach 之间的窗口期将 cpuset 的 CPU/内存掩码清零。

12.6.4.2 cpuset_attach_task -- 设置新 cpuset

cpuset_attach_task() 函数(kernel/cgroup/cpuset.c:3110-3127)执行实际的任务迁移:

// kernel/cgroup/cpuset.c:3110-3127
static void cpuset_attach_task(struct cpuset *cs, struct task_struct *task)
{
    lockdep_assert_cpuset_lock_held();

    if (cs != &top_cpuset)
        guarantee_active_cpus(task, cpus_attach);
    else
        cpumask_andnot(cpus_attach, task_cpu_possible_mask(task),
                   subpartitions_cpus);

    WARN_ON_ONCE(set_cpus_allowed_ptr(task, cpus_attach));

    cpuset_change_task_nodemask(task, &cpuset_attach_nodemask_to);
    cpuset1_update_task_spread_flags(cs, task);
}

关键步骤:

  1. 计算任务在目标 cpuset 中可用的 CPU 掩码(cpus_attach)。对于根 cpuset,排除 subpartitions_cpus 中的 CPU
  2. 调用 set_cpus_allowed_ptr() 更新任务的 CPU 亲和性
  3. 调用 cpuset_change_task_nodemask() 更新任务的内存节点掩码和内存策略

12.6.4.3 cpuset_update_tasks_cpumask -- 批量更新 CPU 亲和性

cpuset_update_tasks_cpumask() 函数(kernel/cgroup/cpuset.c:1057-1081)批量更新 cpuset 中所有任务的 CPU 亲和性:

// kernel/cgroup/cpuset.c:1057-1081
void cpuset_update_tasks_cpumask(struct cpuset *cs, struct cpumask *new_cpus)
{
    struct css_task_iter it;
    struct task_struct *task;
    bool top_cs = cs == &top_cpuset;

    css_task_iter_start(&cs->css, 0, &it);
    while ((task = css_task_iter_next(&it))) {
        const struct cpumask *possible_mask = task_cpu_possible_mask(task);

        if (top_cs) {
            if (task->flags & (PF_KTHREAD | PF_NO_SETAFFINITY))
                continue;
            cpumask_andnot(new_cpus, possible_mask, subpartitions_cpus);
        } else {
            cpumask_and(new_cpus, possible_mask, cs->effective_cpus);
        }
        set_cpus_allowed_ptr(task, new_cpus);
    }
    css_task_iter_end(&it);
}

对于根 cpuset 中的任务,内核跳过内核线程和设置了 PF_NO_SETAFFINITY 的任务。对于其他 cpuset,将任务的 CPU 亲和性限制在 effective_cpus 与 task_cpu_possible_mask() 的交集内。

12.6.4.4 cpuset_update_tasks_nodemask -- 更新内存策略

cpuset_update_tasks_nodemask() 函数(kernel/cgroup/cpuset.c:2619-2668)更新 cpuset 中所有任务的内存节点掩码:

// kernel/cgroup/cpuset.c:2619-2668 (关键部分)
void cpuset_update_tasks_nodemask(struct cpuset *cs)
{
    static nodemask_t newmems;
    struct css_task_iter it;
    struct task_struct *task;

    cpuset_being_rebound = cs;

    guarantee_online_mems(cs, &newmems);

    css_task_iter_start(&cs->css, 0, &it);
    while ((task = css_task_iter_next(&it))) {
        struct mm_struct *mm;
        bool migrate;

        cpuset_change_task_nodemask(task, &newmems);

        mm = get_task_mm(task);
        if (!mm)
            continue;

        migrate = is_memory_migrate(cs);

        mpol_rebind_mm(mm, &cs->mems_allowed);
        if (migrate)
            cpuset_migrate_mm(mm, &cs->old_mems_allowed, &newmems);
        else
            mmput(mm);
    }
    css_task_iter_end(&it);

    cs->old_mems_allowed = newmems;
    cpuset_being_rebound = NULL;
}

cpuset_change_task_nodemask() 使用 mems_allowed_seq seqlock 安全地更新任务的 mems_allowed 和内存策略。mpol_rebind_mm() 重新绑定进程所有 VMA 的内存策略。如果启用了 CS_MEMORY_MIGRATE,还会将进程的页面从旧节点迁移到新节点。

12.6.4.5 cpuset_change_task_nodemask

cpuset_change_task_nodemask() 函数(kernel/cgroup/cpuset.c:2591-2607)使用 seqlock 机制安全地更新任务的内存节点掩码:

// kernel/cgroup/cpuset.c:2591-2607
static void cpuset_change_task_nodemask(struct task_struct *tsk,
                    nodemask_t *newmems)
{
    task_lock(tsk);

    local_irq_disable();
    write_seqcount_begin(&tsk->mems_allowed_seq);

    nodes_or(tsk->mems_allowed, tsk->mems_allowed, *newmems);
    mpol_rebind_task(tsk, newmems);
    tsk->mems_allowed = *newmems;

    write_seqcount_end(&tsk->mems_allowed_seq);
    local_irq_enable();

    task_unlock(tsk);
}

先进行 OR 操作 (nodes_or) 确保旧掩码和新掩码的并集被短暂可见,防止并行分配器看到空的交集。然后重新绑定内存策略,最后设置新的掩码。

12.6.5 cpuset 热插拔处理

12.6.5.1 cpuset_hotplug_update_tasks

cpuset_hotplug_update_tasks() 函数(kernel/cgroup/cpuset.c:3747 开始)处理 CPU 或内存节点热插拔事件后的 cpuset 更新:

// kernel/cgroup/cpuset.c:3747-3770 (关键部分)
static void cpuset_hotplug_update_tasks(struct cpuset *cs, struct tmpmasks *tmp)
{
    static cpumask_t new_cpus;
    static nodemask_t new_mems;
    bool cpus_updated;
    bool mems_updated;
    ...
retry:
    wait_event(cpuset_attach_wq, cs->attach_in_progress == 0);

    mutex_lock(&cpuset_mutex);

    if (cs->attach_in_progress) {
        mutex_unlock(&cpuset_mutex);
        goto retry;
    }

    parent = parent_cs(cs);
    compute_effective_cpumask(&new_cpus, cs, parent);
    nodes_and(new_mems, cs->mems_allowed, parent->effective_mems);

该函数首先等待所有进行中的任务迁移完成(通过 attach_in_progress 计数器),然后重新计算 effective_cpus 和 effective_mems,并在必要时更新分区状态和任务亲和性。

12.6.5.2 CPU offline 时自动调整

当 CPU 变为 offline 时,cpuset 热插拔处理会:

  1. 从所有 cpuset 的 effective_cpus 中移除 offline 的 CPU
  2. 如果分区根失去了所有 CPU,将其标记为无效 (PRS_INVALID_ROOT)
  3. 重新计算调度域
  4. 更新受影响 cpuset 中所有任务的 CPU 亲和性

12.6.5.3 内存节点 hotplug 处理

当内存节点变为 offline 时:

  1. 从所有 cpuset 的 effective_mems 中移除 offline 的节点
  2. 通过 guarantee_online_mems() 确保任务有可用的内存节点
  3. 更新受影响任务的 mems_allowed 和内存策略
  4. 如果启用了 CS_MEMORY_MIGRATE,将页面从 offline 节点迁移到新节点

12.6.6 设备控制器 (devices)

12.6.6.1 设备访问控制

设备控制器通过 devices.allow、devices.deny 和 devices.list 文件控制 cgroup 中进程对设备的访问权限。每个规则指定设备类型(字符/块/所有)、设备号和访问权限(读/写/创建)。

12.6.6.2 BPF 程序附加

在现代实现中,设备访问控制越来越多地通过 BPF 程序实现。bpf-cgroup 设备过滤允许将 BPF 程序附加到 cgroup,提供更灵活的设备访问控制策略。BPF 程序类型 BPF_CGROUP_DEVICE 可以对设备访问请求进行细粒度的过滤决策。

12.6.7 pids 控制器

pids 控制器(kernel/cgroup/pids.c)限制 cgroup 中的进程数量,防止 fork 炸弹等资源耗尽攻击。

12.6.7.1 pids_cgroup 结构

// kernel/cgroup/pids.c:49-66
struct pids_cgroup {
    struct cgroup_subsys_state css;                   // 行50

    atomic64_t counter;                               // 行56
    atomic64_t limit;                                 // 行57
    int64_t    watermark;                             // 行58

    struct cgroup_file events_file;                   // 行61
    struct cgroup_file events_local_file;             // 行62

    atomic64_t events[NR_PIDCG_EVENTS];               // 行64
    atomic64_t events_local[NR_PIDCG_EVENTS];         // 行65
};

counter (行56):当前 cgroup 及其所有子 cgroup 中的进程总数。64 位原子变量可以安全地表示 "max" 值(PIDS_MAX = PID_MAX_LIMIT + 1)。

limit (行57):进程数上限。默认为 PIDS_MAX(无限制)。

watermark (行58):历史峰值,通过 pids_update_watermark() 更新(行96-104)。该更新有轻微的竞争,但内核接受这种不精确性。

12.6.7.2 pids_try_charge/pids_uncharge

pids_try_charge() 函数(kernel/cgroup/pids.c:166-198)在 fork 时层次化地尝试增加进程计数:

// kernel/cgroup/pids.c:166-198
static int pids_try_charge(struct pids_cgroup *pids, int num,
               struct pids_cgroup **fail)
{
    struct pids_cgroup *p, *q;

    for (p = pids; parent_pids(p); p = parent_pids(p)) {
        int64_t new = atomic64_add_return(num, &p->counter);
        int64_t limit = atomic64_read(&p->limit);

        if (new > limit) {
            *fail = p;
            goto revert;
        }
        pids_update_watermark(p, new);
    }
    return 0;

revert:
    for (q = pids; q != p; q = parent_pids(q))
        pids_cancel(q, num);
    pids_cancel(p, num);
    return -EAGAIN;
}

如果任何层级的计数超过限制,回滚所有已增加的计数并返回 -EAGAIN。这导致 fork() 系统调用返回 -EAGAIN。

12.6.7.3 fork 时检查

pids_can_fork() 函数(kernel/cgroup/pids.c:273-284)在 fork 路径中被调用:

// kernel/cgroup/pids.c:273-284
static int pids_can_fork(struct task_struct *task, struct css_set *cset)
{
    struct pids_cgroup *pids, *pids_over_limit;
    int err;

    pids = css_pids(cset->subsys[pids_cgrp_id]);
    err = pids_try_charge(pids, 1, &pids_over_limit);
    if (err)
        pids_event(pids, pids_over_limit);

    return err;
}

如果 charge 失败,pids_event() 函数记录事件并通知用户空间。首次触发限制时,会打印内核日志消息:

// kernel/cgroup/pids.c:248-253
if (atomic64_inc_return(&p->events_local[PIDCG_FORKFAIL]) == 1) {
    pr_info("cgroup: fork rejected by pids controller in ");
    pr_cont_cgroup_path(p->css.cgroup);
    pr_cont("\n");
}

12.6.7.4 事件通知

pids 控制器的事件类型(kernel/cgroup/pids.c:41-47):

// kernel/cgroup/pids.c:41-47
enum pidcg_event {
    PIDCG_MAX,       // 子树中因限制触发 fork 失败
    PIDCG_FORKFAIL,  // 本 cgroup 中因祖先限制触发 fork 失败
    NR_PIDCG_EVENTS,
};

事件通过 pids.events 和 pids.events.local 文件暴露给用户空间。events 包含所有子 cgroup 的累计事件,events.local 仅包含本 cgroup 的事件。

12.6.7.5 pids 控制器子系统注册

pids 控制器通过 pids_cgrp_subsys 注册(kernel/cgroup/pids.c:449-460):

// kernel/cgroup/pids.c:449-460
struct cgroup_subsys pids_cgrp_subsys = {
    .css_alloc     = pids_css_alloc,
    .css_free      = pids_css_free,
    .can_attach    = pids_can_attach,
    .cancel_attach = pids_cancel_attach,
    .can_fork      = pids_can_fork,
    .cancel_fork   = pids_cancel_fork,
    .release       = pids_release,
    .legacy_cftypes = pids_files_legacy,
    .dfl_cftypes   = pids_files,
    .threaded      = true,        // 支持线程化 cgroup
};

注意 .threaded = true,表示 pids 控制器支持线程化 cgroup,可以在进程的线程级别进行进程数限制。

12.6.8 freezer 控制器

freezer 控制器(kernel/cgroup/freezer.c,326行)允许冻结和解冻 cgroup 中的所有任务,用于容器检查点/恢复、批量操作等场景。

12.6.8.1 cgroup_do_freeze -- 冻结/解冻所有任务

cgroup_do_freeze() 函数(kernel/cgroup/freezer.c:174-219)是冻结/解冻操作的入口:

// kernel/cgroup/freezer.c:174-219 (关键部分)
static void cgroup_do_freeze(struct cgroup *cgrp, bool freeze, u64 ts_nsec)
{
    struct css_task_iter it;
    struct task_struct *task;

    lockdep_assert_held(&cgroup_mutex);

    spin_lock_irq(&css_set_lock);
    write_seqcount_begin(&cgrp->freezer.freeze_seq);
    if (freeze) {
        set_bit(CGRP_FREEZE, &cgrp->flags);
        cgrp->freezer.freeze_start_nsec = ts_nsec;
    } else {
        clear_bit(CGRP_FREEZE, &cgrp->flags);
        cgrp->freezer.frozen_nsec += (ts_nsec - cgrp->freezer.freeze_start_nsec);
    }
    write_seqcount_end(&cgrp->freezer.freeze_seq);
    spin_unlock_irq(&css_set_lock);

    css_task_iter_start(&cgrp->self, 0, &it);
    while ((task = css_task_iter_next(&it))) {
        if (task->flags & PF_KTHREAD)
            continue;     // 内核线程不支持冻结
        cgroup_freeze_task(task, freeze);
    }
    css_task_iter_end(&it);
    ...
}

冻结操作使用 seqcount 保护冻结时间戳的读写。冻结开始时记录 freeze_start_nsec,解冻时计算累计冻结时间 frozen_nsec。内核线程 (PF_KTHREAD) 不支持冻结,被跳过。

12.6.8.2 cgroup_freeze_task -- 设置冻结陷阱

cgroup_freeze_task() 函数(kernel/cgroup/freezer.c:152-169)设置或清除任务级别的冻结标志:

// kernel/cgroup/freezer.c:152-169
static void cgroup_freeze_task(struct task_struct *task, bool freeze)
{
    unsigned long flags;

    if (!lock_task_sighand(task, &flags))
        return;

    if (freeze) {
        task->jobctl |= JOBCTL_TRAP_FREEZE;      // 设置冻结陷阱
        signal_wake_up(task, false);               // 唤醒任务以处理信号
    } else {
        task->jobctl &= ~JOBCTL_TRAP_FREEZE;      // 清除冻结陷阱
        wake_up_process(task);                      // 唤醒解冻
    }

    unlock_task_sighand(task, &flags);
}

冻结通过设置 JOBCTL_TRAP_FREEZE 位实现。当任务从内核返回用户空间时,信号处理路径会检查该位,并将任务置于 TASK_FROZEN 状态。

12.6.8.3 cgroup_enter_frozen/cgroup_leave_frozen

cgroup_enter_frozen() 函数(kernel/cgroup/freezer.c:104-117)将任务标记为冻结状态:

// kernel/cgroup/freezer.c:104-117
void cgroup_enter_frozen(void)
{
    struct cgroup *cgrp;

    if (current->frozen)
        return;

    spin_lock_irq(&css_set_lock);
    current->frozen = true;
    cgrp = task_dfl_cgroup(current);
    cgroup_inc_frozen_cnt(cgrp);
    cgroup_update_frozen(cgrp);
    spin_unlock_irq(&css_set_lock);
}

cgroup_leave_frozen() 函数(kernel/cgroup/freezer.c:128-146)将任务从冻结状态恢复。如果 cgroup 仍然设置了 CGRP_FREEZE 标志,任务不会离开冻结状态,而是重新设置 JOBCTL_TRAP_FREEZE 位:

// kernel/cgroup/freezer.c:128-146
void cgroup_leave_frozen(bool always_leave)
{
    struct cgroup *cgrp;

    spin_lock_irq(&css_set_lock);
    cgrp = task_dfl_cgroup(current);
    if (always_leave || !test_bit(CGRP_FREEZE, &cgrp->flags)) {
        cgroup_dec_frozen_cnt(cgrp);
        cgroup_update_frozen(cgrp);
        current->frozen = false;
    } else if (!(current->jobctl & JOBCTL_TRAP_FREEZE)) {
        spin_lock(&current->sighand->siglock);
        current->jobctl |= JOBCTL_TRAP_FREEZE;
        set_thread_flag(TIF_SIGPENDING);
        spin_unlock(&current->sighand->siglock);
    }
    spin_unlock_irq(&css_set_lock);
}

12.6.8.4 cgroup_propagate_frozen -- 向上传播冻结状态

cgroup_propagate_frozen() 函数(kernel/cgroup/freezer.c:36-60)将冻结状态沿层次向上传播:

// kernel/cgroup/freezer.c:36-60
static void cgroup_propagate_frozen(struct cgroup *cgrp, bool frozen)
{
    int desc = 1;

    while ((cgrp = cgroup_parent(cgrp))) {
        if (frozen) {
            cgrp->freezer.nr_frozen_descendants += desc;
            if (!test_bit(CGRP_FREEZE, &cgrp->flags) ||
                (cgrp->freezer.nr_frozen_descendants !=
                 cgrp->nr_descendants))
                continue;
        } else {
            cgrp->freezer.nr_frozen_descendants -= desc;
        }

        if (cgroup_update_frozen_flag(cgrp, frozen))
            desc++;
    }
}

当一个 cgroup 的所有后代都被冻结时(nr_frozen_descendants == nr_descendants),该 cgroup 自身也被标记为冻结。desc 计数器追踪状态变化的 cgroup 数量,用于正确更新更高层级的 nr_frozen_descendants。

12.6.8.5 迁移时的冻结处理

cgroup_freezer_migrate_task() 函数(kernel/cgroup/freezer.c:225-261)处理任务迁移时的冻结状态调整:

// kernel/cgroup/freezer.c:225-261
void cgroup_freezer_migrate_task(struct task_struct *task,
                 struct cgroup *src, struct cgroup *dst)
{
    if (task->flags & PF_KTHREAD)
        return;

    if (!test_bit(CGRP_FREEZE, &src->flags) &&
        !test_bit(CGRP_FREEZE, &dst->flags) &&
        !task->frozen)
        return;

    if (task->frozen) {
        cgroup_inc_frozen_cnt(dst);
        cgroup_dec_frozen_cnt(src);
    }
    cgroup_update_frozen(dst);
    cgroup_update_frozen(src);

    cgroup_freeze_task(task, test_bit(CGRP_FREEZE, &dst->flags));
}

如果源和目标 cgroup 都不处于冻结状态且任务本身也不冻结,可以快速返回。否则,调整冻结计数器并强制任务进入目标 cgroup 的冻结状态。

12.6.8.6 冻结延迟统计

freezer 通过 cgroup.freeze 文件暴露冻结状态,通过 cgroup.events 文件报告冻结事件。冻结延迟统计通过 freeze_start_nsec 和 frozen_nsec 字段追踪,可用于诊断冻结操作的性能影响。

12.6.9 其他控制器概览

12.6.9.1 hugetlb 控制器

hugetlb 控制器限制 cgroup 可以使用的大页 (huge page) 数量。对于每种大页大小(如 2MB、1GB),控制器提供独立的限制和统计接口。

12.6.9.2 rdma 控制器

rdma 控制器限制 cgroup 可以使用的 RDMA (Remote Direct Memory Access) 资源,包括 HCA handles 和 CQ、QP、MR 等RDMA 对象的数量。

12.6.9.3 misc 控制器

misc 控制器是一个通用的杂项资源限制控制器,允许限制不适用于专门控制器的资源类型。它提供了可扩展的框架,新资源类型可以通过注册的方式加入。

12.6.9.4 dmem 控制器

dmem 控制器(CONFIG_CGROUP_DMEM)限制 cgroup 可以使用的设备内存 (device memory) 数量,适用于 GPU、FPGA 等具有独立内存的设备。dmem 控制器也使用 page_counter 框架(page_counter_calculate_protection 函数在 CONFIG_CGROUP_DMEM 启用时也可用,见 include/linux/page_counter.h:102)。

12.7 cgroup namespace 与容器集成

cgroup namespace 是 Linux 命名空间体系的重要组成部分,为容器提供了 cgroup 层次结构的虚拟化视图。通过 cgroup namespace,容器内的进程可以看到以容器自身的 cgroup 为根的虚拟 cgroup 树,而不是宿主机的完整层次结构。本章还将讨论 PSI (Pressure Stall Information) 压力监控机制、BPF 与 cgroup 的深度集成,以及容器运行时与 cgroups v2 的实践。

12.7.1 cgroup namespace 概述

12.7.1.1 CLONE_NEWCGROUP

cgroup namespace 通过 CLONE_NEWCGROUP 标志创建,是 Linux 中第六种命名空间类型(在 mount、PID、network、IPC、user 之后引入)。当进程使用 unshare(CLONE_NEWCGROUP) 或 clone(CLONE_NEWCGROUP) 创建新的 cgroup namespace 时,新 namespace 中的 cgroup 根被设置为调用进程当前所在的 cgroup。

12.7.1.2 与其他 namespace 的关系

cgroup namespace 与其他命名空间独立,但通常与 PID namespace、mount namespace 和 user namespace 一起使用来构建完整的容器隔离环境。cgroup namespace 不影响 cgroup 的功能限制 -- 即使进程在新 namespace 中看到的是虚拟的 cgroup 根,资源限制仍然由宿主机上的实际 cgroup 层次决定。

12.7.1.3 容器场景中的作用

在容器场景中,cgroup namespace 的主要作用是:

  1. 路径虚拟化:容器内的进程看到 / 作为 cgroup 根,而不是宿主机的 /sys/fs/cgroup/.../container-a/
  2. 安全的 cgroup 委托:容器可以安全地管理自身的子 cgroup 层次,而不会影响宿主机或其他容器
  3. 迁移支持:容器检查点/恢复工具可以利用 cgroup namespace 简化状态管理

12.7.2 cgroup_namespace 结构

cgroup namespace 的结构体定义在 cgroup 内部头文件中。通过分析 kernel/cgroup/namespace.c 的代码,我们可以理解其关键字段:

12.7.2.1 核心字段

struct cgroup_namespace {
    struct ns_common      ns;           // 通用命名空间基类
    struct css_set        *root_cset;   // 命名空间的根 css_set
    struct user_namespace *user_ns;     // 关联的 user namespace
    struct ucounts        *ucounts;     // 用户计数
    struct rcu_head       ns_rcu;       // RCU 回收头
};

root_cset:这是 cgroup namespace 的核心字段。它指向创建 namespace 时进程所在的 css_set(cgroup 子系统状态集合)。该 css_set 定义了 namespace 的 cgroup 视图的根 -- namespace 中的所有路径都相对于此 css_set 对应的 cgroup 来解析。

user_ns:关联的 user namespace,用于权限检查。只有拥有 CAP_SYS_ADMIN(在关联的 user namespace 中)的进程才能创建或安装 cgroup namespace。

ns:通用的 ns_common 结构,包含 ops 指针和 inum (inode number),使得 cgroup namespace 可以被通用的 namespace 基础设施管理。

12.7.3 copy_cgroup_ns -- 创建新命名空间

copy_cgroup_ns() 函数定义在 kernel/cgroup/namespace.c:48-90,在 CLONE_NEWCGROUP 标志下被调用:

// kernel/cgroup/namespace.c:48-90
struct cgroup_namespace *copy_cgroup_ns(u64 flags,
                    struct user_namespace *user_ns,
                    struct cgroup_namespace *old_ns)
{
    struct cgroup_namespace *new_ns;
    struct ucounts *ucounts;
    struct css_set *cset;

    BUG_ON(!old_ns);

    if (!(flags & CLONE_NEWCGROUP)) {
        get_cgroup_ns(old_ns);           // 增加引用计数,共享 namespace
        return old_ns;
    }

    /* 权限检查:需要 CAP_SYS_ADMIN */
    if (!ns_capable(user_ns, CAP_SYS_ADMIN))
        return ERR_PTR(-EPERM);

    /* 用户计数检查,防止资源耗尽 */
    ucounts = inc_cgroup_namespaces(user_ns);
    if (!ucounts)
        return ERR_PTR(-ENOSPC);

    /* 获取当前进程的 css_set 作为新 namespace 的锚点 */
    spin_lock_irq(&css_set_lock);
    cset = task_css_set(current);
    get_css_set(cset);                   // 增加 css_set 引用
    spin_unlock_irq(&css_set_lock);

    /* 分配新的 cgroup_namespace */
    new_ns = alloc_cgroup_ns();
    if (IS_ERR(new_ns)) {
        put_css_set(cset);
        dec_cgroup_namespaces(ucounts);
        return new_ns;
    }

    new_ns->user_ns = get_user_ns(user_ns);
    new_ns->ucounts = ucounts;
    new_ns->root_cset = cset;            // 锚定当前 css_set

    ns_tree_add(new_ns);                 // 加入 namespace 树
    return new_ns;
}

关键设计决策:

  1. 共享语义:如果没有 CLONE_NEWCGROUP 标志,子进程共享父进程的 cgroup namespace(仅增加引用计数)
  2. 权限检查:创建新的 cgroup namespace 需要 CAP_SYS_ADMIN 能力(在关联的 user namespace 中)
  3. 资源限制:通过 inc_cgroup_namespaces() 检查用户级别的 namespace 数量限制,防止单个用户耗尽系统资源
  4. css_set 锚定:在 css_set_lock 保护下获取当前进程的 css_set 并增加引用。该 css_set 成为新 namespace 的 cgroup 视图根
  5. 无 cgroup_mutex:注释指出 "It is not safe to take cgroup_mutex here"(kernel/cgroup/namespace.c:71),因为 copy_cgroup_ns 可能在 fork 路径中被调用,此时持有 cgroup_mutex 可能导致死锁

12.7.3.1 alloc_cgroup_ns

alloc_cgroup_ns() 函数(kernel/cgroup/namespace.c:22-34)分配新的 cgroup namespace:

// kernel/cgroup/namespace.c:22-34
static struct cgroup_namespace *alloc_cgroup_ns(void)
{
    struct cgroup_namespace *new_ns __free(kfree) = NULL;
    int ret;

    new_ns = kzalloc_obj(struct cgroup_namespace, GFP_KERNEL_ACCOUNT);
    if (!new_ns)
        return ERR_PTR(-ENOMEM);
    ret = ns_common_init(new_ns);
    if (ret)
        return ERR_PTR(ret);
    return no_free_ptr(new_ns);
}

使用 GFP_KERNEL_ACCOUNT 标志分配内存,确保分配被计入 cgroup 的内核内存使用量。__free(kfree) 自动清理宏确保在错误路径上自动释放内存。

12.7.3.2 free_cgroup_ns

free_cgroup_ns() 函数(kernel/cgroup/namespace.c:36-46)释放 cgroup namespace:

// kernel/cgroup/namespace.c:36-46
void free_cgroup_ns(struct cgroup_namespace *ns)
{
    ns_tree_remove(ns);
    put_css_set(ns->root_cset);         // 释放锚定的 css_set
    dec_cgroup_namespaces(ns->ucounts); // 减少用户计数
    put_user_ns(ns->user_ns);           // 释放关联的 user namespace
    ns_common_free(ns);
    kfree_rcu(ns, ns.ns_rcu);           // RCU 延迟释放
}

释放顺序很重要:先从 namespace 树中移除,然后释放所有引用的资源,最后通过 RCU 延迟释放 namespace 结构本身。RCU 延迟确保正在进行 RCU 读侧临界区操作的其他线程能安全完成。

12.7.4 cgroupns_install -- setns 安装

cgroupns_install() 函数定义在 kernel/cgroup/namespace.c:92-110,在 setns() 系统调用路径中被调用:

// kernel/cgroup/namespace.c:92-110
static int cgroupns_install(struct nsset *nsset, struct ns_common *ns)
{
    struct nsproxy *nsproxy = nsset->nsproxy;
    struct cgroup_namespace *cgroup_ns = to_cg_ns(ns);

    if (!ns_capable(nsset->cred->user_ns, CAP_SYS_ADMIN) ||
        !ns_capable(cgroup_ns->user_ns, CAP_SYS_ADMIN))
        return -EPERM;

    /* 如果已经是同一个 namespace,无需操作 */
    if (cgroup_ns == nsproxy->cgroup_ns)
        return 0;

    get_cgroup_ns(cgroup_ns);
    put_cgroup_ns(nsproxy->cgroup_ns);
    nsproxy->cgroup_ns = cgroup_ns;

    return 0;
}

双向权限检查:安装 cgroup namespace 需要在两个 user namespace 中都拥有 CAP_SYS_ADMIN: - 调用进程的 user namespace(nsset->cred->user_ns) - 目标 cgroup namespace 关联的 user namespace(cgroup_ns->user_ns)

这确保了:只有特权用户才能将进程移入其他 cgroup namespace,且目标 namespace 也必须信任调用者。

12.7.4.1 命名空间操作表

cgroup namespace 的操作函数表定义在 kernel/cgroup/namespace.c:138-144:

// kernel/cgroup/namespace.c:138-144
const struct proc_ns_operations cgroupns_operations = {
    .name     = "cgroup",
    .get      = cgroupns_get,
    .put      = cgroupns_put,
    .install  = cgroupns_install,
    .owner    = cgroupns_owner,
};

.name = "cgroup" 定义了 /proc/<pid>/ns/ 目录下的符号链接名称,即 /proc/<pid>/ns/cgroup。

12.7.5 cgroup 路径虚拟化

12.7.5.1 命名空间内的路径映射

cgroup namespace 的核心功能是路径虚拟化。当进程查询 cgroup 路径时(如通过 /proc/self/cgroup),内核根据当前进程的 cgroup namespace 计算相对于 namespace 根的路径。

假设宿主机上的 cgroup 层次结构为:

/ (root cgroup)
  container-a/
    workload/

如果 container-a cgroup 是某个 cgroup namespace 的根,那么:

  • 在宿主机上,workload 的路径是 /container-a/workload
  • 在该 cgroup namespace 内,workload 的路径显示为 /workload
  • container-a 自身在该 namespace 中显示为 /

12.7.5.2 /proc/self/cgroup 在 namespace 中的显示

/proc/<pid>/cgroup 文件显示进程所属的 cgroup 路径。在 cgroup namespace 内,路径是相对于 namespace 根的。这是通过内核在生成 /proc/self/cgroup 内容时调用 cgroup_path() 类函数实现的,这些函数在计算路径时会考虑当前进程的 cgroup namespace 的 root_cset。

12.7.6 PSI (Pressure Stall Information)

PSI 是 Linux 内核提供的统一的资源压力监控机制,通过 cgroup 文件暴露 CPU、内存、IO 和中断处理的压力信息。

12.7.6.1 PSI 与 cgroup 的关系

PSI 通过 cgroup->psi 指针与 cgroup 集成。cgroup_psi() 内联函数(include/linux/psi.h:35-38)获取 cgroup 的 PSI 组:

// include/linux/psi.h:35-38
static inline struct psi_group *cgroup_psi(struct cgroup *cgrp)
{
    return cgroup_ino(cgrp) == 1 ? &psi_system : cgrp->psi;
}

根 cgroup (inode 1) 使用全局的 psi_system 组,非根 cgroup 使用自己的 psi_group。

psi_cgroup_alloc() 和 psi_cgroup_free() 函数负责在 cgroup 创建和销毁时分配和释放 PSI 组。

12.7.6.2 PSI 文件

每个 cgroup 提供四个 PSI 文件:

  • cpu.pressure:CPU 时间压力
  • memory.pressure:内存分配压力
  • io.pressure:IO 完成压力
  • irq.pressure:中断处理压力

此外,cgroup.pressure 文件提供该 cgroup 的总压力信息。

12.7.6.3 some/full 指标

每个 PSI 文件提供两种指标:

some:至少一个任务在等待资源的时间比例。表示 "有人正在受影响"。

full:所有任务都在等待资源的时间比例(即完全没有进展)。表示 "所有人都被阻塞"。

每种指标提供四个时间窗口的百分比值:

  • 10 秒窗口的平均值
  • 60 秒窗口的平均值
  • 300 秒窗口的平均值
  • 全量窗口(自 cgroup 创建或上次重置以来的累计值)

输出格式为:

some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

12.7.6.4 PSI 与 cgroup rstat 的关系

PSI 的统计数据通过 cgroup rstat 框架聚合。当任务的 PSI 状态发生变化时(例如进入或离开 memstall 状态),更新通过 per-CPU 变量记录,然后通过 rstat 框架在读取时聚合。

psi_memstall_enter() 和 psi_memstall_leave() 函数(include/linux/psi.h:23-24)标记内存分配停滞的开始和结束。这些函数在内存分配路径中被调用,例如在 __mem_cgroup_handle_over_high() 中(mm/memcontrol.c:2347-2349):

// mm/memcontrol.c:2347-2349
psi_memstall_enter(&pflags);
schedule_timeout_killable(penalty_jiffies);
psi_memstall_leave(&pflags);

12.7.6.5 PSI 触发器

PSI 还支持通过 eventfd 机制的主动通知。用户空间可以通过写入 PSI 文件来注册触发器(threshold),当压力超过阈值时,内核通过 eventfd 通知用户空间。这使得管理系统可以根据实时压力信息动态调整资源分配。

psi_trigger_create() 和 psi_trigger_destroy() 函数(include/linux/psi.h:26-27)管理 PSI 触发器的生命周期。

12.7.7 BPF 与 cgroup 集成

BPF (Berkeley Packet Filter) 与 cgroup 的集成是现代 Linux 内核中最重要的可编程性特性之一。通过将 BPF 程序附加到 cgroup,用户可以实现高度灵活的网络、安全和设备访问控制策略。

12.7.7.1 struct cgroup_bpf

每个 cgroup 包含一个 struct cgroup_bpf 实例(include/linux/cgroup-defs.h:622),存储附加到该 cgroup 的所有 BPF 程序:

// include/linux/cgroup-defs.h:622
struct cgroup_bpf bpf;

cgroup_bpf 结构管理 BPF 程序的多级附加机制,支持有效 (effective) 和已附加 (attached) 程序的区分。程序可以附加到 cgroup 层次中的任何层级,子 cgroup 会继承父 cgroup 的 BPF 程序。

12.7.7.2 BPF 程序类型

cgroup 相关的 BPF 程序类型包括:

  • BPF_CGROUP_INET_SOCK_CREATE:在创建 inet socket 时调用,可以修改 socket 的选项
  • BPF_CGROUP_INET_SOCK_RELEASE:在释放 inet socket 时调用
  • BPF_CGROUP_SOCK_OPS:socket 操作回调,用于 TCP 连接管理
  • BPF_CGROUP_DEVICE:设备访问控制,替代传统的 devices 控制器
  • BPF_CGROUP_INET_INGRESS/BPF_CGROUP_INET_EGRESS:网络包过滤
  • BPF_CGROUP_INET4_BIND/BPF_CGROUP_INET6_BIND:bind() 系统调用过滤
  • BPF_CGROUP_INET4_CONNECT/BPF_CGROUP_INET6_CONNECT:connect() 系统调用过滤
  • BPF_CGROUP_UNIX_CONNECT:Unix domain socket connect() 过滤
  • BPF_CGROUP_INET4_GETPEERNAME/BPF_CGROUP_INET6_GETPEERNAME:getpeername() 过滤
  • BPF_CGROUP_INET4_GETSOCKNAME/BPF_CGROUP_INET6_GETSOCKNAME:getsockname() 过滤
  • BPF_CGROUP_UDP4_SENDMSG/BPF_CGROUP_UDP6_SENDMSG:UDP sendmsg() 过滤
  • BPF_CGROUP_UDP4_RECVMSG/BPF_CGROUP_UDP6_RECVMSG:UDP recvmsg() 过滤
  • BPF_CGROUP_SOCK_ADDR:socket 地址操作
  • BPF_CGROUP_GETSOCKOPT:getsockopt() 过滤
  • BPF_CGROUP_SETSOCKOPT:setsockopt() 过滤
  • BPF_CGROUP_SYSCTL:sysctl 写入过滤
  • BPF_CGROUP_GETTYSCMD:系统调用命令获取
  • BPF_CGROUP_UNIX_RECVMSG:Unix recvmsg() 过滤

12.7.7.3 bpf_cgrp_storage -- per-cgroup BPF 存储

// include/linux/cgroup-defs.h:627-629
#ifdef CONFIG_BPF_SYSCALL
    struct bpf_local_storage __rcu *bpf_cgrp_storage;
#endif

每个 cgroup 可以拥有自己的 BPF 本地存储 (bpf_cgrp_storage)。BPF 程序可以使用该存储在 cgroup 级别保持状态,而无需使用全局映射。这对于实现有状态的 cgroup 级别策略非常有用。

BPF 本地存储通过 RCU 保护,确保 BPF 程序可以安全地并发访问存储数据。

12.7.7.4 BPF 程序附加点详解

sock_ops:BPF_CGROUP_SOCK_OPS 程序在 TCP 连接生命周期的关键点被调用,包括连接建立、数据传输和连接关闭。BPF 程序可以调整 TCP 参数(如窗口大小、延迟等),实现自定义的 TCP 优化策略。

device 过滤:BPF_CGROUP_DEVICE 程序替代了传统的 devices 控制器,提供更灵活的设备访问控制。BPF 程序接收设备类型、设备号和访问类型作为输入,返回允许或拒绝的决策。

sysctl 过滤:BPF_CGROUP_SYSCTL 程序可以在 cgroup 级别过滤 sysctl 写入操作。这允许容器运行时限制容器可以修改的 sysctl 参数。

getsockopt/setsockopt:BPF_CGROUP_GETSOCKOPT 和 BPF_CGROUP_SETSOCKOPT 程序可以拦截和修改 socket 选项的读写操作,实现自定义的 socket 行为。

12.7.8 容器运行时与 cgroups v2

12.7.8.1 runc/containerd 与 cgroups v2

runc 是 OCI (Open Container Initiative) 运行时规范的参考实现,直接管理容器的 cgroup 层次结构。从 runc 1.0 开始,完全支持 cgroups v2。

containerd 作为更高级的容器运行时,通过 runc 或其他 OCI 运行时来管理容器,自身负责 cgroup 的生命周期管理,包括:

  1. 创建容器的 cgroup 层次结构
  2. 设置资源限制(CPU、内存、IO、pids 等)
  3. 监控资源使用情况
  4. 在容器销毁时清理 cgroup

12.7.8.2 OCI runtime spec 中的 cgroups 配置

OCI runtime specification 定义了容器配置的标准格式,其中 linux.resources 部分指定 cgroup 资源限制:

{
  "linux": {
    "resources": {
      "memory": {
        "limit": 536870912,
        "swap": 1073741824
      },
      "cpu": {
        "shares": 1024,
        "quota": 50000,
        "period": 100000
      },
      "pids": {
        "limit": 1000
      },
      "blockIO": {
        "weight": 500
      }
    },
    "cgroupsPath": "/container-runtime/container-id"
  }
}

在 cgroups v2 模式下,OCI 运行时将这些配置映射到对应的 cgroup v2 控制文件。例如:

  • memory.limit 映射到 memory.max
  • memory.swap 映射到 memory.swap.max
  • cpu.shares 映射到 cpu.weight
  • cpu.quota/period 映射到 cpu.max
  • pids.limit 映射到 pids.max

12.7.8.3 systemd 与 cgroups v2 的集成

systemd 是现代 Linux 发行版的默认初始化系统,与 cgroups v2 有深度集成:

统一的 cgroup 层次管理:systemd 为每个服务 (service)、范围 (scope) 和切片 (slice) 创建 cgroup,形成三层的资源管理模型。在 cgroups v2 中,所有控制器都在单一层次下管理,使得 systemd 的 cgroup 管理更加简洁。

委托模型:systemd 支持将 cgroup 子树的管理权委托给容器运行时。通过 Delegate=yes 配置选项,systemd 允许容器运行时在服务 cgroup 下创建和管理自己的子 cgroup,而不需要 systemd 的介入。

12.7.8.4 委托模型:systemd 到容器运行时到容器

cgroups v2 的委托模型遵循以下层次:

systemd (init)
  system.slice/
    containerd.service/                    <- systemd 管理
      containerd/                          <- containerd 管理(被委托)
        <container-id>/                    <- 容器运行时管理
          workload/                        <- 容器内 workload
  1. systemd 层:systemd 创建和管理顶层的 slice/service/scope cgroup,设置高层的资源限制
  2. 容器运行时层:容器运行时(如 containerd)在被委托的 cgroup 子树中创建和管理容器的 cgroup
  3. 容器层:容器内部的进程可以在容器的 cgroup 下进一步创建子 cgroup(如果启用了 cgroup namespace 和适当的权限)

这种委托模型确保了:

  • systemd 可以管理所有进程的资源分配(包括容器运行时和容器)
  • 容器运行时可以自主管理容器的 cgroup,无需与 systemd 协调
  • 容器之间有适当的资源隔离
  • 系统管理员可以通过 systemd 工具(如 systemd-run)在更高层级设置资源限制

12.7.8.5 cgroup v2 纯层次结构下的控制器共存

cgroups v2 的纯层次结构 (pure hierarchy) 设计意味着所有控制器都在同一个 cgroup 树上工作。这与 cgroups v1 的多层次结构形成对比。在 v2 中:

  • 每个进程在每个 cgroup 树中只出现一次
  • 所有控制器的配置都在同一个 cgroup 目录下
  • 子 cgroup 的资源限制不能超过父 cgroup 的限制
  • rstat 框架提供统一的统计聚合机制

这种设计简化了 cgroup 的管理和理解,消除了 v1 中多层次结构带来的不一致性和复杂性。

12.7.9 rstat 递归统计框架

rstat (recursive statistics) 是 cgroups v2 的核心统计基础设施,为所有控制器提供高效的 per-CPU 统计聚合机制。

12.7.9.1 css_rstat_updated -- 无锁更新通知

css_rstat_updated() 函数定义在 kernel/cgroup/rstat.c:70-126,是 rstat 更新侧的核心函数:

// kernel/cgroup/rstat.c:70-126 (关键部分)
__bpf_kfunc void css_rstat_updated(struct cgroup_subsys_state *css, int cpu)
{
    struct llist_head *lhead;
    struct css_rstat_cpu *rstatc;
    struct llist_node *self;

    if (!css_uses_rstat(css))
        return;

    lockdep_assert_preemption_disabled();

    rstatc = css_rstat_cpu(css, cpu);
    if (llist_on_list(&rstatc->lnode))
        return;

    self = &rstatc->lnode;
    if (!try_cmpxchg(&rstatc->lnode.next, &self, NULL))
        return;

    lhead = ss_lhead_cpu(css->ss, cpu);
    llist_add(&rstatc->lnode, lhead);
}

该函数使用无锁设计,支持在 softirq、hardirq 甚至 NMI 上下文中安全调用。关键设计:

  1. llist_on_list 检查:如果 css 已经在更新列表中,直接返回。这个检查有竞争,但 smp_mb() 可以在需要严格保证时提供配对
  2. try_cmpxchg 选举:使用 try_cmpxchg() 原子地将 lnode.next 从指向自身(表示不在列表中)改为 NULL,选中胜者来执行 llist_add()
  3. per-CPU lhead:每个 CPU 有独立的 llist 头,消除跨 CPU 竞争

12.7.9.2 __css_process_update_tree -- 构建更新树

__css_process_update_tree() 函数定义在 kernel/cgroup/rstat.c:128-155,在刷新时构建更新树:

// kernel/cgroup/rstat.c:128-155
static void __css_process_update_tree(struct cgroup_subsys_state *css, int cpu)
{
    while (true) {
        struct css_rstat_cpu *rstatc = css_rstat_cpu(css, cpu);
        struct cgroup_subsys_state *parent = css->parent;
        struct css_rstat_cpu *prstatc;

        if (rstatc->updated_next)
            break;          // 已经在树中

        if (!parent) {
            rstatc->updated_next = css;   // 根节点
            break;
        }

        prstatc = css_rstat_cpu(parent, cpu);
        rstatc->updated_next = prstatc->updated_children;
        prstatc->updated_children = css;  // 挂到父节点的子列表

        css = parent;
    }
}

该函数自底向上构建更新树,将每个更新的 css 链接到其父节点的 updated_children 列表中。如果一个 css 已经在树中(updated_next != NULL),则其所有祖先也必然在树中,可以停止遍历。

12.7.9.3 css_rstat_flush -- 读取时刷新

css_rstat_flush() 是 rstat 读取侧的入口函数。它首先调用 css_process_update_tree() 从 per-CPU llist 中构建更新树,然后以自顶向下的顺序遍历更新树,对每个更新的 css 调用子系统注册的 css_rstat_flush 回调函数。

对于内存控制器,该回调刷新 per-CPU 统计到全局统计。对于 IO 控制器,它刷新 per-CPU IO 统计。

12.7.9.4 cgroup_base_stat_flush -- 基础统计刷新

cgroup_base_stat_flush() 刷新 cgroup 的基础统计(CPU 使用时间等)。这些统计包括:

  • user time:用户态 CPU 时间
  • system time:内核态 CPU 时间
  • nice time:低优先级任务的 CPU 时间

基础统计通过 rstat 框架以与控制器统计相同的机制聚合,确保了统一的刷新语义。

12.7.9.5 rstat 的性能特征

rstat 框架的设计优化了更新侧的性能,将开销转移到读取侧:

  • 更新侧:近乎零开销。仅进行一次 llist_on_list 检查和一次 llist_add() 操作,不需要获取任何锁
  • 读取侧:需要遍历更新树并聚合 per-CPU 统计。开销与更新的 cgroup 数量成正比,而不是总 cgroup 数量
  • 惰性刷新:统计不需要实时精确,内核可以在读取时或定期刷新时聚合

这种设计使得 rstat 特别适合高频更新(如每次页面 charge/uncharge)和低频读取(如用户查看统计文件)的场景。