云计算时代,容器底层 cgroup 的代码实现分析

OSCHINA编辑部 发布于 2020/05/20 14:07
阅读 4K+
收藏 46

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

上期回顾点击查看

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

一、cgroup 的初始化和 mount

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

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 的代码实现,与书(《精通 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字段,可以是多种标志的组合,常见的标志如下:

标志

含义

CFTYPE_ONLY_ON_ROOT

只出现在cgroup_root内嵌的cgroup中,也就是cgroup层级结构的根中

CFTYPE_NOT_ON_ROOT

不会出现在cgroup_root内嵌的cgroup中

CFTYPE_NO_PREFIX

根据cftype的名字创建文件时,不需要加前缀

__CFTYPE_ONLY_ON_DFL

只出现在默认cgroup层级结构中

__CFTYPE_NOT_ON_DFL

不会出现在默认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:~$ mount
abcd 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 tasks
1
2
3
…
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,也就是新的层级结构

 

往期回顾

《云计算时代,容器底层 cgroup 如何实现资源分组》

加载中
2
隔壁家的老王
隔壁家的老王

睾端不火系列

二如公子
二如公子
忍不住给你点个赞:laughing:希望越来越多的人对底层和操作系统感兴趣
1
二如公子
二如公子

小尾巴还是要带的,万一xxx了呢~~~ 我爱猫爱码,养猫多多,救助流浪猫也好几年了。但一人之力终究有限,爱猫或爱码的同学可以加我的qq一起撸猫撸码哈(3356954398,二如公子)。

h4cd
h4cd
0
nullpointerxyz
黄海彬
黄海彬
还可以
OSCHINA
登录后可查看更多优质内容
返回顶部
顶部