Cgroup基本原理
一、背景
cgroup在2006年由Google工程师开发,在2008年融入linux2.6.24,目前已经有两个版本v1、v2。其目的是为了管理不同的进程组,监控一组进程的行为和资源分配,其是docker和k8s的基石,同时也被高版本内核的LXC技术所使用。
拿官网的一张图就能很好地理解它到底想干什么,即教授和学生分别分配不同的资源,总之就是根据对不同场景、不同功能(子系统)管理一组进程。

二、什么是cgroup
Cgroup是linux内核用来控制系统资源的机制,它将操作系统中的所有进程以组为单位划分,给这一组进程定义对某一类资源特定的访问权限。Cgroup用子系统(subsystem)来描述所能控制的系统资源,子系统具有多种类型,每个类型的子系统都代表一种系统资源,比如freezer、CPU、memory、IO等。以freezer子系统为例,这个子系统可以对一组线程批量冻结,使用下面命令将打开freezer子系统:mount cgroup none /dev/freezer freezer
该命令将子系统挂载于/dev/freezer目录,接下来可以在/dev/freezer目录下创建若干个目录,例如目录top、background,每个目录代表一组线程的资源分配行为,以cgroup实例描述,那么多级的目录以cgroup实例的形式组成了一个树形结构。接下来,可以给top组或者background组配置组内进程,echo 1199 > /dev/freezer/top/cgroup.procs当前cgroup版本已经不支持同一进程组内不同线程分属于子系统内不同cgroup。配置完top组内的线程后,可以通过操作freezer.state节点配置组内所有线程冻结:echo FROZEN > /dev/freezer/top/freezer.state
解冻组内所有线程:echo THAW > /dev/freezer/top/freezer.state
三、整体架构
在深入了解源码之前,先了解一些cgroup内部的常用术语,子系统、层级、cgroupfs_root、cgroup、css_set、cgroup_subsys_state、cg_cgroup_link、cgroup_subsys_state的实现。
| 名称 | 描述 |
|---|---|
| 子系统 | 不同的子系统将会控制不同的进程行为,比如cpu子系统,其可以控制一组进程被cpu执行的时间占比。 |
| 层级、cgroupfs_root | 层级在内核中表示为cgroupfs_root,一个层级控制一批进程,每个层级内部会绑定一个或多个子系统,一个进程可以在不同的层级,但是每个进程在一个层级中是唯一存在的,这样也是合理的,一个进程想被多个子系统管理,而子系统被分散在不同的层级中,所以进程可以被多个层级管理,但是一个层级中的子系统可以被配置不同的配置项,比如有的目录下配置的是50的cpu使用时间(根据权重),有的目录下配置的是20的cpu使用时间(根据权重),一个进程不可能既是50的cpu利用率又是20的cpu利用率。 |
| cgroup | 每个层级内部是一棵树的组织形式,每一个节点为一个cgroup,具体可以看看cgroup的结构体。 cgroup.h |
| css_set | 可以看到cgroup的确是以树形结构存在的,其可以当做一个目录树,一棵树也就对应着一整个层级,一整个层级内部可以关联一个或多个子系统。每一个task_struct内部都会有一个css_set,一个css_set内部可以找到所有控制着此进程的cgroup,而多个进程可以共用一个css_set,因为多个进程可能使用同一组配置文件,看看内核中对此结构的定义: cgroup.h 发现其内部用tasks链表绑定了一堆与之关联的进程。 ![]() sched.h 可以看到task_struct结构体中含有css_set的指针。 ![]() |
| cgroup_subsys_state | 上面说到每一个层级内部有一堆cgroup,cgroup内部保存了一系列子系统,保存的数组内部的每一个元素都是cgroup_subsys_state。 |
| cg_cgroup_link | 通过sibling、children、parent三个变量可以组织成以下结构。刚才说了一个css_set会找到一系列cgroup,现在需要一个结构将这些分布在不同位置的cgroup收集起来,所以,因为一个进程一个css_set,而多个进程会使用一个cgroup,但是进程又在css_set内部,所以其还可以通过cg_cgroup_link找到所有与之关联的进程,总而言之,cg_cgroup_link的目的就是用来收集不同的cgroup和不同的css_set,所以其内部有两个链表结构。 cgroup.c ![]() 如何巧妙利用cg_cgroup_link可以看如下的两张图: 一个进程和多个cgroup的关系: 一个cgroup和多个进程的关系: |
cgroup_subsys_state的实现:前面说到了cgroup中将保存着不同的cgroup_subsys_state,cgroup_subsys_state如同接口般的存在,不同的子系统的具体数据则在各自子系统的实现中,其将cgroup_subsys_state放在具体实现结构体的头部从而实现解耦,以cpuset为例:
cpuset.c

