☰
linux-insides 深度解析:Linux 内核 Control Groups(cgroups)机制与早期初始化
2026/9/30 2:18:50 网站建设 项目流程
  • 文档
  • 教程
  • 操作系统

【免费下载链接】linux-insides

A book-in-progress about the Linux kernel and its insides.

项目地址:https://gitcode.com/gh_mirrors/li/linux-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,必须先创建它,主要有两种方式:

  1. 直接操作 cgroupfs:在/sys/fs/cgroup下任意子系统目录中创建子目录,该目录会自动生成一组控制文件;
  2. 使用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/tty
  • 5: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/cgroups

ss变量类型为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.

项目地址:https://gitcode.com/gh_mirrors/li/linux-insides
点击查看免费下载

相关推荐

上一篇:什么是DPDK?揭秘数据平面开发套件的终极性能加速能力
下一篇:Witty-Service容器化部署:使用Docker Compose快速搭建集群

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询