一项新概念的产生,必然有其原因,cgroup也不例外,最初由谷歌工程师Paul Menage和Rohit Seth提出【1】:因为计算机硬件能力越来越强大,为了提高机器的使用效率,可以在同一台机器上运行不同运算模型的工作。开始是用process container来命名,后来因为Container有多重含义容易引起误解,就在2007年更名为Control Groups,并被整合进2.6.24内核,它可以限制、记录、隔离进程组所使用的物理资源(如cpu/memory/io等等)及控制使用物理资源的优先级,本文主要参照kernel-5.10源码,对cgroup做下基础介绍。
1.cgroup的组成
我们使用cgroup对进程组做资源处理,离不开下面的组成因素:
cgroupv1可以允许多个层级,以v1的组织方式,如果把用到的子系统attach到同一个层级,子系统的资源控制没有办法解耦,可能会有某些进程受到其它子系统的影响,因此v1需要多个层级的组织方式,把一个层级看做一棵树,可以认为是一个森林。
另外,cgroup、task、subsystem以及hierarchy四者间的相互关系及其基本规则如下:
同一个hierarchy可以附加一个或多个subsystem。
一个subsystem可以附加到多个hierarchy,当且仅当这些hierarchy只有唯一这个subsystem。
对于你创建的每个hierarchy,task只能存在于其中一个cgroup中,即一个task不能存在于同一个hierarchy的不同cgroup中,但是一个task可以存在在不同hierarchy中的多个cgroup中。
进程(task)在fork自身时创建的子任务(child task)默认与原task在同一个cgroup中,但是child task允许被移动到不同的cgroup中。
1.1 subsystem 介绍
子系统主要用来实现资源的控制,因为是具体的控制模块,单独拉出来介绍,随着需求及功能的增加,有如下比较重要且常用的子系统:
cgroup抽离出这些子系统,也是在复用这些资源管理模块,其本质是在这些资源管理模块上附加钩子来实现资源的限制与优先级分配。
1.2 cgroup 关键数据结构
我们感兴趣的是task和cgroup怎样映射的,因为系统可能会有多个层级,每个task也可能会被多个子系统约束,因此一个task可能会存在于多个cgroups中;而每个cgroup中也会控制多个tasks。因此task和cgroup是多对多的关系。为了记录这种关系,可以多加链表,但那样查找及修改效率太差,linux 用css_set和cgrp_cset_link 这两个中间数据结构来完成这个映射。
试想当一个进程克隆子进程时,如果没有指定克隆到某个特定的cgroup,默认子进程会与该进程保持在相同的cgroups中,把共享相同cgroups的这组进程抽离出来,用css_set来标识,每个进程只存在于一个css_set中,一个css_set可能会包含多个进程,所有的css_sets通一个哈希表来组织,当进程在cgroups之间进行迁移时,因为css_set是基于cgroup这种共性所建立的,因此css_set也可以被复用。
进程中cgroup信息:
有了css_set后,虽然相同类型的进程被链接到了一个css_set里,但还是会有css_set与cgroup多对多映射的问题,只不过css_set会比task的个数少一些,这时cgrp_cset_link来了,新增的这第三张表可以极大提高cgroup和css_sets互相查找的效率。
总体连接关系:
另外,css_set一方面记录该cset中的进程信息,也维护了subsys子系统的信息(简称css),也可以认为是两者之间的纽带。
css_set关键成员:
cgroup关键成员:
cgroup_subsys_state,cgroup操作的关键对象,包含文件暴露,子系统操作相关。
其关键成员:
cgroup_subsys子系统,主要包含各子系统的通用操作方法。
关键成员:
cgroup各数据结构关系图:
2. cgroup初始化
cgroup初始化比较简单,主要分两步,cgroup_init_early 和 cgroup_init。
2.1 cgroup_init_early
因为是在boot非常靠前的阶段,主要做下面的工作:
同init_task,系统也初始化了cgrp_dfl_root做为default hierarchy的cgroup_root, 同样,也有一个init_css_set,来初始化init阶段的相关task。在该阶段默认由init进程fork出来的子进程都会挂在init_css_set。
如果有相关子系统需要在init阶段被其它模块使用,会构建该子系统相关的cgroup数据结构,把相关ss/css/cgroup/css_set之间的关系建立起来,最终调用online_css来真正使该css生效,但还没有在文件系统中呈现,并初始化init_css_set的subsys。
2.2 cgroup_init
初始化走到这里,vfs及sysfs已初始化完成,主要工作:
调用cgroup_setup_root初始化cgrp_dfl_root,创建cgroup暴露给用户的相关文件,因为在default hierarchy, 会在/sys/fs/cgroup下创建cgroup_base_files数组中的相关文件,然后调用rebind_subsystems,注意这次其实没有涉及到子系统在不同hierarchy的移动,也没有使能相关的css和暴露ss相关文件。然后把目前存在的所有css_set 与root cgroup建立联系,毕竟这时都在default hierarchy下。
初始化在cgroup_subsys.h里定义好的各子系统,可见2.1。
把init_css_set挂载到cgrp_dfl_root,因此可以通过cgrp_dfl_root获取到所有挂在init_css_set里面的进程。
对每一个要初始化的子系统,初始化要暴露给用户空间的文件并创建。
因为init_css_set的subsys被初始化有变动,重新做哈希链入css_set_table。
注册cgroup和cgroup2文件系统类型。
创建/proc/cgroups文件,用来查看当前系统cgroup的概况。拿ubuntu为例,开机后只有一个hierarchy,里面创建了179个cgroup,14个子系统被使能。
3.cgroup创建与分配task
cgroup vfs基于kernfs,mount过程主要通过kernfs做super block及root目录初始化,另外区分了cgroupv1和cgroupv2,v2支持了thread mode, 而且只有一个‘default unified hierarchy’。因为形态的区别,v1需要查找或创建hierarchy, v2则不需要,另外mount时的参数处理也有一些区别,这里不再详述。
3.1 cgroup创建
当mount成功后,即可以在挂载的文件目录里通过mkdir创建cgroup。
主要流程如下:
找到该cgroup的父节点,并检查当前层级的一些限制是否能满足要求,主要是后代个数及深度限制。
调用cgroup_create来做具体的初始化,初始化管理其生命周期的refcnt,创建所在kernfs的目录,继承父节点目前的冻结状态,以便可以使冻结自动生效,然后把自己链入父节点的children链表,这样方便建立cgroup的树形结构,后面遍历其后代需要用到。因为在default hierarchy, 可能会使能cgroup v2,因此其subtree_control并不会被初始化。在其它层级,会对当前cgroup及其后代的subtree_control和subtree_ss_mask作初始化,两个的具体差异见前面的参数介绍。
调用css_populate_dir在当前目录下创建cgroup通用文件。
针对每个被使能的子系统,创建css及当前子系统的初始化文件(dfl_cftypes
和legacy_cftypes,但会基于是否在default hierarchy做选择创建),因此当手动mkdir cgroup时,会看到很多自动创建的文件。
3.2 分配task给cgroup
这里简单讲下thread mode,我们只能创建domain cgroup,但可以通过写入‘threaded
’ 到 当前cgroup的"cgroup.type"文件,使该cgroup变为threaded,当该cgroup变为threaded cgroup后,不能再更改为domain cgroup。变为threaded后,其最近的domain cgroup父节点会负责资源的统计,成为该threaded cgroup的dom_cgrp,如果这时候你cat下当前父节点cgroup的"cgroup.type"节点,会发现其类型从domain 变为了domain threaded,而如果该父节点所有的子孙都被删除,其又会被恢复为domain。
thread mode可以支持我们以线程粒度分组去控制,但必须在一个threaded domain中。
cgroupv1的"cgroup.procs"、"tasks",cgroupv2的"cgroup.procs"、"cgroup.threads"等节点,都可以通过写具体task pid到节点中来实现cgroup的控制。
以"cgroup.procs"为例,具体流程如下:
从用户输入获取要分配到的目的cgroup。因为是在v2 mode下操作,有区分进程和线程去分开控制,"cgroup.procs"节点会找到当前要操作的task的进程组leader,后续会把该进程组内的所有线程全部迁移到目的cgroup。
获取该task当前所在cgroup,因为是在 default hierarchy,通过cset_group_from_root去查找,查找方式也很容易理解。
cgroup_attach_task 做主要的迁移动作,其中cgroup_migrate_add_src会把该线程组中的所有task所属的cset链入记录迁移过程的数据结构mgctx的preloaded_src_csets链表。
cgroup_migrate_prepare_dst 先从preloaded_src_csets链表中取出源cset,叠加迁移对应的目的cgroup获取目的cset,然后记录源cset和目的cset的对应关系,另外,如果源cset与目的cset的subsys不一致,说明该子系统状态有差异,需要在迁移时重新进行can_attach和attach回调,重新纳入子系统资源管理。
cgroup_migrate_add_task会把该线程组中的task的cg_list迁移到所属cset的mg_tasks链表,标记该task开始了迁移流程。随后把该cset链入mgctx的src_csets链表。
cgroup_migrate_execute首先对有变动的子系统状态调用can_attach进行检测,随后会从src_csets链表中获取要迁移的cset,遍历该cset中的mg_tasks链表中的task,并取出对应目的cset,调用css_set_move_task进行迁移,rcu_assign_pointer(task->cgroups, to); 完成迁移。
4.总结
本文重点对cgroup数据结构及其基本概念进行描述,因为cgroup各数据结构稍微复杂些,所以对其做了重点描述,读者可以对照代码阅读会更有感觉。篇幅限制,具体各cgroup子系统没有涉及,抛砖引玉,感兴趣的同学可以补上。
参考资料:
【1】https://lwn.net/Articles/199643/
【2】Documentation/cgroup-v1/*
【3】Documentation/cgroup-v2.txt
【4】Kernel-5.10源码
长按关注内核工匠微信
Linux内核黑科技| 技术文章 | 精选教程