CGroup的原理和使用

发布时间 2023-05-09 10:56:26作者: he_haha

https://blog.csdn.net/weixin_48101150/article/details/118732507

 

CGroup
Linux CGroup全称Linux Control Group, 是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)。这个项目最早是由Google的工程师在2006年发起(主要是Paul Menage和Rohit Seth),最早的名称为进程容器(process containers)。在2007年时,因为在Linux内核中,容器(container)这个名词太过广泛,为避免混乱,被重命名为cgroup,并且被合并到2.6.24版的内核中去。然后,其它开始了他的发展。

Linux CGroupCgroup 可让您为系统中所运行任务(进程)的用户定义组群分配资源 — 比如 CPU 时间、系统内存、网络带宽或者这些资源的组合。您可以监控您配置的 cgroup,拒绝 cgroup 访问某些资源,甚至在运行的系统中动态配置您的 cgroup。

cgroups的一个设计目标是为不同的应用情况提供统一的接口,从控制单一进程到操作系统层虚拟化

主要功能:

限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)。
进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。
记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间
进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。
进程组控制(Control)。比如:使用freezer子系统可以将进程组挂起和恢复。
基本概念
Cgroups主要由task,cgroup,subsystem及hierarchy构成:

task:在Cgroups中,task就是系统的一个进程。

cgroup:Cgroups中的资源控制都以cgroup为单位实现的。cgroup表示按照某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。

subsystem:Cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。

hierarchy:hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy。

cgroups子系统介绍
cpu 子系统,主要限制进程的 cpu 使用率。
cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
memory 子系统,可以限制进程的 memory 使用量。
blkio 子系统,可以限制进程的块设备 io。
devices 子系统,可以控制进程能够访问某些设备。
net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
net_prio — 这个子系统用来设计网络流量的优先级
freezer 子系统,可以挂起或者恢复 cgroups 中的进程。
ns 子系统,可以使不同 cgroups 下面的进程使用不同的 namespace
hugetlb — 这个子系统主要针对于HugeTLB系统进行限制,这是一个大页文件系统。
cgroups 层级结构
内核使用 cgroup 结构体来表示一个 control group 对某一个或者某几个 cgroups 子系统的资源限制。cgroup 结构体可以组织成一颗树的形式,每一棵cgroup 结构体组成的树称之为一个 cgroups 层级结构。

1.cgroups层级结构可以 attach 一个或者几个 cgroups 子系统,当前层级结构可以对其 attach 的 cgroups 子系统进行资源的限制。每一个 cgroups 子系统只能被 attach 到一个层级结构中。

 

2.创建了 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下的进程列表受到同一份资源的相同限制。

3.一个task不能存在于同一个hierarchy的不同cgroup,但可以存在在不同hierarchy中的多个cgroup

系统每次新建一个hierarchy时,该系统上的所有task默认构成了这个新建的hierarchy的初始化cgroup,这个cgroup也称为root cgroup。

对于你创建的每个hierarchy,task只能存在于其中一个cgroup中,即一个task不能存在于同一个hierarchy的不同cgroup中,但是一个task可以存在在不同hierarchy中的多个cgroup中。

如果操作时把一个task添加到同一个hierarchy中的另一个cgroup中,则会从第一个cgroup中移除。

如下图,cpu和memory被附加到cpu_mem_cg的hierarchy。而net_cls被附加到net hierarchy。并且httpd进程被同时加到了cpu_mem_cg hierarchy的cg1 cgroup中和net hierarchy的cg3 cgroup中。并通过两个hierarchy的subsystem分别对httpd进程进行cpu,memory及网络带宽的限制。

 

4.子task继承父task cgroup的关系

系统中的任何一个task(Linux中的进程)fork自己创建一个子task(子进程)时,子task会自动的继承父task cgroup的关系,在同一个cgroup中,但是子task可以根据需要移到其它不同的cgroup中。父子task之间是相互独立不依赖的。

如下图,httpd进程在cpu_and_mem hierarchy的/cg1 cgroup中并把PID 4537写到该cgroup的tasks中。

之后httpd(PID=4537)进程fork一个子进程httpd(PID=4840)与其父进程在同一个hierarchy的统一个cgroup中,但是由于父task和子task之间的关系独立不依赖的,所以子task可以移到其它的cgroup中。

 

数据结构
进程的task_struct字段中就有指向某个css_set的指针。其中cgroups指针指向了一个css_set结构。cg_list是一个list_head结构,用于将连到同一个css_set的进程组织成一个链表。

struct task_struct {

...
struct css_set __rcu *cgroups;

struct list_head cg_list;
...
}
1
2
3
4
5
6
7
8
css_set存储了与进程相关的cgroups信息

struct css_set {

atomic_t refcount;

struct hlist_node hlist;

struct list_head tasks;

struct list_head cg_links;

struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];

