ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

源码解析容器底层cgroup的实现

2021-05-17 07:03:48  阅读:147  来源: 互联网

标签:ss cgrp cpuset 源码 cgroup 解析 root css


在上一篇文章里,我们探讨了容器底层 cgroup 的作用与数据结构,本文我们将深入分析 cgroup 的代码实现。

一、cgroup 的初始化和 mount

测试环境版本与第一篇一致:

源码解析容器底层cgroup的实现

本篇开始我们将分析 cgroup 的代码实现,与书(《精通 Linux 内核—智能设备开发核心技术》,下同)中的原则一致,我们重点分析核心和难点代码,其他部分在不影响理解的情况下一笔带过。

1.1 cgroup 的初始化

cgroup 的初始化分为两个阶段。

第一阶段:初始化 cgrp_dfl_root 和系统支持的 ss,由cgroup_init_early 函数完成。cgrp_dfl_root,看名字就知道,default cgroup_root,默认的 cgroup 层级结构,它在 cgroup v1 中戏份有限,在 v2 中是 c 位。至于 ss 的初始化,主要是 id 和 name,如果 ss 的 early_init 为真,调用 cgroup_init_subsys 完善它与cgrp_dfl_root 的关系。

cgroup_init_subsys 有助于我们理解 cgroup 和 ss 之间的关系,此处展开讨论,代码如下:

