首页 文章 精选 留言 我的

精选列表

搜索[海洋计算],共10000篇文章
优秀的个人博客,低调大师

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

作者:姜亚华(@二如公子),《精通 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 重新配置也可以。我们关心以下几点: 不能更改top_cpuset的设置,这就是第一篇的课堂作业第一题的答案。 目标cpuset的资源必须是父目录cpuset的子集,而且是子目录cpuset的超集(由validate_change函数实现),这是课堂作业第二题的答案。 配置的资源最终更新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,逻辑上至少包括进程和原groupdetach 和进程和目标 cgroupattach 两部分。三个要素,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 了,它的实现代码较多,但逻辑都是直来直去,我们就不直接分析代码了,主要分以下几步: 调用 cgroup_migrate_add_task 将需要迁移的进程放入 mgctx->tset,然后调用cgroup_migrate_execute 函数,实际的 migrate 过程由它完成。 回调有变动的 ss 的 ss->can_attach 函数,判断是否合法。 遍历需要 migrate 的进程,调用 css_set_move_task(task, from_cset, to_cset, true),进程的 css_set 得到更新。 回调有变动的 ss 的 ss->attach,migrate 正式生效。 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 和 memorynode 的限制生效。 我们分析的限制进程使用 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 的代码实现分析

优秀的个人博客,低调大师

标记 (TAG) 您的函数计算(FC)资源

标签的基本知识 标签是资源Meta信息,标签是一些充当元数据的词和短语,支持用户自定义和系统预制。每个标签都包含一个键(Key)和一个值(Value)。标签是一种资源的属性描述。 标签与资源是 多对多的关系。 标签可让您按各种标准(例如用途、所有者或环境)对 阿里云 资源进行分类。这在您具有相同类型的许多资源时会很有用,您可以根据分配给资源的标签快速识别特定资源。 标签对阿里云资源没有任何语义意义,应严格按字符串进行解析。同时,标签不会自动分配至您的资源。您可以修改标签的key和value,还可以随时删除资源的标签。您可以将标签的值设为空的字符串,但是不能将其设为空值。如果您添加的标签的值与该实例上现有标签的值相同,新的值就会覆盖旧值。如果删除资源,资源的所有标签也会被删除。 可以使用管理控制台 和 API 处理标签。 标签设计详情见最佳实践

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册