Cgroup介紹、應用實例及原理描述

Cgroup介紹

CGroup 是 Control Groups 的縮寫,是 Linux 內核提供的一種可以限制、記錄、隔離進程組 (process groups) 所使用的物力資源 (如 cpu memory i/o 等等) 的機制。2007 年進入 Linux 2.6.24 內核,CGroups 不是全新創造的,它將進程管理從 cpuset 中剝離出來,作者是 Google 的 Paul Menage。CGroups 也是 LXC 爲實現虛擬化所使用的資源管理手段。

CGroup 功能及組成

CGroup 是將任意進程進行分組化管理的 Linux 內核功能。CGroup 本身是提供將進程進行分組化管理的功能和接口的基礎結構,I/O 或內存的分配控制等具體的資源管理功能是通過這個功能來實現的。這些具體的資源管理功能稱爲 CGroup 子系統或控制器。CGroup 子系統有控制內存的 Memory 控制器、控制進程調度的 CPU 控制器等。運行中的內核可以使用的 Cgroup 子系統由/proc/cgroup 來確認。

CGroup 提供了一個 CGroup 虛擬文件系統,作爲進行分組管理和各子系統設置的用戶接口。要使用 CGroup,必須掛載 CGroup 文件系統。這時通過掛載選項指定使用哪個子系統。

CGroup 支持的文件種類

文件名 R/W 用途
Release_agent RW 刪除分組時執行的命令,這個文件只存在於根分組
Notify_on_release RW 設置是否執行 release_agent。爲 1 時執行
Tasks RW 屬於分組的線程 TID 列表
Cgroup.procs R 屬於分組的進程 PID 列表。僅包括多線程進程的線程 leader 的 TID,這點與 tasks 不同
Cgroup.event_control RW 監視狀態變化和分組刪除事件的配置文件


CGroup 相關概念解釋

  • 任務(task)。在 cgroups 中,任務就是系統的一個進程;

  • 控制族羣(control group)。控制族羣就是一組按照某種標準劃分的進程。Cgroups 中的資源控制都是以控制族羣爲單位實現。一個進程可以加入到某個控制族羣,也從一個進程組遷移到另一個控制族羣。一個進程組的進程可以使用 cgroups 以控制族羣爲單位分配的資源,同時受到 cgroups 以控制族羣爲單位設定的限制;

  • 層級(hierarchy)。控制族羣可以組織成 hierarchical 的形式,既一顆控制族羣樹。控制族羣樹上的子節點控制族羣是父節點控制族羣的孩子,繼承父控制族羣的特定的屬性;

  • 子系統(subsystem)。一個Cgroup子系統就是一個資源控制器,比如 cpu 子系統就是控制 cpu 時間分配的一個控制器。子系統必須附加(attach)到一個層級上才能起作用,一個子系統附加到某個層級以後,這個層級上的所有控制族羣都受到這個子系統的控制。

相互關係

  1. 每次在系統中創建新層級時,該系統中的所有任務都是那個層級的默認 cgroup(我們稱之爲 root cgroup,此 cgroup 在創建層級時自動創建,後面在該層級中創建的 cgroup 都是此 cgroup 的後代)的初始成員;
  2. 一個子系統最多隻能附加到一個層級;
  3. 一個層級可以附加多個子系統;
  4. 一個任務可以是多個 cgroup 的成員,但是這些 cgroup 必須在不同的層級;
  5. 系統中的進程(任務)創建子進程(任務)時,該子任務自動成爲其父進程所在 cgroup 的成員。然後可根據需要將該子任務移動到不同的 cgroup 中,但開始時它總是繼承其父任務的 cgroup。

圖 1. CGroup 層級圖
圖 1. CGroup 層級圖
圖 1 所示的 CGroup 層級關係顯示,CPU 和 Memory 兩個子系統有自己獨立的層級系統,而又通過 Task Group 取得關聯關係。

圖 2. CGroup 典型應用架構圖

如圖 2 所示,CGroup 技術可以被用來在操作系統底層限制物理資源,起到 Container 的作用。
圖中每一個 JVM 進程對應一個 Container Cgroup 層級,通過 CGroup 提供的各類子系統,可以對每一個 JVM 進程對應的線程級別進行物理限制,這些限制包括 CPU、內存等等許多種類的資源。下一部分會具體對應用程序進行 CPU 資源隔離進行演示。

CGroup 部署及應用實例

