最新消息: USBMI致力于为网友们分享Windows、安卓、IOS等主流手机系统相关的资讯以及评测、同时提供相关教程、应用、软件下载等服务。

CGroup(控制组)

互联网 admin 5浏览 0评论

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

现在,我们对tasksdevices.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的初始化在内核中分为earlylate两部分。

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_onlinecss_offline回调函数分别表示cgroup成功完成所有分配后的回调函数和cgroup释放之前的回调函数,early_init标志表示应该尽早完成初始化的子系统,idname变量分别表示注册的子系统数组的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_structcss_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##操作符会连接左右两个表达式作为一个标识符。所以当我们传递cpusetcpu等给SUBSYS宏的时候,应该在某个地方定义cpuset_cgrp_subsyscpu_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. 资源管理指南

  2. 控制群组简介

    1.1. 什么是控制群组

    1.2. cgroup 的默认层级

    1.3. Linux Kernel 的资源管控器

    1.4. 附加资源

  3. 使用控制群组

    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. 附加资源

  4. 使用 libcgroup 工具

    3.1. 挂载层级

    3.2. 卸载层级

    3.3. 创建控制群组

    3.4. 删除控制群组

    3.5. 设定 cgroup 参数

    3.6. 将进程移至控制群组

    3.7. 启动控制群组的进程

    3.8. 获区关于控制群组的信息

    3.9. 附加资源

  5. 控制群组应用示例

    4.1. 定义数据库 I/O 的优先级

    4.2. 定义网络流量的优先级

附录. 子系统和可调参数

  1. blkio

    1.1. 权重分配的可调参数

    1.2. I/O 节流可调参数

    1.3. blkio 的通用可调参数

    1.4. 示例应用

  2. cpu

    2.1. CFS 可调度参数

    2.2. RT 可调参数

    2.3. 示例应用

  3. cpuacct

  4. cpuset

  5. devices

  6. freezer

  7. memory

    7.1. 示例应用

  8. net_cls

  9. net_prio

  10. ns

  11. perf_event

  12. 常用可调参数

  13. 附加资源

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

现在,我们对tasksdevices.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的初始化在内核中分为earlylate两部分。

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_onlinecss_offline回调函数分别表示cgroup成功完成所有分配后的回调函数和cgroup释放之前的回调函数,early_init标志表示应该尽早完成初始化的子系统,idname变量分别表示注册的子系统数组的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_structcss_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##操作符会连接左右两个表达式作为一个标识符。所以当我们传递cpusetcpu等给SUBSYS宏的时候,应该在某个地方定义cpuset_cgrp_subsyscpu_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. 资源管理指南

  2. 控制群组简介

    1.1. 什么是控制群组

    1.2. cgroup 的默认层级

    1.3. Linux Kernel 的资源管控器

    1.4. 附加资源

  3. 使用控制群组

    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. 附加资源

  4. 使用 libcgroup 工具

    3.1. 挂载层级

    3.2. 卸载层级

    3.3. 创建控制群组

    3.4. 删除控制群组

    3.5. 设定 cgroup 参数

    3.6. 将进程移至控制群组

    3.7. 启动控制群组的进程

    3.8. 获区关于控制群组的信息

    3.9. 附加资源

  5. 控制群组应用示例

    4.1. 定义数据库 I/O 的优先级

    4.2. 定义网络流量的优先级

附录. 子系统和可调参数

  1. blkio

    1.1. 权重分配的可调参数

    1.2. I/O 节流可调参数

    1.3. blkio 的通用可调参数

    1.4. 示例应用

  2. cpu

    2.1. CFS 可调度参数

    2.2. RT 可调参数

    2.3. 示例应用

  3. cpuacct

  4. cpuset

  5. devices

  6. freezer

  7. memory

    7.1. 示例应用

  8. net_cls

  9. net_prio

  10. ns

  11. perf_event

  12. 常用可调参数

  13. 附加资源

与本文相关的文章

发布评论

评论列表 (0)

  1. 暂无评论