当前位置:数码通 > 摄影

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

来源于 数码通 2023-10-03 02:57

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

往期回顾:点击查看

1。 cgroup的使用

测试环境版本与之前相同:

Ubuntu

(lsb_release -a)

发行商 ID:Ubuntu

说明:Ubuntu 19.10

发布:     19.10

Linux

(uname -a)

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

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

挂载成功后,cgroup_root已经存在,这意味着cgroup层次结构已经构建完成,接下来我们就可以使用cgroup了。

1。 cgroup 的 mkdir

mkdir 比挂载过程稍微简单一些,由 cgroup_mkdir 函数实现。主要逻辑如下:

int cgroup_mkdir(struct kernfs_node *parent_kn, const char *name, umode_t 模式)
{
结构 cgroup *parent, *cgrp;
父 = cgroup_kn_lock_live(parent_kn, false); //1
cgrp = cgroup_create(父级、名称、模式); //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;
返回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步和挂载时一样,css_populate_dir和cgroup_apply_control_enable会为我们创建cftype对应的文件,但有两点不同:

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

其次,挂载时,新的cgroup_root.cgrp重用了与原始cgroup_root.cgrp相关的css(rebind_subsystems,第2部分)。这里创建了一个新的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对象,如下:

结构cgroup_subsys_state *
cpuset_css_alloc(结构cgroup_subsys_state *parent_css)
{
结构体cpuset *cs;
if (!parent_css) //挂载时,返回top_cpuset.css
返回 &top_cpuset.css;
cs = kzalloc(sizeof(*cs), GFP_KERNEL);
alloc_cpumasks(cs, NULL); //#1
set_bit(CS_SCHED_LOAD_BALANCE, &cs->flags);
节点_清除(cs->mems_allowed);
节点_清除(cs-> effective_mems); //#2
fmeter_init(&cs->fmeter);
cs->relax_domain_level = -1;
返回 &cs->css;
}

注意标签 #1 和 #2。新cs的cpus_allowed和mems_allowed被清除。此时读取cpuset.cpus和cpuset.mems时没有任何内容,也就是说无法更改cpu和内存的限制。父目录是继承的,使用前必须正确设置。

2。限制资源

在第一个示例中,我们使用 echo 0-2 > cpuset.cpus 和 echo 0 > cpuset.mems 来限制 /cpuset0 管理的进程使用的 cpu 和内存节点。以cpuset.cpus为例,其cftype如下:

{
.name =“CPU”,
.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。管理流程

在我们的示例中,我们将进程号写入任务文件(echo $$ >tasks)以限制进程仅使用/cpuset0配置的cpu和内存节点。其实写一个cgroup.procs文件也是可以的。它们的cftype文件定义如下:

{
.name = "任务",
.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,
},

两个文件的写入分别是cgroup1_tasks_write和cgroup1_procs_write。它们都是通过调用__cgroup1_procs_write来实现的。唯一的区别是最后一个参数threadgroup不同。前者是假的,后者是真的。从名字就可以看出,如果为false,则只作用于目标进程(线程),如果为true,则作用于线程组。

这里简单解释一下线程组。线程组是属于同一进程的线程的集合。同一线程组的线程将其task_structs通过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步,根据用户空间传入的进程id参数,调用cgroup_procs_write_start获取目标进程的task_struct。当threadgroup为true时,获取线程组leader进程的task_struct。

第3步,调用cgroup_attach_task将进程附加到(附加或连接到)cgroup。

cgroup 和 ss 之间是点对点关系。他们使用bind,这就是所谓的绑定。进程和 cgroup 之间没有对等关系。他们使用attach,这就是所谓的附件。

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

无论进程创建后“走过”了多少组cgroup,进程在创建时就已经附加到了cgroup中。

流程创建的过程书中已经详细分析了。这里只讨论与cgroup相关的部分。

首先调用的是cgroup_fork,如下:

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

直接指向 init_css_set,但这可能是暂时的。 child->cg_list为空,表明新进程尚未附加到任何cgroup。

第二个是cgroup_can_fork,它调用ss->can_fork,ss判断是否可以创建新进程。如果答案是否定的,则整个分叉将失败。

最后一步是cgroup_post_fork,进行最后的调整。主要逻辑如下:

void cgroup_post_fork(struct task_struct *child)
{结构 cgroup_subsys *ss;
struct css_set *cset;
if (可能(child->pid)) {
WARN_ON_ONCE(!list_empty(&child->cg_list));
cset = task_css_set(当前); /* 当前是@child 的父级 */
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(子);
while_each_subsys_mask();
}

首先,current是新进程子进程的父进程。首先获取父进程的css,然后调用css_set_move_task将新进程转移到css中。 css_set_move_task的第二个参数是原始的css。它为 NULL,因为它尚未附加到任何 cgroup (css_set)。 css_set_move_task会将child->cg_list插入到css->tasks链表中,child->cg_list将不再为空。

也就是说,当一个新进程创建时,它会被附加到与父进程相同的cgroup中。

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

void cpuset_fork(struct task_struct *task)
{
if (task_css_is_root(任务, cpuset_cgrp_id))
返回;
set_cpus_allowed_ptr(任务, 当前->cpus_ptr);
任务->mems_allowed = 当前->mems_allowed;
}

回顾第一篇文章中的例子,我们在cpuset下创建了cpuset0目录,配置了资源,管理了进程。修改后,在cpuset下创建cpuset1目录。该进程首先附加到/cpuset0,然后迁移到/cpuset1。以此为例来分析一下迁移过程:

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 $$ > 任务
root@yahua:/sys/fs/cgroup/cpuset/cpuset0# cat 任务
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 $$ > 任务
root@yahua:/sys/fs/cgroup/cpuset/cpuset1# cat 任务
2682
2713
root@yahua:/sys/fs/cgroup/cpuset/cpuset1# cat ../cpuset0/tasks
#没有内容省略,空着

在继续讨论之前,我们先来看看目前的情况。我们在__cgroup1_procs_write函数的第三步中就是cgroup_attach_task。在前面两步中,我们已经获取了目标cgroup(即/cpuset1)和进程的task_struct。

cgroup_attach_task的目的是将进程attach到目标cgroup,逻辑上至少包括两个部分:进程与原组detach以及进程与目标cgroup Attach。 src、dst 和 migrate 这三个元素正好对应三个函数 cgroup_migrate_add_src、cgroup_migrate_prepare_dst 和 cgroup_ 我igrate

首先调用的是cgroup_migrate_add_src。当threadgroup为true时,为线程组的每个线程调用一次。否则,将被调用一次。其主要逻辑如下:

void cgroup_migrate_add_src(struct css_set *src_cset,
结构cgroup *dst_cgrp,
结构 cgroup_mgctx *mgctx)
{
结构 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 = 链接->cgrp;
if (c->root == root) {
分辨率=c; //res就是我们要找的
休息;
}
}

提醒一下,在任何一个cgroup层次结构中,一个进程只能与一个cgroup关联,因此与 /cpuset1 属于同一cgroup_root的只能是 /cpuset0 。

另外,我们只分析一种情况。前面提到Ubuntu挂载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)) {
模板[i] = cgroup_e_css_by_mask(cgrp, ss);
} 别的 {
模板[i] = old_cset->subsys[i];
}
}

