cgroup

1 什么是cgroup?



cgroup ,控制组,它提供了一套机制用于控制一组特定进程对资源的使用。cgroup绑定一个进程集合到一个或多个子系统上。

subsystem,子系统,一个通过cgroup提供的工具和接口来管理进程集合的模块。一个子系统就是一个典型的“资源控制器”,用来调度资源或者控制资源使用的上限。其实每种资源就是一个子系统。子系统可以是以进程为单位的任何东西,比如虚拟化子系统、内存子系统。

hierarchy,层级树,多个cgroup的集合,这些集合构成的树叫hierarchy。可以认为这是一个资源树,附着在这上面的进程可以使用的资源上限必须受树上节点(cgroup)的控制。hierarchy上的层次关系通过cgroupfs虚拟文件系统显示。系统允许多个hierarchy同时存在,每个hierachy包含系统中的部分或者全部进程集合。

cgroupfs是用户管理操纵cgroup的主要接口:通过在cgroupfs文件系统中创建目录,实现cgroup的创建;通过向目录下的属性文件写入内容,设置cgroup对资源的控制;向task属性文件写入进程ID,可以将进程绑定到某个cgroup,以此达到控制进程资源使用的目的;也可以列出cgroup包含的进程pid。这些操作影响的是sysfs关联的hierarchy,对其它hierarchy没有影响。

对于cgroup,其本身的作用只是任务跟踪。但其它系统(比如cpusets,cpuacct),可以利用cgroup的这个功能实现一些新的属性,比如统计或者控制一个cgroup中进程可以访问的资源。举个例子,cpusets子系统可以将进程绑定到特定的cpu和内存节点上。

2 为什么需要cgroup?

这个问题相当于问cgroup重要吗?有哪些地方用到了。回答是重要,又不重要。如果你用到了,那就重要,如果没有用到,那就不重要。呵呵呵~~~~其实挺重要的。cgroup的主要运用是资源跟踪。我接触的场景就是用cgroup控制虚拟机进程或者docker进程可以使用的资源。当你想在linux对应用进程做资源访问控制的时候,cgroup就派上用场了。

3 cgroup怎么实现的?

—— 系统中的每个进程(task_struct,后面用task代指)都持有一个指向css_set结构的指针。

—— 一个css_set结构体包含了一组指向cgroup_subsys_state对象的指针(所以一个task可以附加到多个cgroup上),每个cgroup_subsys_state在系统中都有注册。task结构体没有直接指向hierarchy中一个节点(cgroup)的指针。但可以通过其包含的cgroup_subsys_state间接确定。这样设计的原因是cpu对subsystem state的访问很频繁,但涉及到将task绑定到cgroup的操作却不多。task中还有个双向链表cg_list,这个链表维护所有同属于一个css_set的tasks。

—— 用户可以通过cgroupfs文件系统来浏览cgroup hierarchy。

—— 用户可以列出任意一个cgroup上附着的task PID

cgroup在kernel中除了本身功能的实现外,在kernel中还有两处修改:

—— 在kernel启动时对root cgroup的初始化和css_set结构体的初始化。这个在init/main.c文件中实现。

—— 在task的创建(fork)和退出(exit)阶段,对应地将task与css_set进行绑定和解绑。

另外,cgroup为了向用户提供操作接口,特别开发了一个虚拟文件系统类型(cgroupfs),这个文件系统与sysfs,proc类似。cgroupfs是向用户展示cgroup的hierarchy,通知kernel用户对cgroup改动的窗口。挂载cgroupfs时通过选项(-otype)指定要挂载的子系统类型,如果不指定,默认挂载所有的注册的子系统。

如果新挂载的cgroup关联的hierachy与系统中存在的hierarchy完全一样,那么cgroupfs会拒绝挂载。如果没有匹配到相同的hierarchy,但新挂载hierachy声明的资源正在被已经存在的hierarchy使用,挂载会报-EBUSY错误。

当前cgroup还没有实现向已经存在的cgroup hierarchy绑定新子系统的操作,将子系统从cgroup hierachy解绑也不允许。这些操作在未来也许会支持,但也可能会进一步产生错误恢复的一系列问题。