void cgroup_init_subsys(struct cgroup_subsys *ss, bool early){  ss->root = &cgrp_dfl_root;  css = ss->css_alloc(cgroup_css(&cgrp_dfl_root.cgrp, ss));   init_and_link_css(css, ss, &cgrp_dfl_root.cgrp);    if (early) {        css->id = 1;    } else {        css->id = cgroup_idr_alloc(&ss->css_idr, css, 1, 2, GFP_KERNEL);    }   init_css_set.subsys[ss->id] = css;    //***,三个星号哈   BUG_ON(online_css(css)); 

我们在第一篇中说过,ss 和 cgroup 是多对多的关系,通过 css 实现,cgroup_init_subsys 就是完成这个任务的。cgrp_dfl_root 是一个 cgroup_root,它本身内嵌了一个 cgroup,所以具体点就是申请一个 css,建立它与 cgrp_dfl_root.cgrp 的联系。

它先回调 ss->css_alloc 函数申请 css,css_alloc 的参数表示将要产生的 css 的父css(我们在第一篇讲过,css 的两方面作用),调用 cgroup_init_subsys 的时候,父 css 还不存在,所以最终传递的参数是 NULL。

我们分析的 cgroup 子系统 cpuset 的 css_alloc 回调函数是 cpuset_css_alloc,当它发现传递的参数 parent_css 等于 NULL 的时候,直接返回 &top_cpuset.css,也就是一个全局的 css。全局,意味着牵一发动全身,隐约中找到了第一篇课堂作业第一题的答案。

有了 css 后,调用 init_and_link_css 和 online_css 建立 cgroup 和 ss 的关系就是水到渠成的事情了。online_css 会回调 ss->css_online 函数,对 cpuset 而言,因为 css_alloc 返回的是全局的 css,此处 css_online 并没有实际操作。

init_css_set(三颗星,重点)是 init css_set,是一个全局的 css_set,这里使用申请到的 css 为相应的字段赋值。

第二阶段:绑定 ss 与 cgrp_dfl_root,也就是说系统启动的初期所有的 ss 都与默认的 cgroup 层级结构绑定。由 cgroup_init 函数完成,主要逻辑如下:


int cgroup_init(void){  BUG_ON(cgroup_init_cftypes(NULL, cgroup_base_files));    //1    BUG_ON(cgroup_init_cftypes(NULL, cgroup1_base_files));  BUG_ON(cgroup_setup_root(&cgrp_dfl_root, 0));    //2    for_each_subsys(ss, ssid) {     if (ss->early_init) {    //3            struct cgroup_subsys_state *css = init_css_set.subsys[ss->id];          css->id = cgroup_idr_alloc(&ss->css_idr, css, 1, 2, GFP_KERNEL);        } else {            cgroup_init_subsys(ss, false);      }       cgrp_dfl_root.subsys_mask |= 1 << ss->id;       if (ss->dfl_cftypes == ss->legacy_cftypes) {    //4         WARN_ON(cgroup_add_cftypes(ss, ss->dfl_cftypes));       } else {            WARN_ON(cgroup_add_dfl_cftypes(ss, ss->dfl_cftypes));           WARN_ON(cgroup_add_legacy_cftypes(ss, ss->legacy_cftypes));     }       if (ss->bind)    //5            ss->bind(init_css_set.subsys[ssid]);        css_populate_dir(init_css_set.subsys[ssid]);    }   WARN_ON(sysfs_create_mount_point(fs_kobj, "cgroup"));    //6    WARN_ON(register_filesystem(&cgroup_fs_type));  WARN_ON(register_filesystem(&cgroup2_fs_type)); WARN_ON(!proc_create_single("cgroups", 0, NULL, proc_cgroupstats_show));#ifdef CONFIG_CPUSETS   WARN_ON(register_filesystem(&cpuset_fs_type));#endif    return 

cgroup_base_files 和 cgroup1_base_files 都是 cftype 数组,内核定义它们的时候并没有提供文件操作相关的回调函数,第 1 步中调用 cgroup_init_cftypes 为相关操作赋值。

内核里面有一些代码是 BUG_ON、WARN_ON 等括起来的,cgroup_init 就出现了两种。一般情况下这类代码不涉及具体逻辑,但是少数情况下,工程师可能考虑到代码的简洁和美观这么做了。实际上,不推荐这么做,因为阅读代码的工程师可能看到 XXX_ON 会跳过,影响理解。改成 ret = cgroup_init_cftypes(NULL, cgroup_base_files); BUG_ON(ret); 效果可能好些。

写书与写博客很大的不同点在于写书需要考虑篇幅,写多了显得啰嗦,写博客就不同了,不影响理解的情况下可以说一些有帮助的题外话,所以我会插播一些理解和建议,希望大家不要介意。

第 2 步,调用 cgroup_setup_root 继续设置 cgrp_dfl_root,cgroup_setup_root 我们在 mount 的时候着重介绍。前面说了 cgrp_dfl_root 戏份有限,这里就不给出境机会了。

接下来遍历系统支持的 ss(for_each_subsys)。

第 3 步,遍历系统支持的 ss,如果在 early init 的阶段没有初始化,调用 cgroup_init_subsys 初始化。

第 4 步,设置 ss 相关的 cftype 。cftype 我们在第一篇中就提过了,mount 和 mkdir 时,cgroup 为我们创建的文件就是它来表示的。每个 ss 的 cftype 是 ss 自行定义的,比如 cpuset 的定义如下。


struct cgroup_subsys cpuset_cgrp_subsys = {….legacy_cftypes = legacy_files,.dfl_cftypes = dfl_files,…};

每个cftype都有一个flags字段,可以是多种标志的组合,常见的标志如下:

源码解析容器底层cgroup的实现

cgroup_add_cftypes 不会改变 flags 字段,cgroup_add_dfl_cftypes 和 cgroup_add_legacy_cftypes 调用 cgroup_add_cftypes 实现,只不过会分别给 dfl_cftypes 和 legacy_cftypes 添加 __CFTYPE_ONLY_ON_DFL 和__CFTYPE_NOT_ON_DFL 标志。cpuset 的 dfl_cftypes 和 legacy_cftypes 不同,所以它的 dfl_files 和 legacy_files 会被添加标志。

cgroup_add_cftypes 会遍历我们在第二个参数中指定的 cftype 数组,根据 cftype 的 flags 决定是否在当前目录下创建 cftype 对应的文件,以 cpuset 的 legacy_files 名为“cpus”的 cftype 为例:


static struct cftype legacy_files[] = { {       .name = "cpus",     .seq_show = cpuset_common_seq_show,     .write = cpuset_write_resmask,      .max_write_len = (100U + 6 * NR_CPUS),      .private = FILE_CPULIST,    },…}

它是 legacy_files,被添加了 __CFTYPE_NOT_ON_DFL 标志,除此之外并没有其他标志,所以 cpuset 的目录只要不属于默认的 cgroup 层级结构,都会创建它。另外,它并没有 CFTYPE_NO_PREFIX 标志,所以它的文件名最终是“cpuset.cpus”,也就是我们在第一篇的例子中看到的样子。

除了各个 ss 专属的 cftype 之外,cgroup 定义了 cgroup1_base_files 和cgroup_base_files(适用于默认层级结构)两个通用 cftype 数组,它们不属于某一个ss,cftype 的 ss 字段自然也是 NULL,创建它们的时候不会加前缀,比如例子中的tasks、notify_on_release 和 cgroup.procs(原名就叫 cgroup.procs)。

第 5 步,回调 ss->bind,绑定 ss 和 cgroup。cgroup_init_subsys 函数已经为init_css_set.subsys[ssid] 赋值了(提示,三颗星),ss->bind 的参数是 css,也就是 ss和 cgroup,cpuset 的 bind 实现简化如下:


void cpuset_bind(struct cgroup_subsys_state *root_css){ cpumask_copy(top_cpuset.cpus_allowed,               top_cpuset.effective_cpus); top_cpuset.mems_allowed = top_cpuset.effective_mems;}

对于 top_cpuset,如果你没有啥印象了,提醒下,cpuset 的 css_alloc 在传递的父 css为 NULL 的情况下,返回的就是 top_cpuset.css,剧透一下,它们在 mount 的时候还会重复一遍。

不得不再提一遍,我会在需要特别注意的地方“啰嗦”一点,读完第一遍如果能对它们有大概的印象就算是有收获了。

第 6 步,创建 sysfs 的 fs/cgroup(也就是我们看到的 /sys/fs/cgroup 目录),注册文件系统。cpuset_fs_type 文件系统和我们分析的 cpuset ss 有什么关系呢?cpuset_fs_type 本质上就是一个空壳,完全是由 cpuset ss 实现的。

初始化完毕,接下来我们就可以 mount cgroup 文件系统了。此刻系统支持的 ss 都绑定在默认的 cgroup 层级结构上。

1.2 cgroup 的 mount

mount 的流程在 5.5.5 版本的内核中已经发生了很大变化,有机会我们在后续的篇章中讨论,这里直接进入正题。

mount 的时候可以指定一些参数,由 cgroup1_parse_param 函数解析,除了指定 ss 的名字外,还支持以下参数:


const struct fs_parameter_spec cgroup1_param_specs[] = {    fsparam_flag  ("all",       Opt_all),   fsparam_flag  ("clone_children", Opt_clone_children),   fsparam_flag  ("cpuset_v2_mode", Opt_cpuset_v2_mode),   fsparam_string("name",      Opt_name),  fsparam_flag  ("none",      Opt_none),  fsparam_flag  ("noprefix",  Opt_noprefix),  fsparam_string("release_agent", Opt_release_agent), fsparam_flag  ("xattr",     Opt_xattr), {}};

mount 的时候通过 -o 指定即可,比如我们可以指定 name:


love_cc@yahua:~$ sudo mount -t cgroup -o cpuset,name=cs abcd test/love_cc@yahua:~$ mountabcd on /home/love_cc/test type cgroup (rw,relatime,cpuset,name=cs)

需要注意的是,abcd 并不是指定 name 的,它实际是 dev_name,这是在 cgroup 中这个名字随意而已。但如果我们 mount 的是 ext4 等文件系统,就不能随意了,比如 sudo mount -t ext4 /dev/sdb1 dir。

cgroup 文件系统 mount 的核心逻辑由 cgroup1_get_tree 函数实现,详细讨论它之前有必要弄清一件事情,对 cgroup 而言,一个 mount 的意义何在,对应什么数据结构?回顾第一章,系统启动后,Ubuntu 已经为我们 mount 了很多子系统,每一个 mount 都可以管理一类资源,我们可以利用它们创建子目录,构建一个层级结构。所以 cgroup 的mount 实际上是构建了 cgroup 层级结构,进一步讲,就是构建了一个 cgroup_root(层级结构的根)。

我们在第一篇中强调过,一个 ss 最多只能绑定一个 cgroup 层级结构,那么 mount 的过程需要解决的问题就明朗了。
1.
是否可以复用已经存在的 cgroup_root,如果之前的 cgroup_root 可以满足我们的需要,直接复用。

2.
如果之前的 mount 的 cgroup_root 不能满足我们的需要,本次 mount 会失败,或者替代之前的 mount ?

3.
如果 ss 绑定的 cgroup_root 不存在?没有这个如果,初始化的时候 ss 就已经绑定了 cgrp_dfl_root。

cgroup1_get_tree 调用 cgroup1_root_to_use 解决以下这几个问题:

首先是 mount 参数检查,比如指定了 ss 名字(比如 cpuset)的情况下,就不能再指定all 或者 none;不指定 name 的情况下,不能指定 none;ss 名字、none 和 name 都没有指定的情况下,默认为 all。

然后,查看是否可以复用已有的 cgroup_root,代码片段如下:


for_each_root(root) {       bool name_match = false;        if (root == &cgrp_dfl_root)    //#1         continue;       if (ctx->name) {            if (strcmp(ctx->name, root->name))              continue;           name_match = true;      }       if ((ctx->subsys_mask || ctx->none) &&          (ctx->subsys_mask != root->subsys_mask)) {          if (!name_match)                continue;           return -EBUSY;    //#2      }       ctx->root = root;       return 0;   }   if (!ctx->subsys_mask && !ctx->none)    //#3        return cg_invalf(fc, "cgroup1: No subsys list or none specified"

ctx->name 是 mount 时指定的 name,ctx->subsys_mask 是 mount 时指定的 ss 的掩码(可以指定多个)。

这段代码遍历已经存在的 cgroup_root 。

不能复用 cgrp_dfl_root(标号 #1),简单的解释是 cgrp_dfl_root 主要是给 cgroup v2 用的。

mount 的时候指定了名字,与它同名的 cgroup_root 的掩码一致,则可复用,否则失败(标号 #2)。

mount 的时候指定了名字,不存在与它同名的 cgroup_root,没有指定 ss,且没有指定 none,失败(标号#3)。当然,即使指定了 ss 也不一定成功,还有下一关。

mount 的时候没有指定名字,目标 cgroup_root 的 ss 掩码相同即可。

如果没有找到目标 cgroup_root,也没有失败,cgroup1_root_to_use 接下来就创建一个 cgroup_root,为它初始化,然后调用 cgroup_setup_root 完成设置。

cgroup_setup_root 第二次出现了,它绑定 ss(可以是多个)和 mount 时创建的cgroup_root(cgroup 层级结构),主要逻辑如下:


int cgroup_setup_root(struct cgroup_root *root, u16 ss_mask){   LIST_HEAD(tmp_links);   struct cgroup *root_cgrp = &root->cgrp; struct kernfs_syscall_ops *kf_sops; struct css_set *cset;   ret = allocate_cgrp_cset_links(2 * css_set_count, &tmp_links);  kf_sops = root == &cgrp_dfl_root ?    //1       &cgroup_kf_syscall_ops : &cgroup1_kf_syscall_ops;   root->kf_root = kernfs_create_root(kf_sops,                    KERNFS_ROOT_CREATE_DEACTIVATED |                    KERNFS_ROOT_SUPPORT_EXPORTOP,                       root_cgrp);  root_cgrp->kn = root->kf_root->kn;  ret = css_populate_dir(&root_cgrp->self);    //2    ret = rebind_subsystems(root, ss_mask);    //3  if (ret)        goto destroy_root;    //省略出错处理  list_add(&root->root_list, &cgroup_roots);  cgroup_root_count++;    hash_for_each(css_set_table, i, cset, hlist) {    //4       link_css_set(&tmp_links, cset, root_cgrp);  }   kernfs_activate(root_cgrp->kn); free_cgrp_cset_links(&tmp_links);   return 

第 1 步与后续的文件操作有关,提供 mkdir 等操作。

第 2 步,创建 root_cgrp->self 的 cftype 文件,self 是内嵌的 css,它没有关联任何ss(!css->ss成立),css_populate_dir 创建的文件来自 cgroup1_base_files,第一篇的例子中的 cgroup.procs、tasks 等文件都属于它。

第 3 步,调用 rebind_subsystems 重新绑定指定的 ss,在此之前可能已经和其他cgroup_root 绑定了,所以叫做 rebind。

rebind_subsystems 是重点,主要逻辑如下:


int rebind_subsystems(struct cgroup_root *dst_root, u16 ss_mask){   struct cgroup *dcgrp = &dst_root->cgrp; do_each_subsys_mask(ss, ssid, ss_mask) {    //3.1       if (css_next_child(NULL, cgroup_css(&ss->root->cgrp, ss)) &&            !ss->implicit_on_dfl)    //#1           return -EBUSY;      if (ss->root != &cgrp_dfl_root && dst_root != &cgrp_dfl_root)    //#2           return -EBUSY;  } while_each_subsys_mask(); do_each_subsys_mask(ss, ssid, ss_mask) {        struct cgroup_root *src_root = ss->root;        struct cgroup *scgrp = &src_root->cgrp;     struct cgroup_subsys_state *css = cgroup_css(scgrp, ss);        src_root->subsys_mask &= ~(1 << ssid);    //3.2     WARN_ON(cgroup_apply_control(scgrp));       cgroup_finalize_control(scgrp, 0);      RCU_INIT_POINTER(scgrp->subsys[ssid], NULL);        rcu_assign_pointer(dcgrp->subsys[ssid], css);    //3.3      ss->root = dst_root;        css->cgroup = dcgrp;        hash_for_each(css_set_table, i, cset, hlist)            list_move_tail(&cset->e_cset_node[ss->id],                     &dcgrp->e_csets[ss->id]);        dst_root->subsys_mask |= 1 << ssid;     if (dst_root == &cgrp_dfl_root) {    //#3       } else {            dcgrp->subtree_control |= 1 << ssid;        }       ret = cgroup_apply_control(dcgrp);    //#3      if (ss->bind)    //3.4          ss->bind(css);  } while_each_subsys_mask(); kernfs_activate(dcgrp->kn); ret

又一大段代码……不过请相信我,跟写书一样,我已经把不影响理解的代码缩减了,非重点和难点的逻辑也不会引入代码。

这段代码与 cgroup_init 的逻辑有点类似,不同点在于 cgroup_init 将 ss 和 cgrp_dfl_root 绑定,rebind_subsystems 尝试将 ss 与当前绑定的 cgroup_root(层级结构)解绑,然后绑定到新的 cgroup_root 上。

第 3.1 步,条件检查。

标号 #1,如果尝试绑定的某个 ss 目前绑定的 cgroup_root 已经有子目录了,不允许重新绑定其他 cgroup_root,除非将所有子目录删除。

标号 #2,被解绑和将要绑定的双方至少有一个是 cgrp_dfl_root,否则失败。

前面已经说了,cgrp_dfl_root 在 cgroup v1 中是不会被复用的,所以 mount 的时候不能指定使用 cgrp_dfl_root,但是标号 #2 好像隐含着可以指定 cgrp_dfl_root 的意思?我们只是说了 cgroup v1 不会复用 cgrp_dfl_root,没有说 cgroup v2 不可以啊。v2 确实可以,而且 v1 和 v2 可以共存。“cgroup 的实现将 v1 和 v2 交织在了一起”,有体会了吧,还有几个地方也是这样的(比如标号 #3),单纯从 v1 的角度去理解甚至会觉得代码费解,大家自行阅读代码的时候注意下。

3.2 和 3.3 步就是解绑与绑定了,需要注意的是 css 是复用的,因为标号 #1 已经要求原cgroup_root 不能有子目录了,所以它的 css 其实就是“光杆司令”,整个层级结构只有它自己,复用是没问题,修改下相关的指针即可。

标号 #3,cgroup_apply_control 最终也会调用 css_populate_dir,与 cgroup_setup_root 调用的 css_populate_dir(&root_cgrp->self) 不同,dcgrp 的 css 已经有对应的 ss 了,创建的 cftype 由 ss 决定,在我们的例子中就是 cpuset 的legacy_files,比如 cpuset.cpus、cpuset.mems 等文件。

所以 mount 的时候 cgroup 为我们创建的文件分为两部分,一部分是 ss 无关的,比如 cgroup1_base_files(cgroup.procs、tasks),所有的 ss 一般都有这些文件,另一部分由 ss 自行决定。

除此之外,cgroup_apply_control 还可以涉及到进程的迁移(migrate),原层级结构管理的进程迁移到新的层级结构中,migrate 过程我们在下一篇中讨论。系统启动后,不做任何改动的情况下,我们尝试在 cpuset 目录下读取 tasks 文件:


love_cc@yahua:/sys/fs/cgroup/cpuset$ cat tasks123…1235…

这说明 Ubuntu mount cpuset 的过程中,进程已经从默认的层级结构迁移到 cpuset 层级结构上了。

3.4 步与 cgroup_init 的第 5 步一致。

回到 cgroup_setup_root 的第 4 步,已有的 css_set 在 mount 之前都与原 cgroup_root关联(cgroup_root.cgrp),rebind_subsystems 成功后,调用 link_css_set 建立它们与新 cgroup_root 的关系,原理就是第一篇中说的使用 cgrp_cset_link 实现 css_set 和cgroup 多对多的关系。

我们用 Ubuntu 为我们 mount cpuset 子系统为例总结整个过程。
1.
初始化结束后,init_css_set 是唯一的 css_set,cgrp_dfl_root 是唯一的cgroup_root,那么与 init_css_set 关联的自然是 cgrp_dfl_root.cgrp。

2.
mount cpuset,假设在这之前还未 mount 过其他 ss。mount 过程首先创建了一个新的 cgroup_root,不妨称为 new_root,然后调用 cgroup_setup_root,重新绑定cpuset 到 new_root。

3.
init_css_set 仍然是唯一的 css_set(并没有创建新的),但此刻 cpuset 已经绑定了 new_root,调用 link_css_set 建立它与 new_root 的关系。

cgroup 并不是一个简单的模块,如果第一遍没有完全读懂,记住一句话就够了,mount 创建 cgroup_root,也就是新的层级结构。

作者介绍

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

标签:ss,cgrp,cpuset,源码,cgroup,解析,root,css
来源: https://blog.51cto.com/u_15127629/2780121

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有