CGroup(控制组)
原文链接:.html
1 引言
Cgroups
是Linux内核提供的提供的一种机制,使我们能够为一组进程分配处理器时间、进程数、内存量等或多种资源的组合。控制组是分层组织的,就像进程也是分层组织的一样,子控制组可以从父控制组继承参数。但实际上他们并不相同,控制组与普通进程树的区别是,控制组的不同层次可以同时存在,而进程树始终单个的(single)。每个cgroup层次都被附加到一组cgroup上。
cgroup层次示例:(低) resources
Cgroups
cgroups
cgroups
subsystems
(高)
一个control group subsystem
代表一种类型的cgroup
资源,比如处理器时间或进程数。Linux内核提供12种控制组子系统:
cpuset
- 为一组任务提供单独的处理器和内存节点cpu
- 使用调度程序为cgroup
任务提供对处理器资源的访问cpuacct
- 为控制组生成使用处理器的报告io
- 为块设备提供读写速度限制memory
- 为cgroup中的任务设置内存使用限制devices
- 允许cgroup中的任务访问设备freezer
- 允许cgroup中的任务暂停或继续net_cls
- 允许标记cgroup的网络包net_prio
- 提供一种方法来动态设置每个cgroup每个网络接口的网络流量优先级perf_event
- 提供perf
事件的访问权限hugetlb
- 激活对cgroup的huge pages支持pid
- 设置一个cgroup的进程数
每个cgroup
子系统都依赖于相关的配置选项,例如cpuset
子系统应该通过CONFIG_CPUSETS
内核配置选项启用,io
子系统应该通过CONFIG_BLK_CGROUP
配置启用等。所有这些选项都能在General setup
→ \to →Control Group support
菜单中找到:
可以通过proc
文件系统查看启用的cgroups:
$ 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
或者通过sysfs
$ 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
你可以已经猜到,cgroup机制并不是直接针对Linux内核的需求发明的,而是用户空间的需要。要想使用cgroup,要先通过两种方式创建它。
第一种方式是在/sys/fs/cgroup
目录中的任何subsystem
中创建子目录,并把任务的PID添加到tasks
文件中,该文件会在创建子目录后立即自动创建。
第二种方式是通过libcgroup
库创建、删除和管理cgroups
。(在Fedora中使用libcgroup-tools
库)
我们开始考虑简单的例子。下面的bash脚本将会打印一行到/dev/tty
设备(表示当前进程的控制终端)。
#!/bin/bashwhile :
doecho "print line" > /dev/ttysleep 5
done
如果我们运行这个脚本会看到下面的输出:
$ sudo chmod +x cgroup_test_script.sh
~$ ./cgroup_test_script.sh
print line
print line
print line
...
...
...
现在我们转到cgroupfs
在计算机上挂载的位置,正是/sys/fs/cgroup
目录,但是你也可以挂载到任意位置。
$ cd /sys/fs/cgroup
现在我们前往devices
子目录,该目录代表一种资源,控制cgroup
中的任务对设备的访问权限。
$ cd devices
然后在这创建cgroup_test_group
目录:
$ 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
和devices.deny
文件感兴趣。首先,tasks
文件应该包含要添加到该组的进程的PID。devices.deny
文件应该包含拒绝设备的列表。默认情况下,新建的cgroup对资源没有任何限制。要阻止一个设备(例如/dev/tty
),我们应该向devices.deny
文件中写入下面一行:
$ echo "c 5:0 w" > devices.deny
现在一步一步地看这一行。第一个字母c
表示设备类型,在我们的例子中,/dev/tty
是字符设备。我们可以用ls
命令的输出中进行验证:
$ ls -l /dev/tty
crw-rw-rw- 1 root tty 5, 0 Dec 3 22:48 /dev/tty
可以看到权限列表的第一个字母是c
。第二个部分是5:0
,设备的主序号和次序号,ls
的输出也能看到这些数字。最后一个字母w
禁止任务向特定的任务写入数据。现在我们执行cgroup_test_script.sh
脚本试试:
$ ./cgroup_test_script.sh
print line
print line
print line
...
...
然后把这个进程的pid添加到devices/tasks
文件中:
$ 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
容器的时候也能出现,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容器的时候,docker会为容器中的进程们创建一个cgroup
:
$ docker exec -it mysql-work /bin/bash
$ topPID 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 mysqld71 root 20 0 20248 3028 2732 S 0.0 0.0 0:00.01 bash77 root 20 0 21948 2424 2056 R 0.0 0.0 0:00.00 top
然后我们可以在主机上看见这个:
$ systemd-cglsControl group /:
-.slice
├─docker
│ └─fa2d2085cd1c8d797002c77387d2061f56fefb470892f140d0dc511bd4d9bb61
│ ├─5501 mysqld
│ └─6404 /bin/bash
现在我们对cgroup机制和如何使用它有了一些了解,现在该看一下内核源代码, 开始深入探索cgroup
机制的实现。
2 控制组的早期初始化
刚才我们还几乎完全不了解cgroup
在Linux内核的实现机制,现在我们开始深入研究Linux内核的源代码。与往常一样,我们将从cgroup
的早期初始化开始,cgroup
的初始化在内核中分为early
和late
两部分。
cgroup
的早期初始化起源于Linux内核的早期初始化init/main.c
🌍这个函数调用:
cgroup_init_early();
这个函数定义在kernel/cgroup.c
🌍中,以两个局部变量的定义开始:
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;
};
例如我们可以用name=
选项创建命名cgroup
层次结构(本例中是my_cgrp
),没有任何子系统:
$ 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
变量分别表示注册的子系统数组的ID和子系统的名字。最后一个变量root
表示指向cgroup
层次树的指针。
当然cgroup_subsys
结构很大,还有其他成员变量,但是了解到现在已经够用了。了解完cgroup
的重要结构后,就可以重新回到cgroup_init_early
函数了。这个函数的主要目的是对某些子系统进行早期初始化,这些早期(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
函数,该函数会执行默认统一层次结构。然后为默认cgroup
的状态设置CSS_NO_REF
,禁用此css的引用计数。cgrp_dfl_root
也定义在同一个文件中:
struct cgroup_root cgrp_dfl_root;
它的cgrp
字段是cgroup
结构体,定义在include/linux/cgroup-defs.h
🌍头文件中。我们已经知道,在Linux内核中,进程用task_struct
表示。task_struct
结构体不包含直接链接到任务所属cgroup的链接,但是可以用task_struct
的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;.........
}
所以,与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 || .... |+---------------------+
因此,init_cgroup_root
会给cgrp_dfl_root
填充默认值。下一个任务是将初始化的css_set
分配给代表系统第一个进程的init_task
:
RCU_INIT_POINTER(init_task.cgroups, &init_css_set);
cgroup_init_early
函数要搞的最后一件大事情是初始化early cgroups
。这里会遍历所有注册的子系统,并为其分配唯一的ID和名称,然后为标记为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.c
🌍文件中的宏,只是对cgroup_subsys
数组for
循环进行了扩展。这个数组的定义可以在相同的源代码文件中找到,看起来有点奇怪:
#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys,static struct cgroup_subsys *cgroup_subsys[] = {#include <linux/cgroup_subsys.h>
};
#undef SUBSYS
它定义了一个SUBSYS
宏,带一个参数(子系统名称),然后定义了表示cgroup
子系统的cgroup_subsys
数组。另外可以看到,这个数组是使用linux/cgroup_subsys.h
🌍头文件的内容初始化的,在这个文件里面还会再次看到SUBSYS
宏:
#if IS_ENABLED(CONFIG_CPUSETS)
SUBSYS(cpuset)
#endif#if IS_ENABLED(CONFIG_CGROUP_SCHED)
SUBSYS(cpu)
#endif
...
...
...
<什么>之所以有效是因为在第一次定义SUBSYS
宏之后使用了#undef
语句。看看这个表达式:&_x ## _cgrp_subsys
,##
操作符会连接左右两个表达式作为一个标识符。所以当我们传递cpuset
、cpu
等给SUBSYS
宏的时候,应该在某个地方定义cpuset_cgrp_subsys
和cpu_cgrp_subsys
。我们的猜想是对的。打开kernel/cpuset.c
代码的时候,就能看到定义:
struct cgroup_subsys cpuset_cgrp_subsys = {..........early_init = true,
};
因此,cgroup_init_early
函数的最后一步是通过调用cgroup_init_subsys
初始化early
子系统。下面的子系统会被初始化:
cpuset
;cpu
;cpuacct
.
cgroup_init_subsys
函数使用默认值对给定的子系统进行初始化,例如设置cgroup层次结构的根、通过调用css_alloc
为给定的子系统分配空间、将子系统与父节点连接、为初始进程添加分配的子系统等。
3 libcgroup
这里不是翻译的,是整理各种网上资料写成的。
3.0 一些比较杂乱的代码
# 安装libcgroup
$ yum install -y libcgroup
# 创建两个不同cpu资源分配的组
$ cgcreate -g cpu:/large # 具体命令使用cgcreate -h
$ cgcreate -g cpu:/small
其中冒号左边的是cgroup子系统,右边是cgroup层次结构。
$ cgset -r cpu.shares=512 small
cpu.shares
是cpu控制的一个属性,更多的属性可以到/sys/fs/cgroup/cpu
目录下查看,默认值是1024,值越大,能获得更多的cpu时间
启动一个cgroup程序:
$ cgexec -g cpu:/large echo $((1+2))
查看进程所属的cgroup
$ cat /proc/6691/cgroup
# 6691是dockerd的PID
11:blkio:/system.slice/docker.service
10:pids:/system.slice/docker.service
9:net_prio,net_cls:/
8:freezer:/
7:cpuset:/
6:devices:/system.slice/docker.service
5:cpuacct,cpu:/system.slice/docker.service
4:hugetlb:/
3:memory:/system.slice/docker.service
2:perf_event:/
1:name=systemd:/system.slice/docker.service
$ cgset --copy-from path_to_source_cgroup path_to_target_cgroup
获得修改相关 cgroup 的权限后,请运行用户账户中的 cgset
指令来设定管控器参数。请仅对手动挂载的管控器使用此指令。
3.1 cgset
红帽官方文档
cgset 的语法为:
$ cgset -r parameter=value path_to_cgroup
其中:
parameter
是要设定的参数,它与给定 cgroup 目录中的文件对应;value
是参数值;path_to_cgroup
是“与层级的根相对”的 cgroup 路径。
cgset
设定的值可能会受限于一个特定层级所设定的更高值。例如,在一个系统中,如果 group1 被限定仅可使用 CPU 0,那您就不能设定 group1/subgroup1
使用 CPU 0 和 1,或者仅使用 CPU 1。
您也可以使用 cgset
将一个 cgroup 的参数复制到另一个已有 cgroup 中。使用 cgset 复制参数的句法是:
$ cgset --copy-from path_to_source_cgroup path_to_target_cgroup
其中:
path_to_source_cgroup
是要复制其参数的 cgroup 路径,相对层级的根群组;path_to_target_cgroup
是目标 cgroup 的路径,相对层级的根群组。
3.2 cgclassify
红帽官方文档
您可以运行 cgclassify
指令将进程移动到 cgroup 中:
$ cgclassify -g controllers:path_to_cgroup pidlist
其中:
controllers
是资源控制器列表,以逗号分隔;或者使用 * 来启动与所用可用子系统相关的层级中的进程。请注意,如果几个 cgroup 的名称相同,-g
选项会将进程移至这些群组的每一个。path_to_cgroup
是层级中 cgroup 的路径;pidlist
是进程ID(PID)的列表,以空格隔开。
3.3 cgcreate
红帽官方文档
在您自己创建的层级中,您可以使用 cgcreate
指令来创建临时 cgroup。cgcreate
的句法是:
$ cgcreate -t uid:gid -a uid:gid -g controllers:path
其中:
-t
(可选)—— 指定一个用户(通过用户 ID:uid)和群组(通过群组 ID:gid)来拥有此 cgroup 的 tasks 伪文件。此用户可在该 cgroup 中添加任务。
请注意,从 cgroup 中移除进程的唯一方法是将进程移至另一个 cgroup。如想要移除进程,用户必须拥有 “目标” cgroup 的写入权限;但是源 cgroup 的写入权限并不重要。-a
(可选)—— 指定一个用户(通过用户 ID:uid)和群组(通过群组 ID:gid)来拥有此 cgroup 的全部伪文件而不是 tasks 。此用户可以修改 cgroup 中任务存取系统资源的权限。-g
—— 指定 cgroup 应该被建于其中的层级,类似于“管控器”和这些层级的列表(以逗号分隔)。管控器的此项列表之后是一个冒号以及相对层级的子群组“路径”。请不要将层级挂载点包含于路径中。
3.4 cgdelete
可以用与 cgcreate
句法相似的 cgdelete
指令来移除 cgroup。请以 root
身份运行以下指令:
cgdelete controllers:path
其中:
- controller 是管控器的逗号分隔清单。
- path 是与该层级的根相对的 cgroup 路径。
例如:
$ cgdelete net_prio:/test-subgroup
指定 -r
选项时,cgdelete
也可以递归式移除所有子群组。
请注意,当您删除一个 cgroup,它的全部进程会移动到其父群组。
3.5 红帽文档目录
-
资源管理指南
-
控制群组简介
1.1. 什么是控制群组
1.2. cgroup 的默认层级
1.3. Linux Kernel 的资源管控器
1.4. 附加资源
-
使用控制群组
2.1. 创建控制群组
2.1.1. 用 systemd-run 创建临时 cgroup
2.1.2. 创建永久 cgroup
2.2. 删除控制群组
2.3. 修改 cgroup
2.3.1. 在命令列界面设定参数
2.3.2. 修改单位文件
2.4. 获得关于控制群组的信息
2.4.1. 列出已启动的服务(Listing Units)
2.4.2. 查看控制群组的层级
2.4.3. 查看资源管控器
2.4.4. 监控资源消耗量
2.5. 附加资源
-
使用 libcgroup 工具
3.1. 挂载层级
3.2. 卸载层级
3.3. 创建控制群组
3.4. 删除控制群组
3.5. 设定 cgroup 参数
3.6. 将进程移至控制群组
3.7. 启动控制群组的进程
3.8. 获区关于控制群组的信息
3.9. 附加资源
-
控制群组应用示例
4.1. 定义数据库 I/O 的优先级
4.2. 定义网络流量的优先级
附录. 子系统和可调参数
-
blkio
1.1. 权重分配的可调参数
1.2. I/O 节流可调参数
1.3. blkio 的通用可调参数
1.4. 示例应用
-
cpu
2.1. CFS 可调度参数
2.2. RT 可调参数
2.3. 示例应用
-
cpuacct
-
cpuset
-
devices
-
freezer
-
memory
7.1. 示例应用
-
net_cls
-
net_prio
-
ns
-
perf_event
-
常用可调参数
-
附加资源
CGroup(控制组)
原文链接:.html
1 引言
Cgroups
是Linux内核提供的提供的一种机制,使我们能够为一组进程分配处理器时间、进程数、内存量等或多种资源的组合。控制组是分层组织的,就像进程也是分层组织的一样,子控制组可以从父控制组继承参数。但实际上他们并不相同,控制组与普通进程树的区别是,控制组的不同层次可以同时存在,而进程树始终单个的(single)。每个cgroup层次都被附加到一组cgroup上。
cgroup层次示例:(低) resources
Cgroups
cgroups
cgroups
subsystems
(高)
一个control group subsystem
代表一种类型的cgroup
资源,比如处理器时间或进程数。Linux内核提供12种控制组子系统:
cpuset
- 为一组任务提供单独的处理器和内存节点cpu
- 使用调度程序为cgroup
任务提供对处理器资源的访问cpuacct
- 为控制组生成使用处理器的报告io
- 为块设备提供读写速度限制memory
- 为cgroup中的任务设置内存使用限制devices
- 允许cgroup中的任务访问设备freezer
- 允许cgroup中的任务暂停或继续net_cls
- 允许标记cgroup的网络包net_prio
- 提供一种方法来动态设置每个cgroup每个网络接口的网络流量优先级perf_event
- 提供perf
事件的访问权限hugetlb
- 激活对cgroup的huge pages支持pid
- 设置一个cgroup的进程数
每个cgroup
子系统都依赖于相关的配置选项,例如cpuset
子系统应该通过CONFIG_CPUSETS
内核配置选项启用,io
子系统应该通过CONFIG_BLK_CGROUP
配置启用等。所有这些选项都能在General setup
→ \to →Control Group support
菜单中找到:
可以通过proc
文件系统查看启用的cgroups:
$ 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
或者通过sysfs
$ 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
你可以已经猜到,cgroup机制并不是直接针对Linux内核的需求发明的,而是用户空间的需要。要想使用cgroup,要先通过两种方式创建它。
第一种方式是在/sys/fs/cgroup
目录中的任何subsystem
中创建子目录,并把任务的PID添加到tasks
文件中,该文件会在创建子目录后立即自动创建。
第二种方式是通过libcgroup
库创建、删除和管理cgroups
。(在Fedora中使用libcgroup-tools
库)
我们开始考虑简单的例子。下面的bash脚本将会打印一行到/dev/tty
设备(表示当前进程的控制终端)。
#!/bin/bashwhile :
doecho "print line" > /dev/ttysleep 5
done
如果我们运行这个脚本会看到下面的输出:
$ sudo chmod +x cgroup_test_script.sh
~$ ./cgroup_test_script.sh
print line
print line
print line
...
...
...
现在我们转到cgroupfs
在计算机上挂载的位置,正是/sys/fs/cgroup
目录,但是你也可以挂载到任意位置。
$ cd /sys/fs/cgroup
现在我们前往devices
子目录,该目录代表一种资源,控制cgroup
中的任务对设备的访问权限。
$ cd devices
然后在这创建cgroup_test_group
目录:
$ 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
和devices.deny
文件感兴趣。首先,tasks
文件应该包含要添加到该组的进程的PID。devices.deny
文件应该包含拒绝设备的列表。默认情况下,新建的cgroup对资源没有任何限制。要阻止一个设备(例如/dev/tty
),我们应该向devices.deny
文件中写入下面一行:
$ echo "c 5:0 w" > devices.deny
现在一步一步地看这一行。第一个字母c
表示设备类型,在我们的例子中,/dev/tty
是字符设备。我们可以用ls
命令的输出中进行验证:
$ ls -l /dev/tty
crw-rw-rw- 1 root tty 5, 0 Dec 3 22:48 /dev/tty
可以看到权限列表的第一个字母是c
。第二个部分是5:0
,设备的主序号和次序号,ls
的输出也能看到这些数字。最后一个字母w
禁止任务向特定的任务写入数据。现在我们执行cgroup_test_script.sh
脚本试试:
$ ./cgroup_test_script.sh
print line
print line
print line
...
...
然后把这个进程的pid添加到devices/tasks
文件中:
$ 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
容器的时候也能出现,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容器的时候,docker会为容器中的进程们创建一个cgroup
:
$ docker exec -it mysql-work /bin/bash
$ topPID 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 mysqld71 root 20 0 20248 3028 2732 S 0.0 0.0 0:00.01 bash77 root 20 0 21948 2424 2056 R 0.0 0.0 0:00.00 top
然后我们可以在主机上看见这个:
$ systemd-cglsControl group /:
-.slice
├─docker
│ └─fa2d2085cd1c8d797002c77387d2061f56fefb470892f140d0dc511bd4d9bb61
│ ├─5501 mysqld
│ └─6404 /bin/bash
现在我们对cgroup机制和如何使用它有了一些了解,现在该看一下内核源代码, 开始深入探索cgroup
机制的实现。
2 控制组的早期初始化
刚才我们还几乎完全不了解cgroup
在Linux内核的实现机制,现在我们开始深入研究Linux内核的源代码。与往常一样,我们将从cgroup
的早期初始化开始,cgroup
的初始化在内核中分为early
和late
两部分。
cgroup
的早期初始化起源于Linux内核的早期初始化init/main.c
🌍这个函数调用:
cgroup_init_early();
这个函数定义在kernel/cgroup.c
🌍中,以两个局部变量的定义开始:
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;
};
例如我们可以用name=
选项创建命名cgroup
层次结构(本例中是my_cgrp
),没有任何子系统:
$ 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
变量分别表示注册的子系统数组的ID和子系统的名字。最后一个变量root
表示指向cgroup
层次树的指针。
当然cgroup_subsys
结构很大,还有其他成员变量,但是了解到现在已经够用了。了解完cgroup
的重要结构后,就可以重新回到cgroup_init_early
函数了。这个函数的主要目的是对某些子系统进行早期初始化,这些早期(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
函数,该函数会执行默认统一层次结构。然后为默认cgroup
的状态设置CSS_NO_REF
,禁用此css的引用计数。cgrp_dfl_root
也定义在同一个文件中:
struct cgroup_root cgrp_dfl_root;
它的cgrp
字段是cgroup
结构体,定义在include/linux/cgroup-defs.h
🌍头文件中。我们已经知道,在Linux内核中,进程用task_struct
表示。task_struct
结构体不包含直接链接到任务所属cgroup的链接,但是可以用task_struct
的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;.........
}
所以,与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 || .... |+---------------------+
因此,init_cgroup_root
会给cgrp_dfl_root
填充默认值。下一个任务是将初始化的css_set
分配给代表系统第一个进程的init_task
:
RCU_INIT_POINTER(init_task.cgroups, &init_css_set);
cgroup_init_early
函数要搞的最后一件大事情是初始化early cgroups
。这里会遍历所有注册的子系统,并为其分配唯一的ID和名称,然后为标记为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.c
🌍文件中的宏,只是对cgroup_subsys
数组for
循环进行了扩展。这个数组的定义可以在相同的源代码文件中找到,看起来有点奇怪:
#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys,static struct cgroup_subsys *cgroup_subsys[] = {#include <linux/cgroup_subsys.h>
};
#undef SUBSYS
它定义了一个SUBSYS
宏,带一个参数(子系统名称),然后定义了表示cgroup
子系统的cgroup_subsys
数组。另外可以看到,这个数组是使用linux/cgroup_subsys.h
🌍头文件的内容初始化的,在这个文件里面还会再次看到SUBSYS
宏:
#if IS_ENABLED(CONFIG_CPUSETS)
SUBSYS(cpuset)
#endif#if IS_ENABLED(CONFIG_CGROUP_SCHED)
SUBSYS(cpu)
#endif
...
...
...
<什么>之所以有效是因为在第一次定义SUBSYS
宏之后使用了#undef
语句。看看这个表达式:&_x ## _cgrp_subsys
,##
操作符会连接左右两个表达式作为一个标识符。所以当我们传递cpuset
、cpu
等给SUBSYS
宏的时候,应该在某个地方定义cpuset_cgrp_subsys
和cpu_cgrp_subsys
。我们的猜想是对的。打开kernel/cpuset.c
代码的时候,就能看到定义:
struct cgroup_subsys cpuset_cgrp_subsys = {..........early_init = true,
};
因此,cgroup_init_early
函数的最后一步是通过调用cgroup_init_subsys
初始化early
子系统。下面的子系统会被初始化:
cpuset
;cpu
;cpuacct
.
cgroup_init_subsys
函数使用默认值对给定的子系统进行初始化,例如设置cgroup层次结构的根、通过调用css_alloc
为给定的子系统分配空间、将子系统与父节点连接、为初始进程添加分配的子系统等。
3 libcgroup
这里不是翻译的,是整理各种网上资料写成的。
3.0 一些比较杂乱的代码
# 安装libcgroup
$ yum install -y libcgroup
# 创建两个不同cpu资源分配的组
$ cgcreate -g cpu:/large # 具体命令使用cgcreate -h
$ cgcreate -g cpu:/small
其中冒号左边的是cgroup子系统,右边是cgroup层次结构。
$ cgset -r cpu.shares=512 small
cpu.shares
是cpu控制的一个属性,更多的属性可以到/sys/fs/cgroup/cpu
目录下查看,默认值是1024,值越大,能获得更多的cpu时间
启动一个cgroup程序:
$ cgexec -g cpu:/large echo $((1+2))
查看进程所属的cgroup
$ cat /proc/6691/cgroup
# 6691是dockerd的PID
11:blkio:/system.slice/docker.service
10:pids:/system.slice/docker.service
9:net_prio,net_cls:/
8:freezer:/
7:cpuset:/
6:devices:/system.slice/docker.service
5:cpuacct,cpu:/system.slice/docker.service
4:hugetlb:/
3:memory:/system.slice/docker.service
2:perf_event:/
1:name=systemd:/system.slice/docker.service
$ cgset --copy-from path_to_source_cgroup path_to_target_cgroup
获得修改相关 cgroup 的权限后,请运行用户账户中的 cgset
指令来设定管控器参数。请仅对手动挂载的管控器使用此指令。
3.1 cgset
红帽官方文档
cgset 的语法为:
$ cgset -r parameter=value path_to_cgroup
其中:
parameter
是要设定的参数,它与给定 cgroup 目录中的文件对应;value
是参数值;path_to_cgroup
是“与层级的根相对”的 cgroup 路径。
cgset
设定的值可能会受限于一个特定层级所设定的更高值。例如,在一个系统中,如果 group1 被限定仅可使用 CPU 0,那您就不能设定 group1/subgroup1
使用 CPU 0 和 1,或者仅使用 CPU 1。
您也可以使用 cgset
将一个 cgroup 的参数复制到另一个已有 cgroup 中。使用 cgset 复制参数的句法是:
$ cgset --copy-from path_to_source_cgroup path_to_target_cgroup
其中:
path_to_source_cgroup
是要复制其参数的 cgroup 路径,相对层级的根群组;path_to_target_cgroup
是目标 cgroup 的路径,相对层级的根群组。
3.2 cgclassify
红帽官方文档
您可以运行 cgclassify
指令将进程移动到 cgroup 中:
$ cgclassify -g controllers:path_to_cgroup pidlist
其中:
controllers
是资源控制器列表,以逗号分隔;或者使用 * 来启动与所用可用子系统相关的层级中的进程。请注意,如果几个 cgroup 的名称相同,-g
选项会将进程移至这些群组的每一个。path_to_cgroup
是层级中 cgroup 的路径;pidlist
是进程ID(PID)的列表,以空格隔开。
3.3 cgcreate
红帽官方文档
在您自己创建的层级中,您可以使用 cgcreate
指令来创建临时 cgroup。cgcreate
的句法是:
$ cgcreate -t uid:gid -a uid:gid -g controllers:path
其中:
-t
(可选)—— 指定一个用户(通过用户 ID:uid)和群组(通过群组 ID:gid)来拥有此 cgroup 的 tasks 伪文件。此用户可在该 cgroup 中添加任务。
请注意,从 cgroup 中移除进程的唯一方法是将进程移至另一个 cgroup。如想要移除进程,用户必须拥有 “目标” cgroup 的写入权限;但是源 cgroup 的写入权限并不重要。-a
(可选)—— 指定一个用户(通过用户 ID:uid)和群组(通过群组 ID:gid)来拥有此 cgroup 的全部伪文件而不是 tasks 。此用户可以修改 cgroup 中任务存取系统资源的权限。-g
—— 指定 cgroup 应该被建于其中的层级,类似于“管控器”和这些层级的列表(以逗号分隔)。管控器的此项列表之后是一个冒号以及相对层级的子群组“路径”。请不要将层级挂载点包含于路径中。
3.4 cgdelete
可以用与 cgcreate
句法相似的 cgdelete
指令来移除 cgroup。请以 root
身份运行以下指令:
cgdelete controllers:path
其中:
- controller 是管控器的逗号分隔清单。
- path 是与该层级的根相对的 cgroup 路径。
例如:
$ cgdelete net_prio:/test-subgroup
指定 -r
选项时,cgdelete
也可以递归式移除所有子群组。
请注意,当您删除一个 cgroup,它的全部进程会移动到其父群组。
3.5 红帽文档目录
-
资源管理指南
-
控制群组简介
1.1. 什么是控制群组
1.2. cgroup 的默认层级
1.3. Linux Kernel 的资源管控器
1.4. 附加资源
-
使用控制群组
2.1. 创建控制群组
2.1.1. 用 systemd-run 创建临时 cgroup
2.1.2. 创建永久 cgroup
2.2. 删除控制群组
2.3. 修改 cgroup
2.3.1. 在命令列界面设定参数
2.3.2. 修改单位文件
2.4. 获得关于控制群组的信息
2.4.1. 列出已启动的服务(Listing Units)
2.4.2. 查看控制群组的层级
2.4.3. 查看资源管控器
2.4.4. 监控资源消耗量
2.5. 附加资源
-
使用 libcgroup 工具
3.1. 挂载层级
3.2. 卸载层级
3.3. 创建控制群组
3.4. 删除控制群组
3.5. 设定 cgroup 参数
3.6. 将进程移至控制群组
3.7. 启动控制群组的进程
3.8. 获区关于控制群组的信息
3.9. 附加资源
-
控制群组应用示例
4.1. 定义数据库 I/O 的优先级
4.2. 定义网络流量的优先级
附录. 子系统和可调参数
-
blkio
1.1. 权重分配的可调参数
1.2. I/O 节流可调参数
1.3. blkio 的通用可调参数
1.4. 示例应用
-
cpu
2.1. CFS 可调度参数
2.2. RT 可调参数
2.3. 示例应用
-
cpuacct
-
cpuset
-
devices
-
freezer
-
memory
7.1. 示例应用
-
net_cls
-
net_prio
-
ns
-
perf_event
-
常用可调参数
-
附加资源