卸载cgroupfs时,如果它的子cgroupfs还在活动,那么子cgroupfs还是会持续生效。直到所有的子cgroupfs不再活动,卸载cgroupfs才会真正生效。

cgroupfs下不能再挂载其它类型的文件系统。所有对cgroup的查询修改都只通过cgroupfs文件系统来完成。

系统中的所有task,在/proc/pid目录下都有一个名为cgroup的文件,这个文件展示了该task相对cgroupfs 根的路径。通过查看这个文件,可以了解一个进程在cgroup hierarchy的位置。以此得到task可以使用的资源信息。

cgroupfs中目录表示cgroup,每个目录在创建时默认生成如下的属性文件,这些文件描述了cgroup的信息:

—— tasks: 所有附属于这个cgroup的进程ID列表。tasks文件中增加进程ID,表示将进程加入这个cgroup,进程能够使用的资源受cgroup限制。

—— cgroup.procs: 所有附属于这个cgroup线程组ID,将TGID写入这个文件后,TGID所在进程包含的所有线程都加入这个cgroup,这些线程受cgroup限制。


PID:这是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。
TGID:在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程,它们处于一个线程组,该线程组的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID;线程组组长的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。
PGID:另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作,例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。
SID:几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID。
—— notify_on_release flag: 标记退出时是否运行release agent



——    release_agent: 制定要运行的release agent的路径,这个属性文件只在cgroup的顶层目录中存在。

以上文件是每个cgroup基本的属性文件,对于不同的子系统,对应的cgroup可能会有其它附加的属性文件,存在于其对应的cgroup目录之下。

通过mkdir命令创建cgroup,通过向目录下的文件写入适当的数值设置修改cgroup的属性。

嵌套的cgroups,指定了层级结构,以此将系统资源划分成嵌套的,动态可变的更小的资源块。

一个进程可以附加到多个不同的cgroup中,只要这些cgroup不在同一个层级树上即可。因为cgroupfs会保证新挂载的cgroup关联的层级树全局唯一。子进程在被创建后默认附加到父进程所在的cgroup,后面用户可以根据需要将其移动到别的cgroup。

当进程从一个cgroup被移动到另一个cgroup。进程的task_struct会获取一个新的css_set指针:如果这个cgroup所在的css_set已经存在就重用这个css_set,否则就新分配一个css_set。kernel会在全局的hash表中查找确认cgroup所属的css_set是否存在。

4 notify_on_release 是做什么的?

如果cgroup中使能notify_on_release,cgroup中的最后一个进程被移除,最后一个子cgroup也被删除时,cgroup会主动通知kernel。接收到消息的kernel会执行release_agent文件中指定的程序。notify_on_release默认是关闭的,release_agent的内容默认为空,子cgroup在创建时会继承父cgroup中notify_on_relase和release_agent的属性。所以这两个文件只存在于cgroupfs的顶层目录中。

5 clone_children有什么用?

clone_chilren仅针对cpu绑定(cpuset),如果clone_children使能,新的cpuset cgroup在初始化时会继承父cgroup的属性。

6 cgroup怎么用?

假设现在要将一个新的任务加入到cgroup,功能是将该任务的进程在指定的cpu上运行,因此我们使用"cpuset"cgroup 子系统,操作的大致步骤如下:

1)mount -t tmpfs cgroup_root /sys/fs/cgroup

挂载cgroup根文件系统,类型为tmpfs

2)mkdir /sys/fs/cgroup/cpuset

在cgroupfs根目录下创建子cgroup,名为cpuset

3)mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset

将名为cpuset的cgroup关联到cpuset子系统

4)在cpuset目录下创建目录,生成一个子cgroup,属性文件中写入相应内容,设置属性。

5)启动需要限制的进程,查找其对应的进程ID,将其写入对应的task文件中

以下操作步骤是创建一个名为"Charlie"的cgroup,这个cgroup的资源包含cpu2,cpu3和内存节点1,将shell进程附加到这个cgroup。

mount -t tmpfs cgroup_root /sys/fs/cgroup

