控制组简介
简介
这是 linux 内核揭秘 的新一章的第一部分。你可以根据这部分的标题猜测 - 这一部分将涉及 Linux 内核中的 控制组
或 cgroups
机制。
Cgroups
是由 Linux 内核提供的一种机制,它允许我们分配诸如处理器时间、每组进程的数量、每个 cgroup
的内存大小,或者针对一个或一组进程的上述资源的组合。Cgroups
是按照层级结构组织的,这种机制类似于通常的进程,他们也是层级结构,并且子 cgroups
会继承其上级的一些属性。但实际上他们还是有区别的。cgroups
和进程之间的主要区别在于,多个不同层级的 cgroup
可以同时存在,而进程树则是单一的。同时存在的多个不同层级的 cgroup
并不是任意的,因为每个 cgroup
层级都要附加到一组 cgroup
"子系统"中。
每个 cgroup
子系统代表一种资源,如针对某个 cgroup
的处理器时间或者 pid 的数量,也叫进程数。Linux 内核提供对以下 12 种 cgroup
子系统的支持:
cpuset
- 为cgroup
内的任务分配独立的处理器和内存节点;cpu
- 使用调度程序对cgroup
内的任务提供 CPU 资源的访问;cpuacct
- 生成cgroup
中所有任务的处理器使用情况报告;io
- 限制对块设备的读写操作;memory
- 限制cgroup
中的一组任务的内存使用;devices
- 限制cgroup
中的一组任务访问设备;freezer
- 允许cgroup
中的一组任务挂起/恢复;net_cls
- 允许对cgroup
中的任务产生的网络数据包进行标记;net_prio
- 针对cgroup
中的每个网络接口提供一种动态修改网络流量优先级的方法;perf_event
- 支持访问cgroup
中的性能事件;hugetlb
- 为cgroup
开启对大页内存的支持;pid
- 限制cgroup
中的进程数量。
每个 cgroup
子系统是否被支持均与相关配置选项有关。例如,cpuset
子系统应该通过 CONFIG_CPUSETS
内核配置选项启用,io
子系统通过 CONFIG_BLK_CGROUP
内核配置选项等。所有这些内核配置选项都可以在 General setup → Control Group support
菜单里找到:
你可以通过 proc 虚拟文件系统在计算机上查看已经启用的 cgroup
:
或者通过 sysfs 虚拟文件系统查看:
正如你所猜测的那样,cgroup
机制不只是针对 Linux 内核的需求而创建的,更多的是用户空间层面的需求。要使用 cgroup
,需要先创建它。我们可以通过两种方式来创建。
第一种方法是在 /sys/fs/cgroup
目录下的任意子系统中创建子目录,并将任务的 pid 添加到 tasks
文件中,这个文件在我们创建子目录后会自动创建。
第二种方法是使用 libcgroup
库提供的工具集来创建/销毁/管理 cgroups
(在 Fedora 中是 libcgroup-tools
)。
我们来看一个简单的例子。下面的 bash 脚本会持续把一行信息输出到代表当前进程的控制终端的设备:
因此,如果我们运行这个脚本,将看到下面的结果:
现在让我们进入系统中 cgroupfs
的挂载点。前面说到,它位于 /sys/fs/cgroup
目录,但你可以将它挂载到任何你希望的地方。
接着我们进入 devices
子目录,这个子目录表示允许或拒绝 cgroup
中的任务访问的设备:
然后在这里创建 cgroup_test_group
目录:
创建 cgroup_test_group
目录之后,会在目录下生成以下文件:
现在我们重点关注 tasks
和 devices.deny
这两个文件。第一个文件 tasks
包含的是要附加到 cgroup_test_group
cgroup
的 pid,第二个文件 devices.deny
包含的是拒绝访问的设备列表。新创建的 cgroup
默认对设备没有任何访问限制。为了禁止访问某个设备(在我们的示例中是 /dev/tty
),我们应该向 devices.deny
写入下面这行:
我们来对这行进行详细解读。第一个字符 c
表示一种设备类型,我们示例中的 /dev/tty
是“字符设备”,我们可以通过 ls
命令的输出对此进行验证:
可以看到权限列表中的第一个字符是 c
。第二部分的 5:0
是设备的主次设备号,你也可以在 ls
命令的输出中看到。最后的字符 w
表示禁止 cgroups
中的任务对指定的设备执行写入操作。现在让我们再次运行 cgroup_test_script.sh
脚本:
没有任何效果。再把这个进程的 pid 加到我们 cgroup
的 devices/tasks
文件:
现在,脚本的运行结果和预期的一样:
在你运行 docker 容器的时候也会出现类似的情况:
因此,在 docker
容器的启动过程中,docker
会为这个容器中的进程创建一个 cgroup
:
我们可以在宿主机上看到这个 cgroup
:
现在我们了解了一些关于 cgroup
的机制,如何手动使用它,以及这个机制的用途。是时候深入 Linux 内核源码来了解这个机制的实现了。
cgroup
的早期初始化
cgroup
的早期初始化现在,在我们刚刚看到关于 Linux 内核的 cgroup
机制的一些理论之后,我们可以开始深入到 Linux 的内核源码,以便更深入的了解这种机制。 与往常一样,我们将从 cgroup
的初始化开始。在 Linux 内核中,cgroups
的初始化分为两个部分:早期和晚期。在这部分我们只考虑“早期”的部分,“晚期”的部分会在下一部分考虑。
Cgroups
的早期初始化是在 Linux 内核的早期初始化期间从 init/main.c 中调用:
函数开始的。这个函数定义在源文件 kernel/cgroup.c 中,从下面两个局部变量的定义开始:
cgroup_sb_opts
结构体的定义也可以在这个源文件中找到:
用来表示 cgroupfs
的挂载选项。例如,我们可以使用 name=
选项创建指定名称的 cgroup 层级(本示例中以 my_cgrp
命名),不附加到任何子系统:
第二个变量 - ss
是 cgroup_subsys
结构体,这个结构体定义在 include/linux/cgroup-defs.h 头文件中。你可以从这个结构体的名称中猜到,这个变量表示一个 cgroup
子系统。这个结构体包含多个字段和回调函数,如:
例如,css_online
和 css_offline
回调分别在 cgroup 成功完成所有分配之后和 cgroup 释放之前调用,early_init
标志位用来标记子系统是否要提前初始化,id
和 name
字段分别表示在 cgroup 中已注册的子系统的唯一标识和子系统的”名称“。最后的 root
字段指向 cgroup 层级结构的根。
当然,cgroup_subsys
结构体还有一些其他字段,比上面展示的要多,不过目前了解这么多已经够了。现在我们了解了与 cgroups
机制有关的重要结构体,让我们再回到 cgroup_init_early
函数。这个函数的主要目的是对一些子系统进行早期初始化。你可能已经猜到了,这些需要”早期“初始化的子系统的 cgroup_subsys->early_init
字段应该为 1
。来看看哪些子系统可以提前初始化吧。
在两个局部变量定义之后,我们可以看到下面几行代码:
这里我们可以看到 init_cgroup_root
函数的调用,它会使用缺省的层级结构进行初始化。接着我们在缺省的 cgroup
中设置 CSS_NO_REF
标志来禁止这个 css 的引用计数。cgrp_dfl_root
的定义也在这个文件中:
这里的 cgrp
字段是 cgroup
结构体,你也许已经猜到了,它表示一个 cgroup
,cgroup
定义在 include/linux/cgroup-defs.h 头文件中。我们知道一个进程在 Linux 内核中是用 task_struct
结构体表示的, task_struct
并不包含直接访问这个任务所属的 cgroup
的链接,但是可以通过 task_struct
的 css_set
字段访问。这个 css_set
结构体拥有指向子系统状态数组的指针:
通过 cgroup_subsys_state
结构体,一个进程可以找到其所属的 cgroup
:
所以,cgroups
相关数据结构的整体情况如下:
因此,init_cgroup_root
函数使用默认值设置 cgrp_dfl_root
。接下来的工作是把初始化的 css_set
分配给 init_task
,它表示系统中的第一个进程:
cgroup_init_early
函数里最后一件重要的任务是 early cgroups
的初始化。在这里,我们遍历所有已注册的子系统,给子系统分配一个唯一的标识号和名称,并且对标记为早期的子系统调用 cgroup_init_subsys
函数:
这里的 for_each_subsys
是 kernel/cgroup.c 源文件中的一个宏定义,正好扩展成基于 cgroup_subsys
数组的 for 循环。这个数组的定义可以在该源文件中找到,它看起来有点不寻常:
它被定义为 SUBSYS
宏,它接受一个参数(子系统名称),并定义了 cgroup 子系统的 cgroup_subsys
数组。另外,我们可以看到这个数组是使用 linux/cgroup_subsys.h 头文件的内容进行初始化。如果我们看一下这个头文件,就会发现一组具有给定子系统名称的 SUBSYS
宏:
可以这样定义是因为第一个 SUBSYS
的宏定义后面的 #undef
语句。来看看 &_x ## _cgrp_subsys
表达式,在 C
语言的宏定义中,##
操作符连接左右两边的表达式,所以当我们把 cpuset
、cpu
等参数传给 SUBSYS
宏时,其实是在定义 cpuset_cgrp_subsys
、cp_cgrp_subsys
。确实如此,在 kernel/cpuset.c 源文件中你可以看到这些结构体的定义:
因此,cgroup_init_early
函数中的最后一步是调用 cgroup_init_subsys
函数完成早期子系统的初始化,下面的早期子系统将被初始化:
cpuset
;cpu
;cpuacct
.
cgroup_init_subsys
函数使用缺省值对指定的子系统进行初始化。比如,设置层级结构的根,使用 css_alloc
回调函数为指定的子系统分配空间,将一个子系统链接到一个已经存在的子系统,为初始进程分配子系统等。
至此,早期子系统就初始化结束了。
结束语
这是第一部分的结尾,它描述了 Linux 内核中 cgroup
机制的引入,我们讨论了与 cgroup
机制相关的一些理论和初始化步骤,在接下来的部分中,我们将继续深入讨论 cgroup
更实用的方面。
如果你有任何问题或建议,可以写评论给我,也可以在 twitter 上联系我。
请注意,英语不是我的第一语言,对于任何不便,我深表歉意。如果你发现任何错误,请给我发送一个 PR 到 linux-insides.
链接
最后更新于