linux之cgroup(1)基础概念总结

Linux之cgroup子系统基础知识

Author:Once Day Date:2023年3月19日

漫漫长路,才刚刚开始…

推荐参考文档:

1.概述

cgroup是于2.6内核由Google公司主导引入的,是Linux内核实现资源虚拟化的技术基石,LXC(Linux Containers)和docker容器所用到的资源隔离技术,正是Cgroup

cgroup子系统的全称是control groups,提供对CPU、内存、网络等资源实现精细化控制的能力。允许对某一个进程,或某一组进程所用到的资源进行控制。

cgroupnamespace都会对进程进行分组,但两者作用不一样,namespace会隔离进程组之间的资源,cgroup则可以对一组进程进行统一的资源监控和限制。

cgroup有两个版本,v1v2v1版本功能很多,但比较零散,因此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版本对应文件系统为cgroup2fsv1版本对应文件系统为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可通过cgroupfstmpfs文件系统来控制各类参数,这是一种虚拟的文件系统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是按照挂载顺序生成的一个编号。cpucpuacct挂载到同一个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,但是顺序和唯一性都不能保证。

一般来说,taskcgroups.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

可以看到,指定了-s700temp/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常见子系统参数

下面直接列出收集的各类子系统控制参数的文档,没有具体去测试,可参考下面的原始文档:

由于篇幅限制,后续的各子系统参数整理将分别以专门文章总结,当然最主要的目的,还是想一边实际操作,一边总结对错。

猜你喜欢

转载自blog.csdn.net/Once_day/article/details/129679566