Control Group v2
原文:
https://docs.kernel.org/admin-guide/cgroup-v2.html
日期:2015年10月
作者:Tejun Heo(Linux内核维护者)
文档说明
本文档是cgroup v2的官方设计规范,完整描述其用户态可见的接口与行为,包括:
•核心架构设计原则
•所有控制器的具体行为规范
•系统级约束与约定
版本要求:所有后续修改必须同步更新本文档。
v1文档位置:Documentation/admin-guide/cgroup-v1/index.rst
Tejun Heo 是 Linux 内核的重要维护者,尤其在控制组(cgroups)领域做出了重大贡献。以下是关于他的主要事迹介绍:
Tejun Heo 是 Linux 内核的重要维护者,尤其在控制组(cgroups)领域做出了重大贡献。以下是关于他的主要事迹介绍:
cgroups 子系统维护者
Tejun Heo 是 Linux 内核中控制组(cgroups)子系统的核心维护者,主导了 cgroups v2 的设计与实现。
他解决了 cgroups v1 的许多设计缺陷(如多层级复杂性、线程粒度和资源竞争问题),推动了更简洁、统一的 cgroups v2 架构。
cgroups v2 的设计
统一层级:cgroups v2 采用单一层级结构,避免了 v1 多层级导致的混乱。
线程粒度支持:通过 cgroup.threads文件支持线程级资源控制,解决了 v1 中进程与线程混合管理的难题。
资源分配模型改进:引入权重(weight)、限制(max)、保护(low)等更清晰的资源分配策略。
其他内核贡献
块设备层(Block Layer):优化 I/O 调度和存储性能。
工作队列(Workqueue):重构了内核异步任务处理机制,提升效率和可维护性。
社区影响
他的工作被容器技术(如 Docker、Kubernetes)广泛依赖,cgroups v2 成为现代 Linux 资源管理的标准。
通过邮件列表和内核峰会积极推动技术讨论,以严谨的工程态度著称。
个人风格
以解决复杂系统问题见长,代码注重长期可维护性。
在 2015 年的文档中,他明确了 cgroups v2 的设计原则,强调简洁性和一致性。
Tejun Heo 的贡献使得 Linux 资源管理更加高效和可靠,为云计算和容器化技术奠定了坚实基础。
介绍
术语
"cgroup" 是 "control group" 的缩写,且永远不大写。单数形式用于指代整个功能特性,也作为限定词使用,如 "cgroup controllers"。当明确指代多个独立的控制组时,使用复数形式 "cgroups"。
什么是cgroup?
cgroup是一种将进程按层级结构组织起来,并以可控且可配置的方式沿层级分配系统资源的机制。
cgroup主要由两部分组成——核心部分和控制器。cgroup核心主要负责按层级组织进程。cgroup控制器通常负责沿层级分配特定类型的系统资源,但也存在一些实用控制器,其用途并非资源分配。
cgroups形成树状结构,系统中的每个进程都只属于一个cgroup。一个进程的所有线程都属于同一个cgroup。创建时,所有进程都会被放入父进程当时所属的cgroup中。进程可以被迁移到另一个cgroup。进程的迁移不会影响已存在的子进程。
在遵循特定结构约束的前提下,可以有针对性地在cgroup上启用或禁用控制器。所有控制器的行为都是层级化的——如果在某个cgroup上启用了控制器,它将影响属于该cgroup及其包含的整个子层级中所有cgroup的进程。当在嵌套的cgroup上启用控制器时,它总是会进一步限制资源分配。层级中靠近根节点设置的资源限制不能被远离根节点的设置覆盖。
基础操作
挂载
与v1不同,cgroup v2只有单一层级结构。可通过以下命令挂载cgroup v2层级:
mount -t cgroup2 none $MOUNT_POINT
cgroup2文件系统的魔数为
0x63677270
(“cgrp”)。所有支持v2且未绑定到v1层级的控制器会自动绑定到v2层级,并显示在根目录下。未在v2层级中活跃使用的控制器可绑定到其他层级。这允许以完全向后兼容的方式混合v2层级与传统的v1多层级结构。
控制器只有在当前层级不再被引用时才能跨层级移动。由于每个cgroup的控制器状态是异步销毁的,且控制器可能存在残留引用,因此在最终卸载前一层级后,控制器可能不会立即出现在v2层级上。同样,控制器需要完全禁用才能从统一层级中移出,且禁用后的控制器可能需要一些时间才能用于其他层级。此外,由于控制器间的依赖关系,可能需要同时禁用其他控制器。
虽然动态移动控制器对开发和手动配置有用,但强烈不建议在生产环境中动态切换v2和其他层级。建议在系统启动后使用控制器之前,就确定层级结构和控制器关联。
在过渡到v2期间,系统管理软件可能仍会在启动时自动挂载v1 cgroup文件系统,从而在手动干预之前劫持所有控制器。为了方便测试和实验,内核参数
cgroup_no_v1=
可以禁用v1控制器,使其始终在v2中可用。
cgroup v2目前支持以下挂载选项:
’nsdelegate
将cgroup命名空间视为委托边界。此选项是系统范围的,只能在挂载时设置或通过init命名空间重新挂载修改。非init命名空间的挂载会忽略此选项。详情请参阅"委托"部分。
favordynmods
以增加fork和exit等热路径操作为代价,减少动态cgroup修改(如任务迁移和控制器开关)的延迟。创建cgroup、启用控制器并通过
CLONE_INTO_CGROUP
初始化的静态使用模式不受此选项影响。
memory_localevents
仅填充当前cgroup的
memory.events
数据,不包括子树。这是传统行为,默认行为(无此选项)是包含子树计数。此选项是系统范围的,只能在挂载时设置或通过init命名空间重新挂载修改。非init命名空间的挂载会忽略此选项。
memory_recursiveprot
递归地将
memory.min
和
memory.low
保护应用于整个子树,无需显式向下传播到叶子cgroup。这允许保护整个子树彼此不受干扰,同时保留这些子树内部的自由竞争。这本应是默认行为,但作为挂载选项以避免破坏依赖原始语义的设置(例如在更高树级别指定虚假的高"绕过"保护值)。
memory_hugetlb_accounting
将HugeTLB内存使用计入cgroup的总体内存使用(用于统计报告和内存保护)。这是一种新行为,可能影响现有设置,因此必须通过此挂载选项显式启用。
需要注意以下几点:
内存控制器不涉及HugeTLB池管理。预分配的池不属于任何人。具体来说,当新HugeTLB folio分配到池时,从内存控制器的角度看不会被计入。只有在实际使用时(例如在缺页时)才会计入cgroup。主机内存超配管理在配置硬限制时需考虑这一点。通常,HugeTLB池管理应通过其他机制(如HugeTLB控制器)完成。
未能将HugeTLB folio计入内存控制器会导致
SIGBUS
。即使HugeTLB池仍有可用页面(但达到cgroup限制且回收尝试失败),也可能发生这种情况。
将HugeTLB内存计入内存控制器会影响内存保护和回收动态。任何用户空间调优(如调整low、min限制)都需要考虑这一点。
未选择此选项时使用的HugeTLB页面不会被内存控制器跟踪(即使后续重新挂载cgroup v2)。
pids_localevents
此选项恢复
pids.events:max
的v1类行为,即仅统计本地(cgroup内部)的fork失败。无此选项时,
pids.events.max
表示cgroup子树中的任何
pids.max
强制执行。
线程管理
cgroup v2支持对部分控制器实现线程粒度控制,以满足跨进程线程组的层级资源分配需求。默认情况下,进程的所有线程属于同一cgroup(该cgroup也作为非线程专属资源消耗的宿主域)。线程模式允许线程分散在子树中,同时为它们维护公共资源域。
支持线程模式的控制器称为
线程化控制器
(threaded controllers),不支持的称为
域控制器
(domain controllers)。
将cgroup标记为线程化会使其作为线程化cgroup加入父cgroup的资源域。父cgroup可能是另一个线程化cgroup(其资源域在层级中更靠上)。线程化子树的根(即最近的未线程化祖先)称为
线程化域
或
线程根
,作为整个子树的资源域。
在线程化子树中:
进程的线程可放入不同cgroup
不受"无内部进程"约束限制(非叶子cgroup即使不含线程也可启用线程化控制器)
线程化域cgroup承载子树的所有域资源消耗,因此无论是否包含进程都被视为具有内部资源消耗,且不能拥有非线程化的已填充子cgroup。根cgroup不受"无内部进程"约束,故可同时作为线程化域和域cgroup的父级。
cgroup的当前操作模式通过
cgroup.type
文件显示,包含三种状态:
普通域(domain)
作为线程化子树根的域(domain threaded)
线程化cgroup(threaded)
新创建的cgroup默认为域cgroup,可通过写入
threaded
转换为线程化(单向操作):
echo threaded > cgroup.type
转换需满足以下条件:
父cgroup必须是有效的(线程化)域或线程化cgroup
若父cgroup是未线程化域,则不得启用任何域控制器或包含已填充的域子cgroup(根cgroup除外)
拓扑无效的cgroup(如新创建的域cgroup未连接到可承载子域的父级)会显示
domain (invalid)
状态,相关操作将返回
EOPNOTSUPP
错误。
域cgroup在以下情况转为线程化域:
子cgroup变为线程化
启用
cgroup.subtree_control
中的线程化控制器且该cgroup包含进程
cgroup.threads
文件列出cgroup中所有线程ID(格式与行为同
cgroup.procs
,但操作粒度是线程级)。写入
cgroup.threads
仅能在同一线程化域内移动线程。
线程化域cgroup作为整个子树的资源域:
其
cgroup.procs
包含子树中所有进程PID(子树内部不可读)
子树中任意位置写入
cgroup.procs
可迁移目标进程的所有线程
仅线程化控制器可在线程化子树中启用。启用后,控制器仅统计和控制与该cgroup及其后代中线程相关的资源消耗,非线程专属消耗归属于线程化域cgroup。
当前支持的线程化控制器包括:
cpu
cpuset
perf_event
pids
填充状态通知
每个非根cgroup的
cgroup.events
文件包含
populated
字段:
0
:该cgroup及其后代无存活进程
1
:存在存活进程
当值变化时触发
poll
和
[id]notify
事件。例如:可在某子树的所有进程退出后触发清理操作。填充状态更新与通知是递归的。
示例(括号内数字表示各cgroup的进程数):
A(4) - B(0) - C(1) \ D(0)
此时A、B、C的
populated
为1,D为0。当C的进程退出后,B和C的
populated
会变为0,并生成文件修改事件。
控制器管理
可用性
当控制器满足以下条件时,会在cgroup中可用:
内核支持(已编译且未禁用)
未绑定到v1层级
列于
cgroup.controllers
文件中
此时控制器接口文件会暴露在该cgroup目录下,允许观察或控制目标资源的分配。
启用与禁用
每个cgroup的
cgroup.controllers
文件列出所有可启用的控制器:
cd /sys/fs/cgroup # cat cgroup.controllers cpu io memory
默认不启用任何控制器。通过写入
cgroup.subtree_control
文件启用/禁用控制器:
echo “+cpu +memory -io” > cgroup.subtree_control
规则说明:
仅
cgroup.controllers
列出的控制器可操作
多操作同时指定时,全部成功或全部失败
对同一控制器的多次操作,最后一次生效
层级控制逻辑
启用控制器的cgroup将控制该资源在其直接子cgroup间的分配。例如以下层级(括号内为启用的控制器):
A(cpu,memory) - B(memory) - C() \ D()
A启用
cpu
和
memory
,控制B的CPU和内存分配
B仅启用
memory
,故C和D自由竞争CPU,但内存分配受B控制
控制器接口文件(非
cgroup.
前缀)由父cgroup创建/管理:
在B启用
cpu
会在C/D创建
cpu.
前缀文件
在B禁用
memory
会移除C/D的
memory.
前缀文件
约束条件
自上而下约束
资源分配遵循自上而下原则:
非根cgroup只能启用父cgroup已启用的控制器
若子cgroup启用了某控制器,父cgroup无法禁用该控制器
无内部进程约束
非根cgroup必须满足以下条件才能启用域控制器:
自身不包含任何进程
所有进程必须迁移到子cgroup
此约束保证:
启用域控制器的层级中,进程仅存在于叶子节点
避免子cgroup与父cgroup内部进程竞争资源
根cgroup不受此限制,因其包含:
未关联到其他cgroup的进程
匿名资源消耗(由各控制器特殊处理)
特殊说明
若cgroup未启用任何控制器:
可自由创建子cgroup并迁移进程
需先完成进程迁移再启用控制器
委托机制
委托模型
cgroup可通过两种方式委托:
特权降级
:授予用户对目录及其
cgroup.procs
、
cgroup.threads
、
cgroup.subtree_control
文件的写入权限
命名空间自动委托
:若设置
nsdelegate
挂载选项,在创建cgroup命名空间时自动委托
由于资源控制接口文件控制父级资源分配,委托方不应获得其写入权限:
•
方法1
:通过权限控制禁止访问这些文件
•
方法2
:通过挂载命名空间隐藏命名空间外的文件,内核会拒绝从cgroup命名空间内部对命名空间根目录下所有文件的写入(
/sys/kernel/cgroup/delegate
列表中的文件除外,包括
cgroup.procs
、
cgroup.threads
、
cgroup.subtree_control
等)
两种委托的最终效果相同:用户可在委托目录下构建子层级、自由组织进程并分配从父级接收的资源。所有控制器的限制与设置均为层级化,确保子层级内的操作无法突破父级施加的资源约束。
当前cgroup对委托子层级的cgroup数量和嵌套深度无限制,但未来可能显式限制。
委托隔离性
委托的子层级具有隔离性——委托方无法将进程移入或移出该子层级。
对降级用户的隔离实现
:
非root用户迁移进程时需满足以下条件:
对目标
cgroup.procs
文件有写入权限
对源和目标cgroup共同祖先的
cgroup.procs
文件有写入权限
这两项约束确保委托方可在子层级内自由迁移进程,但无法从外部拉入或向外部推出进程。
示例
:
假设用户U0被委托了C0和C1,并创建了子cgroup:
若用户U0尝试将C10中的进程PID写入
C00/cgroup.procs
:
虽对目标文件有写入权限
但共同祖先位于委托点之上,用户U0无其
cgroup.procs
的写入权限
操作将被拒绝(返回
-EACCES
)
对命名空间的隔离实现
:
要求源和目标cgroup均能从执行迁移操作的进程所在命名空间访问。若任一方不可达,迁移将被拒绝(返回
-ENOENT
)。
使用指南
一次性组织与持续控制
进程跨cgroup迁移是相对昂贵的操作,且状态性资源(如内存)不会随进程迁移。这是明确的设计选择——迁移操作与热路径执行在同步成本上存在固有权衡。
建议做法
:
在启动时根据系统逻辑和资源结构将工作负载分配到对应cgroup
动态调整资源分配应通过修改控制器的接口文件实现
避免
频繁迁移进程作为资源限制调整手段
避免命名冲突
cgroup及其子cgroup的接口文件共享同一目录,可能因创建子cgroup导致名称冲突。
命名规范
:
核心接口文件前缀为
cgroup.
(如
cgroup.procs
)
控制器接口文件前缀为
控制器名.
(如
cpu.weight
)
控制器名称由小写字母和
_
组成,且不以
_
开头
接口文件不会以常见工作负载分类术语开头或结尾(如
job
、
service
、
slice
、
unit
、
workload
)
注意事项
:
cgroup不主动防止命名冲突
用户需自行确保子cgroup命名不与接口文件重复
资源分配模型
cgroup控制器根据资源类型和使用场景实现多种分配方案,主要模型如下:
权重分配(Weights)
机制
:
父级资源按子cgroup的权重比例分配
仅当前能使用资源的子cgroup参与分配(工作保留性)
适用于无状态资源(如CPU时间片)
参数范围
:
权重值:
[1, 10000]
(默认值100)
支持双向对称比例调整,同时保持直观数值范围
特点
:
只要权重值在范围内,所有配置组合均有效
无需拒绝配置变更或进程迁移操作
示例
:
cpu.weight
—— 按比例分配CPU时间给活跃子cgroup
限额分配(Limits)
机制
:
子cgroup的消耗量不得超过设定限额
支持超额分配(子cgroup限额总和可超过父级资源总量)
参数范围
:
限额值:
[0, max]
(默认值
max
表示无限制)
特点
:
因支持超额分配,所有配置组合均有效
无需拒绝配置变更或进程迁移操作
示例
:
io.max
—— 限制cgroup在IO设备上的最大带宽(BPS)或IOPS
资源保护与分配模型
资源保护(Protections)
机制
:
当所有祖先cgroup的资源使用量低于其保护阈值时,当前cgroup的资源消耗将受到保护(不超过配置值)
保护机制可以是
硬性保证
或
尽力而为的软性边界
支持超额配置(子cgroup的保护总量可超过父级可用资源,实际仅保护父级可用部分)
参数范围
:
保护值:
[0, max]
(默认值
0
,表示无保护)
特点
:
因支持超额配置,所有配置组合均有效
不会因保护配置拒绝进程迁移或参数修改
示例
:
memory.low
—— 对内存使用提供尽力而为的软性保护
独占分配(Allocations)
机制
:
为cgroup
独占分配
固定数量的有限资源(如实时CPU时间片)
禁止超额配置
(子cgroup分配总量不得超过父级可用资源)
参数范围
:
分配值:
[0, max]
(默认值
0
,表示不分配资源)
特点
:
无效配置(如子级分配总和超限)会被拒绝
若资源为进程执行的必要条件(如实时CPU),可能拒绝迁移请求
示例
:
cpu.rt.max
—— 硬性分配实时CPU时间片
接口文件格式规范
cgroup接口文件需遵循以下格式标准(按优先级排序):
1. 换行分隔值
适用场景
:单次写入仅支持单个值
读取示例
:
VAL0 VAL1 ...
写入规则
:每次写入必须为完整单行(如
echo "VAL0" > file
)
2. 空格分隔值
适用场景
:只读文件或支持单次写入多值
示例
:
VAL0 VAL1 ...
写入规则
:支持单次写入多个空格分隔值(如
echo "VAL0 VAL1" > file
)
3. 扁平键值对
格式
:
KEY0 VAL0 KEY1 VAL1 ...
写入规则
:
每次仅允许写入单个键的值(如
echo "KEY0 VAL0" > file
)
键名区分大小写
4. 嵌套键值对
格式
:
复制KEY0 SUB_KEY0=VAL00 SUB_KEY1=VAL01... KEY1 SUB_KEY0=VAL10 SUB_KEY1=VAL11... ...
写入规则
:
每次仅允许操作单个主键(如
echo "KEY0 SUB_KEY0=VAL00" > file
)
子键可无序/省略(如仅更新部分子键值)
通用规则
1.
读写一致性
:可写文件的写入格式需与读取格式匹配(允许省略尾部字段)
2.
快捷操作
:控制器可为高频场景提供简化写入语法(如省略默认键名)
3.
错误处理
:无效格式写入必须返回
EINVAL
错误
cgroup接口规范
基本原则
1.
单一功能单一文件
:每个功能配置应当集中在一个文件中
2.
根cgroup豁免
:根cgroup不参与资源控制,不应包含资源控制接口文件
3.
时间单位规范
:默认使用微秒(μs),若使用其他单位必须明确标注单位后缀
数值格式标准
1.
百分比数值
:使用包含至少两位小数的百分数格式(如13.40)
2.
权重配置
:
•
接口文件必须命名为"weight"
•
取值范围[1, 10000]
•
默认值设为100(对应100%)
3.
资源限制配置
:
•
绝对资源保证/限制:使用"min"/"max"命名
•
尽力而为资源保证/限制:使用"low"/"high"命名
•
无限值统一使用"max"表示
默认值配置规范
1.
默认值标记
:
•
必须使用"default"作为键名
•
必须作为文件中的第一条记录
2.
默认值修改
:
•
支持两种格式:
echo
"default 125"
> file
echo
125 > file
3.
覆盖规则
:
•
设置特定覆盖值:
echo
"8:16 170"
> file
•
清除特定覆盖值(恢复默认):
echo
"8:0 default"
> file
事件通知机制
1.
事件文件
:
•
对非高频事件,应当创建"events"接口文件
•
文件内容为事件键值对列表
2.
通知触发
:
•
当可通知事件发生时
•
必须生成文件修改事件
示例说明
# 初始状态 # cat cgroup-example-interface-file default 150 8:0 300 # 修改默认值 echo 125 > file # 或 echo "default 125" > file # 设置设备8:16的覆盖值 echo "8:16 170" > file # 清除设备8:0的覆盖值 echo "8:0 default" > file # 最终状态 # cat cgroup-example-interface-file default 125 8:16 170
核心接口文件
所有 cgroup 核心文件均以
cgroup.
为前缀。
1. cgroup.type
类型
:读写单值文件(仅非根 cgroup 存在)
读取值
:
"domain"
:普通有效的域 cgroup
"domain threaded"
:作为线程化子树根的域 cgroup
"domain invalid"
:处于无效状态的 cgroup(无法填充或启用控制器,但可转为线程化 cgroup)
"threaded"
:线程化 cgroup(属于线程化子树成员)
写入操作
:写入
"threaded"
可将 cgroup 转为线程化(单向操作)。
例如我的手机是domain
2. cgroup.procs
类型
:读写换行分隔值文件(所有 cgroup 存在)
读取
:列出属于该 cgroup 的所有进程 PID(无序,可能重复)。
写入
:写入 PID 可迁移对应进程至该 cgroup。需满足:
对目标
cgroup.procs
有写入权限
对源和目标 cgroup 的共同祖先
cgroup.procs
有写入权限
线程化 cgroup
:读取返回
EOPNOTSUPP
(因所有进程属于线程根),写入会迁移进程的所有线程。
3. cgroup.threads
类型
:读写换行分隔值文件(所有 cgroup 存在)
读取
:列出属于该 cgroup 的所有线程 TID(无序,可能重复)。
写入
:写入 TID 可迁移对应线程至该 cgroup。需满足:
对目标
cgroup.threads
有写入权限
源 cgroup 与目标 cgroup 在同一资源域
对共同祖先
cgroup.procs
有写入权限
4. cgroup.controllers
类型
:只读空格分隔值文件(所有 cgroup 存在)
内容
:列出该 cgroup 可用的所有控制器(无序)。
实测如下:
5. cgroup.subtree_control
类型
:读写空格分隔值文件(初始为空)
读取
:列出已启用控制器(控制资源分配到子 cgroup)。
写入
:以
+
(启用)或
-
(禁用)前缀操作控制器(如
"+cpu -memory"
)。多次操作同一控制器时,最后一次生效。
6. cgroup.events
类型
:只读扁平键值文件(非根 cgroup 存在)
键值
:
populated
:
1
(含存活进程或其子 cgroup 含进程)或
0
frozen
:
1
(已冻结)或
0
事件
:值变更时触发文件修改事件。
7. cgroup.max.descendants
类型
:读写单值文件(默认
"max"
)
功能
:限制子 cgroup 的最大数量(超限时创建失败)。
8. cgroup.max.depth
类型
:读写单值文件(默认
"max"
)
功能
:限制当前 cgroup 下的最大嵌套深度(超限时创建子 cgroup 失败)。
9. cgroup.stat
类型
:只读扁平键值文件
键值
:
nr_descendants
:可见子 cgroup 总数
nr_dying_descendants
:待销毁子 cgroup 总数(删除后进入此状态)
nr_subsys_
:当前及子 cgroup 的活动子系统数(如
memory
)
nr_dying_subsys_
:当前及子 cgroup 的待销毁子系统数
10. cgroup.freeze
类型
:读写单值文件(非根 cgroup 存在,默认
"0"
)
操作
:
写入
"1"
:冻结该 cgroup 及所有子 cgroup(进程停止运行)
写入
"0"
:解冻
特性
:
冻结状态传播:祖先 cgroup 冻结会导致后代冻结
进程可被终止或迁移(迁入停止,迁出恢复运行)
不影响 cgroup 树操作(可删除冻结的空 cgroup 或创建子 cgroup)
11. cgroup.kill
类型
:只写单值文件(非根 cgroup 存在)
操作
:写入
"1"
杀死该 cgroup 及所有子 cgroup 中的进程(发送
SIGKILL
)。
线程化 cgroup
:写入返回
EOPNOTSUPP
(因需操作整个线程组)。
12. cgroup.pressure
类型
:读写单值文件(默认
"1"
)
操作
:
"0"
:禁用 PSI(Pressure Stall Information)统计
"1"
:启用 PSI 统计
特性
:非层级化(不影响后代 cgroup)。
我的android 15 手机没有该文件节点
13. irq.pressure
类型
:读写嵌套键值文件
功能
:显示 IRQ/SOFTIRQ 的压力停滞信息(详见
Documentation/accounting/psi.rst
)。
我的android 15 手机没有该文件节点
CPU控制器
CPU控制器负责调节CPU时间片的分配。该控制器为普通调度策略实现了
权重
和
绝对带宽限制
模型,并为实时调度策略实现了
绝对带宽分配
模型。
在上述所有模型中,时间片分配仅基于时间维度,不考虑任务执行频率。(可选的)利用率钳制功能允许向
schedutil
CPU频率调节器提示:
最低期望频率
(CPU必须始终提供的频率)
最高期望频率
(CPU不应超过的频率)
警告
当前cgroup v2的CPU控制器
不支持
实时进程的带宽控制。对于启用
CONFIG_RT_GROUP_SCHED
选项的内核(用于实时进程的组调度),必须满足以下条件才能启用CPU控制器:
所有实时进程必须位于
根cgroup
系统管理软件可能在启动过程中已将实时进程放入非根cgroup,需手动迁移至根cgroup
若禁用
CONFIG_RT_GROUP_SCHED
,此限制不适用,部分接口文件可影响或统计实时进程。
仅CPU控制器受此配置影响
,其他控制器对实时进程的资源控制不受限制。
CPU接口文件
进程与CPU控制器的交互取决于其调度策略和底层调度器。CPU控制器将进程分为三类:
1.
公平调度类(fair-class)进程
2.
支持
cgroup_set_weight
回调的BPF调度器进程
3.
其他进程
:包括
SCHED_FIFO
、
SCHED_RR
、
SCHED_DEADLINE
以及不支持
cgroup_set_weight
回调的BPF调度器进程
(详见
Documentation/scheduler/sched-ext.rst
)
文件说明
1. cpu.stat
类型
:只读扁平键值文件(无论控制器是否启用均存在)
全局统计项
(涵盖所有进程):
•
usage_usec
:总CPU使用时间
•
user_usec
:用户态CPU时间
•
system_usec
:内核态CPU时间
公平调度类统计项
(仅控制器启用时显示):
•
nr_periods
:调度周期数
•
nr_throttled
:被限制次数
•
throttled_usec
:被限制总时间
•
nr_bursts
:突发使用次数
•
burst_usec
:突发使用总时间
2. cpu.weight
类型
:读写单值文件(非根cgroup,默认值
100
)
取值范围
:
[1, 10000]
(若
cpu.idle=1
则显示为
0
)
作用范围
:仅影响公平调度类和支持
cgroup_set_weight
回调的BPF调度器进程
3. cpu.weight.nice
类型
:读写单值文件(非根cgroup,默认值
0
)
取值范围
:
[-20, 19]
(对应
nice
值)
功能
:通过
nice
值读写权重(范围更小、粒度更粗)
作用范围
:同
cpu.weight
4. cpu.max
类型
:读写双值文件(非根cgroup,默认值
max 100000
)
格式
:
$MAX $PERIOD
(如
50000 100000
表示每100ms分配50ms)
•
max
表示无限制
•
单数值写入时仅更新
$MAX
作用范围
:仅影响公平调度类进程
5. cpu.max.burst
类型
:读写单值文件(非根cgroup,默认值
0
)
取值范围
:
[0, $MAX]
(
$MAX
来自
cpu.max
)
作用范围
:仅影响公平调度类进程
6. cpu.pressure
类型
:读写嵌套键值文件
功能
:显示CPU压力停滞信息(详见
Documentation/accounting/psi.rst
)
作用范围
:统计所有进程
7. cpu.uclamp.min
类型
:读写单值文件(非根cgroup,默认值
0
)
格式
:百分比有理数(如
12.34
表示12.34%)
功能
:设置最低利用率限制(钳制任务级设置,含实时进程)
约束
:实际值不超过
cpu.uclamp.max
作用范围
:影响所有进程
8. cpu.uclamp.max
类型
:读写单值文件(非根cgroup,默认值
max
)
格式
:百分比有理数(如
98.76
表示98.76%)
功能
:设置最高利用率限制(钳制任务级设置,含实时进程)
作用范围
:影响所有进程
9. cpu.idle
类型
:读写单值文件(非根cgroup,默认值
0
)
功能
:设为
1
时,cgroup内进程视为
SCHED_IDLE
优先级(相对同级cgroup极低优先级)
作用范围
:仅影响公平调度类进程
内存控制器
内存控制器负责内存资源的分配与管理。由于内存具有状态性且与回收压力紧密相关,其分配模型较为复杂。当前已跟踪以下主要内存使用类型:
用户态内存(页面缓存和匿名内存)
内核数据结构(如目录项和索引节点)
TCP套接字缓冲区
接口文件说明
所有内存数值以字节为单位,写入值若非页大小(
PAGE_SIZE
)倍数,读取时可能向上取整。
1. 基础控制文件
文件
类型
默认值
功能说明
memory.current
只读单值
-
当前cgroup及其后代的内存使用总量
memory.min
读写单值
0
硬保护:内存使用低于此值时绝不回收;若系统无可用内存则触发OOM
memory.low
读写单值
0
软保护:内存使用低于此值时仅当无其他可回收内存才回收
memory.high
读写单值
max
限流阈值:超限时进程被抑制并面临高回收压力(不触发OOM)
memory.max
读写单值
max
硬限制:超限且无法回收时触发OOM
memory.reclaim
只写嵌套键
-
主动触发内存回收(如echo "1G" > memory.reclaim)
memory.peak
读写单值
-
记录cgroup历史峰值内存使用,写入非空字符串可重置计数
memory.oom.group
读写单值
0
设为1时OOM killer将cgroup视为不可分割单元(全杀或全留)
2. 事件监控文件
文件
类型
监控内容
memory.events
只读扁平键
层级化事件统计(如low/high/oom触发次数)
memory.events.local
只读扁平键
仅本地cgroup事件统计(非层级化)
memory.stat
只读扁平键
详细内存分类统计(含anon/file/slab等20+项)
memory.numa_stat
只读嵌套键
按NUMA节点分列的内存使用统计
3. Swap控制文件
文件
类型
默认值
功能说明
memory.swap.current
只读单值
-
当前swap使用总量
memory.swap.high
读写单值
max
swap限流阈值(超限后抑制新分配)
memory.swap.max
读写单值
max
swap硬限制(超限后禁止swap-out)
memory.swap.events
只读扁平键
-
swap事件统计(如high/max触发次数)
4. Zswap控制文件
文件
类型
默认值
功能说明
memory.zswap.current
只读单值
-
zswap压缩后端内存使用量
memory.zswap.max
读写单值
max
zswap硬限制(超限后拒绝新存储)
memory.zswap.writeback
读写单值
1
设为0时禁用所有swap-out(含zswap回写)
5. 压力监控文件
文件
类型
功能说明
memory.pressure
只读嵌套键
内存压力停滞信息(详见PSI文档)
关键机制说明
1.
保护与限制层级
•
memory.min
和
memory.low
受祖先cgroup设置约束
•
若子cgroup保护值总和超父级可用内存,按实际使用比例分配保护额度
2.
Swap管理特性
•
memory.swap.high
用于优雅限流,
memory.swap.max
用于强制限制
•
降低swap限制后,现有swap条目逐步回收,可能暂时超限
3.
内存所有权规则
•
内存归属创建它的cgroup,进程迁移不转移已分配内存
•
跨cgroup共享的内存可能被任意cgroup计费,但倾向于留在有足够配额的cgroup
4.
NUMA亲和性
•
memory.numa_stat
提供NUMA节点粒度统计,辅助结合CPU分配优化性能
使用建议
1.
主要控制策略
•
用
memory.high
实现弹性限制,依赖全局内存压力自动调节
•
超限时管理代理可动态调整配额或终止任务
2.
内存压力评估
•
当前缺乏内置压力监测机制,需结合
memory.stat
和
memory.pressure
人工判断
3.
性能优化
•
对高频访问的共享文件,使用
posix_fadvise(POSIX_FADV_DONTNEED)
明确释放所有权
•
监控
workingset_*
统计项识别活跃工作集
4.
OOM处理
•
对关键服务设置
memory.oom.group=1
避免部分杀死
•
豁免进程需设置
oom_score_adj=-1000
IO控制器
该控制器负责IO资源的分配,支持
权重比例分配
和
绝对带宽/IOPS限制
两种模式(权重模式需使用
cfq-iosched
调度器,且不适用于
blk-mq
设备)。
接口文件说明
1. 统计监控文件
文件
类型
功能说明
io.stat
只读嵌套键
按设备号($MAJ:$MIN)统计:
• rbytes/wbytes:读写字节数
• rios/wios:读写IO次数
• dbytes/dios:丢弃操作统计
示例输出
:
8:16 rbytes=1459200 wbytes=314773504 rios=192 wios=353 dbytes=0 dios=0 8:0 rbytes=90430464 wbytes=299008000 rios=8950 wios=1252 dbytes=50331648 dios=3021
2. 权重控制文件
文件
类型
默认值
功能说明
io.weight
读写扁平键
default 100
• 首行为默认权重(1-10000)
• 后续行可覆盖特定设备权重(如8:16 200)
• 写default恢复默认
示例操作
:
echo "default 150" > io.weight # 修改默认权重 echo "8:0 50" >> io.weight # 设置设备8:0的权重 echo "8:0 default" >> io.weight # 恢复设备8:0的默认权重
3. 带宽限制文件
文件
类型
功能说明
io.max
读写嵌套键
按设备号限制:
• rbps/wbps:读写带宽(字节/秒)
• riops/wiops:读写IOPS
示例操作
:
echo "8:16 rbps=2097152 wiops=120" > io.max # 限制设备8:16读带宽2MB/s、写IOPS 120 echo "8:16 wiops=max" > io.max # 取消写IOPS限制
输出示例
:
8:16 rbps=2097152 wbps=max riops=max wiops=120
4. 高级控制文件(仅根cgroup)
文件
类型
功能说明
io.cost.qos
读写嵌套键
配置IO成本模型的QoS:
• enable=1启用控制
• rpct/wpct:延迟百分位(0-100)
• rlat/wlat:延迟阈值(微秒)
• min/max:缩放比例(1-10000)
io.cost.model
读写嵌套键
配置成本模型参数:
• model=linear:线性模型
• `[r
QoS配置示例
:
echo "8:16 enable=1 ctrl=auto rpct=95 rlat=75000 wpct=95 wlat=150000 min=50 max=150" > io.cost.qos
5. 压力监控文件
文件
类型
功能说明
io.pressure
只读嵌套键
显示IO压力停滞信息(详见PSI文档)
关键特性
1.
权重分配
•
权重值范围
[1, 10000]
,默认
100
•
高权重cgroup获得更多IO时间片
2.
限制模式
•
支持按设备设置
带宽(BPS)
和
IOPS
硬限制
•
临时突发允许超限,但持续超限会触发延迟
3.
动态调节
•
io.cost.qos
可基于延迟百分位动态调整IO速率
•
ctrl=auto
时内核自动优化参数,
ctrl=user
时手动配置
4.
成本模型
•
线性模型(
model=linear
)计算IO基础成本
•
工具
iocost_coef_gen.py
可生成设备特定系数
使用建议
1.
关键服务保障
•
为高优先级cgroup分配更高权重(如
io.weight=5000
)
•
对延迟敏感服务配置
io.cost.qos
的严格延迟阈值
2.
带宽限制场景
•
限制备份服务的写带宽:
echo "8:0 wbps=104857600" > io.max
(100MB/s)
•
限制日志服务的IOPS:
echo "8:16 wiops=500" > io.max
3.
监控与调优
•
通过
io.stat
实时监控设备级IO负载
•
结合
io.pressure
识别IO瓶颈
回写机制(Writeback)
回写机制负责将
脏页
(通过缓冲写入或共享内存映射修改)异步写入底层文件系统。它位于
内存域
和
IO域
之间,通过平衡脏页生成和回写IO来调节脏内存比例。
cgroup回写控制
内存控制器
:定义脏内存比例的计算和维护范围(内存域)。
IO控制器
:定义执行脏页回写的IO域。
限制规则
:同时检查
全局
和
cgroup级
的脏内存状态,并应用更严格的限制。
文件系统支持
当前支持cgroup回写的文件系统:
ext2、ext4、btrfs、f2fs、xfs
其他文件系统
:所有回写IO归属于
根cgroup
内存与回写的所有权差异
维度
内存所有权
回写所有权
跟踪粒度
按页(page)
按inode(索引节点)
归属变更
首次使用时分配,释放前不变
动态调整(若某cgroup长期占多数脏页)
外源页(Foreign Pages)
定义
:内存归属于某cgroup,但其inode归属于另一cgroup的脏页。
处理
:若某cgroup长期占多数脏页,回写机制会将inode所有权切换至该cgroup。
限制场景
单inode多cgroup写入
:可能导致IO归属错误(不建议使用此模式)。
系统参数与cgroup回写
sysctl参数
cgroup回写行为
vm.dirty_background_ratio
按比例限制脏内存,上限受内存控制器和系统空闲内存约束
vm.dirty_ratio
同上
vm.dirty_background_bytes
转换为相对于总可用内存的比例,行为同vm.dirty_background_ratio
vm.dirty_bytes
同上
IO延迟控制(IO Latency)
功能
为cgroup设置延迟目标(
io.latency
),若平均延迟超限,则限制
低优先级同级cgroup
的IO。
层级约束
:仅
同级cgroup
相互影响(如右图A/B/C相互制约,D/F相互制约,G不受影响)。
限制机制
1.
队列深度限制
:从无限制逐步降至
1个IO
。
2.
人工延迟注入
:对无法直接限制的IO(如交换、元数据IO),通过
use_delay
和
delay
字段统计延迟(单次最多1秒)。
接口文件
文件
功能
io.latency
设置延迟目标(微秒),如8:16 target=75000
io.stat
新增统计项:
• depth:当前队列深度
• avg_lat:指数移动平均延迟
IO优先级(IO Priority)
策略属性(io.prio.class)
策略
行为
数值
no-change
不修改IO优先级
0
promote-to-rt
非RT请求升为RT类,优先级设为4;RT类保持不变
1
restrict-to-be
无类/RT类请求降为BE类,优先级设为0;IDLE类保持不变
2
idle
所有请求设为IDLE类(最低优先级)
3
优先级类数值
类
数值
IOPRIO_CLASS_NONE
0
IOPRIO_CLASS_RT
1
IOPRIO_CLASS_BE
2
IOPRIO_CLASS_IDLE
3
请求优先级设置逻辑
1.
若策略为
promote-to-rt
:设为
RT
类,优先级=4。
2.
否则:取
策略数值
与
请求当前类数值
的较大者作为最终类。
关键建议
1.
避免多cgroup并发写入同一inode
,以防IO归属错误。
2.
IO延迟调优
:
•
初始值设为高于设备预期延迟
•
根据
io.stat
的
avg_lat
调整,最终值比观测值高10-15%
3.
优先级策略
:对延迟敏感任务使用
promote-to-rt
,后台任务用
idle
。
PID控制器
PID控制器用于限制cgroup内可创建的进程/线程数量。当达到指定限制时,将阻止新的fork()或clone()操作。
接口文件说明
文件
类型
默认值
功能说明
pids.max
读写单值
max
硬性限制cgroup及其子cgroup中的进程总数
pids.current
只读单值
-
当前cgroup及其子cgroup中的进程总数
pids.peak
只读单值
-
记录cgroup及其子cgroup中进程数量的历史峰值
pids.events
只读扁平键
-
层级化事件统计:
• max:达到pids.max限制的次数
pids.events.local
只读扁平键
-
本地事件统计(不包含子cgroup事件)
关键特性
1.
限制粒度
•
统计单位是内核级线程ID(TID),即每个线程计为1个进程
•
包含cgroup自身及其所有子cgroup的进程总数
2.
限制执行
•
通过
fork()
/
clone()
创建新进程时,若导致
pids.current > pids.max
则返回
-EAGAIN
•
但
通过其他方式(如直接附加进程)可能使
pids.current
暂时超限
3.
典型应用场景
•
防止fork炸弹耗尽系统PID
•
限制容器内进程数量(作为内存控制的补充)
示例操作
# 设置进程数限制为100 echo 100 > pids.max # 查看当前进程数 cat pids.current # 监控突破限制事件 tail -f pids.events
注意:该控制器不限制通过
exec
替换进程的操作,仅限制新进程的创建。
Cpuset控制器
该控制器用于将任务限制在指定的CPU和内存节点上运行,尤其适用于NUMA系统以减少跨节点内存访问和争用。
接口文件说明
1. CPU分配
文件
类型
功能说明
cpuset.cpus
读写多值文件
请求该cgroup使用的CPU列表(如0-4,6,8-10),空值表示继承父cgroup或使用所有CPU
cpuset.cpus.effective
只读多值文件
实际可用的在线CPU列表(受父cgroup约束)
cpuset.cpus.exclusive
读写多值文件
独占CPU列表(用于创建分区,需满足与兄弟cgroup的互斥规则)
cpuset.cpus.exclusive.effective
只读多值文件
实际可用的独占CPU列表(用于分区)
cpuset.cpus.isolated
只读多值文件
(仅根cgroup)显示所有隔离分区中的CPU
2. 内存节点分配
文件
类型
功能说明
cpuset.mems
读写多值文件
请求该cgroup使用的内存节点列表(如0-1,3),空值表示继承父cgroup或使用所有节点
cpuset.mems.effective
只读多值文件
实际可用的在线内存节点列表(受父cgroup约束)
3. 分区控制
文件
类型
功能说明
cpuset.cpus.partition
读写单值文件
设置分区状态:
• member:普通成员
• root:分区根
• isolated:无负载均衡的隔离分区
关键概念
1. 层级约束
子cgroup的CPU/内存节点必须是父cgroup的子集
根cgroup默认包含所有CPU和内存节点
2. 分区(Partition)
分区根
:独占一组CPU,外部cgroup无法使用这些CPU
本地分区
:父cgroup也是分区根(
cpuset.cpus.exclusive
可隐式继承)
远程分区
:父cgroup非分区根(需显式设置
cpuset.cpus.exclusive
)
隔离分区
:CPU禁用调度器负载均衡和工作队列(需手动绑定任务到各CPU)
3. 独占CPU规则
同一父cgroup下,兄弟cgroup的独占CPU不能重叠
独占CPU分配需通过
cpuset.cpus.exclusive
显式声明
4. 状态有效性
有效分区根
需满足:
•
父cgroup是有效分区根(本地分区)
•
cpuset.cpus.exclusive.effective
非空(允许离线CPU)
•
若无任务关联,
cpuset.cpus.effective
可为空
5. 动态事件
CPU/内存热插拔或配置变更可能导致分区状态失效
通过
poll
/
inotify
监控
cpuset.cpus.partition
状态变化
操作示例
1. 设置CPU和内存节点
# 分配CPU 0-3和内存节点0-1 echo "0-3" > cpuset.cpus echo "0-1" > cpuset.mems
2. 创建独占分区
# 声明独占CPU 2-3 echo "2-3" > cpuset.cpus.exclusive # 设置为分区根 echo "root" > cpuset.cpus.partition
3. 创建隔离分区
# 声明独占CPU 4-5并隔离 echo "4-5" > cpuset.cpus.exclusive echo "isolated" > cpuset.cpus.partition
4. 查看实际分配
cat cpuset.cpus.effective # 实际可用CPU cat cpuset.mems.effective # 实际可用内存节点 cat cpuset.cpus.partition # 当前分区状态
注意事项
1.
内存迁移成本
:修改
cpuset.mems
会迁移任务内存,建议在任务启动前设置
2.
分区切换风险
:将分区根改为
member
会导致子分区失效
3.
隔离CPU
:通过内核启动参数
isolcpus
预配置的CPU需放入隔离分区使用
设备控制器(Device Controller)
该控制器通过BPF程序管理设备文件的访问权限(包括
mknod
创建设备文件及读写现有设备文件)。
实现机制
无接口文件
:完全基于
cgroup BPF
实现
控制方式
:
1.
编写类型为
BPF_PROG_TYPE_CGROUP_DEVICE
的BPF程序
2.
通过
BPF_CGROUP_DEVICE
标志附加到目标cgroup
执行逻辑
:
•
程序接收
bpf_cgroup_dev_ctx
结构体参数(描述设备访问类型及设备号)
•
返回
0
拒绝访问(
-EPERM
),非
0
允许访问
示例参考
内核源码中的示例程序:
tools/testing/selftests/bpf/progs/dev_cgroup.c
RDMA控制器
该控制器用于分配和统计RDMA(远程直接内存访问)资源。
接口文件
文件
类型
功能说明
rdma.max
读写嵌套键
设置RDMA设备资源硬限制:
• hca_handle:最大HCA句柄数
• hca_object:最大HCA对象数
rdma.current
只读嵌套键
显示当前RDMA资源使用量
示例配置
:
# 限制mlx4设备的HCA句柄数为2,对象数为2000 echo "mlx4_0 hca_handle=2 hca_object=2000" > rdma.max # 查看当前使用量 cat rdma.current # 输出示例:mlx4_0 hca_handle=1 hca_object=20
设备内存控制器(DMEM)
该控制器管理设备内存区域(如GPU显存)的分配与统计,计量单位为字节。
接口文件
文件
类型
功能说明
dmem.max
读写嵌套键
设置设备内存区域硬限制(如
drm/0000:03:00.0/vram0 1073741824
)
dmem.min
读写嵌套键
设置设备内存最小保障量
dmem.low
读写嵌套键
设置设备内存软保护阈值
dmem.capacity
只读嵌套键
(仅根cgroup)显示设备内存总容量(含内核保留部分)
dmem.current
只读嵌套键
显示当前设备内存使用量
示例配置
:
# 限制vram0区域最大使用1GB echo "drm/0000:03:00.0/vram0 1073741824" > dmem.max # 查看显存总容量(根cgroup) cat dmem.capacity # 输出示例:drm/0000:03:00.0/vram0 8514437120
HugeTLB控制器
用于限制和控制大页(HugeTLB)的使用量,并在缺页异常时强制实施限制。
接口文件
文件
类型
功能说明
hugetlb.
.current
只读单值
显示当前
大页的使用量(非根cgroup)
hugetlb.
.max
读写单值
设置/显示
大页的硬限制(默认max,非根cgroup)
hugetlb.
.events
只读扁平键
层级化事件统计:
• max:因超限导致的分配失败次数
hugetlb.
.events.local
只读扁平键
本地事件统计(非层级化)
hugetlb.
.numa_stat
只读嵌套键
按NUMA节点统计大页使用量(单位:字节)
示例操作
:
# 限制2MB大页使用不超过10页 echo "10" > hugetlb.2048KB.max # 查看当前使用量 cat hugetlb.2048KB.current # 监控分配失败事件 tail -f hugetlb.2048KB.events
Misc控制器
管理无法抽象为其他资源的标量资源(需通过
CONFIG_CGROUP_MISC
启用)。
接口文件
文件
类型
功能说明
misc.capacity
只读扁平键
(仅根cgroup)显示平台可用资源及容量(如res_a 50)
misc.current
只读扁平键
显示当前资源使用量(含子cgroup)
misc.peak
只读扁平键
显示历史峰值使用量
misc.max
读写扁平键
设置资源硬限制(可超过capacity)
misc.events
只读扁平键
层级化事件统计:
• max:资源使用即将超限的次数
misc.events.local
只读扁平键
本地事件统计(非层级化)
示例操作
:
# 限制res_a资源使用不超过1单位 echo "res_a 1" > misc.max # 查看当前使用量 cat misc.current # 输出示例:res_a 3 res_b 0 # 监控超限事件 tail -f misc.events
资源所有权规则
资源首次使用时归属当前cgroup,释放前不转移
迁移进程
不会
将已使用的资源转移至目标cgroup
其他控制器
perf_event控制器
自动启用
:在cgroup v2层级挂载时自动生效,支持按cgroup路径过滤perf事件
向后兼容
:即使v2层级已启用,仍可迁移至传统层级
非规范性信息
CPU控制器根cgroup行为
根cgroup的线程被视为各自独立的子cgroup
线程权重由其
nice
值决定(映射关系见
sched_prio_to_weight
数组,
nice 0
对应权重100)
IO控制器根cgroup行为
根cgroup进程托管在隐式叶子节点中
分配IO资源时,该节点视为权重为200的普通子cgroup
cgroup命名空间
基础功能
创建方式
:通过
clone(2)
或
unshare(2)
使用
CLONE_NEWCGROUP
标志
作用
:虚拟化
/proc/$PID/cgroup
文件及cgroup挂载点的视图
行为示例
# 命名空间创建前 cat /proc/self/cgroup 0::/batchjobs/container_id1 # 显示完整路径 # 创建新命名空间后 unshare -C cat /proc/self/cgroup 0::/ # 仅显示命名空间根路径
关键特性
进程范围
:多线程进程中任一线程调用
unshare
会影响整个进程
生命周期
:当无进程或挂载引用时销毁(底层cgroup保留)
信息隔离
:防止容器内进程获取宿主cgroup路径(如
/batchjobs/container_id1
)
与传统层级差异
v2层级
:自然支持全进程范围的命名空间切换
传统层级
:此行为可能导致意外结果
根视图与命名空间
cgroupns根目录
定义
:调用
unshare(2)
时进程所在的cgroup路径(如
/batchjobs/container_id1
)
初始命名空间
:根cgroup为真实根路径
/
固定性
:即使进程后续迁移到其他cgroup,原cgroupns根目录保持不变
进程视图隔离
命名空间内进程
:仅能通过
/proc/$PID/cgroup
看到相对于其cgroupns根的路径
初始命名空间
:始终显示完整全局路径
兄弟命名空间
:显示相对于调用者cgroupns根的路径(如
/../container_id2/sub_cgrp_1
)
示例
:
# 在cgroupns内查看进程路径(根为/batchjobs/container_id1) cat /proc/7353/cgroup 0::/sub_cgrp_1 # 在初始命名空间查看同一进程 cat /proc/7353/cgroup 0::/batchjobs/container_id1/sub_cgrp_1
迁移与命名空间操作
跨命名空间移动进程
条件
:进程需有目标cgroup的访问权限
路径显示
:迁移后路径相对于调用者cgroupns根(如
/../container_id2
)
不推荐场景
:应避免让cgroupns内进程感知外部层级
setns(2)限制
权限要求
:
•
对当前用户命名空间有
CAP_SYS_ADMIN
•
对目标cgroupns的用户命名空间有
CAP_SYS_ADMIN
行为
:附加进程需手动移至目标cgroupns根
与其他命名空间的交互
挂载cgroupfs
操作
:在非初始cgroupns内挂载统一层级
mount -t cgroup2 none
$MOUNT_POINT
效果
:以cgroupns根为文件系统根目录
权限
:需对用户和挂载命名空间有
CAP_SYS_ADMIN
容器隔离
机制
:
•
/proc/self/cgroup
虚拟化
•
命名空间私有cgroupfs挂载
目的
:提供完全隔离的cgroup视图
内核编程支持
回写(Writeback)支持
文件系统需通过以下函数支持cgroup回写:
1.
wbc_init_bio(@wbc, @bio)
•
调用时机
:在bio关联设备队列后、提交前
•
功能
:将bio绑定到inode所属cgroup和请求队列
2.
wbc_account_cgroup_owner(@wbc, @folio, @bytes)
•
调用时机
:写入数据段时(推荐随数据添加至bio时调用)
3.
启用支持
:在
super_block
中设置
SB_I_CGROUPWB
标志
•
例外处理
:某些文件系统特性(如日志模式)需跳过
wbc_init_bio
,直接使用
bio_associate_blkg
废弃的v1特性
1.
层级限制
:
•
不支持多层级(含命名层级)
•
所有v1挂载选项失效
2.
接口变更
:
•
移除
tasks
文件,
cgroup.procs
不再排序
•
移除
cgroup.clone_children
3.
替代方案
:
•
/proc/cgroups
无效,改用根cgroup的:
•
cgroup.controllers
:列出可用控制器
•
cgroup.stat
:全局统计信息
cgroup v1的问题与v2的设计理由
多层级问题
cgroup v1允许任意数量的层级,每个层级可以托管任意数量的控制器。虽然这看似提供了高度的灵活性,但在实践中并不实用。
例如,由于每个控制器只有一个实例,像
freezer
这样的通用控制器虽然可以在所有层级中使用,但只能绑定到一个层级。这个问题进一步加剧的原因是:一旦层级被填充,控制器就无法迁移到其他层级。另一个问题是,绑定到同一层级的所有控制器被迫拥有完全相同的层级视图,无法根据特定控制器的需求调整粒度。
在实践中,这些问题严重限制了哪些控制器可以放在同一层级,大多数配置最终只能为每个控制器创建独立的层级。只有紧密相关的控制器(如
cpu
和
cpuacct
)适合放在同一层级。这通常意味着用户空间最终需要管理多个相似的层级,每次执行层级管理操作时都必须在每个层级上重复相同的步骤。
此外,支持多层级的代价很高。它不仅极大地复杂化了cgroup核心的实现,更重要的是,多层级支持限制了cgroup的通用使用方式以及控制器的功能。
层级数量没有限制,这意味着线程的cgroup成员关系无法用有限长度描述。键(key)可能包含任意数量的条目,且长度不受限制,这使得操作变得非常笨拙,并导致新增仅用于标识成员关系的控制器,进而加剧了层级数量膨胀的原始问题。
此外,由于控制器无法对其他控制器的层级拓扑有任何预期,每个控制器都必须假设所有其他控制器都绑定在完全正交的层级上。这使得控制器之间无法协作,或者至少协作起来非常繁琐。
在大多数用例中,将控制器放在完全正交的层级上并不必要。通常需要的是能够根据特定控制器调整不同的粒度级别。换句话说,从特定控制器的视角来看,层级可以从叶子节点向根节点折叠。例如,某个配置可能不关心内存如何分配超过某个层级,但仍希望控制CPU周期的分配方式。
cgroup v1的问题与v2的设计理由
线程粒度问题
cgroup v1允许进程的线程属于不同的cgroup。这对某些控制器来说没有意义,这些控制器最终实现了不同的方式来忽略这种情况,但更重要的是,它模糊了暴露给单个应用程序的API和系统管理接口之间的界限。
通常情况下,进程内部的知识仅对进程本身可用;因此,与进程的服务级组织不同,对进程的线程进行分类需要目标进程所属应用程序的主动参与。
cgroup v1有一个定义模糊的委托模型,该模型与线程粒度结合后被滥用。cgroup被委托给单个应用程序,以便它们可以创建和管理自己的子层级,并控制资源分配。这实际上将cgroup提升到了类似于系统调用的API状态,暴露给普通程序。
首先,cgroup的接口从根本上就不适合以这种方式暴露。为了让进程访问自己的控制项,它必须从
/proc/self/cgroup
中提取目标层级的路径,通过附加控制项的名称构造路径,然后打开并读取或写入。这不仅极其笨拙和不寻常,而且本质上是竞态条件(race condition)的。没有传统的方法来定义跨步骤的事务,也无法保证进程实际上是在操作自己的子层级。
cgroup控制器实现了许多控制项,这些控制项永远不会被接受为公共API,因为它们只是在系统管理的伪文件系统中添加控制项。cgroup最终拥有一些没有适当抽象或优化的接口控制项,直接暴露了内核内部细节。这些控制项通过定义不清的委托机制暴露给单个应用程序,实际上滥用了cgroup作为实现公共API的捷径,而没有经过必要的审查。
这对用户空间和内核都很痛苦。用户空间最终得到了行为不当且抽象不足的接口,而内核则无意中暴露并锁定了一些结构。
内部节点与线程之间的竞争
cgroup v1允许线程属于任何cgroup,这导致了一个有趣的问题:属于父cgroup及其子cgroup的线程会竞争资源。这很糟糕,因为两种不同类型的实体在竞争,并且没有明显的方法来解决它。不同的控制器采取了不同的处理方式。
cpu控制器
:将线程和cgroup视为等效,并将nice级别映射到cgroup权重。这在某些情况下有效,但当子cgroup希望分配特定比例的CPU周期且内部线程数量波动时,比例会不断变化。此外,从nice级别到权重的映射并不明显或通用,还有一些其他控制项根本不适用于线程。
io控制器
:隐式地为每个cgroup创建一个隐藏的叶子节点来托管线程。隐藏的叶子节点拥有所有带
leaf_
前缀的控制项副本。虽然这允许对内部线程进行等效控制,但存在严重缺点:它总是添加一个不必要的额外嵌套层,使接口混乱,并显著增加了实现的复杂性。
memory控制器
:无法控制内部任务和子cgroup之间发生的情况,行为也没有明确定义。曾尝试添加临时行为和控制项以针对特定工作负载定制行为,但这会导致长期难以解决的问题。
多个控制器在处理内部任务时遇到了困难,并提出了不同的解决方案;不幸的是,所有这些方法都存在严重缺陷,而且行为差异巨大,使得cgroup整体上高度不一致。
这显然是一个需要从cgroup核心以统一方式解决的问题。
其他接口问题
cgroup v1在没有监督的情况下发展,并出现了大量的特性和不一致性。cgroup核心的一个问题是如何通知空cgroup——为每个事件fork并执行一个用户空间辅助程序。事件传递既不是递归的,也不是可委托的。该机制的限制还导致了内核内事件传递过滤机制,进一步复杂化了接口。
控制器接口也有问题。一个极端的例子是控制器完全忽略层级组织,将所有cgroup视为直接位于根cgroup下。一些控制器向用户空间暴露了大量不一致的实现细节。
控制器之间也没有一致性。当创建新的cgroup时,一些控制器默认不施加额外限制,而另一些则禁止任何资源使用,直到显式配置。相同类型控制的配置项使用了完全不同的命名方案和格式。统计和信息控制项的命名随意,甚至在同一个控制器中也使用了不同的格式和单位。
cgroup v2在适当的地方建立了通用约定,并更新控制器,使其暴露最小且一致的接口。
控制器问题与改进
内存控制器
原始下限(软限制)
:定义为默认未设置的限制。因此,全局回收偏好的cgroup集合是选择加入(opt-in)而非选择退出(opt-out)。优化这些主要是负查找的成本非常高,以至于实现尽管规模庞大,甚至无法提供基本期望的行为。
•
软限制没有层级意义。所有配置的组被组织在一个全局红黑树中,并被视为平等的对等体,无论它们在层级中的位置如何。这使得子树委托变得不可能。
•
软限制回收过程过于激进,不仅引入了高分配延迟,还因过度回收影响系统性能,甚至使功能自相矛盾。
memory.low边界
:是一种自上而下分配的保留。当cgroup在其有效下限内时,它享有回收保护,这使得子树委托成为可能。当超过有效下限时,它还享有与其超额成比例的回收压力。
原始上限(硬限制)
:定义为严格的限制,即使需要调用OOM killer也不能让步。但这通常与充分利用可用内存的目标相悖。工作负载的内存消耗在运行时变化,这要求用户过度分配。但使用严格的上限需要相当准确的工作集大小预测或为限制添加余量。由于工作集大小估计困难且容易出错,且错误会导致OOM终止,大多数用户倾向于选择更宽松的限制,最终浪费宝贵资源。
memory.high边界
:可以设置得更加保守。当达到时,它通过强制分配进入直接回收来限制分配,但从不调用OOM killer。因此,选择过于激进的高边界不会终止进程,而是会导致性能逐渐下降。用户可以监控这一点并进行纠正,直到找到仍可接受性能的最小内存占用。
在极端情况下,如果存在许多并发分配且组内回收完全崩溃,高边界可能会被突破。但即便如此,从其他组或系统的余量中满足分配通常比终止组更好。否则,
memory.max
可以限制这种溢出,并最终控制错误甚至恶意的应用程序。
将原始的
memory.limit_in_bytes
设置为低于当前使用量会受到竞态条件的影响,其中并发分配可能导致限制设置失败。而
memory.max
会首先设置限制以防止新分配,然后回收并OOM终止,直到满足新限制——或者写入
memory.max
的任务被终止。
内存+交换(swap)的联合统计和限制被替换为对交换空间的真正控制。
原始cgroup设计中支持内存+交换联合设施的主要论点是,全局或父级压力总是能够交换子组的所有匿名内存,无论子组自己的(可能不可信的)配置如何。然而,不可信的组可以通过其他方式破坏交换——例如在紧密循环中引用其匿名内存——管理员在过度分配不可信作业时不能假设完全可交换性。
对于可信作业,联合计数器并不是直观的用户空间接口,并且违背了cgroup控制器应统计和限制特定物理资源的理念。交换空间是系统中与其他资源一样的资源,这就是为什么统一层级允许单独分配它。
cgroup2提交记录
原文:
https://lwn.net/Articles/785081/
该实现已合并至Linux 5.2内核,成为cgroup v2的标准功能。其设计显著提升了容器化场景下的进程控制能力。
邮件核心内容翻译
主题:[PATCH v10 0/9] cgroup v2的freezer功能实现
发件人:Roman Gushchin (Facebook内核团队)
日期:2019年4月5日
摘要:
本补丁集为cgroup v2实现了freezer控制器,其功能与v1版本类似,但接口设计遵循cgroup v2规范,并提供更优的用户体验:
•支持终止冻结进程(v1无法杀死冻结进程)
•完善ptrace调试支持
•无需单独启用控制器(与v1需加载freezer子系统不同)
•更简洁的sysfs接口(通过cgroup.freeze文件控制)
技术演进关键点
版本迭代
主要改进
v10→v9 移除冗余信号检查,重构vfork支持
v9→v8 增加vfork测试用例,重命名状态变量(stopped→frozen)
v8→v7 简化冻结任务计数逻辑,合并nr_stopped和nr_frozen
v7→v6 修复"stopped+frozen"双重状态处理,优化cgroup_exit()检查
v6→v5 支持与系统级freezer协同工作,增强SIGSTOP/PTRACE兼容性
v5→v4 重构信号处理逻辑(基于Oleg Nesterov建议)
v4→v3 移除css_set_lock依赖,修复文档和锁问题
v3→v2 放弃TASK_FROZEN状态,改用TASK_INTERRUPTIBLE
v2→v1版本变更的完整翻译和技术解析
核心补丁说明
1.文件重构•将freezer.c重命名为legacy_freezer.c(v1旧实现)•新建freezer.c实现v2版本
2.基础设施•新增__cgroup_task_count()辅助函数统计任务数•用css_set_lock保护cgroup层级计数
3.核心功能•通过写入cgroup.freeze文件控制冻结状态bash复制echo 1 > /sys/fs/cgroup/
/cgroup.freeze # 冻结 echo 0 > /sys/fs/cgroup/
/cgroup.freeze # 解冻•冻结状态传播:父cgroup冻结时,子cgroup同步冻结
4.测试与调试•新增6个kselftest用例(覆盖SIGSTOP/PTRACE等场景)•添加内核tracepoint便于问题排查c下载复制运行TRACE_EVENT(cgroup_freeze, ...) // 追踪冻结状态变更
5.文档更新•在cgroup-v2.rst中详细说明接口用法和限制
与v1 freezer的关键差异
特性
v1 freezer
v2 freezer
控制文件
freezer.state
cgroup.freeze
进程状态
TASK_STOPPED
TASK_INTERRUPTIBLE
信号处理
完全屏蔽信号
允许SIGKILL终止进程
调试支持
难以ptrace冻结进程
完整支持ptrace
层级传播
需手动同步父子状态
自动继承父cgroup冻结状态
性能影响
1.延迟优化冻结/解冻操作平均延迟从v1的120ms降至v2的35ms(测试环境:64核服务器)
2.资源消耗•每个cgroup增加约16字节内存开销(用于状态跟踪)•无额外锁竞争(复用css_set_lock)
补丁序号
标题
核心功能
技术要点
1/9
cgroup: rename freezer.c into legacy_freezer.c
文件重构
将v1冻结实现重命名为legacy_freezer.c,为v2实现腾出命名空间
2/9
cgroup: implement __cgroup_task_count() helper
辅助函数
新增原子计数器__cgroup_task_count(),优化进程迁移时的性能
3/9
cgroup: protect cgroup->nr_(dying_)descendants by css_set_lock
锁机制优化
用css_set_lock保护cgroup层级计数,解决并发修改问题
4/9
cgroup: cgroup v2 freezer
核心实现
引入cgroup.freeze文件控制接口,支持进程冻结/解冻
5/9
kselftests: cgroup: don't fail on cg_kill_all() error in cg_destroy()
测试修复
允许测试用例在进程终止失败时继续执行
6/9
kselftests: cgroup: add freezer controller self-tests
测试扩展
新增6个测试用例,覆盖SIGSTOP/PTRACE等场景
7/9
cgroup: make TRACE_CGROUP_PATH irq-safe
调试增强
确保cgroup跟踪点(tracepoint)在中断上下文中安全调用
8/9
cgroup: add tracing points for cgroup v2 freezer
监控支持
新增cgroup_freeze/cgroup_unfreeze跟踪点
9/9
cgroup: document cgroup v2 freezer interface
文档完善
在cgroup-v2.rst中详细说明接口用法
提交记录
Documentation/admin-guide/cgroup-v2.rst | 27 +
include/linux/cgroup-defs.h | 33 +
include/linux/cgroup.h | 43 +
include/linux/sched.h | 2 +
include/linux/sched/jobctl.h | 2 +
include/trace/events/cgroup.h | 55 ++
kernel/cgroup/Makefile | 4 +-
kernel/cgroup/cgroup-internal.h | 8 +-
kernel/cgroup/cgroup-v1.c | 16 -
kernel/cgroup/cgroup.c | 151 +++-
kernel/cgroup/freezer.c | 647 +++++--------
kernel/cgroup/legacy_freezer.c | 481 ++++++++++
kernel/fork.c | 2 +
kernel/signal.c | 70 +-
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/cgroup_util.c | 58 +-
tools/testing/selftests/cgroup/cgroup_util.h | 5 +
tools/testing/selftests/cgroup/test_freezer.c | 851 ++++++++++++++++++
19 files changed, 2026 insertions(+), 432 deletions(-)
create mode 100644 kernel/cgroup/legacy_freezer.c
create mode 100644 tools/testing/selftests/cgroup/test_freezer.c