您现在的位置是:首页 > 文章详情

云计算时代,容器底层 cgroup 如何使用

日期:2020-06-09点击:419

作者:姜亚华(@二如公子 ),《精通 Linux 内核——智能设备开发核心技术》的作者,一直从事与 Linux 内核和 Linux 编程相关的工作,研究内核代码十多年,对多数模块的细节如数家珍。曾负责华为手机 Touch、Sensor 的驱动和软件优化(包括 Mate、荣耀等系列),以及 Intel 安卓平台 Camera 和 Sensor 的驱动开发(包括 Baytrail、Cherrytrail、Cherrytrail CR、Sofia 等)。现负责 DMA、Interrupt、Semaphore 等模块的优化与验证(包括 Vega、Navi 系列和多款 APU 产品)。

往期回顾:点击查看

一、cgroup 的使用

测试环境版本与之前一致:

Ubuntu

(lsb_release -a)

Distributor ID: Ubuntu

Description:    Ubuntu 19.10

Release:        19.10

Linux

(uname -a)

Linux yahua 5.5.5 #1 SMP … x86_64 x86_64 x86_64 GNU/Linux

前面的文章中,我们探讨了容器底层 cgroup 的数据结构代码实现,本期是 cgroup 系列的最后一篇文章,我们将继续探讨在 mount 成功后,我们如何使用 cgroup 来实现进程限制。

在 mount 成功后,cgroup_root 已经存在了,也就是说 cgroup 层级结构已经搭建好了,接下来我们就可以使用 cgroup 了。

1. cgroup 的 mkdir

mkdir 比 mount 的过程稍简单,由 cgroup_mkdir 函数实现,主要逻辑如下:

