控制组(cgroup)负责核算和控制 Linux 任务集合。cgroup 不是进程树节点:任务在 fork 时继承父级 cgroup,但管理器随后可以迁移它。因此,cgroup 为服务管理器和容器提供资源边界,而命名空间提供全局资源的视图。
本文描述 cgroup 第 2 版(cgroup v2)。其文档化接口是 cgroup2 文件系统;文件和受支持的控制器取决于正在运行的内核配置和可用硬件。
统一层次结构和成员关系 #
Cgroup v2 只有一个统一层次结构。每个任务恰好属于该层次结构中的一个 cgroup,包括未命名的根 cgroup。子 cgroup 继承祖先允许的约束和控制器配置,因此子级不能获得其父级未委托的 CPU、内存或设备。
层次结构由 cgroup2 挂载表示,通常为 /sys/fs/cgroup:
findmnt -t cgroup2
cat /proc/self/cgroup
ps -o pid,cgroup,comm -p $$每个目录都是一个 cgroup。创建目录会创建子 cgroup,向其 cgroup.procs 写入 PID 会迁移进程的线程组:
sudo mkdir /sys/fs/cgroup/build
printf '%s\n' "$$" | sudo tee /sys/fs/cgroup/build/cgroup.procs应使用服务管理器,而不是手动移动它自己的进程。例如,systemd 会为切片、作用域和服务创建并管理 cgroup;手动迁移可能违反管理器的核算和生命周期假设。
域 cgroup 和线程化 cgroup #
普通层次结构由域 cgroup 组成,其控制器将进程作为一个整体应用。“无内部进程”规则阻止非根域 cgroup 同时包含进程并向子级分发域控制器。应将工作负载移入叶节点,并使用空的中间节点在子 cgroup 之间分配控制器:
根 cgroup 不受此规则约束。通过 cgroup.type 配置的线程化子树允许使用 cgroup.threads 放置单个线程。它面向线程感知控制器,并具有额外的结构约束;它不会将每个控制器都变成每线程限制。在混合线程化子树和域子树之前,应检查 cgroup.type 和内核文档。
控制器和启用 #
控制器是实现资源策略的内核代码。可用性和启用是两个不同概念:
cgroup.controllers列出父级为该 cgroup 的子级提供的控制器。cgroup.subtree_control列出已启用、用于控制这些子级的控制器。写入+name或-name可以修改它。- 只有当控制器、配置和设备上下文使其适用时,控制器接口文件才会出现。
对于新的子树,应先在父级上启用控制器,然后配置子 cgroup。非根域 cgroup 在为其子级启用域控制器时不得包含进程。
cd /sys/fs/cgroup
cat cgroup.controllers
sudo sh -c 'printf "+cpu +memory +pids\n" > cgroup.subtree_control'
sudo mkdir build
sudo sh -c 'printf "50000 100000\n" > build/cpu.max'
sudo sh -c 'printf "2G\n" > build/memory.max'
sudo sh -c 'printf "512\n" > build/pids.max'示例中的 cpu.max 允许每 100000 微秒周期使用 50000 微秒 CPU 时间。它限制聚合 CPU 带宽,但不会预留专用 CPU。有效控制器集合仍然受所有祖先限制。
控制器接口 #
根目录中的控制器名称标识特定内核公开的内容。当前 v2 文档涵盖以下资源控制器族:
| 控制器 | 代表性文件 | 策略 |
|---|---|---|
| CPU | cpu.weight、cpu.max、cpu.stat | 比例共享、带宽上限和节流统计。 |
| Memory | memory.current、memory.min、memory.low、memory.high、memory.max、memory.swap.max、memory.events | 核算、保护、节流/回收压力、硬限制、交换限制和 OOM 事件。 |
| I/O | io.weight、io.max、io.stat、io.pressure | 比例和按设备的带宽/IOPS 控制及统计。 |
| Cpuset | cpuset.cpus、cpuset.cpus.effective、cpuset.mems、cpuset.mems.effective | 请求的和实际有效的 CPU 及 NUMA 节点放置。 |
| PIDs | pids.current、pids.max、pids.events | 当前进程数、硬进程限制和触及限制事件。 |
| HugeTLB | hugetlb.<size>.current、hugetlb.<size>.max、hugetlb.<size>.events | 按大页大小统计使用量和限制。 |
| RDMA | rdma.max、rdma.current、rdma.events | 按 RDMA 设备限制 HCA 对象和句柄、使用量及失败。 |
| Misc | misc.current、misc.max、misc.events | 已注册标量资源的限制。 |
memory.high 是回收和节流边界,而 memory.max 是硬限制;当回收无法推进时,它可能触发 cgroup 本地 OOM 处理。memory.oom.group 可以请求按工作负载组进行杀死。在更改限制前读取 memory.events、memory.stat 和压力停顿信息;即使限制足以容纳稳定状态 RSS,在缺页、fork 或缓存增长期间仍可能失败。
cpuset.cpus 和 cpuset.mems 表达受父级约束的请求。它们的 *.effective 对应项显示实际授予的 CPU 和 NUMA 节点,包括热插拔影响。CPU 亲和性和 cpuset 会取交集,因此任一机制都可能减少任务可运行的位置。
常见 Cgroup 文件 #
除控制器文件外,cgroup v2 还提供整个层次结构范围的文件:
cgroup.procs列出 cgroup 中的线程组 ID,并接受 PID 迁移。cgroup.threads列出线程化子树中的线程,并接受线程 ID。cgroup.events报告 cgroup 或其后代是否有成员等状态;poll 或 inotify 可以观察变化。cgroup.freeze停止和恢复非根子树中的执行,cgroup.kill杀死非根子树中的所有进程。cgroup.max.depth和cgroup.max.descendants限制层次结构增长。cgroup.stat、cgroup.type和cgroup.controllers公开层次结构和控制器状态。
文件写入是操作,而不是配置事务。管理器必须处理 EBUSY、权限失败、热插拔变化、并发迁移和部分设置。当接口允许无界值时,限制可以写为 max。
委托、命名空间和容器 #
委托使下级管理器能够控制一个子树,但不授予其控制祖先的权限。委托者创建空 cgroup,在其父级上启用必要控制器,为子树分配所有权或访问权限,并允许受委托者创建和管理后代。受委托者只能在该边界以下运行,因为祖先配置决定其可用资源。
systemd 的 Delegate=yes 是建立此类边界的常见方式。容器运行时随后组合多个机制:
- 创建或选择 cgroup,并应用主机资源策略;
- 创建用户、挂载、PID、网络和其他命名空间;
- 安排私有挂载视图,并在 PID 命名空间存在后挂载
/proc; - 执行工作负载,并保留主机侧句柄用于生命周期操作。
cgroup 命名空间改变进程看到的 cgroup 路径视图:/proc/PID/cgroup 和 cgroup 文件系统路径相对于该命名空间设置根路径。它不会创建独立的控制器层次结构、放宽祖先限制或隔离资源核算。主机侧管理器仍保留权威层次结构。
内核表示 #
用户可见的树由内核中的 cgroup 对象支持。task_struct 指向一个 css_set;css_set 为活动控制器归组任务的 cgroup_subsys_state(CSS)引用。控制器将其状态嵌入 CSS 或与 CSS 关联,并在核算和强制执行路径中使用该关联。任务可以共享 css_set,从而避免为每个任务复制相同的控制器状态组合。
这些实现细节解释了为什么迁移必须一致地更新控制器成员关系,但它们不是稳定的用户空间 ABI。对于特定内核版本,请查阅 kernel/cgroup/cgroup.c 和 include/linux/cgroup-defs.h。
与 Cgroup 第 1 版的对比 #
第 1 版允许多个独立层次结构,并允许将控制器附加到不同层次结构。因此,一个任务可以为不同控制器拥有不同成员关系,这使得应用程序的资源边界难以推理。Cgroup v2 使用一个层次结构、共同的委托规则和一致的控制器接口。系统可以在迁移期间同时挂载 v1 和 v2,但控制器不能同时绑定到两个版本。
权威的特定版本参考是 cgroup v2 内核文档。应检查目标内核的 cgroup.controllers 和该文档,而不要假设某个控制器或接口文件存在。
评论
在 GitHub 上阅读或评论(发表评论需要登录)