Linux之cgroup子系统基础知识
Author:Once Day Date:2023年3月19日
漫漫长路,才刚刚开始…
推荐参考文档:
目录
1.概述
cgroup
是于2.6内核由Google公司主导引入的,是Linux内核实现资源虚拟化的技术基石,LXC(Linux Containers)
和docker容器所用到的资源隔离技术,正是Cgroup
。
cgroup
子系统的全称是control groups,提供对CPU、内存、网络等资源实现精细化控制的能力。允许对某一个进程,或某一组进程所用到的资源进行控制。
cgroup
和namespace
都会对进程进行分组,但两者作用不一样,namespace
会隔离进程组之间的资源,cgroup
则可以对一组进程进行统一的资源监控和限制。
cgroup
有两个版本,v1
和v2
,v1
版本功能很多,但比较零散,因此v2
在这方面提供了改进,提供了一个具有增强资源管理能力的统一控制系统。
v2
版本需要较新的Linux内核才能提供支持,Linux
内核版本为5.8
或更高版本,对于通用的Linux发行版本,支持如下:
- Container-Optimized OS(从 M97 开始)
- Ubuntu(从 21.10 开始,推荐 22.04+)
- Debian GNU/Linux(从 Debian 11 Bullseye 开始)
- Fedora(从 31 开始)
- Arch Linux(从 2021 年 4 月开始)
- RHEL 和类似 RHEL 的发行版(从 9 开始)
更多详细信息可参考文档:
确定当前设备上的cgroup版本,可以使用下面的命令:
stat -fc %T /sys/fs/cgroup/
上面命令本质是读取/sys/fs/cgroup
目录的文件系统,这是一个虚拟文件系统。因此使用df
命令也可以读取相关信息。
onceday->~:# stat -fc %T /sys/fs/cgroup
cgroup2fs
onceday->~:# stat -fc %T /sys/fs/cgroup/
tmpfs
v2
版本对应文件系统为cgroup2fs
,v1
版本对应文件系统为tmpfs
。
1.1 cgroup基础概念
一般而言,cgroup中有以下四种概念名词:
task
,任务,对应于系统中运行的一个实体,一般指进程。subsystem
,子系统,即具体的资源控制器(resource controller),用来使用对某个子系统的资源控制,如CPU,memory,网络等。cgroup
,控制组,一组任务和子系统的关系,指定了资源管理策略。hierarchy
,层级树,由一系列cgroup组成的树状结构,每一个节点都是cgroup,子节点可以继承父节点的属性。
下面给出了它们之间的关系:
通过组合这些概念,便能形成复杂的控制链关系,但是会有一些约束,如下:
- 一个层级树(Hierarchy)可以附加一个或多个subsystem,也可以没有。
- 一个subsystem只能同时属于一个层级树(hierarchy)。例如memory子系统不能同时属于两个层级树。
- 每次新建层级树(Hierarchy)时,该系统上所有task构成了这个新层级树的初始化cgroup,即
Root cgroup
。在每个层级树(hierarchy)中,一个task只能属于其中一个cgroup,当添加task到新的cgroup时,会默认从旧的cgroup中移除。但是一个task可以同时存在于不同的c层级树中。 - 子进程会继承父进程的cgroup关系,但两者之间是独立的,因此子进程可以移到新的cgourp中,且不影响父进程的cgroup关系。
从上面的关系和描述来看,层级树(Hierarchy)代表一种控制关系,但控制什么,则由其所属的子系统(subsystem)决定。也就是说,我们可以指定一组控制规则(Hierarchy),然后用这组控制规则去控制CPU和memeory,以及其他资源。
每个层级树(Hierarchy)里面默认有一个Root cgroup,这个涵盖所有进程,这实际上相当于没有控制。然后在此基础上,再指定更进一步的控制策略,逐层依赖,从而不同进程的资源就区分开来了。
每一个进程特定的资源控制策略只能有一种(一个层级树里只能属于一个cgroup),但不同资源的控制策略可以不一样(可以同时属于多个层级树)。
1.2 cgroup子系统(resource controller)
推荐参考文档:
下面是常见的cgroup支持的资源控制器,也就是子系统(subsystem)。
名称 | 描述 |
---|---|
Block IO(blkio) | 限制块设备(磁盘、SSD、USB 等)的 IO 速率。 有两种策略,按时间比例分配和最大IO速率限制。 |
CPU(cpu) | 限制调度器分配的 CPU 时间。 最开始是保证最小CPU运行时间。 在支持CPU带宽控制(LInux 3.2)时,还可以定义最大CPU时间。 |
CPU Accounting(cpuacct) | 生成 cgroup 中任务使用 CPU 的报告。 |
CPU Set(cpuset) | 可以用来绑定cgroup中的进程到指定的CPU和NUMA集合中。 |
Devices (device) | 允许或者拒绝 cgroup中任务对设备的访问。 |
Freezer(freezer) | 挂起或者重启 cgroup 中的任务,子cgroup中任务同样受影响。 |
Memory(memory) | 限制 cgroup 中任务使用内存的量。 并生成任务当前内存(进程内存,内核内存,swap内存)的使用情况报告。 |
Network Classifier(net_cls) | 为 cgroup 中的报文设置上特定的 classid 标志。 这样 tc 等工具就能根据标记对网络进行配置。 只能对cgroup中任务的主动发包标记,收包则不受此控制。 |
Network Priority(net_prio) | 对每个网络接口设置报文的优先级 |
perf event(perf_event) | 限制对cgroup中业务进行perf监控。 |
huge table(gugetlb) | 限制HugeTLB的使用。 |
progress id(pids) | 限制cgroup中进程创建的数量。 |
Remote DMA(rdma) | 限制cgroup中远程DMA资源的使用。 |
使用lssubsys -a
可以查看当前系统上支持的资源:
onceday->~:# lssubsys -a
cpuset
cpu
cpuacct
blkio
memory
......
lssubsys可以列出当前系统上支持的各类子系统,其常见命令用法如下:
lssubsys -i
,如果子系统处于层次树结构中,则显示附加的层次结构编号。lssubsys -m
,显示子系统的挂载点,只显示所示层次结构的第一个装载点。lssubsys -M
,显示子系统的挂载点,显示所示层次结构的所有装载点。
上面的couset/cpu/cpuacct
这三个都是对CPU资源进行控制的子系统。
例如,cpu
可分配对应的执行时间,cpuset
可配置逻辑核。
使用lssubsys -am
查看当前各子系统的挂载点:
onceday->~:# lssubsys -am
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
1.3 cgroup文件系统
cgroup可通过cgroupfs
或tmpfs
文件系统来控制各类参数,这是一种虚拟的文件系统VFS(Virtual File System)。
VFS文件系统能够把具体文件系统的细节隐藏起来,给用户态进程提供一个统一的文件系统 API 接口。
VFS文件系统抽象了四种元对象,如下:
- 超级块对象(superblock object),存放注册的文件系统信息,这里即读取cgroup配置信息的tmpfs(v1)/cgroupfs(v2)文件系统。
- 索引节点对象(inode object),存放具体的文件信息,即存放与cgroup节点相关的信息,如创建、删除文件等操作的具体实现。
- 文件对象(file ogject),定义了对文件的各种操作,即对cgroup中各种属性对象的具体操作。
- 目录项对象(dentry object),在每个文件系统中,内核在查找某一个路径中的文件时,会为内核路径上的每一个分量都生成一个目录项对象,通过目录项对象能够找到对应的 inode 对象,目录项对象一般会被缓存,从而提高内核查找速度。
通过VFS文件系统,用户对cgroup文件系统的操作,可以转换为对内核cgroup数据结构的操作。具体的函数实现可以在内核代码去查看对应的VFS对象注册函数,正常情况下可以忽略。
2. cgroup基础使用方法
首先一般需要创建层级树(hierarchy),然后在层级树上挂载子系统,并创建各类cgroup。最后放入控制规则和对应task即可。
2.1 创建层级树hierarchy
(V1版本cgroup)
创建一个层级树很简单,如下即可:
mkdir cgroup/cpu_memory
然后使用下面命令挂载层级树hierarchy
,并且指定对应的子系统subsystem。
mount -t cgroup -o cpu,cpuset,memory cpu_memory cgroup/cpu_memory
-t
后面接文件系统的类型,那么这里肯定就是cgroup
虚拟文件系统了。-o
是使用逗号分割的挂载参数,这里是三个子系统。cpu_memory
是源资源路径,实际可以默认为none,该参数不重要。cgroup/cpu_memory
是实际的挂载点,这个路径最好使用绝对路径。
一般来说,输入挂载命令之后,基本都是报出下面的错误:
onceday->~:# mount -t cgroup -o cpu,cpuset,memory cpu_memory cgroup/cpu_memory
mount: /root/cgroup/cpu_memory: cpu_memory already mounted or mount point busy.
这很正常,因为子系统subsystem只能被挂载到一个层级之上,实际上现在的Linux系统中systemd
会自动地挂载这些V1版本cgroups子系统,如cpu/cpuset/memeory
等等。
systemd
一般都把这些子系统挂载在固定的目录下,即sys/fs/cgroup
,这刚好对应lssubsys -m
命令的输出结果,如下:
onceday->~:# lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/pids
注意,这里谈论的仅是v1版本的cgroup操作,V2版本会略有些不同,比如没有上面的这些挂载文件夹,取而代之是其他的文件内容。
也可以使用mount -t cgroup
列出当前v1
版本的cgroup子系统挂载信息:
onceday->~:# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,name=systemd)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
如果想删除该层级树(hierarchy),可以使用解除挂载命令,如下:
umount /sys/fs/cgroup/pids
需要注意,移除挂载的子系统时,必须先清除掉里面所有的cgroup(只保留root cgroup)。否则,umount
命令只是将挂载文件夹隐藏了,实际上并未清除。
例如下面首先移除了两个挂载点,(perf_event里面没有子cgroup,但pids里面有很多子cgroup):
umount /sys/fs/cgroup/perf_event/
umount /sys/fs/cgroup/pids/
移除挂载之后,从lssubsys -m
命令已经无法看到挂载点了:
onceday->cgroup:# lssubsys -m
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
hugetlb /sys/fs/cgroup/hugetlb
然后重新挂载pids和perf_event子系统,发现实际上可以挂载到多个层级树之上:
onceday->cgroup:# lssubsys -M
cpuset /sys/fs/cgroup/cpuset
cpu,cpuacct /sys/fs/cgroup/cpu,cpuacct
blkio /sys/fs/cgroup/blkio
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
perf_event /sys/fs/cgroup/perf_event
perf_event /sys/fs/cgroup/memory
hugetlb /sys/fs/cgroup/hugetlb
pids /sys/fs/cgroup/perf_event
pids /sys/fs/cgroup/pids
pids /sys/fs/cgroup/memory
可以看到pids都挂载到三个层级树上了,根据man文档说明,这是一种特殊情况,在这种情况下,三个挂载层级树提供的视图(view)是一致的,也就是说,只要确保控制规则只有一套,那是可以挂载多个hierarchy。
不过这里测试,即使存在子cgroup也可以挂载多个点,应该是有更深层次的原因,后续文章再分析吧。
可以通过内核文件系统查看当前子系统的挂载情况:
onceday->cgroup:# cat /proc/cgroups |column -t
#subsys_name hierarchy num_cgroups enabled
cpuset 3 3 1
cpu 4 4 1
cpuacct 4 4 1
blkio 5 1 1
memory 8 116 1
devices 7 96 1
perf_event 11 2 1
hugetlb 2 1 1
pids 9 101 1
hierarchy
是按照挂载顺序生成的一个编号。cpu
和cpuacct
挂载到同一个hierarchy
,所以编号是相同的。
2.2 创建cgroup控制组
创建控制组非常简单,如下:
mkdir /sys/fs/cgroup/perf_event/cg2
这将创建一个新的空cgroup,如下:
onceday->cgroup:# ll perf_event/cg2/
total 0
drwxr-xr-x 2 root root 0 Mar 20 22:03 ./
dr-xr-xr-x 3 root root 0 Mar 20 22:03 ../
-rw-r--r-- 1 root root 0 Mar 20 21:59 cgroup.clone_children
-rw-r--r-- 1 root root 0 Mar 20 22:03 cgroup.procs
-rw-r--r-- 1 root root 0 Mar 20 21:59 notify_on_release
-rw-r--r-- 1 root root 0 Mar 20 22:00 tasks
上面的这四个文件是不同资源通用的文件,其含义如下:
tasks
:当前 cgroup 包含的任务(task)pid 列表,把某个进程的 pid 添加到这个文件中就等于把进程移到该 cgroup中。cgroup.procs
:当前 cgroup 中包含的 thread group 列表,使用逻辑和tasks
相同。notify_on_release
:0 或者 1,是否在 cgroup 销毁的时候执行 notify。如果为 1,那么当这个 cgroup 最后一个任务离开时(退出或者迁移到其他 cgroup),并且最后一个子 cgroup 被删除时,系统会执行release_agent
中指定的命令。release_agent
:需要执行的命令。
一般采用下面命令来写入进程到cgroups.procs
中,$$
是对应的PID:
echo $$ > /sys/fs/cgroup/cpu/cg1/cgroup.procs
有以下规则:
- 一次应该只写入一个进程PID到这个文件中。
- 如果
$$=0
,表示将写文件的进程加入对应的cgroups中,即执行写文件动作的进程。 - 当写入PID到
cgroup.procs
中时,所有的线程也会自动全部移到对应的cgroup中。 - 在一个层级树(hierarchy)里,一个进程只能属于一个cgroup,当写入新cgroup时,旧cgroup中自动移除。
cgroup.procs
可以直接读取,将呈现所有成员的PID,但是顺序和唯一性都不能保证。
一般来说,task
和cgroups.procs
之间的区别在于,cgroups.procs
中记录的是线程的集合—即整个进程。而task
中可以单独添加一个线程,task中是隶属于该cgroup的全部线程的集合。
因此,可以单独的将某个线程单独加入到一个cgroup的task中,而不影响到其他线程,线程使用的是内核thread id,即通过gettid(2)或clone(2)返回的ID。
2.3 移除cgroup
移除一个cgroup,首先要求没有子cgroup,且不能包含正在执行的进程(僵尸进程除外)。如果存在执行的进程,那么就会产生错误,如下:
onceday->cgroup:# echo 6333 > perf_event/cg2/cg21/tasks
onceday->cgroup:# cat perf_event/cg2/cg21/cgroup.procs
6333
onceday->cgroup:# rmdir perf_event/cg2/cg21/
rmdir: failed to remove 'perf_event/cg2/cg21/': Device or resource busy
这个时候,可以将正在执行的线程移到父cgroup中去(移动就会删除旧cgroup中进程),或者直接shutdown。
onceday->cgroup:# echo 6333 > perf_event/cgroup.procs
onceday->cgroup:# cat perf_event/cg2/cg21/cgroup.procs
onceday->cgroup:# rmdir perf_event/cg2/cg21/
这个时候,子cgroup中没有正在执行的进程了,因此就能正常删除了。
2.4 通告机制
内核提供一个通告机制,当一个cgroup中不包含子cgroup且没有任何进程成员时,那么该cgroup就为空,此时会触发通告机制。
在release_agent
中可以注册一个程序(写入程序的可执行路径),那么当该cgroup变成空的时,就会调用该程序。
在调用release_agent
中的程序时,会将当前cgroup的路径pathname
作为命令行参数传递进去。
默认release_agent
中为空,所以不会有程序被调用。可以在挂载cgroup
文件系统是通过下面的参数来指定可执行程序的路径:
mount -o release_agent=pathname
此外,notify_on_release
中的值(0或1)是最终决定是否调用release_agent
中程序的关键。当创建子cgroup时,会继承父cgroup中对应文件的值。
这个通告机制典型使用者就是systemd
,用来追踪各种服务的启动和结束,systemd
一般创建一个没有子系统的层级树(hierarchy),然后利用cgorup构建整个服务框架。
2.5 cgroup-tools工具包
这个工具包可以通过命令来操作cgroup。安装方式如下:
sudo apt-get install -y cgroup-tools
最简单的命令是lssubsys
,列出当前设备上支持的子系统,这个前面已使用很多次了。
可以使用cgcreate
来为用户创建指定的cgroups:
onceday->cgroup:# cgcreate --help
Usage: cgcreate [-h] [-f mode] [-d mode] [-s mode] [-t <tuid>:<tgid>] [-a <agid>:<auid>] -g <controllers>:<path> [-g ...]
Create control group(s)
-a <tuid>:<tgid> Owner of the group and all its files
-d, --dperm=mode Group directory permissions
-f, --fperm=mode Group file permissions
-g <controllers>:<path> Control group which should be added
-h, --help Display this help
-s, --tperm=mode Tasks file permissions
-t <tuid>:<tgid> Owner of the tasks file
cgcreate命令的参数主要是指定cgroup文件目录的权限,如下:
-a
,指定cgroup除tasks
文件以外的全部其他文件的用户和组,即管理资源参数的用户。-t
,指定cgroup中tasks
文件的用户和组。-d/-f/-s
,都是指定对应目录、文件的权限。-g
,指定资源控制器(子系统)和对应的cgroup挂载路径。
如下,创建一个属于root用户的cgroup,未指定参数默认继承父cgroup。
onceday->cgroup:# cgcreate -a root -t root -s 700 -g perf_event,pids:temp
onceday->cgroup:# ll perf_event/
total 0
dr-xr-xr-x 4 root root 0 Mar 20 22:03 ./
drwxr-xr-x 12 root root 280 Mar 20 16:48 ../
......
-rw-r--r-- 1 root root 0 Mar 20 21:31 tasks
drwxr-xr-x 2 root root 0 Mar 20 23:19 temp/
onceday->cgroup:# ll pids/
total 0
dr-xr-xr-x 7 root root 0 Mar 20 21:49 ./
drwxr-xr-x 12 root root 280 Mar 20 16:48 ../
......
-rw-r--r-- 1 root root 0 Mar 20 21:31 tasks
drwxr-xr-x 2 root root 0 Mar 20 23:19 temp/
onceday->cgroup:# ll perf_event/temp/tasks
-rw------- 1 root root 0 Mar 20 23:19 perf_event/temp/tasks
可以看到,指定了-s
为700
的temp/tasks
其权限和符cgroup不一样。默认创建的路径为/sys/fs/cgroup
。
cgdelete可以用来删除一个cgroup,如下所示:
onceday->cgroup:# cgdelete --help
Usage: cgdelete [-h] [-r] [[-g] <controllers>:<path>] ...
Remove control group(s)
-g <controllers>:<path> Control group to be removed (-g is optional)
-h, --help Display this help
-r, --recursive Recursively remove all subgroups
这个命令比较简单,支持递归删除,一般使用如下即可cgdelete -g perf_event:temp
。可以删除那些还存在执行进程的cgroup,此时那些进程会移动到父cgroup中。
cgset可以设置cgroup的参数,如下:
onceday->cgroup:# cgset --help
Usage: cgset [-r <name=value>] <cgroup_path> ...
or: cgset --copy-from <source_cgroup_path> <cgroup_path> ...
Set the parameters of given cgroup(s)
-r, --variable <name> Define parameter to set
--copy-from <source_cgroup_path> Control group whose parameters will be copied
比如设置cgroup中能使用的核数,cgset -r cpuset.cpus=0-1 ./temp_cgroup
。
以及从其他cgroup拷贝参数到另外一个cgroup中,cgset --copy-from ./group1 ./group2
。
cgexec在某个cgroup中执行某个程序,并把程序添加到对应的cgroup中:
cgexec -g cpu.memory:temp_cgroup ./my_helloworld
cgclassify将某个已存在的程序移到cgroup中去,需要知道PID。
cgclassify -g perf_event:temp $$ 1725
$$
代表当前的进程ID,因此上面命令将当前的bash shell以及PID=1725的进程移到perf_event下的temp控制组中。
cgget可以读取cgroup的配置,使用如下:
onceday->cgroup:# cgget -g cpuset -a system
system:
cpuset.memory_pressure: 0
cpuset.memory_migrate: 0
......
上面是默认输出cpuset
控制器(子系统下)的system控制组配置,可以使用-r name
指定需要输出的参数,-a
默认输出全部参数。
cgclear用于清除cgroup配置(慎用,清除后除非使用cgsnapshot备份,否则无法找回):
cgclear
,不带参数下默认清除所有各种子系统的配置参数,直接所有子系统解除挂载。cgclear -e
,只移除那些空的cgroup。cgclear -i/-L
,根据指定的模板文件移除对应的cgroup。
此外,还可以使用cgsnapshot保存配置快照,使用cgconfigparser加载配置文件。
3. cgroup常见子系统参数
下面直接列出收集的各类子系统控制参数的文档,没有具体去测试,可参考下面的原始文档:
由于篇幅限制,后续的各子系统参数整理将分别以专门文章总结,当然最主要的目的,还是想一边实际操作,一边总结对错。