Linux内核:容器底层cgroup如何使用
在前面的文章中,我們探討了容器底層 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); //1cgrp = cgroup_create(parent, name, mode); //2ret = cgroup_kn_set_ugid(cgrp->kn);ret = css_populate_dir(&cgrp->self); //3ret = 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.cssreturn &top_cpuset.css;cs = kzalloc(sizeof(*cs), GFP_KERNEL); alloc_cpumasks(cs, NULL);??? //#1set_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,邏輯上至少包括進程和原group detach 和進程和目標 cgroup attach 兩部分。三個要素,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 函數實現,核心邏輯如下:
遍歷進程,使 cpu 和 memory node 的限制生效。
我們分析的限制進程使用 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))??? //1goto 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);??? //3if (cpumask_test_cpu(task_cpu(p), new_mask))??? //4goto out;if (task_running(rq, p) || p->state == TASK_WAKING) {??? //5struct 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 的討論差不多了,絕大部分篇幅集中討論最常用的操作,但實際上還不完整,其余操作大家可以自行繼續當前的思路閱讀。
作者介紹
姜亞華,《精通 Linux 內核——智能設備開發核心技術》的作者,一直從事與 Linux 內核和 Linux 編程相關的工作,研究內核代碼十多年,對多數模塊的細節如數家珍。曾負責華為手機 Touch、Sensor 的驅動和軟件優化(包括 Mate、榮耀等系列),以及 Intel 安卓平臺 Camera 和 Sensor 的驅動開發(包括 Baytrail、Cherrytrail、Cherrytrail CR、Sofia 等)。現負責 DMA、Interrupt、Semaphore 等模塊的優化與驗證(包括 Vega、Navi 系列和多款 APU 產品)。
推薦閱讀
社區抗議LibreOffice商業化,開源就不能賺錢?
MySQL也替換了master、slave
開撕,“谷歌違反協議”
一個方案搞定模型量化到端側部署全流程
GitHub上持續沖榜,ElasticJob重啟
總結
以上是生活随笔為你收集整理的Linux内核:容器底层cgroup如何使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用Azure Application
- 下一篇: 通过Windows Visual Stu