Cgroup是一种资源控制机制,它将操作系统中的所有进程以组为单位划分,所有进程组以层级结构进行组织。cgroup为每个进程组都指定一组访问资源的行为,这些行为限制了该组进程对资源的访问。
1基本概念
子系统(subsystem)实际上是cgroup对进程组进行资源控制的具体体现。子系统具有多种类型,每个类型的子系统都代表一种系统资源,比如CPU、memory等。当创建一个cgroup实例时,必须至少指定一种子系统。也就是说,这个新建的进程组在访问子系统对应的系统资源时就有了一些限制。具体的限制项与子系统的类型有关。
Cgroup中进程组的层级关系与Linux中进程的层级关系比较类似。在Linux操作系统中,一个进程通过fork()系统调用创建了一个子进程,这两个进程之间存在父子这样的等级关系,并且子进程可以继承父进程的一些资源。系统中所有的进程形成一个树形的等级关系,每个进程都唯一的位于进程树中的某一个位置。
对于cgroup来说,cgroup实例之间也是具体级别关系的,但是它们层级关系是为了更细粒度的对进程组进行资源控制。同时,子cgroup会继承父cgroup的对资源的控制属性。
2子系统与层级的关系
如果要创建一个cgroup实例,通过下面的命令即可完成:
1 | mount –t cgroup –o cpu,memory cpu_memory /cgroup/cpu_memory |
可以看到,通过Linux中挂载文件系统的方法就可以创建cgroup实例。其中-t选项指定文件系统类型为cgroup类型,-o指定本次创建的cgroup实例与cpu和momory子系统(或资源)关联,cpu_momory指定了当前cgroup实例在整个cgroup树中所处的层级名称,最后的路径为文件系统挂载点。关于cgroup文件系统本文稍后说明。
图1是一个系统中的cgroup结构图,它包含两个层级,即第一层级cpu_mem和第二层级cpuset_net。接下来以该图为例说明cgroup子系统和层级之间的设置规则。
图1
1. 系统中第一个被创建的cgroup被称为root cgroup,该cgroup的成员包含系统中所有的进程。
如图1所示,root cgroup位于cpu_mem层,它包含了系统中的所有进程;root cgroup又被分为cgroup1和cgroup2两个子cgroup,它们位于第二层级cpuset_net中。
2. 一个子系统只能位于一个层级中。
如图1中所示,cpu子系统位于第一层级cpu_mem中,那么这个子系统将不能再位于第二层级中。不过这种设置规则并不影响子cgroup对cpu子系统的使用,因为所有的子cgroup都将继承root cgroup所属的cpu子系统。
3. 每个层级中可以关联多个子系统。
图1中第一层级cpu_mem关联了cpu和memory两个子系统。
4. 一个进程可以位于不同层级的cgroup中。
由于root cgroup包含了系统中所有的进程,因此cgroup2中的进程P也位于root cgroup中。从资源控制角度来说,进程P所在的进程组在访问cpu、memory和net时会受到资源限制。
5. 一个进程创建了子进程后,该子进程默认为父进程所在cgroup的成员。
子进程被创建后,继承父进程的cgroup,但是后续可根据需求将子进程移动到其他cgroup中。
Cgroup自身通过文件系统的形式在内核中实现,通过对子系统配置文件的读写即可完成对进程组资源的控制。不过,cgroup对各种资源的实际控制则分布到整个内核代码中。下面从CPU、内存和I/O三个方面说明Cgroup对资源的控制过程。
1 CPU控制
Cgroup对进程组使用CPU的限制是通过cpu和cpuset两个子系统来完成的。cpu子系统主要限制进程的时间片大小,cpuset子系统可为进程指定cpu和内存节点。
1) cpu子系统
cpu子系统主要分布在内核的调度系统中,通过该子系统中的cpu.shares文件可对进程组设置权重。
根据CFS的原理,一个进程的权重越大,那么它的被调度的可能性就越大。那么进程组的权重如何在CFS中体现?CFS将进程和进程组视为同一个调度体,并用sched_entity结构来表示,每个结构中包含该调度体的权重以及虚拟运行时间等。
因此,用户可通过CPU子系统中的cpu.shares文件来控制进程组对CPU的使用。
2) cpuset子系统
通过cpuset子系统中的cpuset.cpus和cpuset.mems可对进程组设定可访问的CPU和内存节点。内核使用cpuset结构来描述cpuset子系统的属性信息,其中该结构中的cpus_allowed和mems_allowed两个字段分别保存上述两个文件的值。同时,进程描述符中也有cpus_allowed和mems_allowed两个字段,其值与cpuset结构保持同步。
cpuset限制进程所能访问的CPU主要通过两方面。首先体现在进程的创建,如果父进程新建子进程时没有指定CLONE_STOPPED标志,则父进程将调用wake_up_new_task()将子进程状态设置为TASK_RUNNING,并将其加入就绪队列。为子进程选择就绪队列的具体工作则由select_task_rq()完成,其内部实现将涉及CPU的检查操作,即在cpus_allowed所指定的cpu范围内为当前进程分配CPU。
其次,当调度器在调度一个进程时,也要通过select_task_rq()进行同样的对比选择。这样就可以保证cgroup实例中的进程只在cpus_allowed所限定的cpu中运行。
cpuset进程所能访问内存节点的限制表现在物理内存的分配过程中。Linux内核将物理内存在逻辑上分为node、zone和page,内核通过alloc_pages()来实现物理内存的分配工作。alloc_pages()的主要工作是在所有物理内存中选择node,再在当前node中选择zone,最终在zone中分配一个物理页框。其中,在选择node的过程中会进行mems_allowed的判断过程。
2 内存控制
cgroup对内存的控制通过memory子系统完成,其控制作用主要体现在对内存使用量的限制,同时为当前cgroup生成一份内存使用情况报告。
在具体实现的过程中,cgroup通过内核中的resource counter机制实现内存的限制。resource counter相当于一个通用的资源计数器,在内核中通过res_counter结构来描述。该结构可用于记录某类资源的当前使用量、最大使用量以及上限等信息。具体描述如下:
1 | struct res_counter { |
2 | /* |
3 | * the current resource consumption level |
4 | */ |
5 | unsigned long long usage; |
6 | /* |
7 | * the maximal value of the usage from the counter creation |
8 | */ |
9 | unsigned long long max_usage; |
10 | /* |
11 | * the limit that usage cannot exceed |
12 | */ |
13 | unsigned long long limit; |
14 | /* |
15 | * the limit that usage can be exceed |
16 | */ |
17 | unsigned long long soft_limit; |
18 | /* |
19 | * the number of unsuccessful attempts to consume the resource |
20 | */ |
21 | unsigned long long failcnt; |
22 | /* |
23 | * the lock to protect all of the above. |
24 | * the routines below consider this to be IRQ-safe |
25 | */ |
26 | spinlock_t lock; /* |
27 | * Parent counter, used for hierarchial resource accounting |
28 | */ |
29 | struct res_counter *parent; |
30 | }; |
内核使用mem_cgroup结构描述cgroup中内存的使用信息,其内部嵌入了res_counter结构。因此res_counter中的每个字段则表示对内存使用量的记录。用户态下memory子系统所导出的配置文件与该结构中的字段互相对应,比如mem.limit_in_bytes表示当前cgroup可使用内存的最大上线,该文件与res_counter结构中的limit字段对应。也就是说,当用户在用户态向mem.limit_in_bytes文件写入值后,则res_counter中的limit字段相应更新。
内核对res_counter进行操作时有三个基本函数:res_counter_init()对res_counter进行初始化;当分配资源时,res_counter_charge()记录资源的使用量,并且该函数还会检查使用量是否超过了上限,并且记录当前资源使用量的最大值;当资源被释放时,res_counter_uncharge()则减少该资源的使用量。
cgroup对内存资源的限制主要是将上述三个函数分布到内存的分配单元中,比如,系统发生缺页异常时,由于页表项未分配而申请内存时,由于页缓存而分配内存等。
3 块I/O控制
Cgroup中通过blkio子系统完成对块设备I/O的控制。具体的控制主要通过blkio.weight文件在用户态设定当前进程组访问块I/O的权重,也就是控制进程组占有I/O的时间。
blkio子系统对块I/O的控制代码主要分布在I/O调度算法中,目前内核中默认的调度算法为CFQ(完全公平队列),该算法与进程调度算法CFS比较类似。