cgroup 基礎介紹

一項新概念的產生,必然有其原因,cgroup 也不例外,最初由谷歌工程師 Paul Menage 和 Rohit Seth 提出【1】:因爲計算機硬件能力越來越強大,爲了提高機器的使用效率,可以在同一臺機器上運行不同運算模型的工作。開始是用 process container 來命名,後來因爲 Container 有多重含義容易引起誤解,就在 2007 年更名爲 Control Groups,並被整合進 2.6.24 內核,它可以限制、記錄、隔離進程組所使用的物理資源(如 cpu/memory/io 等等)及控制使用物理資源的優先級,本文主要參照 kernel-5.10 源碼,對 cgroup 做下基礎介紹。

1.cgroup 的組成

我們使用 cgroup 對進程組做資源處理,離不開下面的組成因素:

圖片

cgroupv1 可以允許多個層級,以 v1 的組織方式,如果把用到的子系統 attach 到同一個層級,子系統的資源控制沒有辦法解耦,可能會有某些進程受到其它子系統的影響,因此 v1 需要多個層級的組織方式,把一個層級看做一棵樹,可以認爲是一個森林。

另外,cgroup、task、subsystem 以及 hierarchy 四者間的相互關係及其基本規則如下:

  1. 同一個 hierarchy 可以附加一個或多個 subsystem。

  2. 一個 subsystem 可以附加到多個 hierarchy,當且僅當這些 hierarchy 只有唯一這個 subsystem。

  3. 對於你創建的每個 hierarchy,task 只能存在於其中一個 cgroup 中,即一個 task 不能存在於同一個 hierarchy 的不同 cgroup 中,但是一個 task 可以存在在不同 hierarchy 中的多個 cgroup 中。

  4. 進程(task)在 fork 自身時創建的子任務(child task)默認與原 task 在同一個 cgroup 中,但是 child task 允許被移動到不同的 cgroup 中。

1.1 subsystem 介紹

子系統主要用來實現資源的控制,因爲是具體的控制模塊,單獨拉出來介紹,隨着需求及功能的增加,有如下比較重要且常用的子系統:

圖片

cgroup 抽離出這些子系統,也是在複用這些資源管理模塊,其本質是在這些資源管理模塊上附加鉤子來實現資源的限制與優先級分配。

1.2 cgroup 關鍵數據結構

我們感興趣的是 task 和 cgroup 怎樣映射的,因爲系統可能會有多個層級,每個 task 也可能會被多個子系統約束,因此一個 task 可能會存在於多個 cgroups 中;而每個 cgroup 中也會控制多個 tasks。因此 task 和 cgroup 是多對多的關係。爲了記錄這種關係,可以多加鏈表,但那樣查找及修改效率太差,linux 用 css_set 和 cgrp_cset_link 這兩個中間數據結構來完成這個映射。

試想當一個進程克隆子進程時,如果沒有指定克隆到某個特定的 cgroup,默認子進程會與該進程保持在相同的 cgroups 中,把共享相同 cgroups 的這組進程抽離出來,用 css_set 來標識,每個進程只存在於一個 css_set 中,一個 css_set 可能會包含多個進程,所有的 css_sets 通一個哈希表來組織,當進程在 cgroups 之間進行遷移時,因爲 css_set 是基於 cgroup 這種共性所建立的,因此 css_set 也可以被複用。

進程中 cgroup 信息:

圖片

有了 css_set 後,雖然相同類型的進程被鏈接到了一個 css_set 裏,但還是會有 css_set 與 cgroup 多對多映射的問題,只不過 css_set 會比 task 的個數少一些,這時 cgrp_cset_link 來了,新增的這第三張表可以極大提高 cgroup 和 css_sets 互相查找的效率。

總體連接關係:

圖片

另外,css_set 一方面記錄該 cset 中的進程信息,也維護了 subsys 子系統的信息(簡稱 css),也可以認爲是兩者之間的紐帶。

css_set 關鍵成員:

