CGroup的原理和使用

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的一个设计目标是为不同的应用情况提供统一的接口,从控制单一进程到操作系统层虚拟化

主要功能:

  1. 限制进程组可以使用的资源数量(Resource limiting )。比如:memory子系统可以为进程组设定一个memory使用上限,一旦进程组使用的内存达到限额再申请内存,就会出发OOM(out of memory)。
  2. 进程组的优先级控制(Prioritization )。比如:可以使用cpu子系统为某个进程组分配特定cpu share。
  3. 记录进程组使用的资源数量(Accounting )。比如:可以使用cpuacct子系统记录某个进程组使用的cpu时间
  4. 进程组隔离(Isolation)。比如:使用ns子系统可以使不同的进程组使用不同的namespace,以达到隔离的目的,不同的进程组有各自的进程、网络、文件系统挂载空间。
  5. 进程组控制(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子系统介绍

  1. cpu 子系统,主要限制进程的 cpu 使用率。
  2. cpuacct 子系统,可以统计 cgroups 中的进程的 cpu 使用报告。
  3. cpuset 子系统,可以为 cgroups 中的进程分配单独的 cpu 节点或者内存节点。
  4. memory 子系统,可以限制进程的 memory 使用量。
  5. blkio 子系统,可以限制进程的块设备 io。
  6. devices 子系统,可以控制进程能够访问某些设备。
  7. net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块(traffic control)对数据包进行控制。
  8. net_prio — 这个子系统用来设计网络流量的优先级
  9. freezer 子系统,可以挂起或者恢复 cgroups 中的进程。
  10. ns 子系统,可以使不同 cgroups 下面的进程使用不同的 namespace
  11. hugetlb — 这个子系统主要针对于HugeTLB系统进行限制,这是一个大页文件系统。

cgroups 层级结构

内核使用 cgroup 结构体来表示一个 control group 对某一个或者某几个 cgroups 子系统的资源限制。cgroup 结构体可以组织成一颗树的形式,每一棵cgroup 结构体组成的树称之为一个 cgroups 层级结构。

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

image-20210614160004934

2.创建了 cgroups 层级结构中的节点(cgroup 结构体)之后,可以把进程加入到某一个节点的控制任务列表中,一个节点的控制列表中的所有进程都会受到当前节点的资源限制。同时某一个进程也可以被加入到不同的 cgroups 层级结构的节点中,因为不同的 cgroups 层级结构可以负责不同的系统资源。所以说进程和 cgroup 结构体是一个多对多的关系。

image-20210614160108105

上面这个图从整体结构上描述了进程与 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及网络带宽的限制。

image-20210614161534314

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中。

image-20210614161733536

数据结构

进程的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;
...
}

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;

};

其中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;

};

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;
};

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;

};

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挂载点:

image-20210614164353252

创建隔离组

cd /sys/fs/cgroup/memory/
mkdir test

目录创建完成会自动生成以下文件

ls test

image-20210614175626270

先写测试程序

#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;
}

程序很简单,无限循环,来向系统申请内存,它会每秒消耗1M的内存。

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

image-20210614175748895

sudo sh -c "echo 5M > memory.limit_in_bytes"

设置内存限额为5M

sudo sh -c "echo $$ >> cgroup.procs"

把当前bash加入到test中,即所有在此bash下创建的进程都会加入到test中

cat memory.oom_control

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

sudo sh -c "echo 0 > memory.swappiness"

oom会受到swap空间的影响,设置他为0,把它禁用。

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

image-20210614180518797

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

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

image-20210614180736830

运行程序发现停在这里了

image-20210614181000085

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

image-20210614181115870

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

image-20210614181258469

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

image-20210614181329686

参考

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

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值