講解 CGroup 設計原理前,我們先來做一個簡單的實驗。實驗基於 Linux Centos v7.0 64 位版本(CentOS Linux release 7.0.1406 (Core)),JDK1.7。實驗目的是運行一個佔用 CPU 的 Java 程序,如果不用 CGroup 物理隔離 CPU 核,那程序會由操作系統層級自動挑選 CPU 核來運行程序。由於操作系統層面採用的是時間片輪詢方式隨機挑選 CPU 核作爲運行容器,所以會在本機器上 24 個 CPU 核上隨機執行。如果採用 CGroup 進行物理隔離,我們可以選擇某些 CPU 核作爲指定運行載體。
安裝cgroup工具:

yum install libcgroup-tools libcgroup -y
  1. Java程序代碼
//開啓 4 個用戶線程,其中 1 個線程大量佔用 CPU 資源,其他 3 個線程則處於空閒狀態
public class HoldCPUMain {
 public static class HoldCPUTask implements Runnable{

@Override
public void run() {
// TODO Auto-generated method stub
while(true){
double a = Math.random()*Math.random();//佔用 CPU
System.out.println(a);
}
}
}

 public static class LazyTask implements Runnable{

@Override
public void run() {
// TODO Auto-generated method stub
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}//空閒線程
}
}

 }

public static void main(String[] args){
 for(int i=0;i<10;i++){
 new Thread(new HoldCPUTask()).start();
 }
 }
}

清單 1 程序會啓動 10 個線程,這 10 個線程都在做佔用 CPU 的計算工作,它們可能會運行在 1 個 CPU 核上,也可能運行在多個核上,由操作系統決定。我們稍後會在 Linux 機器上通過命令在後臺運行清單 1 程序。本實驗需要對 CPU 資源進行限制,所以我們在 cpu_and_set 子系統上創建自己的層級“xiezhongtian”。

清單 2. 創建層級

cd /sys/fs/cgroup/cpuset
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset# mkdir xiezhongtian
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset# ls -lrt
總用量 0
-rw-r--r-- 1 root root 0 10月 23 09:32 cgroup.clone_children
-rw-r--r-- 1 root root 0 10月 23 09:32 tasks
drwxr-xr-x 2 root root 0 10月 30 12:22 xiezhongtian
-rw-r--r-- 1 root root 0 10月 30 15:30 release_agent
-rw-r--r-- 1 root root 0 10月 30 15:30 notify_on_release
drwxr-xr-x 4 root root 0 10月 30 15:30 kubepods
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.sched_relax_domain_level
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.mems
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.memory_pressure_enabled
-r--r--r-- 1 root root 0 10月 30 15:30 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.memory_migrate
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.mem_exclusive
-r--r--r-- 1 root root 0 10月 30 15:30 cpuset.effective_mems
-r--r--r-- 1 root root 0 10月 30 15:30 cpuset.effective_cpus
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.cpus
-rw-r--r-- 1 root root 0 10月 30 15:30 cpuset.cpu_exclusive
-r--r--r-- 1 root root 0 10月 30 15:30 cgroup.sane_behavior
-rw-r--r-- 1 root root 0 10月 30 15:30 cgroup.procs
cd xiezhongtian/
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# ls -lrt
總用量 0
-rw-r--r-- 1 root root 0 10月 30 12:22 tasks
-rw-r--r-- 1 root root 0 10月 30 12:22 notify_on_release
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.sched_relax_domain_level
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.mems
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.memory_spread_page
-r--r--r-- 1 root root 0 10月 30 12:22 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.memory_migrate
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.mem_exclusive
-r--r--r-- 1 root root 0 10月 30 12:22 cpuset.effective_mems
-r--r--r-- 1 root root 0 10月 30 12:22 cpuset.effective_cpus
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.cpus
-rw-r--r-- 1 root root 0 10月 30 12:22 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 10月 30 12:22 cgroup.procs
-rw-r--r-- 1 root root 0 10月 30 12:22 cgroup.clone_children
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# 

通過 mkdir 命令新建文件夾xiezhongtian,由於已經預先加載 cpu_and_set 子系統成功,所以當文件夾創建完畢的同時,cpu_and_set 子系統對應的文件夾也會自動創建。
運行 Java 程序前,我們需要確認 cpu_and_set 子系統安裝的目錄,如清單 3 所示。

清單 3. 確認目錄

root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# lscgroup |grep xiezhongtian
cpuset:/xiezhongtian