int cgroup_mkdir(struct kernfs_node *parent_kn, const char *name, umode_t mode) { struct cgroup *parent, *cgrp; parent = cgroup_kn_lock_live(parent_kn, false); //1 cgrp = cgroup_create(parent, name, mode); //2 ret = cgroup_kn_set_ugid(cgrp->kn); ret = css_populate_dir(&cgrp->self); //3 ret = cgroup_apply_control_enable(cgrp); kernfs_activate(cgrp->kn); ret = 0; return ret; }

 第 1 步,获得父目录对应的 cgroup。无论是 cgroup_setup_root 还是接下来要说的cgroup_create,在创建文件的时候都将 cgroup 赋值给了 kernfs_node 的 priv。所以这里其实就是返回 parent_kn->priv 字段,不过要经过参数检查。

第 2 步,调用 cgroup_create:创建 cgroup,调用 kernfs_create_dir 创建目录,建立新cgroup 和父 cgroup 的父子关系。

第 3 步,和 mount 的时候一样,css_populate_dir 和 cgroup_apply_control_enable 会为我们创建 cftype 对应的文件,不过有两点区别:

首先,带 CFTYPE_ONLY_ON_ROOT 标志的 cftype 不会出现在这里,比如cgroup.sane_behavior 和 release_agent。

其次,mount 的时候,新 cgroup_root.cgrp 复用了原 cgroup_root.cgrp 相关的css(rebind_subsystems,第二篇),这里新建了一个 cgroup,cgroup_apply_control_enable 需要为我们创建新的 css(ss->css_alloc(parent_css))并建立 cgroup 和 ss 的多对多关系(init_and_link_css和online_css)。 

mount 的时候,cpuset 的 css_alloc 返回的是全局的 top_cpuset.css,这里创建一个新的 cpuset 对象并初始化,如下:

struct cgroup_subsys_state * cpuset_css_alloc(struct cgroup_subsys_state *parent_css) { struct cpuset *cs;  if (!parent_css)    //mount的时候,返回top_cpuset.css   return &top_cpuset.css;  cs = kzalloc(sizeof(*cs), GFP_KERNEL); alloc_cpumasks(cs, NULL);    //#1  set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags); nodes_clear(cs->mems_allowed); nodes_clear(cs->effective_mems);    //#2 fmeter_init(&cs->fmeter); cs->relax_domain_level = -1;  return &cs->css; }

注意标号 #1 和 #2,新 cs 的 cpus_allowed 和 mems_allowed 都被清零,此时读取cpuset.cpus 和 cpuset.mems 也是没有内容的,也就是说对 cpu 和 memory 的限制并不能从父目录继承,在使用前必须正确设置它们。

2. 限制资源

我们在第一篇的例子中通过 echo 0-2 > cpuset.cpus 和 echo 0 > cpuset.mems 限制 /cpuset0 管理的进程使用的 cpu 和 memory node,以 cpuset.cpus 为例,它的 cftype 如下:

{   .name = "cpus",   .seq_show = cpuset_common_seq_show,   .write = cpuset_write_resmask,   .max_write_len = (100U + 6 * NR_CPUS),   .private = FILE_CPULIST, },

最终调用的是 cpuset_write_resmask,后者调用 update_cpumask。

update_cpumask 的目的是更新我们在 mkdir 时创建的 cpuset(cpuset_css_alloc),当然了,之前已经配置过的 cpuset 重新配置也可以。我们关心以下几点:

  1.  不能更改top_cpuset的设置,这就是第一篇的课堂作业第一题的答案。
  2. 目标cpuset的资源必须是父目录cpuset的子集,而且是子目录cpuset的超集(由validate_change函数实现),这是课堂作业第二题的答案。
  3. 配置的资源最终更新cpuset的cpus_allowed字段。

 可以看到,类似课堂作业中描述的类似限制,是需要 ss 自行实现的,cgroup 本身并不保证这点,尝试开发新的 ss 的时候需要注意这点。

3. 管理进程

我们在例子中将进程号写到 tasks 文件(echo $$ > tasks),以限制进程只能使用 /cpuset0 配置的 cpu 和 memory node。实际上,写 cgroup.procs 文件也是可以的。它们的 cftype 文件定义如下:

{   .name = "tasks",   .seq_start = cgroup_pidlist_start,   .seq_next = cgroup_pidlist_next,   .seq_stop = cgroup_pidlist_stop,   .seq_show = cgroup_pidlist_show,   .private = CGROUP_FILE_TASKS,   .write = cgroup1_tasks_write, }, {   .name = "cgroup.procs",   .seq_start = cgroup_pidlist_start,   .seq_next = cgroup_pidlist_next,   .seq_stop = cgroup_pidlist_stop,   .seq_show = cgroup_pidlist_show,   .private = CGROUP_FILE_PROCS,   .write = cgroup1_procs_write, },

两个文件的 write 分别是 cgroup1_tasks_write 和 cgroup1_procs_write,它们都是调用__cgroup1_procs_write 实现的,区别仅在于最后一个参数 threadgroup 不同,前者为false,后者为 true。看名字就知道,为 false 的情况下,仅作用于目标进程(线程),为 true 的情况下,作用于线程组。

这里对线程组稍作说明。线程组是属于同一个进程的线程的集合,同一个线程组的线程,它们的 task_struct 都通过 thread_group 字段链接到同一个链表中,链表的头为线程组领导进程的 task_struct 的 thread_group 字段,可以据此来遍历线程组。

__cgroup1_procs_write 可以分成以下 3 步:

第1步,调用 cgroup_kn_lock_live 获得文件所在的目录的 cgroup,实际上就是kernfs_node->parent->priv,kernfs_node->parent 是文件所在目录的 kernfs_node,priv 就是目标 cgroup。

第2步,调用 cgroup_procs_write_start 根据用户空间传递的进程 id 参数获得目标进程的 task_struct,threadgroup 为 true 的情况下,获得的是线程组领导进程的task_struct。

第3步,调用 cgroup_attach_task 将进程 attach 到(依附于或者连接)cgroup。

cgroup 和 ss 之间是对等的关系,使用的是 bind,称之为绑定;进程和 cgroup 之间并不是对等的关系,使用的是 attach,称之为依附。 

请注意,我们举例中仅涉及 cpuset,并不意味着某个进程只与 cpuset 有关,进程和cgroup 的关系是通过 css_set 实现的,也就是说是一组 cgroup。我们没有更改其他cgroup 层级结构的配置,这意味着进程关联的是它们的 cgroup_root,并不是没有关联。

先不论进程被创建后,“辗转”了几组cgroup,进程被创建时就已经attach cgroup了。

进程创建的过程在书里已经详细地分析过了,这里仅讨论与cgroup相关的部分。

首先被调用的是cgroup_fork,如下:

void cgroup_fork(struct task_struct *child) { RCU_INIT_POINTER(child->cgroups, &init_css_set); INIT_LIST_HEAD(&child->cg_list); }

直接指向了 init_css_set,不过这有可能是暂时的。child->cg_list 是空的,说明新进程还没有 attach 到任何 cgroup。

其次是 cgroup_can_fork,它调用 ss->can_fork,由 ss 判断是否可以创建新进程,如果答案是否,整个 fork 会失败。

最后是 cgroup_post_fork,做最后的调整,主要逻辑如下:

void cgroup_post_fork(struct task_struct *child) { struct cgroup_subsys *ss; struct css_set *cset;  if (likely(child->pid)) {   WARN_ON_ONCE(!list_empty(&child->cg_list));   cset = task_css_set(current); /* current is @child's parent */   get_css_set(cset);   cset->nr_tasks++;   css_set_move_task(child, NULL, cset, false); }  do_each_subsys_mask(ss, i, have_fork_callback) {   ss->fork(child); } while_each_subsys_mask(); }

首先,current 是新进程 child 的父进程,先获得父进程的 css,然后调用css_set_move_task 将新进程转移到该 css 上。css_set_move_task 的第二个参数是原css,这里是 NULL 是因为还没有 attach 到任何 cgroup(css_set)上。css_set_move_task 会将 child->cg_list 插入 css->tasks 链表上,child->cg_list 不再为空。

也就是说,新进程在创建时会被 attach 到与父进程同一组 cgroup 上。

其次,如果 ss 定义了 fork,调用 ss->fork,以 cpuset 为例,它会为新进程复制父进程的设置,如下:

void cpuset_fork(struct task_struct *task) { if (task_css_is_root(task, cpuset_cgrp_id))   return;  set_cpus_allowed_ptr(task, current->cpus_ptr); task->mems_allowed = current->mems_allowed; }

回顾下第一篇的例子,我们在 cpuset 下创建的 cpuset0 目录,配置资源,管理进程。修改下,在 cpuset 下再创建一个 cpuset1 目录,进程先 attach 到 /cpuset0,然后migrate 到 /cpuset1上,以此为例分析 migrate 的过程:

love_cc@yahua:/sys/fs/cgroup/cpuset$ sudo mkdir cpuset0 love_cc@yahua:/sys/fs/cgroup/cpuset$ sudo mkdir cpuset1 love_cc@yahua:/sys/fs/cgroup/cpuset$ cd cpuset0/ root@yahua:/sys/fs/cgroup/cpuset/cpuset0# echo 0-2 > cpuset.cpus root@yahua:/sys/fs/cgroup/cpuset/cpuset0# echo 0 > cpuset.mems root@yahua:/sys/fs/cgroup/cpuset/cpuset0# echo $$ > tasks root@yahua:/sys/fs/cgroup/cpuset/cpuset0# cat tasks 2682 2690 root@yahua:/sys/fs/cgroup/cpuset/cpuset0# cd ../cpuset1/ root@yahua:/sys/fs/cgroup/cpuset/cpuset1# echo 0-1 > cpuset.cpus root@yahua:/sys/fs/cgroup/cpuset/cpuset1# echo 0 > cpuset.mems root@yahua:/sys/fs/cgroup/cpuset/cpuset1# echo $$ > tasks root@yahua:/sys/fs/cgroup/cpuset/cpuset1# cat tasks 2682 2713 root@yahua:/sys/fs/cgroup/cpuset/cpuset1# cat ../cpuset0/tasks #没有省略内容,空的

继续讨论之前,先理一下目前的状况,我们在 __cgroup1_procs_write 函数的第 3 步cgroup_attach_task,之前的两步我们已经获得了目标 cgroup(也就是 /cpuset1)和进程的 task_struct。

cgroup_attach_task 的目的是将进程 attach 到目标 cgroup,逻辑上至少包括进程和原group detach 和进程和目标 cgroup attach 两部分。三个要素,src、dst 和 migrate,正好对应三个函数 cgroup_migrate_add_src、cgroup_migrate_prepare_dst 和cgroup_migrate

首先被调用的是 cgroup_migrate_add_src,threadgroup 为 true 的情况下,对线程组的每个线程调用一次,否则调用一次即可,它的主要逻辑如下:

void cgroup_migrate_add_src(struct css_set *src_cset,        struct cgroup *dst_cgrp,        struct cgroup_mgctx *mgctx) { struct cgroup *src_cgrp;  src_cgrp = cset_cgroup_from_root(src_cset, dst_cgrp->root);  src_cset->mg_src_cgrp = src_cgrp; src_cset->mg_dst_cgrp = dst_cgrp; get_css_set(src_cset); list_add_tail(&src_cset->mg_preload_node, &mgctx->preloaded_src_csets);

第一个参数 src_cset 表示进程原来的 css_set,也就是 task_struct 的 cgroups 字段。

首先要做的就是在目标 cgroup(dst_cgrp,也就是 /cpuset1)所属的 cgroup_root 中找到原 cgroup(src_cgrp,也就是 /cpuset0),它跟目标 cgroup 属于同一个cgroup_root,查找的过程就变成找到 src_cset 对应的某个 cgroup,它的 root 字段与dst_cgrp->root 相等,如下:

list_for_each_entry(link, &cset->cgrp_links, cgrp_link) {   struct cgroup *c = link->cgrp;   if (c->root == root) {    res = c;    //res就是我们要找的    break;   } }

提醒下,任何一个 cgroup 层级结构中,进程只能关联其中一个 cgroup,所以与 /cpuset1 属于同一个 cgroup_root 的只能是 /cpuset0 。

另外,我们分析的只是一种情况,前面说的 Ubuntu mount cpuset 的时候,进程从默认的层级结构迁移到 cpuset 上,原 cgroup 和目标 cgroup 实际上属于不同的cgroup_root,返回的是目标 cgroup_root 的 cgrp。

cgroup_migrate_add_src 的第三个参数 mgctx 是 cgroup_attach_task 的局部变量,函数结束前将 src_cset 插入到 mgctx->preloaded_src_csets 等待后续处理。

cgroup_migrate_prepare_dst 遍历 mgctx->preloaded_src_csets上的 src_cset,根据src_cset 和 src_cset->mg_dst_cgrp 查找当前已经存在的 css_set 是否有某个 css_set与期望一致,没有则创建新的 css_set 并赋以期望值。

期望,一致,两方面。

怎么描述我们的期望呢,进程只是从 /cpuset0 移到 /cpuset1 上,关联的其他 cgroup 层级结构的 cgroup 并没有变化,所以以原 css_set 作为模板,调整 cpuset 层级结构上的css 即可,实际的代码也大致如此,如下:

for_each_subsys(ss, i) {   if (root->subsys_mask & (1UL << i)) {    template[i] = cgroup_e_css_by_mask(cgrp, ss);   } else {    template[i] = old_cset->subsys[i];   } }

root 就是发生变动的层级结构的 cgroup_root,在我们的例子中就是 cpuset,至于cgroup_e_css_by_mask,这里的 e 是 effective,不考虑 cgroup v2 的情况下,也可以理解为 cgroup_css(cgrp, ss),也就是 /cpuset1 和 cpuset ss 对应的 css 。

某个 css_set(简称 cset)与我们的期望一致,需要满足以下两点。

首先,cset->subsys 与 template 一致,其实还是与 v2 有关。

其次,cset 的 css(cgrp_links字段)中,属于当前 cgroup_root 的,关联的 cgroup 是目标值,也就是 /cpuset1;不属于当前 cgroup_root 的,与 old_cset 关联的 cgroup 相等。

css_set 的 subsys 和 cgrp_links 都表示它关联的 css,二者有什么区别?subsys 在css_set 被创建后不会改变,cgrp_links 可以动态调整。比如 cgroup_setup_root 中调用的 link_css_set,修改的只是 cgrp_links。

如果找不到一致的 css_set,创建一个新的,按照要求的两点给它赋值。

接下来就是 cgroup_migrate 了,它的实现代码较多,但逻辑都是直来直去,我们就不直接分析代码了,主要分以下几步:

  1. 调用 cgroup_migrate_add_task 将需要迁移的进程放入 mgctx->tset,然后调用cgroup_migrate_execute 函数,实际的 migrate 过程由它完成。
  2. 回调有变动的 ss 的 ss->can_attach 函数,判断是否合法。
  3. 遍历需要 migrate 的进程,调用 css_set_move_task(task, from_cset, to_cset, true),进程的 css_set 得到更新。
  4. 回调有变动的 ss 的 ss->attach,migrate 正式生效。
  5. cpuset 的 attach 由 cpuset_attach 函数实现,核心逻辑如下:
cgroup_taskset_for_each(task, css, tset) {   WARN_ON_ONCE(set_cpus_allowed_ptr(task, cpus_attach));   cpuset_change_task_nodemask(task, &cpuset_attach_nodemask_to);   cpuset_update_task_spread_flag(cs, task); }

遍历进程,使 cpu 和 memory node 的限制生效。

我们分析的限制进程使用 cpu 由 set_cpus_allowed_ptr 调用 __set_cpus_allowed_ptr 实现,主要逻辑如下:

int __set_cpus_allowed_ptr(struct task_struct *p,       const struct cpumask *new_mask, bool check) { const struct cpumask *cpu_valid_mask = cpu_active_mask; unsigned int dest_cpu; struct rq_flags rf; struct rq *rq;  rq = task_rq_lock(p, &rf); update_rq_clock(rq);  if (cpumask_equal(p->cpus_ptr, new_mask))    //1   goto out;  dest_cpu = cpumask_any_and(cpu_valid_mask, new_mask);    //2 if (dest_cpu >= nr_cpu_ids) {   ret = -EINVAL;   goto out; }  do_set_cpus_allowed(p, new_mask);    //3  if (cpumask_test_cpu(task_cpu(p), new_mask))    //4   goto out;  if (task_running(rq, p) || p->state == TASK_WAKING) {    //5   struct migration_arg arg = { p, dest_cpu };   task_rq_unlock(rq, p, &rf);   stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg);   return 0; } else if (task_on_rq_queued(p)) {   rq = move_queued_task(rq, &rf, p, dest_cpu); } out: task_rq_unlock(rq, p, &rf); return ret; }

满屏都是进程调度章节的内容,在此解释如下:

第1步,如果没有改变,直接退出。

第2步,指定的资源是否合法,如果不合法,返回错误。

第3步,do_set_cpus_allowed 会调用 p->sched_class->set_cpus_allowed 由具体的调度类实现,调度类一般会更新 task_struct 的 cpus_mask 字段。

第4步,进程当前所在的 cpu 是否在限制范围内,如果在,不需要额外处理。

第5步,进程被限制,不能使用当前所在的 cpu,如果正在运行则停止并 migrate,如果正在等待执行,移到其他 cpu 上。

 cgroup v1 的讨论差不多了,绝大部分篇幅集中讨论最常用的操作,但实际上还不完整,其余操作大家可以自行继续当前的思路阅读。

 

往期回顾

容器底层 cgroup 如何实现资源分组?

容器底层 cgroup 的代码实现分析

原文链接:https://www.oschina.net/question/2918182_2317045
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章