mkdir /sys/fs/cgroup/cpuset

mount -t cgroup cpuset -o cpuset /sys/fs/cgroup/cpuset

cd /sys/fs/cgroup/cpuset

mkdir Charlie

cd Charlie

echo 2-3 > cpuset.cpus

echo 1 > cpuset.mems

echo $$ > tasks

sh

cat /proc/self/cgroup <!-- more --> 1 配置容器内进程内存使用大小及将进程加入容器;


#echo 10485760 > /mnt/mtd/cpu_memory/A/memory.limit_in_bytes



–/mnt/mtd/cpu_memory/A/tasks里的进程内存不能超过10485760=10M bytes;



#echo pid > /mnt/mtd/cpu_memory/A/tasks



2 进程使用内存大小检查发生在缺页异常中:
do_page_falut->handle_mm_fault->



1>匿名页:handle_pte_fault->do_anonymous_page()->mem_cgroup_try_charge()->try_charge;



2>文件页:add_to_page_cache_lru->__add_to_page_cache_locked()->->mem_cgroup_try_charge()->try_charge;



3 进程cgroup机制生效过程:



1>配置tasks时,cgroup_attach_task根据pid找到了task_struct,并配置了task->cgroups(即css_set);



2>当容器内的进程申请内存时,它根据自己的task->cgroups(即css_set)找到css再根据css(即cgroup_subsys_state)找到mem_cgroup



mem_cgroup保存了cgroup机制的内存控制信息,详见上文分析;



3>对容器内进程进行cgroup控制,都是通过类似mem_cgroup/task_group等信息完成的,进程是如何找到各自的mem_cgroup/task_group等信息的?



是通过mem_cgroup/task_group中css与task->cgroups(即css_set)关联.即cgroup使用过程中都是从进程的task->cgroups出发的.



4 cgroup控制的关键是弄清楚每个容器内进程mem_cgroup/task_group等信息的管理和使用.



cgroups(Control Groups) 是 linux 内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。这篇文章主要介绍了linux cgroups 简介,需要的朋友可以参考下



Cgroups是什么?
cgroups(Control Groups) 是 linux 内核提供的一种机制,这种机制可以根据需求把一系列系统任务及其子任务整合(或分隔)到按资源划分等级的不同组内,从而为系统资源管理提供一个统一的框架。简单说,cgroups 可以限制、记录任务组所使用的物理资源。本质上来说,cgroups 是内核附加在程序上的一系列钩子(hook),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。



本文以 Ubuntu 16.04 系统为例介绍 cgroups,所有的 demo 均在该系统中演示。



为什么要了解 cgroups



在以容器技术为代表的虚拟化技术大行其道的时代了解 cgroups 技术是非常必要的!比如我们可以很方便的限制某个容器可以使用的 CPU、内存等资源,这究竟是如何实现的呢?通过了解 cgroups 技术,我们可以窥探到 linux 系统中整个资源限制系统的脉络。从而帮助我们更好的理解和使用 linux 系统。



cgroups 的主要作用



实现 cgroups 的主要目的是为不同用户层面的资源管理提供一个统一化的接口。从单个任务的资源控制到操作系统层面的虚拟化,cgroups 提供了四大功能:



资源限制:cgroups 可以对任务是要的资源总额进行限制。
比如设定任务运行时使用的内存上限,一旦超出就发 OOM。
优先级分配:通过分配的 CPU 时间片数量和磁盘 IO 带宽,实际上就等同于控制了任务运行的优先级。
资源统计:cgoups 可以统计系统的资源使用量,比如 CPU 使用时长、内存用量等。这个功能非常适合当前云端产品按使用量计费的方式。
任务控制:cgroups 可以对任务执行挂起、恢复等操作。
相关概念



Task(任务) 在 linux 系统中,内核本身的调度和管理并不对进程和线程进行区分,只是根据 clone 时传入的参数的不同来从概念上区分进程和线程。这里使用 task 来表示系统的一个进程或线程。



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