輸出顯示 cpuset 的目錄是 cpuset,cpu:/xiezhongtian,由於本實驗所採用的 Java 程序是多線程程序,所以需要使用 cgexec 命令來幫助啓動,而不能如網絡上有些材料所述,採用 java –jar 命令啓動後,將 pid 進程號填入 tasks 文件即可的錯誤方式。清單 4 即採用 cgexec 命令啓動 java 程序,需要使用到清單 3 定位到的 cpuset 目錄地址。

清單 4. 運行

root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# cat tasks
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian#
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# cgexec -g cpuset:/xiezhongtian java -jar /home/emindsoft/workdir/devops/Cgroup/test/test.jar

cpuset .cpus 文件中設置需要限制只有 0-3 這 4個 CPU 核可以被用來運行上述清單 4 啓動的 Java 多線程程序。當然 CGroup 還可以限制具體每個核的使用百分比,這裏不再做過多的描述,請讀者自行翻閱 CGroup 官方材料。

清單 5.cpu 核限制

root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# cat cpuset.cpus 
0-3

接下來,通過 TOP 命令獲得清單 4 啓動的 Java 程序的所有相關線程 ID,將這些 ID 寫入到 Tasks 文件。

清單 6. 設置線程 ID

root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# cat tasks 
root@emindsoft-Inspiron-5759:/sys/fs/cgroup/cpuset/xiezhongtian# 

全部設置完畢後,我們可以通過 TOP 命令查看具體的每一顆 CPU 核上的運行情況,發現只有 0-3 這 4顆 CPU 核上有計算資源被調用,可以進一步通過 TOP 命令確認全部都是清單 4 所啓動的 Java 多線程程序的線程。

清單 7. 運行結果

這裏寫代碼片

總體上來說,CGroup 的使用方式較爲簡單,目前主要的問題是網絡上已有的中文材料缺少詳細的配置步驟,一旦讀者通過反覆實驗,掌握了配置方式,使用上應該不會有大的問題。

Cgroup 設計原理分析
CGroups 的源代碼較爲清晰,我們可以從進程的角度出發來剖析 cgroups 相關數據結構之間的關係。在 Linux 中,管理進程的數據結構是 task_struct,其中與 cgroups 有關的代碼如清單 8 所示:

清單 8.task_struct 代碼

#ifdef CONFIG_CGROUPS 
/* Control Group info protected by css_set_lock */ 
struct css_set *cgroups; 
/* cg_list protected by css_set_lock and tsk->alloc_lock */ 
struct list_head cg_list; 
#endif

其中 cgroups 指針指向了一個 css_set 結構,而 css_set 存儲了與進程有關的 cgroups 信息。cg_list 是一個嵌入的 list_head 結構,用於將連到同一個 css_set 的進程組織成一個鏈表。下面我們來看 css_set 的結構,代碼如清單 9 所示:

清單 9.css_set 代碼

struct css_set { 
atomic_t refcount;
struct hlist_node hlist; 
struct list_head tasks; 
struct list_head cg_links; 
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; 
struct rcu_head rcu_head; 
};

其中 refcount 是該 css_set 的引用數,因爲一個 css_set 可以被多個進程公用,只要這些進程的 cgroups 信息相同,比如:在所有已創建的層級裏面都在同一個 cgroup 裏的進程。hlist 是嵌入的 hlist_node,用於把所有 css_set 組織成一個 hash 表,這樣內核可以快速查找特定的 css_set。tasks 指向所有連到此 css_set 的進程連成的鏈表。cg_links 指向一個由 struct_cg_cgroup_link 連成的鏈表。
Subsys 是一個指針數組,存儲一組指向 cgroup_subsys_state 的指針。一個 cgroup_subsys_state 就是進程與一個特定子系統相關的信息。通過這個指針數組,進程就可以獲得相應的 cgroups 控制信息了。cgroup_subsys_state 結構如清單 10 所示:

清單 10.cgroup_subsys_state 代碼

struct cgroup_subsys_state { 
struct cgroup *cgroup; 
atomic_t refcnt; 
unsigned long flags; 
struct css_id *id; 
};

cgroup 指針指向了一個 cgroup 結構,也就是進程屬於的 cgroup。進程受到子系統的控制,實際上是通過加入到特定的 cgroup 實現的,因爲 cgroup 在特定的層級上,而子系統又是附和到上面的。通過以上三個結構,進程就可以和 cgroup 連接起來了:task_struct->css_set->cgroup_subsys_state->cgroup。cgroup 結構如清單 11 所示:
清單 11.cgroup 代碼