圖片

圖片

cgroup 關鍵成員:

圖片

圖片

cgroup_subsys_state,cgroup 操作的關鍵對象,包含文件暴露,子系統操作相關。

其關鍵成員:

圖片

cgroup_subsys 子系統,主要包含各子系統的通用操作方法。

關鍵成員:

圖片

cgroup 各數據結構關係圖:

圖片

2. cgroup 初始化

cgroup 初始化比較簡單,主要分兩步,cgroup_init_early 和 cgroup_init。

2.1 cgroup_init_early

因爲是在 boot 非常靠前的階段,主要做下面的工作:

  1. 同 init_task,系統也初始化了 cgrp_dfl_root 做爲 default hierarchy 的 cgroup_root, 同樣,也有一個 init_css_set,來初始化 init 階段的相關 task。在該階段默認由 init 進程 fork 出來的子進程都會掛在 init_css_set。

  2. 如果有相關子系統需要在 init 階段被其它模塊使用,會構建該子系統相關的 cgroup 數據結構,把相關 ss/css/cgroup/css_set 之間的關係建立起來,最終調用 online_css 來真正使該 css 生效,但還沒有在文件系統中呈現,並初始化 init_css_set 的 subsys。

2.2 cgroup_init

初始化走到這裏,vfs 及 sysfs 已初始化完成,主要工作:

  1. 調用 cgroup_setup_root 初始化 cgrp_dfl_root,創建 cgroup 暴露給用戶的相關文件,因爲在 default hierarchy, 會在 / sys/fs/cgroup 下創建 cgroup_base_files 數組中的相關文件,然後調用 rebind_subsystems,注意這次其實沒有涉及到子系統在不同 hierarchy 的移動,也沒有使能相關的 css 和暴露 ss 相關文件。然後把目前存在的所有 css_set 與 root cgroup 建立聯繫,畢竟這時都在 default hierarchy 下。

  2. 初始化在 cgroup_subsys.h 裏定義好的各子系統,可見 2.1。

  3. 把 init_css_set 掛載到 cgrp_dfl_root,因此可以通過 cgrp_dfl_root 獲取到所有掛在 init_css_set 裏面的進程。

  4. 對每一個要初始化的子系統,初始化要暴露給用戶空間的文件並創建。

  5. 因爲 init_css_set 的 subsys 被初始化有變動,重新做哈希鏈入 css_set_table。

  6. 註冊 cgroup 和 cgroup2 文件系統類型。

  7. 創建 / proc/cgroups 文件,用來查看當前系統 cgroup 的概況。拿 ubuntu 爲例,開機後只有一個 hierarchy,裏面創建了 179 個 cgroup,14 個子系統被使能。

圖片

3.cgroup 創建與分配 task

cgroup vfs 基於 kernfs,mount 過程主要通過 kernfs 做 super block 及 root 目錄初始化,另外區分了 cgroupv1 和 cgroupv2,v2 支持了 thread mode, 而且只有一個‘default unified hierarchy’。因爲形態的區別,v1 需要查找或創建 hierarchy, v2 則不需要,另外 mount 時的參數處理也有一些區別,這裏不再詳述。

3.1 cgroup 創建

當 mount 成功後,即可以在掛載的文件目錄裏通過 mkdir 創建 cgroup。

主要流程如下:

  1. 找到該 cgroup 的父節點,並檢查當前層級的一些限制是否能滿足要求,主要是後代個數及深度限制。

  2. 調用 cgroup_create 來做具體的初始化,初始化管理其生命週期的 refcnt,創建所在 kernfs 的目錄,繼承父節點目前的凍結狀態,以便可以使凍結自動生效,然後把自己鏈入父節點的 children 鏈表,這樣方便建立 cgroup 的樹形結構,後面遍歷其後代需要用到。因爲在 default hierarchy, 可能會使能 cgroup v2,因此其 subtree_control 並不會被初始化。在其它層級,會對當前 cgroup 及其後代的 subtree_control 和 subtree_ss_mask 作初始化,兩個的具體差異見前面的參數介紹。

  3. 調用 css_populate_dir 在當前目錄下創建 cgroup 通用文件。

  4. 針對每個被使能的子系統,創建 css 及當前子系統的初始化文件(dfl_cftypes

和 legacy_cftypes,但會基於是否在 default hierarchy 做選擇創建),因此當手動 mkdir cgroup 時,會看到很多自動創建的文件。

