云计算时代,容器底层 cgroup 的代码完成分析

编辑部的故事 发布于 05/20 14:07
浏览 4K+
收藏 41

阿里云连袂百名贸易领袖、技巧大年夜咖,带您一探行进中的数字新基建!>>>

作者:姜亚华(@二如公子 ),《精通 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
学的动
黄海彬
黄海彬
还可以
前往顶部
顶部