接下来可以看看几个cgroup内部用于结构转换的函数,从而加深对上面介绍的结构的理解。
cgroup.h
此函数的入参为进程结构体和子设备id,子设备id是什么?如果在make menuconfig编译内核时全选了内核支持的子系统,那么cpuset的id就为0,我们可以看到直接调用task_subsys_state方法得到cgroup_subsys_state,随后拿到其中的cgroup即可从进程中拿到所对应的cgroup。

cgroup.h

即通过如下方式去寻找对应子控制器的cgroup。

再来看看find_existing_css_set方法,如何寻找一个已经存在的css_set。
cgroup.c
看看传进来的入参,老的css_set,进程新进入的cgroup,一个css数组。
其实就是遍历所有的css_set,拼凑出此进程移入的新的子控制组后的css数组,看看有没有css_set已经满足这些子控制组的配置信息了。


cgroup的初始化
Linux内一切皆文件,cgroup同样也是基于VFS实现的。
main.c
在内核启动时就对其进行了初始化,因为main代码较长,我们截出来和本节有关的部分进行分析。
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
...
cgroup_init_early();
...
cgroup_init();
}cgroup_init_early
cgroup.c
- 在early的初始化中,初始化了全局init_css_set里的数据结构。
- 随后初始化全局rootnode。
- 将rootnode内部的root_list连接到全局roots链表。
- 设置0号进程cgroups指向init_css_set。
- 在全局cg_cgroup_link init_css_set_link中分别连接cgroup和css。
- 遍历所有子系统,执行初始化动作。

cgroup.c
来看看如何初始化rootnode的。
发现只是初始化了rootnode的相应结构。

那么上述的初始化子系统中的subsys数组又是什么呢。
cgroup.c
我们可以看到,此数组在编译的时候就已经生成,在此版本将会有五种文件子系统cpuset、debug、ns、cpu_cgroup、cpuacct。


cgroup_subsys.h

cpuset.c
分别看看这五个cgroup_subsys结构。

cgroup_debug.c

ns_group.c

sched.c

sched.c

可以看到,需要早些初始化的有cpuset子系统和cpu子系统,因为它们的early_init标志位设置为了1,我们以cpuset为例,来看看其初始化流程。
cgroup.c
其通过cgroup_subsys内提供的create函数指针对cgroup_subsys_state进行构建。
随后初始化此cgroup_subsys_state。


cpuset.c
以cpuset为例,看看其create方法。因为此时传进来的dummytop的parent肯定是空的,所以直接返回的是全局top_cpuset内部的css成员变量。

至此,early_init结束后将出现如下结构。

cgroup_init
cgroup.c
- 此方法较为简单,将刚刚没有early初始化的子系统进行初始化。
- 随后注册进入文件系统。
- 在proc目录下创建cgroups目录,并赋予proc的文件操作函数指针。

文档参考:


上面说到每一个层级内部有一堆cgroup,cgroup内部保存了一系列子系统,保存的数组内部的每一个元素都是cgroup_subsys_state。