3.2 分配 task 給 cgroup

這裏簡單講下 thread mode,我們只能創建 domain cgroup,但可以通過寫入‘threaded

’ 到 當前 cgroup 的 "cgroup.type" 文件,使該 cgroup 變爲 threaded,當該 cgroup 變爲 threaded cgroup 後,不能再更改爲 domain cgroup。變爲 threaded 後,其最近的 domain cgroup 父節點會負責資源的統計,成爲該 threaded cgroup 的 dom_cgrp,如果這時候你 cat 下當前父節點 cgroup 的 "cgroup.type" 節點,會發現其類型從 domain 變爲了 domain threaded,而如果該父節點所有的子孫都被刪除,其又會被恢復爲 domain。

thread mode 可以支持我們以線程粒度分組去控制,但必須在一個 threaded domain 中。

cgroupv1 的 "cgroup.procs"、"tasks",cgroupv2 的 "cgroup.procs"、"cgroup.threads" 等節點,都可以通過寫具體 task pid 到節點中來實現 cgroup 的控制。

以 "cgroup.procs" 爲例,具體流程如下:

  1. 從用戶輸入獲取要分配到的目的 cgroup。因爲是在 v2 mode 下操作,有區分進程和線程去分開控制,"cgroup.procs" 節點會找到當前要操作的 task 的進程組 leader,後續會把該進程組內的所有線程全部遷移到目的 cgroup。

  2. 獲取該 task 當前所在 cgroup,因爲是在 default hierarchy,通過 cset_group_from_root 去查找,查找方式也很容易理解。

  3. cgroup_attach_task 做主要的遷移動作,其中 cgroup_migrate_add_src 會把該線程組中的所有 task 所屬的 cset 鏈入記錄遷移過程的數據結構 mgctx 的 preloaded_src_csets 鏈表。

  4. cgroup_migrate_prepare_dst 先從 preloaded_src_csets 鏈表中取出源 cset,疊加遷移對應的目的 cgroup 獲取目的 cset,然後記錄源 cset 和目的 cset 的對應關係,另外,如果源 cset 與目的 cset 的 subsys 不一致,說明該子系統狀態有差異,需要在遷移時重新進行 can_attach 和 attach 回調,重新納入子系統資源管理。

  5. cgroup_migrate_add_task 會把該線程組中的 task 的 cg_list 遷移到所屬 cset 的 mg_tasks 鏈表,標記該 task 開始了遷移流程。隨後把該 cset 鏈入 mgctx 的 src_csets 鏈表。

  6. cgroup_migrate_execute 首先對有變動的子系統狀態調用 can_attach 進行檢測,隨後會從 src_csets 鏈表中獲取要遷移的 cset,遍歷該 cset 中的 mg_tasks 鏈表中的 task,並取出對應目的 cset,調用 css_set_move_task 進行遷移,rcu_assign_pointer(task->cgroups, to); 完成遷移。

4. 總結

本文重點對 cgroup 數據結構及其基本概念進行描述,因爲 cgroup 各數據結構稍微複雜些,所以對其做了重點描述,讀者可以對照代碼閱讀會更有感覺。篇幅限制,具體各 cgroup 子系統沒有涉及,拋磚引玉,感興趣的同學可以補上。

參考資料:

【1】https://lwn.net/Articles/199643/

【2】Documentation/cgroup-v1/*

【3】Documentation/cgroup-v2.txt

【4】Kernel-5.10 源碼

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/uaV50tdQ5CdceQ1LTMcTdg