- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
导读
本文是 linux-insides 系列中 Cgroups 章节 的入门指南,围绕 Linux 内核的control groups(简称cgroups)机制展开:先讲清它是什么、与普通进程树的本质区别,再逐个拆解内核支持的十二种control group subsystem(控制器),随后通过/proc/cgroups、/sys/fs/cgroup及一个真实的设备访问限制脚本演示如何手动创建和操作 cgroup,最后深入kernel/cgroup/cgroup.c源码,剖析cgroup_init_early的早期初始化流程。读完本文,你将掌握 cgroups 的层次模型、子系统配置选项、用户态实操方法,以及内核中cgroup_subsys、css_set、cgroup_subsys_state等核心数据结构与SUBSYS宏的构建原理。
什么是 Control Groups:为进程集合分配资源的内核机制
Cgroups是 Linux 内核提供的一种特殊机制,它允许我们把一类"资源"——例如处理器时间、每组进程数量、每组内存大小,或这些资源的组合——分配给一个进程或一组进程。它是容器技术(如 Docker)与资源隔离、配额管控的底层基石。
Cgroups以**层次化(hierarchically)**方式组织,这一点与普通进程相似:子cgroup会从父cgroup继承一组特定参数。但两者有一个关键区别:
同一时刻可以存在多棵不同的 cgroup 层级树,而普通进程树永远只有一棵。
这不是随意的设计。每一棵 cgroup 层级树都挂接在一组control group subsystem上,子系统才是资源语义的真正载体。
十二种 Control Group Subsystem
Linux 内核为 cgroups 提供了以下十二种子系统(也称控制器),每个代表一种资源类型:
| 子系统 | 作用 |
|---|---|
cpuset | 将单个/多个处理器(processor)和内存节点(memory nodes)分配给组内的任务 |
cpu | 使用调度器(scheduler)为 cgroup 中的任务提供处理器资源访问 |
cpuacct | 生成组内处理器使用情况的报告(CPU 计费) |
io | 设置对块设备(block devices)读写的限额 |
memory | 设置组内任务的内存使用上限 |
devices | 允许或拒绝组内任务访问设备 |
freezer | 挂起(suspend)/ 恢复(resume)组内任务 |
net_cls | 允许为组内任务产生的网络数据包打上 classid 标记 |
net_prio | 为组内任务按网络接口动态设置网络流量优先级 |
perf_event | 为组内任务提供 perf 事件访问能力 |
hugetlb | 为组内任务激活大页(huge pages)支持 |
pid | 设置组内进程数量的上限 |
子系统与内核配置选项的对应关系
每个control group subsystem都依赖于对应的内核配置选项。例如:
cpuset子系统需通过CONFIG_CPUSETS内核配置选项启用;io子系统需通过CONFIG_BLK_CGROUP内核配置选项启用;- 其余子系统同理,各自对应一个
CONFIG_*开关。
所有这些内核配置选项都可以在General setup → Control Group support菜单中找到:
图中可见该菜单下展开的全部控制器选项(如Cpuset controller对应CONFIG_CPUSETS、IO controller对应CONFIG_BLK_CGROUP),[*]表示已编译进内核、[ ]表示未启用。这也与后文源码分析中include/linux/cgroup_subsys.h里IS_ENABLED(CONFIG_CPUSETS)等条件编译逻辑一一对应。
查看当前系统已启用的 cgroups
通过 procfs 查看子系统注册表
在运行中的 Linux 系统上,可以通过 procfs 查看已启用的控制组及每个子系统的层级、组数与启用状态:
$ cat /proc/cgroups #subsys_name hierarchy num_cgroups enabled cpuset 8 1 1 cpu 7 66 1 cpuacct 7 66 1 blkio 11 66 1 memory 9 94 1 devices 6 66 1 freezer 2 1 1 net_cls 4 1 1 perf_event 3 1 1 net_prio 4 1 1 hugetlb 10 1 1 pids 5 69 1各列含义:subsys_name为子系统名称,hierarchy为该子系统所属层级编号,num_cgroups为当前该层级中的控制组数量,enabled表示是否启用(1 为启用)。注意示例中cpu与cpuacct位于同一层级(hierarchy 7),net_cls与net_prio也共享层级 4——这正是多子系统挂接同一棵层级树的表现。
通过 sysfs 查看 cgroupfs 挂载点
cgroup 文件系统(cgroupfs)默认挂载在/sys/fs/cgroup,每个子系统在挂载点下对应一个目录:
$ ls -l /sys/fs/cgroup/ total 0 dr-xr-xr-x 5 root root 0 Dec 2 22:37 blkio lrwxrwxrwx 1 root root 11 Dec 2 22:37 cpu -> cpu,cpuacct lrwxrwxrwx 1 root root 11 Dec 2 22:37 cpuacct -> cpu,cpuacct dr-xr-xr-x 5 root root 0 Dec 2 22:37 cpu,cpuacct dr-xr-xr-x 2 root root 0 Dec 2 22:37 cpuset dr-xr-xr-x 5 root root 0 Dec 2 22:37 devices dr-xr-xr-x 2 root root 0 Dec 2 22:37 freezer dr-xr-xr-x 2 root root 0 Dec 2 22:37 hugetlb dr-xr-xr-x 5 root root 0 Dec 2 22:37 memory lrwxrwxrwx 1 root root 16 Dec 2 22:37 net_cls -> net_cls,net_prio dr-xr-xr-x 2 root root 0 Dec 2 22:37 net_cls,net_prio lrwxrwxrwx 1 root root 16 Dec 2 22:37 net_prio -> net_cls,net_prio dr-xr-xr-x 2 root root 0 Dec 2 22:37 perf_event dr-xr-xr-x 5 root root 0 Dec 2 22:37 pids dr-xr-xr-x 5 root root 0 Dec 2 22:37 systemd注意cpu -> cpu,cpuacct这类符号链接:它们表示多个子系统被挂接到同一个层级,指向合并后的真实目录(如cpu,cpuacct),这与/proc/cgroups中二者共享同一hierarchy编号的观察完全一致。目录中的systemd则是 systemd 为用户会话创建的 cgroup 树。
实战:手动创建并使用一个 cgroup
control groups机制并非仅为内核自身需要而设计,它更多是为用户空间服务的。要使用一个 cgroup,必须先创建它,主要有两种方式:
- 直接操作 cgroupfs:在
/sys/fs/cgroup下任意子系统目录中创建子目录,该目录会自动生成一组控制文件; - 使用
libcgroup库工具:通过libcgroup(Fedora 上对应的软件包名为libcgroup-tools)创建、销毁和管理 cgroups。
下面用第一种方式做一个完整的设备访问限制实验。
第一步:准备一个持续输出的脚本
下面的 bash 脚本会向/dev/tty(代表当前进程控制终端的字符设备)循环打印一行文本:
#!/bin/bash while : do echo "print line" > /dev/tty sleep 5 done给它执行权限并运行:
$ sudo chmod +x cgroup_test_script.sh ~$ ./cgroup_test_script.sh print line print line print line ... ... ...第二步:在 devices 子系统下创建控制组
进入 cgroupfs 挂载点(默认是/sys/fs/cgroup,也可以挂载到任意位置),进入devices子系统目录并创建测试组目录:
$ cd /sys/fs/cgroup # cd devices # mkdir cgroup_test_group创建目录后,cgroup_test_group内会自动生成以下控制文件:
/sys/fs/cgroup/devices/cgroup_test_group$ ls -l total 0 -rw-r--r-- 1 root root 0 Dec 3 22:55 cgroup.clone_children -rw-r--r-- 1 root root 0 Dec 3 22:55 cgroup.procs --w------- 1 root root 0 Dec 3 22:55 devices.allow --w------- 1 root root 0 Dec 3 22:55 devices.deny -r--r--r-- 1 root root 0 Dec 3 22:55 devices.list -rw-r--r-- 1 root root 0 Dec 3 22:55 notify_on_release -rw-r--r-- 1 root root 0 Dec 3 22:55 tasks本实验关注两个文件:
tasks:写入要加入该控制组的进程 pid;devices.deny:写入被拒绝访问的设备规则。
新建的组默认没有任何设备访问限制。要禁止设备(本例为/dev/tty),向devices.deny写入下面这行规则:
# echo "c 5:0 w" > devices.deny逐段解读这条规则:
c:设备类型,/dev/tty是字符设备(char device)。用ls验证——权限列表第一位的c即为字符设备标志:
~$ ls -l /dev/tty crw-rw-rw- 1 root root 5, 0 Dec 3 22:48 /dev/tty5:0:设备的主设备号与次设备号(ls输出中同样可见,即5, 0);w:禁止任务对该设备执行写操作。
第三步:把进程加入控制组
重新运行脚本,并把脚本进程的 pid 写入该组的tasks文件:
~$ ./cgroup_test_script.sh print line print line print line ...# echo $(pidof -x cgroup_test_script.sh) > /sys/fs/cgroup/devices/cgroup_test_group/tasks结果正如预期——在打印若干行之后,写操作被内核拒绝:
~$ ./cgroup_test_script.sh print line print line print line print line print line print line ./cgroup_test_script.sh: line 5: /dev/tty: Operation not permitted进阶观察:Docker 容器背后的 cgroup
类似的情况在运行 Docker 容器时也会发生。容器启动时,Docker 会为容器内的进程创建专属 cgroup:
~$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES fa2d2085cd1c mariadb:10 "docker-entrypoint..." 12 days ago Up 4 minutes 0.0.0.0:3306->3306/tcp mysql-work ~$ cat /sys/fs/cgroup/devices/docker/fa2d2085cd1c8d797002c77387d2061f56fefb470892f140d0dc511bd4d9bb61/tasks | head -3 5501 5584 5585 ... ... ...进入容器观察进程:
$ docker exec -it mysql-work /bin/bash $ top PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 mysql 20 0 963996 101268 15744 S 0.0 0.6 0:00.46 mysqld 71 root 20 0 20248 3028 2732 S 0.0 0.0 0:00.01 bash 77 root 20 0 21948 2424 2056 R 0.0 0.0 0:00.00 top在宿主机上可以直观地看到这棵 cgroup 层级树——mysqld等进程被收拢到 docker 容器专属的 cgroup 节点下:
$ systemd-cgls Control group /: -.slice ├─docker │ └─fa2d2085cd1c8d797002c77387d2061f56fefb470892f140d0dc511bd4d9bb61 │ ├─5501 mysqld │ └─6404 /bin/bash至此,我们已经掌握了 cgroups 的用途与手工操作方式。接下来进入内核源码,剖析该机制的实现。
源码剖析:cgroups 的早期初始化
cgroups 的初始化在内核中分为**早期(early)与晚期(late)**两个阶段,本文聚焦早期部分。早期初始化始于init/main.c中调用的cgroup_init_early()函数(该函数定义在kernel/cgroup/cgroup.c中)。这个调用点也是 Initialization 章节 所述start_kernel早期任务清单中的一环("enable early cgroups subsystem")。
函数入口与两个关键局部变量
int __init cgroup_init_early(void) { static struct cgroup_sb_opts __initdata opts; struct cgroup_subsys *ss; ... ... ... }cgroup_sb_opts结构定义于同一源文件中,代表 cgroupfs 的挂载选项:
struct cgroup_sb_opts { u16 subsys_mask; unsigned int flags; char *release_agent; bool cpuset_clone_children; char *name; bool none; };借助这些选项,可以创建命名层级树:例如下面的命令挂载一棵名为my_cgrp、不挂接任何子系统的独立 cgroup 层级:
$ mount -t cgroup -oname=my_cgrp,none /mnt/cgroupsss变量类型为cgroup_subsys结构,定义于include/linux/cgroup-defs.h头文件。顾名思义,它代表一个 cgroup 子系统,内含各类字段与回调函数:
struct cgroup_subsys { int (*css_online)(struct cgroup_subsys_state *css); void (*css_offline)(struct cgroup_subsys_state *css); ... ... ... bool early_init:1; int id; const char *name; struct cgroup_root *root; ... ... ... }各字段含义:
css_online/css_offline:回调函数,分别在 cgroup 成功完成所有内存分配之后、以及 cgroup 即将被释放之前被调用;early_init:位标志,标记哪些子系统可以/应当被早期初始化;id:该子系统在已注册子系统数组中的唯一标识;name:子系统名称;root:指向该 cgroup 层级树根的指针。
(cgroup_subsys实际包含更多字段,此处仅列出与早期初始化直接相关的部分。)
初始化默认统一层级
cgroup_init_early的主要职责是完成部分子系统的早期初始化,而这些子系统都满足cgroup_subsys->early_init = 1。定义完局部变量后,首先看到:
init_cgroup_root(&cgrp_dfl_root, &opts); cgrp_dfl_root.cgrp.self.flags |= CSS_NO_REF;init_cgroup_root会执行默认统一层级(default unified hierarchy)的初始化;随后为默认 cgroup 的 state 设置CSS_NO_REF标志,以对该 css 禁用引用计数。cgrp_dfl_root定义于同一源文件:
struct cgroup_root cgrp_dfl_root;其cgrp字段的类型为cgroup结构(同样定义在include/linux/cgroup-defs.h),代表一个 cgroup 本身。
进程如何关联到 cgroup:task_struct → css_set → cgroup
众所周知,进程在内核中由task_struct表示。但task_struct并不直接保存指向其所在 cgroup 的指针,而是通过css_set字段间接到达:
task_struct.cgroups指向css_set结构;css_set持有指向一组子系统状态的数组指针:
struct css_set { ... ... .... struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; ... ... ... }- 通过
cgroup_subsys_state,进程可以拿到它所挂接的 cgroup:
struct cgroup_subsys_state { ... ... ... struct cgroup *cgroup; ... ... ... }整体数据结构关系如下:
+-------------+ +---------------------+ +------------->+---------------------+ +----------------+ | task_struct | | css_set | | | cgroup_subsys_state | | cgroup | +-------------+ | | | +---------------------+ +----------------+ | | | | | | | | flags | | | | | | +---------------------+ | cgroup.procs | | | | | | | cgroup |--------->| id | | | | | | +---------------------+ | .... | |-------------+ |---------------------+----+ +----------------+ | cgroups | ------> | cgroup_subsys_state | array of cgroup_subsys_state |-------------+ +---------------------+------------------>+---------------------+ +----------------+ | | | | | cgroup_subsys_state | | cgroup | +-------------+ +---------------------+ +---------------------+ +----------------+ | | | flags | +---------------------+ | cgroup.procs | | cgroup |--------->| id | +---------------------+ | .... | | cgroup_subsys | +----------------+ +---------------------+ | | ↓ +---------------------+ | cgroup_subsys | +---------------------+ | id | | name | | css_online | | css_ofline | | attach | | .... | +---------------------+把初始 css_set 挂到 init_task
init_cgroup_root以默认值填充cgrp_dfl_root之后,下一步是为系统首个进程init_task赋予初始的css_set:
RCU_INIT_POINTER(init_task.cgroups, &init_css_set);遍历子系统并初始化 early 子系统
cgroup_init_early的最后一大步是初始化"早期 cgroup":遍历所有已注册子系统,为它们分配唯一编号与名称,并对标记为 early 的子系统调用cgroup_init_subsys:
for_each_subsys(ss, i) { ss->id = i; ss->name = cgroup_subsys_name[i]; if (ss->early_init) cgroup_init_subsys(ss, true); }for_each_subsys是定义在kernel/cgroup/cgroup.c中的宏,展开后就是遍历cgroup_subsys数组的for循环。该数组的定义方式相当巧妙——借助SUBSYS宏与头文件展开:
#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys, static struct cgroup_subsys *cgroup_subsys[] = { #include <linux/cgroup_subsys.h> }; #undef SUBSYS这里的核心是##运算符:它将宏参数左右两边的标识符拼接起来。向SUBSYS传入cpuset、cpu等名称后,就生成了&cpuset_cgrp_subsys、&cpu_cgrp_subsys这样的初始化项。include/linux/cgroup_subsys.h头文件中的内容如下:
#if IS_ENABLED(CONFIG_CPUSETS) SUBSYS(cpuset) #endif #if IS_ENABLED(CONFIG_CGROUP_SCHED) SUBSYS(cpu) #endif ... ... ...之所以能够先展开#include再#undef,正是因为第一次定义SUBSYS宏之后紧跟的#undef SUBSYS语句。注意:这里的IS_ENABLED(CONFIG_CPUSETS)条件编译,正是上一节 menuconfig 中CONFIG_CPUSETS选项在源码层的直接体现——配置项启用与否决定了子系统是否被编入数组。
作为佐证,打开kernel/cgroup/cpuset.c可以看到对应的子系统实例定义:
struct cgroup_subsys cpuset_cgrp_subsys = { ... ... ... .early_init = true, };会被早期初始化的子系统
cgroup_init_early中通过cgroup_init_subsys完成初始化的早期子系统共有三个:
cpuset;cpu;cpuacct。
cgroup_init_subsys以默认值初始化给定子系统,具体包括:设置层级树根、通过css_alloc回调为该子系统分配空间、若存在父级则与之建立关联、把分配好的子系统挂到初始进程上,等等。
至此,早期子系统初始化完毕,后续的"晚期"初始化(cgroup_init)则与start_kernel中其余子系统一同进行,相关内容可在 Initialization 章节的 linux-initialization-10.md 中看到其被提及。
延伸阅读:与本主题相关的仓库内资源
- Cgroups 章节总览:本章目录,后续会继续深入 cgroups 的实践层面(晚期初始化、层级树、控制器实现等);
- SUMMARY.md:全书目录,可定位其余章节;
- Initialization/linux-initialization-4.md:
start_kernel中"enable early cgroups subsystem"的上下文; - Initialization/linux-initialization-8.md:调度器初始化中与 cgroup 相关的
CONFIG_CGROUP_SCHED、task_group、RT/DL 带宽等实现; - Initialization/linux-initialization-10.md:提及
cgroup_init对剩余 cgroup 子系统的初始化。
结论
本文是第一篇关于 Linux 内核Control Groups机制的介绍,覆盖了两方面内容:一是理论层面——cgroups 的定义、与进程树的区别、十二种子系统及其内核配置选项;二是初始化层面——cgroup_init_early早期初始化流程,包括cgroup_sb_opts/cgroup_subsys/css_set/cgroup_subsys_state等核心结构、SUBSYS宏与cgroup_subsys.h的数组构造技巧,以及cpuset、cpu、cpuacct三个早期子系统的初始化。在后续篇章中,我们还将继续深入 cgroups 更偏实践的部分(如晚期的cgroup_init、层级树建立与控制器逻辑)。
如果你对本文有任何疑问或建议,欢迎提交 issue 或 PR 到 linux-insides 仓库;也请注意作者母语并非英语,文中如有拼写或表达错误,欢迎通过 PR 指正。
- 文档
- 教程
- 操作系统
【免费下载链接】linux-insides
A book-in-progress about the Linux kernel and its insides.
相关推荐
linux-insides 精读:Linux 内核 Control Groups(cgroups)机制——从 12 大子系统、手动实操到早期初始化源码解析
linux insides 精读:Linux 内核 Control Groups(cgroups)机制——从 12 大子系统、手动实操到早期初始化源码解析 Co
文档教程操作系统linux-insides-zh 深度解读:Linux 内核控制组(cgroups)机制入门与早期初始化源码分析
linux insides zh 深度解读:Linux 内核控制组(cgroups)机制入门与早期初始化源码分析 本篇技术指南以本仓库(hust open at
Linux 内核揭秘:控制组(cgroups)机制与早期初始化源码深度解析
Linux 内核揭秘:控制组(cgroups)机制与早期初始化源码深度解析 本篇文章是《Linux 内核揭秘》系列中“控制组”章节的核心导读,围绕 Cgroup
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考