基本原理
cgroups是Linux内核的一个特性,主要用来对共享资源进行隔离、限制、审计等。通过cgroup可以将定额的资源分配给特定的一组进程。
默认情况下,编译内核时打开cgroup的系统中所有进程位于同一个cgroup,就是根,这个cgroup享有所有的系统资源。
可以通过cgroup文件系统建立一个新的cgroup,然后配置这个新的cgroup,配置的内容包括为其分配进程、分配资源等。这个创建和分配的所有过程都是cgroup文件系统通过shell echo
写进文件完成的。
cgroup本身时分层的,一个根层下面就像一棵树一样可以分很多层。每一层的cgroup文件系统目录下都有该层对应的资源配置文件。这些可以配置的资源都是cgroup子系统。
子系统(subsystem)实际上是cgroup对进程组进行资源控制的具体体现。子系统具有多种类型,每个类型的子系统都代表一种系统资源,比如CPU、memory
等。当创建一个cgroup实例时,必须至少指定一种子系统。也就是说,这个新建的进程组在访问子系统对应的系统资源时就有了一些限制。具体的限制项与子系统的类型有关。
cgroup中进程组的层级关系与Linux中进程的层级关系比较类似。在Linux操作系统中,一个进程通过fork()
系统调用创建了一个子进程,这两个进程之间存在父子这样的等级关系,并且子进程可以继承父进程的一些资源。系统中所有的进程形成一个树形的等级关系,每个进程都唯一的位于进程树中的某一个位置。
对于cgroup来说,cgroup实例之间也是有具体级别关系的,但是它们层级关系是为了更细粒度的对进程组进行资源控制。同时,子cgroup会继承父cgroup的对资源的控制属性。
子系统与层级关系
一个子系统就是一个资源控制器。cgroup九个子系统分别是:
cpu
子系统,主要限制进程的 cpu 使用率。cpuacct
子系统,可以统计 cgroups 中的进程的 cpu 使用报告。cpuset
子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。memory
子系统,可以限制进程的 memory 使用量。blkio
子系统,可以限制进程的块设备 io。devices
子系统,可以控制进程能够访问某些设备。net_cls
子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用tc 模块(traffic control)对数据包进行控制。freezer
子系统,可以挂起或者恢复 cgroups 中的进程。ns
子系统,可以使不同 cgroups 下面的进程使用不同的 namespace。
hierarchy层级结构
内核使用 cgroup 结构体来表示一个 control group 对某一个或者某几个 cgroups 子系统的资源限制。
hierarchy的功能是把一组cgroup串成一个树状的形状,一棵这样的树是一个hierarchy,通过这种树状结构,cgroup可以做到继承。例如系统对一组定时的任务进程通过cgroup1限制了CPU的使用率,然后其中有一个定时任务需要限制磁盘IO,就可以创建cgroup2,使其继承与cgroup1,这样cgroup2继承了cgroup1对CPU使用率的限制,同时也限制了磁盘IO。
一个hierarchy可以attach一个或多个子系统,当前层级结构可以对其 attach 的子系统进行资源的限制。每一个子系统只能被attach到一个hierarchy中。一个进程也可以属于多个cgroup,但是这些cgroup必须属于不同的hierarchy。
当建立第一个cgroup时,系统会把所有的进程都放进去,对应树形结构的树根,例如cat /sys/fs/cgroup/cpu/task
会输出所有进程的pid。
如果在下级cgroup,例如/cgroup/test的task
中将一个进程的PID echo
到文件,就会发现这个进程并没有从上层cgroup的task
列表中消失 ,而是同时出现在了下层test cgroup的task
文件中,这就表示该进程现在归test cgroup控制。但由于test cgroup是下层 cgroup ,所以进程依然存在于上cgroup,而同层的 cgroup 之间却不能含有相同的进程。
示例:
下图时一个cgroup结构图,包含两个层级,即第一层级cpu_mem
和第二层级cpuset_net
。
- 系统中第一个被创建的cgroup被称为
root cgroup
,该cgroup的成员包含系统中所有的进程。其下又包含从cgroup1
和cgroup2
两个cgroup,位于第二层级cpuset_net
中。 - 一个子系统只能位于一个层级中。在图中,
cpu
子系统位于第一层级cpu_mem
中,那么这个子系统将不能再位于第二层级中。不过第二层级会继承第一层级的cpu子系统。 - 每个层级中可以关联多个子系统。第一层级关联了
cpu
和memory
子系统。 - 一个进程可以位于不同层级的cgroup中。由于
root cgroup
包含了系统中所有的进程,因此cgroup2
中的进程P也位于root cgroup
中。从资源控制角度来说,进程P所在的进程组在访问cpu、memory
和net
时会受到资源限制。 - 一个进程创建了子进程后,该子进程默认为父进程所在的cgroup的成员。
在创建了 cgroups 层级结构中的节点(cgroup 结构体)之后,可以把进程加入到某一个节点的控制任务列表中,一个节点的控制列表中的所有进程都会受到当前节点的资源限制。同时某一个进程也可以被加入到不同的 cgroups 层级结构的节点中,因为不同的 cgroups 层级结构可以负责不同的系统资源。所以说进程和 cgroup 结构体是一个多对多的关系。
上面这个图从整体结构上描述了进程与 cgroups 之间的关系。最下面的P
代表一个进程。每一个进程的描述符中有一个指针指向了一个辅助数据结构css_set(cgroups subsystem set)
。 指向某一个css_set
的进程会被加入到当前css_set
的进程链表中。一个进程只能隶属于一个css_set
,一个css_set
可以包含多个进程,隶属于同一css_set
的进程受到同一个css_set
所关联的资源限制。
上图中的"M×N Linkage"
说明的是css_set
通过辅助数据结构可以与 cgroups 节点进行多对多的关联。但是 cgroups 的实现不允许css_set
同时关联同一个cgroups层级结构下多个节点。 这是因为 cgroups 对同一种资源不允许有多个限制配置。
一个css_set
关联多个 cgroups 层级结构的节点时,表明需要对当前css_set
下的进程进行多种资源的控制。而一个 cgroups 节点关联多个css_set
时,表明多个css_set
下的进程列表受到同一份资源的相同限制。
VFS文件系统
cgroup需要提供接口给用户态,有两个选择,一个是提供系统调用,另一个是提供虚拟文件系统。cgroup选择个后者,它提供了一种名为cgroup的文件系统。
VFS(Virtual File System) 是一个内核抽象层,能够隐藏具体文件系统的实现细节,从而给用户态进程提供一套统一的 API 接口。
VFS 使用了一种通用文件系统的设计,具体的文件系统只要实现了 VFS 的设计接口,就能够注册到 VFS 中,从而使内核可以读写这种文件系统。
这很像面向对象设计中的抽象类与子类之间的关系,抽象类负责对外接口的设计,子类负责具体的实现。其实,VFS本身就是用 c 语言实现的一套面向对象的接口。
cgroup通过VFS向用户层提供接口,用户通过挂载,创建目录,读写文件的方式与cgroup交互。
通用文件模型
VFS 通用文件模型中包含以下四种元数据结构:
- 超级块对象(superblock object),用于存放已经注册的文件系统的信息。比如ext2,ext3等这些基础的磁盘文件系统,还有用于读写socket的socket文件系统,以及当前的用于读写cgroups配置信息的 cgroups 文件系统等。
- 索引节点对象(inode object),用于存放具体文件的信息。对于一般的磁盘文件系统而言,inode 节点中一般会存放文件在硬盘中的存储块等信息;对于socket文件系统,inode会存放socket的相关属性,而对于cgroups这样的特殊文件系统,inode会存放与 cgroup 节点相关的属性信息。这里面比较重要的一个部分是一个叫做 inode_operations 的结构体,这个结构体定义了在具体文件系统中创建文件,删除文件等的具体实现。
- 文件对象(file object),一个文件对象表示进程内打开的一个文件,文件对象是存放在进程的文件描述符表里面的。同样这个文件中比较重要的部分是一个叫 file_operations 的结构体,这个结构体描述了具体的文件系统的读写实现。当进程在某一个文件描述符上调用读写操作时,实际调用的是 file_operations 中定义的方法。 对于普通的磁盘文件系统,file_operations 中定义的就是普通的块设备读写操作;对于socket文件系统,file_operations 中定义的就是 socket 对应的 send/recv 等操作;而对于cgroups这样的特殊文件系统,file_operations 中定义的就是操作 cgroup 结构体等具体的实现。
- 目录项对象(dentry object),在每个文件系统中,内核在查找某一个路径中的文件时,会为内核路径上的每一个分量都生成一个目录项对象,通过目录项对象能够找到对应的 inode 对象,目录项对象一般会被缓存,从而提高内核查找速度。
cgroup文件系统的实现
基于VFS实现的文件系统,都必须实现VFS通用文件模型定义的这些对象,并实现这些对象定义的部分函数。cgroup文件系统也不例外。
cgroup文件系统类型的结构体:
static struct file_system_type cgroup_fs_type = {
.name = "cgroup",
.mount = cgroup_mount,
.kill_sb = cgroup_kill_sb,
};
这里面两个函数分别代表安装和卸载某一个 cgroup 文件系统所需要执行的函数。每次把某一个 cgroups 子系统安装到某一个装载点的时候,cgroup_mount 方法就会被调用,这个方法会生成一个 cgroups_root(cgroups层级结构的根)并封装成超级快对象。
cgroup超级块对象定义的操作:
static const struct super_operations cgroup_ops = {
.statfs = simple_statfs,
.drop_inode = generic_delete_inode,
.show_options = cgroup_show_options,
.remount_fs = cgroup_remount,
};
这里只有部分函数的实现,这是因为对于特定的文件系统而言,所支持的操作可能仅是 super_operations 中所定义操作的一个子集,比如说对于块设备上的文件对象,肯定是支持类似 fseek 的查找某个位置的操作,但是对于 socket 或者 cgroups 这样特殊的文件系统,就不支持这样的操作。
inode
对象和file
对象的定义的特殊实现函数:
static const struct inode_operations cgroup_dir_inode_operations = {
.lookup = cgroup_lookup,
.mkdir = cgroup_mkdir,
.rmdir = cgroup_rmdir,
.rename = cgroup_rename,
};
static const struct file_operations cgroup_file_operations = {
.read = cgroup_file_read,
.write = cgroup_file_write,
.llseek = generic_file_llseek,
.open = cgroup_file_open,
.release = cgroup_file_release,
};
从这些代码可以推断出,cgroups 通过实现 VFS 的通用文件系统模型,把维护 cgroups 层级结构的细节,隐藏在 cgroups 文件系统的这些实现函数中。
从另一个方面说,用户在用户态对 cgroups 文件系统的操作,通过 VFS 转化为对 cgroups 层级结构的维护。通过这样的方式,内核把 cgroups 的功能暴露给了用户态的进程。
简单用法及说明
挂载cgroup文件系统
和所有的文件系统一样,cgroup文件系统需要先挂载,通过mount
命令挂载 cgroups 文件系统,格式为:
mount -t cgroup -o subsystems name /cgroup/name
其中-t cgroup
指定文件系统类型为cgroup类型,-o subsystems
表示需要关联的cgroups子系统,可以用逗号隔开,表示同时挂载多个子系统,name
表示实例的名称,/cgroup/name
表示挂载点,这条命令同时在内核中创建了一个cgroups 层级结构。(目前大多操作系统启动时已经默认挂载/sys/fs/cgroup
)
比如挂载 cpuset, cpu, cpuacct, memory
4个subsystem
到/cgroup/cpu_and_mem
目录下,就可以使用 mount -t cgroup -o remount,cpu,cpuset,memory cpu_and_mem /cgroup/cpu_and_mem
。
cgroup文件系统的一个特点是用户可以同时挂载多个cgroup文件系统,但是要保证每个cgroup文件系统用到的子系统没有重叠。也就是说,如果已经有一个cgroup文件系统的挂载包含了某一个子系统,其它cgroup文件系统就不能包含这个子系统。
ubuntu和centos的做法是每个cgroup文件系统的挂载基本只包含一个子系统。
ll /sys/fs/cgroup/
total 0
drwxr-xr-x. 2 root root 0 May 25 04:19 blkio
lrwxrwxrwx. 1 root root 11 May 25 04:19 cpu -> cpu,cpuacct
lrwxrwxrwx. 1 root root 11 May 25 04:19 cpuacct -> cpu,cpuacct
drwxr-xr-x. 3 root root 0 May 25 04:19 cpu,cpuacct
drwxr-xr-x. 2 root root 0 May 25 04:19 cpuset
drwxr-xr-x. 2 root root 0 May 25 04:19 devices
drwxr-xr-x. 2 root root 0 May 25 04:19 freezer
drwxr-xr-x. 2 root root 0 May 25 04:19 hugetlb
drwxr-xr-x. 2 root root 0 May 25 04:19 memory
lrwxrwxrwx. 1 root root 16 May 25 04:19 net_cls -> net_cls,net_prio
drwxr-xr-x. 2 root root 0 May 25 04:19 net_cls,net_prio
lrwxrwxrwx. 1 root root 16 May 25 04:19 net_prio -> net_cls,net_prio
drwxr-xr-x. 2 root root 0 May 25 04:19 perf_event
drwxr-xr-x. 2 root root 0 May 25 04:19 pids
drwxr-xr-x. 4 root root 0 May 25 04:19 systemd
以CPU子系统所在的cgroup文件系统为例:
ll /sys/fs/cgroup/cpu/
total 0
-rw-r--r--. 1 root root 0 May 25 04:19 cgroup.clone_children
--w--w--w-. 1 root root 0 May 25 04:19 cgroup.event_control
-rw-r--r--. 1 root root 0 May 25 04:19 cgroup.procs
-r--r--r--. 1 root root 0 May 25 04:19 cgroup.sane_behavior
-r--r--r--. 1 root root 0 May 25 04:19 cpuacct.stat
-rw-r--r--. 1 root root 0 May 25 04:19 cpuacct.usage
-r--r--r--. 1 root root 0 May 25 04:19 cpuacct.usage_percpu
drwxr-xr-x. 3 root root 0 May 25 04:52 cpu_c1
-rw-r--r--. 1 root root 0 May 25 04:19 cpu.cfs_period_us
-rw-r--r--. 1 root root 0 May 25 04:19 cpu.cfs_quota_us
-rw-r--r--. 1 root root 0 May 25 04:19 cpu.rt_period_us
-rw-r--r--. 1 root root 0 May 25 04:19 cpu.rt_runtime_us
-rw-r--r--. 1 root root 0 May 25 04:19 cpu.shares
-r--r--r--. 1 root root 0 May 25 04:19 cpu.stat
-rw-r--r--. 1 root root 0 May 25 04:19 notify_on_release
-rw-r--r--. 1 root root 0 May 25 04:19 release_agent
-rw-r--r--. 1 root root 0 May 25 04:19 tasks
总共包含的文件可以分为三类:
- 目录。再cgroup文件系统中目录对应一个具体的cgroup。
- 通用文件。以
cgroup.
为前缀的文件和三个无前缀的文件notify_on_release, release_agent, tasks
,这些通用文件再每个cgroup文件系统都会出现,无论指定的是哪个子系统。 - 子系统专有文件。这些文件是配置子系统的接口,子系统不同,文件也不同。例如上面的以
cpu.
为前缀的文件。
在cgroup文件系统中,创建(删除)一个目录就是创建(删除)一个cgroup,创建完目录后,会自动产生很多文件,在cgroup文件系统的根目录下(在上面的例子中是/sys/fs/cgroup/cpu
)的大多数文件都会出现在cgroup文件系统的下级目录中,除了少数在代码实现中标记为只在根目录中出现的文件,例如cgroup.sane_behavior
。
各个子系统的通用文件:
cgroup.procs
此文件的内容和cgroup中所有进程的进程号关联。这里的进程号实际上是线程组号(thread group ID)。向这个文件写入一个线程组号,就会把整个线程组中的线程都加入此文件所属的cgroup。cgroup.clone_chidren
这个文件和一个标志关联,而此标志只被cpuset
子系统使用。当此标志为1时,创建新的有关cpuset
的cgroup时,子cgroup从父cgroup中复制cpuset
配置。cgroup.sane_behavior
显示cgroup文件系统的sane_behavior
的状态。随着内核控制组子系统的开发,一些旧的做法不再合适,但是为了兼容旧的应用又不好完全清除。于是开发者提供了一个选项__DEVEL__sane_behavior
,供挂载时使用。如果有这个挂载选项,老旧的不合适的代码逻辑就不会出现,tasks、notify_on_release、release_agent
这三个文件也不会出现。tasks
旧机制,尽量不要使用。此文件关联控制组中所有线程的线程号。notify_on_release
旧机制,尽量不要使用。通过它来存取notify_on_release
标志。当此标志为 1 时,在 cgroup退出时,内核要通知用户态。所谓退出是指此 cgroup 不再有进程,也不再有子 cgroup。release_agent
旧机制,尽量不要使用。此文件中存有一个路径,当内核需要通知用户态 cgroup 退出事件时,内核运行此路径所指的可执行文件。
子节点和进程
挂载某一个cgroups子系统到挂载点之后,就可以通过在挂载点下面建立文件夹创建cgroups层级结构中的节点,当创建完文件夹后,会自动在其中创建相关的配置文件。创建目录的动作也就是创建了下个层级的一个分支,还可以在这个目录中继续创建目录,形成第三层级。
然后在相应的配置文件中写上具体的配置。例如在cpu.shares
中写入整数值可以控制该cgroup获得的时间片。
创建并配置完节点后,将进程的pid写入子节点下面的task文件中即可。
示例:
# 在cpu层级创建一个新的cgroup节点
# 命名为cpu_c1
mkdir /cgroup/cpu/cpu_c1
# 设置`cpu.shares`文件
# 将cpu_c1目录下的cpu.shares文件值设为2048,这样在系统出现cpu争抢时
# 属于cpu_c1这个cgroup的进程占用资源是其它进程的2倍。
echo 2048 >> /cgroup/cpu/cpu_c1/cpu.shares
# 将进程加入到`cpu_c1`这个cgroup节点。
echo pid >> /cgroup/cpu/cpu_c1/tasks
查看已经挂载的cgroup
mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
源码分析
cgroup分为三个部分:
- 描述子系统(subsys)和cgroup等对象、和它们的依附关系等数据结构;
- 提供给用户空间的文件系统接口;
- 各子系统。
数据结构
任务(task)
task对应linux中的进程,task和cgroup实例之间是多对多的关系,cgroup实例和子系统是一对多的关系,task和子系统也是多对多的关系(task可以依附多个cgroup,一个cgroup可能依附了多个子系统)。
要描述这些关系不容易,如果task通过各cgroup来引用各子系统再从subsys获取到资源限制,这比较低效。内核用css_set
来包含多个子系统的组合,每个task对应若干css_set
结构体,即通过css_set
知道它受哪些限制,加快了访问速度,而且子系统组合是有限的,减少了内核数据结构的复杂度。
实际上css_set就是若干cgroup_subsys_state
实例的集合,而每个cgroup_subsys_state
实例描述了一个子系统。
css_set
css_set
结构体表示一种资源限制的集合(比如cpu 20% mem 40%
和cpu 30% mem 20%
是不同的资源限制,用不同的css_set)并且连接进程和subsys。
// include/linux/cgroup.h
struct css_set {
…
// 关联此css_set的进程链表
struct list_head tasks;
// 关联的cgroup链表,通过cg_cgroup_link结构体来连接
struct list_head cgrp_links;
// cg_links用于链接所有关于此css_set的cgroup,cgroup并不是直接连接到此list_head,而是通过cg_cgroup_link结构体连接。
// 一个数组,每个元素指向一个cgroup_subsys_state,这个数组元素与subsystem的个数一一对应
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];
…
};
task_struct:
进程结构体
// include/linux/sched.h
struct task_struct {
…
#ifdef CONFIG_CGROUPS
// 该进程关联的css_set
struct css_set __rcu *cgroups;
// 链接到关联css_set的task链表
struct list_head cg_list;
#endif
…
}
task可以通过task_struct->css_set->cgroup_subsys_state找到该task所受子系统的限制。
cgroup
在结构体cgroup
中,一个cgroup
实例就是cgroup文件系统中的一个cgroup文件,cgroup
结构体主要作用是关联css_set
和subsys
。
对于task
到cgroup
不需要直接连接,可以通过css_set
引用到cgroup
。
// include/linux/cgroup.h
// 子节点cgroup
struct cgroup {
…
unsigned long flags; // 用于标识当前cgroup的状态
struct list_head sibling; // 兄弟节点链表
struct list_head children; // 子节点链表
struct cgroup *parent; // 父节点
struct list_head files; /* my files */
struct dentry *dentry; // 该cgroup对应的目录的描述
struct cgroupfs_root *root; // 指向hierarchy结构体,每个cgroup文件系统都有一个cgroupfs_root,在mount时创建
struct cgroup_subsys_state __rcu *subsys[CGROUP_SUBSYS_COUNT]; // 提供给各子系统的限制资源统计数据
struct list_head cset_links;
struct list_head e_csets[CGROUP_SUBSYS_COUNT]; // 使用的所有子系统的每个链表
…
};
大致可以分为4部分:
1.层级结构成员
用于描述cgroup层级结构的成员,包括sibling children parent
三个成员;
2.文件描述相关成员
包括files dentry root
三个成员。
内核cgroup子系统创造了名为cgroup的虚拟文件系统。每个cgroup节点都和此文件系统中的一个目录相联系。
结构体中成员dentry *dentry
指向与之相关联的目录,成员list_head files
指向目录下的文件,成员cgroupfs_root *root
指向cgroup文件系统的根节点。
3.子系统相关成员
首先先看cgroup_subsys
结构体:
cgroup_subsys
cgroup通过此结构体操作各个子系统,每个子系统都需要实现这样的一个结构体。
struct cgroup_subsys {
struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss,
struct cgroup *cgrp);
void (*pre_destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
void (*destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp);
int (*can_attach)(struct cgroup_subsys *ss,
struct cgroup *cgrp, struct task_struct *tsk);
void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp,
struct cgroup *old_cgrp, struct task_struct *tsk);
void (*fork)(struct cgroup_subsys *ss, struct task_struct *task);
void (*exit)(struct cgroup_subsys *ss, struct task_struct *task);
int (*populate)(struct cgroup_subsys *ss,
struct cgroup *cgrp);
void (*post_clone)(struct cgroup_subsys *ss, struct cgroup *cgrp);
void (*bind)(struct cgroup_subsys *ss, struct cgroup *root);
int subsys_id; // 表示了子系统的ID
int active; // 表示子系统是否被激活
int disabled; // 子系统是否被禁止
int early_init;
const char *name; // 子系统名称
struct cgroupfs_root *root; // 被附加到的层级挂载点
struct list_head sibling; // 用于连接被附加到同一个层级的所有子系统
void *private; // 私有数据
};
cgroup_subsys
结构包含了很多函数指针,通过这些函数指针,cgroup可以对子系统进行一些操作。例如向 cgroup 的 tasks 文件添加要控制的进程PID时,就会调用 cgroup_subsys
结构的 attach()
函数。当在层级中创建新目录时,就会调用 create()
函数创建一个子系统的资源控制统计信息对象 cgroup_subsys_state
,并且调用 populate()
函数创建子系统相关的资源控制信息文件。
每个subsys有自己的属性和操作,用相同的结构体来描述subsys有点太困难,cgroup_subsys描述的是subsys通用的部分,各种不同的子系统是有自己的结构体的。
例如内存子系统:
struct cgroup_subsys mem_cgroup_subsys = {
.name = "memory",
.subsys_id = mem_cgroup_subsys_id,
.create = mem_cgroup_create,
.pre_destroy = mem_cgroup_pre_destroy,
.destroy = mem_cgroup_destroy,
.populate = mem_cgroup_populate,
.attach = mem_cgroup_move_task,
.early_init = 0,
};
另外 Linux 内核还定义了一个 cgroup_subsys
结构的数组 subsys
,用于保存所有子系统的 cgroup_subsys
结构,如下:
static struct cgroup_subsys *subsys[] = {
cpuset_subsys,
debug_subsys,
ns_subsys,
cpu_cgroup_subsys,
cpuacct_subsys,
mem_cgroup_subsys
};
cgroup_subsys_state
结构体cgroup_subsys_state
是一个"中介",介于结构体cgroup
和结构体cgroup_subsys
之间。
// include/linux/cgroup.h
struct cgroup_subsys_state {
// 本subsys所依附的cgroup
struct cgroup *cgroup;
// 用到的子系统
struct cgroup_subsys *ss;
...
}
可以看到其中并不包含子系统的相关资源统计信息,而真正的信息是存在具体子系统结构体中的。
例如mem_cgroup:
struct mem_cgroup {
struct cgroup_subsys_state css;
struct res_counter res;
struct mem_cgroup_lru_info info;
int prev_priority;
struct mem_cgroup_stat stat;
};
第一个字段就是指向cgroup_subsys_state
,除此之外,其余部分信息由mem_cgroup自己维护,因此他们之间的关系:
cgroup
和cgroup_subsys_state
的关系:
4.进程相关成员
cset_links
。
cgroup节点和进程是多对多映射。一个cgrou节点中可以有多个进程,一个进程也可以加入多个cgroup。
假设有 3 个进程,4 个控制组,进程 1 用到控制组 1 和 3,进程 2 用到控制组 2 和 4,进程 3 用到控制组 2 和 4。从进程的角度看和从控制组的角度看,如图所示。
在结构cgroup中有:
// include/linux/cgroup.h
struct cgroup {
…
struct list_head cset_links;
…
}
结构 cgroup 的成员 cset_links
和另一个结构 cgrp_cset_link
关联:
kernel/cgroup.c
struct cgrp_cset_link {
/* the cgroup and css_set this link associates */
struct cgroup *cgrp;
struct css_set *cset;
/* list of cgrp_cset_links anchored at cgrp->cset_links */
struct list_head cset_link;
/* list of cgrp_cset_links anchored at css_set->cgrp_links */
struct list_head cgrp_link;
};
联系在一起,还是上面那个例子,内核中的数据结构如图所示。
cgroup_root
和cgroup
是使用两个不同结构表示的。
文件系统接口
cgroup文件系统是cgroup提供给用户层的控制接口,cgroup文件系统挂不挂载,cgroup都是生效运行的。
mount函数
挂载最终会调用到内核函数cgroup_get_sb()
,其中比较重要的部分:
static int cgroup_get_sb(struct file_system_type *fs_type,
int flags, const char *unused_dev_name,
void *data, struct vfsmount *mnt)
{
...
struct cgroupfs_root *root;
...
root = kzalloc(sizeof(*root), GFP_KERNEL);
...
ret = rebind_subsystems(root, root->subsys_bits);
...
struct cgroup *cgrp = &root->top_cgroup;
cgroup_populate_dir(cgrp);
...
}
首先会调用 kzalloc()
函数创建一个 cgroupfs_root
结构。
cgroupfs_root
结构主要用于描述这个挂载点的信息:
struct cgroupfs_root {
struct super_block *sb; // 挂载的文件系统超级块
unsigned long subsys_bits; // 附加到此层级的子系统标志
unsigned long actual_subsys_bits; // 附加到此层级的子系统标志
struct list_head subsys_list; // 附加到此层级的子系统(cgroup_subsys)列表
struct cgroup top_cgroup; // 此层级的根cgroup
int number_of_cgroups; // 层级中有多少个cgroup
struct list_head root_list; // 连接系统中所有的cgroupfs_root
unsigned long flags; // 标志位
char release_agent_path[PATH_MAX];
};
其中比较重要的是subsys_list
,表示附加到此层级上的所有子系统;top_cgroup
字段表示此层级的根cgroup
。
然后调用rebind_subsystems()
函数,把挂载时指定的子系统添加到cgroupsfs_root
结构实例root.subsys_list
链表上,并且为此层级结构的根cgroup.subsys
字段设置各个子系统的资源控制统计信息对象。
最后调用cgroup_populate_dir()
函数向挂载目录创建管理文件。
attach函数
当向tasks文件中写入pid时,会用调用attach_task_by_pid()
函数。
static int attach_task_by_pid(struct cgroup *cgrp, char *pidbuf)
{
pid_t pid;
struct task_struct *tsk;
int ret;
if (sscanf(pidbuf, "%d", &pid) != 1) // 读取进程pid
return -EIO;
if (pid) { // 如果有指定进程pid
...
tsk = find_task_by_vpid(pid); // 通过pid查找对应进程的进程描述符
if (!tsk || tsk->flags & PF_EXITING) {
rcu_read_unlock();
return -ESRCH;
}
...
} else {
tsk = current; // 如果没有指定进程pid, 就使用当前进程
...
}
ret = cgroup_attach_task(cgrp, tsk); // 调用 cgroup_attach_task() 把进程添加到cgroup中
...
return ret;
}
此函数中会先判断是否指定了pid,如果没有指定则默认为当前进程,然后调用cgroup_attach_task()
函数把进程添加到cgroup
中。
cgroup_attach_task()
:
int cgroup_attach_task(struct cgroup *cgrp, struct task_struct *tsk)
{
int retval = 0;
struct cgroup_subsys *ss;
struct cgroup *oldcgrp;
struct css_set *cg = tsk->cgroups;
struct css_set *newcg;
struct cgroupfs_root *root = cgrp->root;
...
newcg = find_css_set(cg, cgrp); // 根据新的cgroup查找css_set对象
...
rcu_assign_pointer(tsk->cgroups, newcg); // 把进程的cgroups字段设置为新的css_set对象
...
// 把进程添加到css_set对象的tasks列表中
write_lock(&css_set_lock);
if (!list_empty(&tsk->cg_list)) {
list_del(&tsk->cg_list);
list_add(&tsk->cg_list, &newcg->tasks);
}
write_unlock(&css_set_lock);
// 调用各个子系统的attach函数
for_each_subsys(root, ss) {
if (ss->attach)
ss->attach(ss, cgrp, oldcgrp, tsk);
}
...
return 0;
}
此函数首先会find_css_set()
函数查找或创建一个css_set
对象,此css_set
对象包含了该进程所在的cgroup
中所包含的子系统,通过css_set
能快速查询到该进程被限制的资源。
然后把进程实例tsk.cgroup
字段设置成该css_set
,然后把此进程添加到css_set.tasks
列表中,表示关联此css_set
的进程列表。
最后调用层级结构上的所有子系统的attach()
函数,对新增进程进行一系列其它操作(具体子系统处理会有所不同)。
各个子系统的实现
上面可以知道,内核中的cgroup机制提供一个框架,通过在其中添加子系统,可以实现对进程的不同资源进行限制。以cpuacct为例,其子系统实现在kernel/sched/cpuacct.c,这是在调度系统里添加的一个子系统,用于进程占用时间片的统计。
struct cgroup_subsys cpuacct_subsys = {
.name = "cpuacct",
.css_alloc = cpuacct_css_alloc,
.css_free = cpuacct_css_free,
.subsys_id = cpuacct_subsys_id,
.base_cftypes = files,
.early_init = 1,
};
static struct cftype files[] = {
{
.name = "usage",
.read_u64 = cpuusage_read,
.write_u64 = cpuusage_write,
},
{
.name = "usage_percpu",
.read_seq_string = cpuacct_percpu_seq_read,
},
{
.name = "stat",
.read_map = cpuacct_stats_show,
},
{ } /* terminate */
};
/* track cpu usage of a group of tasks and its child groups */
struct cpuacct {
struct cgroup_subsys_state css;
/* cpuusage holds pointer to a u64-type object on every cpu */
u64 __percpu *cpuusage;
struct kernel_cpustat __percpu *cpustat;
};
void cpuacct_charge(struct task_struct *tsk, u64 cputime)
{
struct cpuacct *ca;
int cpu;
cpu = task_cpu(tsk);
rcu_read_lock();
ca = task_ca(tsk);
while (true) {
u64 *cpuusage = per_cpu_ptr(ca->cpuusage, cpu);
*cpuusage += cputime;
ca = parent_ca(ca);
if (!ca)
break;
}
rcu_read_unlock();
}
cpuacct实现很简单,定义了cpuacct_subsys,它的属性只有三个,内嵌cgroup_subsys_state的结构体也只有两个额外的percpu变量。其运行原理是在进程调度的时候调用cpuacct_charge将进程运行时间记录到进程相关的cgroup的cpuacct子系统的结构体中。在用户读相关cgroup文件的时候将其打印出来。
总结
cgroup的组指的是一组进程。如果不做干预,进程创建的子进程会被自动加入到进程所在的控制组之中。控制组的优点是可以针对一组数量不定的进程进行控制和统计。
控制组没有增加新的系统调用,而是实现了一种新的文件系统 cgroup。有了 cgroup 文件系统,创建和删除控制组就转化为创建和删除目录,查看资源使用情况和调整参数就转化为读写文件。相比系统调用,操作文件无疑更加方便。