机制与早期初始化)
文档教程操作系统【免费下载链接】linux-insidesA 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 SubsystemLinux 内核为 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 7net_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 树。实战手动创建并使用一个 cgroupcontrol groups机制并非仅为内核自身需要而设计它更多是为用户空间服务的。要使用一个 cgroup必须先创建它主要有两种方式直接操作 cgroupfs在/sys/fs/cgroup下任意子系统目录中创建子目录该目录会自动生成一组控制文件使用libcgroup库工具通过libcgroupFedora 上对应的软件包名为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写入要加入该控制组的进程 piddevices.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, 0w禁止任务对该设备执行写操作。第三步把进程加入控制组重新运行脚本并把脚本进程的 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 -onamemy_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 本身。进程如何关联到 cgrouptask_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进程可以拿到它所挂接的 cgroupstruct 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_taskinit_cgroup_root以默认值填充cgrp_dfl_root之后下一步是为系统首个进程init_task赋予初始的css_setRCU_INIT_POINTER(init_task.cgroups, init_css_set);遍历子系统并初始化 early 子系统cgroup_init_early的最后一大步是初始化早期 cgroup遍历所有已注册子系统为它们分配唯一编号与名称并对标记为 early 的子系统调用cgroup_init_subsysfor_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完成初始化的早期子系统共有三个cpusetcpucpuacct。cgroup_init_subsys以默认值初始化给定子系统具体包括设置层级树根、通过css_alloc回调为该子系统分配空间、若存在父级则与之建立关联、把分配好的子系统挂到初始进程上等等。至此早期子系统初始化完毕后续的晚期初始化cgroup_init则与start_kernel中其余子系统一同进行相关内容可在 Initialization 章节的 linux-initialization-10.md 中看到其被提及。延伸阅读与本主题相关的仓库内资源Cgroups 章节总览本章目录后续会继续深入 cgroups 的实践层面晚期初始化、层级树、控制器实现等SUMMARY.md全书目录可定位其余章节Initialization/linux-initialization-4.mdstart_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-insidesA book-in-progress about the Linux kernel and its insides.项目地址https://gitcode.com/gh_mirrors/li/linux-insides点击查看免费下载相关推荐linux-insides 精读Linux 内核 Control Groupscgroups机制——从 12 大子系统、手动实操到早期初始化源码解析linux insides 精读Linux 内核 Control Groupscgroups机制——从 12 大子系统、手动实操到早期初始化源码解析 Co文档教程操作系统linux-insides-zh 深度解读Linux 内核控制组cgroups机制入门与早期初始化源码分析linux insides zh 深度解读Linux 内核控制组cgroups机制入门与早期初始化源码分析 本篇技术指南以本仓库hust open atLinux 内核揭秘控制组cgroups机制与早期初始化源码深度解析Linux 内核揭秘控制组cgroups机制与早期初始化源码深度解析 本篇文章是《Linux 内核揭秘》系列中“控制组”章节的核心导读围绕 Cgroup上一篇什么是DPDK揭秘数据平面开发套件的终极性能加速能力下一篇Witty-Service容器化部署使用Docker Compose快速搭建集群创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考