struct rcu_head rcu_head;

};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
其中refcount是该css_set的引用数,因为一个css_set可以被多个进程共用,只要这些进程的cgroups信息相同,比如:在所有已创建的层级里面都在同一个cgroup里的进程。

hlist用于把所有css_set组织成一个hash表,这样内核可以快速查找特定的css_set。

tasks指向所有连到此css_set的进程连成的链表。

cg_links指向一个由struct cg_cgroup_link连成的链表。

subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。通过这个指针数组,进程就可以获得相应的cgroups控制信息了。

cgroup_subsys_state存放进程与一个特定子系统相关的信息。

struct cgroup_subsys_state {

struct cgroup *cgroup;

atomic_t refcnt;

unsigned long flags;

struct css_id *id;

};
1
2
3
4
5
6
7
8
9
10
11
cgroup指针指向了一个cgroup结构,也就是进程属于的cgroup。进程受到子系统的控制,实际上是通过加入到特定的cgroup实现的,因为cgroup在特定的层级上,而子系统又是附加到曾经上的。通过以上三个结构,进程就可以和cgroup连接起来了:task_struct->css_set->cgroup_subsys_state->cgroup。

cgroup存放子系统的相关信息

struct cgroup {

unsigned long flags;

atomic_t count;

struct list_head sibling;

struct list_head children;

struct cgroup *parent;

struct dentry *dentry;

struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT];

struct cgroupfs_root *root;

struct cgroup *top_cgroup;

struct list_head css_sets;

struct list_head release_list;

struct list_head pidlists;

struct mutex pidlist_mutex;

struct rcu_head rcu_head;

struct list_head event_list;

spinlock_t event_list_lock;
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
sibling,children和parent三个list_head负责将同一层级的cgroup连接成一颗cgroup树。

subsys是一个指针数组,存储一组指向cgroup_subsys_state的指针。这组指针指向了此cgroup跟各个子系统相关的信息,这个跟css_set中的道理是一样的。

root指向了一个cgroupfs_root的结构,就是cgroup所在的层级对应的结构体。

top_cgroup指向了所在层级的根cgroup,也就是创建层级时自动创建的那个cgroup。

css_set指向一个由struct cg_cgroup_link连成的链表,跟css_set中cg_links一样。

css_set和croup之间的关系如何设计,多对多的关系必须添加一个中间结构来将两者联系起来。

cg_cgroup_link就是这个中间结构

struct cg_cgroup_link {

struct list_head cgrp_link_list;

struct cgroup *cgrp;

struct list_head cg_link_list;

struct css_set *cg;

};
1
2
3
4
5
6
7
8
9
10
11
cgrp_link_list连入到cgroup->css_set指向的链表,cgrp则指向此cg_cgroup_link相关的cgroup。

Cg_link_list则连入到css_set->cg_links指向的链表,cg则指向此cg_cgroup_link相关的css_set。

CGroup使用
查看cgroup挂载点:

 

创建隔离组

cd /sys/fs/cgroup/memory/
1
mkdir test
1
目录创建完成会自动生成以下文件

ls test
1


先写测试程序

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define MB (1024 * 1024)

int main(int argc, char *argv[])
{
char *p;
int i = 0;
while(1) {
p = (char *)malloc(MB);
memset(p, 0, MB);
printf("%dM memory allocated\n", ++i);
sleep(1);
}

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
程序很简单,无限循环,来向系统申请内存,它会每秒消耗1M的内存。

下面开始写入一些具体的参数:

 

sudo sh -c "echo 5M > memory.limit_in_bytes"
1
设置内存限额为5M

sudo sh -c "echo $$ >> cgroup.procs"
1
把当前bash加入到test中,即所有在此bash下创建的进程都会加入到test中

cat memory.oom_control
1
查看一下oom开关,一般cgroup下oom是不能关闭的。默认为0,即开启。

sudo sh -c "echo 0 > memory.swappiness"
1
oom会受到swap空间的影响,设置他为0,把它禁用。

接下来运行测试程序,当分配到5M时,会关闭bash。因为被kill。

 

我们关掉oom,这样程序就会暂停,这时来观察一下。

sudo sh -c "echo 1 >> memory.oom_control"
1
cat memory.oom_control
1


运行程序发现停在这里了

 

再开终端查看oom,发现under_oom为1,表示当前已经oom了。

 

在当前终端设置内存限额为7M.

 

回到第一个终端可以看到程序又开始继续分配了,最终停在了5M处

 

参考

https://www.cnblogs.com/lisperl/archive/2012/04/18/2455027.html

https://elixir.bootlin.com/linux/v4.8/source/Documentation/cgroup-v1/memory.txt

https://segmentfault.com/a/1190000008125359
————————————————
版权声明:本文为CSDN博主「书笑生」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_48101150/article/details/118732507