Subsystem(子系统) cgroups 中的子系统就是一个资源调度控制器(又叫 controllers)。比如 CPU 子系统可以控制 CPU 的时间分配,内存子系统可以限制内存的使用量。以笔者使用的 Ubuntu 16.04.3 为例,其内核版本为 4.10.0,支持的 subsystem 如下( cat /proc/cgroups):
blkio 对块设备的 IO 进行限制。
cpu 限制 CPU 时间片的分配,与 cpuacct 挂载在同一目录。
cpuacct 生成 cgroup 中的任务占用 CPU 资源的报告,与 cpu 挂载在同一目录。
cpuset 给 cgroup 中的任务分配独立的 CPU(多处理器系统) 和内存节点。
devices 允许或禁止 cgroup 中的任务访问设备。
freezer 暂停/恢复 cgroup 中的任务。
hugetlb 限制使用的内存页数量。
memory 对 cgroup 中的任务的可用内存进行限制,并自动生成资源占用报告。
net_cls 使用等级识别符(classid)标记网络数据包,这让 Linux 流量控制器(tc 指令)可以识别来自特定 cgroup 任务的数据包,并进行网络限制。
net_prio 允许基于 cgroup 设置网络流量(netowork traffic)的优先级。
perf_event 允许使用 perf 工具来监控 cgroup。
pids 限制任务的数量。



Hierarchy(层级) 层级有一系列 cgroup 以一个树状结构排列而成,每个层级通过绑定对应的子系统进行资源控制。层级中的 cgroup 节点可以包含零个或多个子节点,子节点继承父节点挂载的子系统。一个操作系统中可以有多个层级。



cgroups 的文件系统接口



cgroups 以文件的方式提供应用接口,我们可以通过 mount 命令来查看 cgroups 默认的挂载点:



复制代码 代码如下:
$ mount | grep cgroup



第一行的 tmpfs 说明 /sys/fs/cgroup 目录下的文件都是存在于内存中的临时文件。
第二行的挂载点 /sys/fs/cgroup/systemd 用于 systemd 系统对 cgroups 的支持,相关内容笔者今后会做专门的介绍。
其余的挂载点则是内核支持的各个子系统的根级层级结构。



需要注意的是,在使用 systemd 系统的操作系统中,/sys/fs/cgroup 目录都是由 systemd 在系统启动的过程中挂载的,并且挂载为只读的类型。换句话说,系统是不建议我们在 /sys/fs/cgroup 目录下创建新的目录并挂载其它子系统的。这一点与之前的操作系统不太一样。



下面让我们来探索一下 /sys/fs/cgroup 目录及其子目录下都是些什么:



/sys/fs/cgroup 目录下是各个子系统的根目录。我们以 memory 子系统为例,看看 memory 目录下都有什么?



这些文件就是 cgroups 的 memory 子系统中的根级设置。比如 memory.limit_in_bytes 中的数字用来限制进程的最大可用内存,memory.swappiness 中保存着使用 swap 的权重等等。



既然 cgroups 是以这些文件作为 API 的,那么我就可以通过创建或者是修改这些文件的内容来应用 cgroups。具体该怎么做呢?比如我们怎么才能限制某个进程可以使用的资源呢?接下来我们就通过简单的 demo 来演示如何使用 cgroups 限制进程可以使用的资源。



查看进程所属的 cgroups



可以通过 /proc/[pid]/cgroup 来查看指定进程属于哪些 cgroup:



每一行包含用冒号隔开的三列,他们的含义分别是:



cgroup 树的 ID, 和 /proc/cgroups 文件中的 ID 一一对应。
和 cgroup 树绑定的所有 subsystem,多个 subsystem 之间用逗号隔开。这里 name=systemd 表示没有和任何 subsystem 绑定,只是给他起了个名字叫 systemd。
进程在 cgroup 树中的路径,即进程所属的 cgroup,这个路径是相对于挂载点的相对路径。
既然 cgroups 是以这些文件作为 API 的,那么我就可以通过创建或者是修改这些文件的内容来应用 cgroups。具体该怎么做呢?比如我们怎么才能限制某个进程可以使用的资源呢?接下来我们就通过简单的 demo 来演示如何使用 cgroups 限制进程可以使用的资源。



cgroups 工具