struct cgroup { 
unsigned long flags; 
atomic_t count; 
struct list_head sibling; 
struct list_head children; 
struct cgroup *parent; 
struct dentry *dentry; 
struct cgroup_subsys_state *subsys[CGROUP_SUBSYS_COUNT]; 
struct cgroupfs_root *root;
struct cgroup *top_cgroup; 
struct list_head css_sets; 
struct list_head release_list; 
struct list_head pidlists;
struct mutex pidlist_mutex; 
struct rcu_head rcu_head; 
struct list_head event_list; 
spinlock_t event_list_lock; 
};

sibling,children 和 parent 三個嵌入的 list_head 負責將統一層級的 cgroup 連接成一棵 cgroup 樹。
subsys 是一個指針數組,存儲一組指向 cgroup_subsys_state 的指針。這組指針指向了此 cgroup 跟各個子系統相關的信息,這個跟 css_set 中的道理是一樣的。
root 指向了一個 cgroupfs_root 的結構,就是 cgroup 所在的層級對應的結構體。這樣一來,之前談到的幾個 cgroups 概念就全部聯繫起來了。
top_cgroup 指向了所在層級的根 cgroup,也就是創建層級時自動創建的那個 cgroup。
css_set 指向一個由 struct_cg_cgroup_link 連成的鏈表,跟 css_set 中 cg_links 一樣。
下面分析一個 css_set 和 cgroup 之間的關係,cg_cgroup_link 的結構如清單 12 所示:

清單 12.cg_cgroup_link 代碼

struct cg_cgroup_link { 
struct list_head cgrp_link_list; 
struct cgroup *cgrp; 
struct list_head cg_link_list; 
struct css_set *cg; };

cgrp_link_list 連入到 cgrouo->css_set 指向的鏈表,cgrp 則指向此 cg_cgroup_link 相關的 cgroup。
cg_link_list 則連入到 css_set->cg_lonks 指向的鏈表,cg 則指向此 cg_cgroup_link 相關的 css_set。
cgroup 和 css_set 是一個多對多的關係,必須添加一箇中間結構來將兩者聯繫起來,這就是 cg_cgroup_link 的作用。cg_cgroup_link 中的 cgrp 和 cg 就是此結構提的聯合主鍵,而 cgrp_link_list 和 cg_link_list 分別連入到 cgroup 和 css_set 相應的鏈表,使得能從 cgroup 或 css_set 都可以進行遍歷查詢。

那爲什麼 cgroup 和 css_set 是多對多的關係呢?

一個進程對應一個 css_set,一個 css_set 存儲了一組進程 (有可能被多個進程共享,所以是一組) 跟各個子系統相關的信息,但是這些信息由可能不是從一個 cgroup 那裏獲得的,因爲一個進程可以同時屬於幾個 cgroup,只要這些 cgroup 不在同一個層級。舉個例子:我們創建一個層級 A,A 上面附加了 cpu 和 memory 兩個子系統,進程 B 屬於 A 的根 cgroup;然後我們再創建一個層級 C,C 上面附加了 ns 和 blkio 兩個子系統,進程 B 同樣屬於 C 的根 cgroup;那麼進程 B 對應的 cpu 和 memory 的信息是從 A 的根 cgroup 獲得的,ns 和 blkio 信息則是從 C 的根 cgroup 獲得的。因此,一個 css_set 存儲的 cgroup_subsys_state 可以對應多個 cgroup。另一方面,cgroup 也存儲了一組 cgroup_subsys_state,這一組 cgroup_subsys_state 則是 cgroup 從所在的層級附加的子系統獲得的。一個 cgroup 中可以有多個進程,而這些進程的 css_set 不一定都相同,因爲有些進程可能還加入了其他 cgroup。但是同一個 cgroup 中的進程與該 cgroup 關聯的 cgroup_subsys_state 都受到該 cgroup 的管理 (cgroups 中進程控制是以 cgroup 爲單位的) 的,所以一個 cgroup 也可以對應多個 css_set。
從前面的分析,我們可以看出從 task 到 cgroup 是很容易定位的,但是從 cgroup 獲取此 cgroup 的所有的 task 就必須通過這個結構了。每個進程都回指向一個 css_set,而與這個 css_set 關聯的所有進程都會鏈入到 css_set->tasks 鏈表,而 cgroup 又通過一箇中間結構 cg_cgroup_link 來尋找所有與之關聯的所有 css_set,從而可以得到與 cgroup 關聯的所有進程。最後,我們看一下層級和子系統對應的結構體。層級對應的結構體是 cgroupfs_root 如清單 13 所示:

清單 13.cgroupfs_root 代碼

struct cgroupfs_root { 
struct super_block *sb; 
unsigned long subsys_bits; 
int hierarchy_id;
unsigned long actual_subsys_bits; 
struct list_head subsys_list; 
struct cgroup top_cgroup; 
int number_of_cgroups; 
struct list_head root_list; 
unsigned long flags; 
char release_agent_path[PATH_MAX]; 
char name[MAX_CGROUP_ROOT_NAMELEN]; 
};

sb 指向該層級關聯的文件系統數據塊。subsys_bits 和 actual_subsys_bits 分別指向將要附加到層級的子系統和現在實際附加到層級的子系統,在子系統附加到層級時使用。hierarchy_id 是該層級唯一的 id。top_cgroup 指向該層級的根 cgroup。number_of_cgroups 記錄該層級 cgroup 的個數。root_list 是一個嵌入的 list_head,用於將系統所有的層級連成鏈表。子系統對應的結構體是 cgroup_subsys,代碼如清單 14 所示。

清單 14. cgroup_subsys 代碼

struct cgroup_subsys { 
struct cgroup_subsys_state *(*create)(struct cgroup_subsys *ss, 
struct cgroup *cgrp); 
int (*pre_destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp); 
void (*destroy)(struct cgroup_subsys *ss, struct cgroup *cgrp); 
int (*can_attach)(struct cgroup_subsys *ss,
 struct cgroup *cgrp, struct task_struct *tsk, bool threadgroup); 
void (*cancel_attach)(struct cgroup_subsys *ss, 
struct cgroup *cgrp, struct task_struct *tsk, bool threadgroup); 
void (*attach)(struct cgroup_subsys *ss, struct cgroup *cgrp, 
struct cgroup *old_cgrp, struct task_struct *tsk, bool threadgroup); 
void (*fork)(struct cgroup_subsys *ss, struct task_struct *task); 
void (*exit)(struct cgroup_subsys *ss, struct task_struct *task); 
int (*populate)(struct cgroup_subsys *ss, struct cgroup *cgrp); 
void (*post_clone)(struct cgroup_subsys *ss, struct cgroup *cgrp); 
void (*bind)(struct cgroup_subsys *ss, struct cgroup *root);
int subsys_id; 
int active; 
int disabled; 
int early_init; 
bool use_id; 
#define MAX_CGROUP_TYPE_NAMELEN 32 
const char *name; 
struct mutex hierarchy_mutex; 
struct lock_class_key subsys_key; 
struct cgroupfs_root *root; 
struct list_head sibling; 
struct idr idr; 
spinlock_t id_lock; 
struct module *module; 
};

cgroup_subsys 定義了一組操作,讓各個子系統根據各自的需要去實現。這個相當於 C++中抽象基類,然後各個特定的子系統對應 cgroup_subsys 則是實現了相應操作的子類。類似的思想還被用在了 cgroup_subsys_state 中,cgroup_subsys_state 並未定義控制信息,而只是定義了各個子系統都需要的共同信息,比如該 cgroup_subsys_state 從屬的 cgroup。然後各個子系統再根據各自的需要去定義自己的進程控制信息結構體,最後在各自的結構體中將 cgroup_subsys_state 包含進去,這樣通過 Linux 內核的 container_of 等宏就可以通過 cgroup_subsys_state 來獲取相應的結構體。
從基本層次順序定義上來看,由 task_struct、css_set、cgroup_subsys_state、cgroup、cg_cgroup_link、cgroupfs_root、cgroup_subsys 等結構體組成的 CGroup 可以基本從進程級別反應之間的響應關係。後續文章會針對文件系統、各子系統做進一步的分析。

結束語

就象大多數開源技術一樣,CGroup 不是全新創造的,它將進程管理從 cpuset 中剝離出來。通過物理限制的方式爲進程間資源控制提供了簡單的實現方式,爲 Linux Container 技術、虛擬化技術的發展奠定了技術基礎,本文的目標是讓初學者可以通過自己動手的方式簡單地理解技術,將起步門檻放低。

參考文檔:

Docker背後的內核知識——cgroups資源限制

INTRODUCTION TO CONTROL GROUPS

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章