root 是更改后的层次结构的 cgroup_root,在我们的示例中它是 cpuset。至于cgroup_e_css_by_mask,这里的e是有效的。在不考虑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。两者有什么区别? css_set创建后subsys不会改变,cgrp_links可以动态调整。例如,cgroup_setup_root中调用的link_css_set仅修改cgrp_links。

如果找不到一致的css_set,则创建一个新的,并根据需要的两个点为其赋值。

下一步是cgroup_migrate。它的实现代码很多,但逻辑很简单。我们不会直接分析代码。主要分为以下几个步骤:

  1. 调用cgroup_migrate_add_task将需要迁移的进程放入mgctx->tset中,然后调用cgroup_migrate_execute函数,就完成了实际的迁移过程。
  2. 回调改变后的ss的ss->can_attach函数,判断是否合法。
  3. ? 。
  4. 回调改变的ss的ss->attach,migrate正式生效。
  5. cpuset的attach是通过cpuset_attach函数实现的。核心逻辑如下:
cgroup_taskset_for_each(任务,css,tset){
WARN_ON_ONCE(set_cpus_allowed_ptr(任务, cpus_attach));
cpuset_change_task_nodemask(任务, &cpuset_attach_nodemask_to);
cpuset_update_task_spread_flag(cs, 任务);
}

遍历进程,使cpu和内存节点限制生效。

我们分析的限制进程使用 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 的代码实现分析

登录后参与评论