在介绍通过 systemd 应用 cgroups 之前,我们先使用 cgroup-bin 工具包中的 cgexec 来演示 demo。Ubuntu 默认没有安装 cgroup-bin 工具包,请通过下面的命令安装:



复制代码 代码如下:
$ sudo apt install cgroup-bin
demo:限制进程可用的 CPU



在我们使用 cgroups 时,最好不要直接在各个子系统的根目录下直接修改其配置文件。推荐的方式是为不同的需求在子系统树中定义不同的节点。比如我们可以在 /sys/fs/cgroup/cpu 目录下新建一个名称为 nick_cpu 的目录:
$ cd /sys/fs/cgroup/cpu
$ sudo mkdir nick_cpu
然后查看新建的目录下的内容:



是不是有点吃惊,cgroups 的文件系统会在创建文件目录的时候自动创建这些配置文件!



让我们通过下面的设置把 CPU 周期限制为总量的十分之一:
$ sudo su$ echo 100000 > nick_cpu/cpu.cfs_period_us
$ echo 10000 > nick_cpu/cpu.cfs_quota_us
上面的两个参数眼熟吗?没错,笔者在《Docker: 限制容器可用的 CPU》一文中介绍的 “–cpu-period=100000 –cpu-quota=200000” 就是由它们实现的。



然后创建一个 CPU 密集型的程序:
void main()
{ unsigned int i, end;
end = 1024 * 1024 * 1024;
for(i = 0; i < end;
) { i ++; }}
保存为文件 cputime.c 编译并通过不同的方式执行:
$ gcc cputime.c -o cputime
$ sudo su$ time ./cputime
$ time cgexec -g cpu:nick_cpu ./cputime



time 命令可以为我们报告程序执行消耗的时间,其中的 real 就是我们真实感受到的时间。使用 cgexec 能够把我们添加的 cgroup 配置 nick_cpu 应用到运行 cputime 程序的进程上。 上图显示,默认的执行只需要 2s 左右。通过 cgroups 限制 CPU 资源后需要运行 23s。



demo:限制进程可用的内存



这次我们来限制进程可用的最大内存,在 /sys/fs/cgroup/memory 下创建目录nick_memory:
$ cd /sys/fs/cgroup/memory
$ sudo mkdir nick_memory
下面的设置把进程的可用内存限制在最大 300M,并且不使用 swap:


物理内存 + SWAP <= 300 MB;10241024300 = 314572800$ sudo su$ echo 314572800 > nick_memory/memory.limit_in_bytes$ echo 0 > nick_memory/memory.swappiness


然后创建一个不断分配内存的程序,它分五次分配内存,每次申请 100M:



#include#include#include#define CHUNK_SIZE 1024 * 1024 * 100void main(){ char *p; int i; for(i = 0; i < 5; i ++) { p = malloc(sizeof(char) * CHUNK_SIZE); if(p == NULL) { printf("fail to malloc!"); return ; } // memset() 函数用来将指定内存的前 n 个字节设置为特定的值 memset(p, 0, CHUNK_SIZE); printf("malloc memory %d MB\n", (i + 1) * 100); }}



把上面的代码保存为 mem.c 文件,然后编译:
$ gcc mem.c -o mem
执行生成的 mem 程序:
$ ./mem
此时一切顺利,然后加上刚才的约束试试:
$ cgexec -g memory:nick_memory ./mem



由于内存不足且禁止使用 swap,所以被限制资源的进程在申请内存时被强制杀死了。



下面再使用 stress 程序测试一个类似的场景(通过 stress 程序申请 500M 的内存):
$ sudo cgexec -g memory:nick_memory stress –vm 1 –vm-bytes 500000000 –vm-keep –verbose



stress 程序能够提供比较详细的信息,进程被杀掉的方式是收到了 SIGKILL(signal 9) 信号。



实际应用中往往要同时限制多种的资源,比如既限制 CPU 资源又限制内存资源。使用 cgexec 实现这样的用例其实很简单,直接指定多个 -g 选项就可以了:
$ cgexec -g cpu:nick_cpu -g memory:nick_memory ./cpumem


Category linux