CGroup 介紹、應用例項及原理描述

天府雲創發表於2017-06-20

cgroups(控制組)是Linux核心的一個功能,用來限制報告和分離一個程式組的資源(CPU、記憶體、磁碟輸入輸出等)。這個工作是由Google的工程師(主要是Paul Menage和Rohit Seth)在2006年以“process containers(程式容器)”的名字開始的;在2007年的晚些時候被重新命名為控制組(由於在核心中“容器”這個名詞的歧義引起的混亂)並被合併到2.6.24版的核心中去。自那以後,又新增了很多功能和控制器。

CGroup 技術被廣泛用於 Linux 作業系統環境下的物理分割,是 Linux Container 技術的底層基礎技術,是虛擬化技術的基礎。

Cgroups是control groups的縮寫,最初由Google工程師提出,後來編進linux核心。

Cgroups是實現IaaS虛擬化(kvm、lxc等),PaaS容器沙箱(Docker等)的資源管理控制部分的底層基礎。

我們熟知的著名開源Docker容器和Plesk等應用就是基於linux CGroup 技術之上的。

cgroups的一個設計目標是為不同的應用情況提供統一的介面,從控制單一程式(像nice)到系統級虛擬化(像opeNVZLinux-VServerLXC)。cgroups提供:

  • 資源限制:組可以被設定不超過設定的記憶體限制;這也包括虛擬記憶體。原來的分頁機制是在Linux研討會Containers: Challenges with the memory resource controller and its performance報告中提出的。
  • 優先化:一些組可能會得到大量的CPU或磁碟輸入輸出通量。
  • 報告:用來衡量系統確實把多少資源用到適合的目的上。
  • 分離:為組分離名稱空間,這樣一個組不會看到另一個組的程式、網路連線和檔案。
  • 控制:凍結組或檢查點和重啟動。


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 支援的檔案種類

表 1. 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 相關概念解釋

  1. 任務(task)。在 cgroups 中,任務就是系統的一個程式;

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

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

  4. 子系統(subsystem)。一個子系統就是一個資源控制器,比如 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 取得關聯關係。

CGroup 特點

  1. 在 cgroups 中,任務就是系統的一個程式。

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

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

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

CGroup 應用架構

圖 2. CGroup 典型應用架構圖
圖 2. CGroup 典型應用架構圖

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

CGroup 部署及應用例項

講解 CGroup 設計原理前,我們先來做一個簡單的實驗。實驗基於 Linux Centosv6.564 位版本,JDK1.7。實驗目的是執行一個佔用 CPU 的 Java 程式,如果不用 CGroup 物理隔離 CPU 核,那程式會由作業系統層級自動挑選 CPU 核來執行程式。由於作業系統層面採用的是時間片輪詢方式隨機挑選 CPU 核作為執行容器,所以會在本機器上 24 個 CPU 核上隨機執行。如果採用 CGroup 進行物理隔離,我們可以選擇某些 CPU 核作為指定執行載體。

清單 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 子系統上建立自己的層級“zhoumingyao”。

清單 2. 建立層級
[root@facenode4 cpu_and_set]# ls -rlt
總用量 0
-rw-r--r-- 1 root root 0 3 月 21 17:21 release_agent
-rw-r--r-- 1 root root 0 3 月 21 17:21 notify_on_release
-r--r--r-- 1 root root 0 3 月 21 17:21 cpu.stat
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpu.shares
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.sched_relax_domain_level
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.mems
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.memory_spread_page
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.memory_pressure_enabled
-r--r--r-- 1 root root 0 3 月 21 17:21 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.memory_migrate
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.cpus
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpu.rt_period_us
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 3 月 21 17:21 cpu.cfs_period_us
-r--r--r-- 1 root root 0 3 月 21 17:21 cgroup.procs
drwxr-xr-x 2 root root 0 3 月 21 17:22 test
drwxr-xr-x 2 root root 0 3 月 23 16:36 test1
-rw-r--r-- 1 root root 0 3 月 25 19:23 tasks
drwxr-xr-x 2 root root 0 3 月 31 19:32 single
drwxr-xr-x 2 root root 0 3 月 31 19:59 single1
drwxr-xr-x 2 root root 0 3 月 31 19:59 single2
drwxr-xr-x 2 root root 0 3 月 31 19:59 single3
drwxr-xr-x 3 root root 0 4 月 3 17:34 aaaa
[root@facenode4 cpu_and_set]# mkdir zhoumingyao
[root@facenode4 cpu_and_set]# cd zhoumingyao
[root@facenode4 zhoumingyao]# ls -rlt
總用量 0
-rw-r--r-- 1 root root 0 4 月 30 14:03 tasks
-rw-r--r-- 1 root root 0 4 月 30 14:03 notify_on_release
-r--r--r-- 1 root root 0 4 月 30 14:03 cpu.stat
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpu.shares
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.sched_relax_domain_level
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.sched_load_balance
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.mems
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.memory_spread_slab
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.memory_spread_page
-r--r--r-- 1 root root 0 4 月 30 14:03 cpuset.memory_pressure
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.memory_migrate
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.mem_hardwall
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.mem_exclusive
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.cpus
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpuset.cpu_exclusive
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpu.rt_runtime_us
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpu.rt_period_us
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 4 月 30 14:03 cpu.cfs_period_us
-r--r--r-- 1 root root 0 4 月 30 14:03 cgroup.procs

通過 mkdir 命令新建資料夾 zhoumingyao,由於已經預先載入 cpu_and_set 子系統成功,所以當資料夾建立完畢的同時,cpu_and_set 子系統對應的資料夾也會自動建立。

執行 Java 程式前,我們需要確認 cpu_and_set 子系統安裝的目錄,如清單 3 所示。

清單 3. 確認目錄
[root@facenode4 zhoumingyao]# lscgroup 
cpuacct:/
devices:/
freezer:/
net_cls:/
blkio:/
memory:/
memory:/test2
cpuset,cpu:/
cpuset,cpu:/zhoumingyao
cpuset,cpu:/aaaa
cpuset,cpu:/aaaa/bbbb
cpuset,cpu:/single3
cpuset,cpu:/single2
cpuset,cpu:/single1
cpuset,cpu:/single
cpuset,cpu:/test1
cpuset,cpu:/test

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

清單 4. 執行 Java 程式

[root@facenode4 zhoumingyao]# cgexec -g cpuset,cpu:/zhoumingyao java -jar test.jars

我們在 cpuset.cpus 檔案中設定需要限制只有 0-10 這 11 個 CPU 核可以被用來執行上述清單 4 啟動的 Java 多執行緒程式。當然 CGroup 還可以限制具體每個核的使用百分比,這裡不再做過多的描述,請讀者自行翻閱 CGroup 官方材料。

清單 5.cpu 核限制
[root@facenode4 zhoumingyao]# cat cpuset.cpus
0-10

接下來,通過 TOP 命令獲得清單 4 啟動的 Java 程式的所有相關執行緒 ID,將這些 ID 寫入到 Tasks 檔案。

清單 6. 設定執行緒 ID
[root@facenode4 zhoumingyao]# cat tasks
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2714
2715
2716
2718

全部設定完畢後,我們可以通過 TOP 命令檢視具體的每一顆 CPU 核上的執行情況,發現只有 0-10 這 11 顆 CPU 核上有計算資源被呼叫,可以進一步通過 TOP 命令確認全部都是清單 4 所啟動的 Java 多執行緒程式的執行緒。

清單 7. 執行結果
top - 14:43:24 up 44 days, 59 min, 6 users, load average: 0.47, 0.40, 0.33
Tasks: 715 total, 1 running, 714 sleeping, 0 stopped, 0 zombie
Cpu0 : 0.7%us, 0.3%sy, 0.0%ni, 99.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu1 : 1.0%us, 0.7%sy, 0.0%ni, 98.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu2 : 0.3%us, 0.3%sy, 0.0%ni, 99.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu3 : 1.0%us, 1.6%sy, 0.0%ni, 97.5%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu4 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu5 : 1.3%us, 1.9%sy, 0.0%ni, 96.8%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu6 : 3.8%us, 5.4%sy, 0.0%ni, 90.8%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu7 : 7.7%us, 9.9%sy, 0.0%ni, 82.4%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu8 : 4.8%us, 6.1%sy, 0.0%ni, 89.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu9 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu10 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu11 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu12 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu13 : 0.0%us, 0.0%sy, 0.0%ni, 72.8%id, 0.0%wa, 0.0%hi, 4.3%si, 0.0%st
Cpu14 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu15 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu16 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu17 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu18 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu19 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu20 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu21 : 0.3%us, 0.3%sy, 0.0%ni, 99.3%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu22 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Cpu23 : 0.0%us, 0.0%sy, 0.0%ni,100.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st
Mem: 32829064k total, 5695012k used, 27134052k free, 533516k buffers
Swap: 24777720k total, 0k used, 24777720k free, 3326940k cached

總體上來說,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 可以基本從程式級別反應之間的響應關係。後續文章會針對檔案系統、各子系統做進一步的分析。

百度私有PaaS雲就是使用輕量的cgoups做的應用之間的隔離,以下是關於百度架構師許立強,對於虛擬機器VM,應用沙盒,cgroups技術選型的理解

 

 

本文用指令碼執行示例程式,來驗證Cgroups關於cpu、記憶體、io這三部分的隔離效果。

測試機器:CentOS release 6.4 (Final)

啟動Cgroups

service cgconfig start   #開啟cgroups服務
chkconfig cgconfig on   #開啟啟動

在/cgroup,有如下資料夾,預設是多掛載點的形式,即各個子系統的配置在不同的子資料夾下

[root@localhost /]# ls /cgroup/
blkio  cpu  cpuacct  cpuset  devices  freezer  memory  net_cls

 

cgroups管理程式cpu資源

跑一個耗cpu的指令碼

x=0
while [ True ];do
    x=$x+1
done;

top可以看到這個指令碼基本佔了100%的cpu資源

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND          
30142
root 20 0 104m 2520 1024 R 99.7 0.1 14:38.97 sh

下面用cgroups控制這個程式的cpu資源

mkdir -p /cgroup/cpu/foo/   #新建一個控制組foo
echo 50000 > /cgroup/cpu/foo/cpu.cfs_quota_us  #將cpu.cfs_quota_us設為50000,相對於cpu.cfs_period_us的100000是50%
echo 30142 > /cgroup/cpu/foo/tasks

然後top的實時統計資料如下,cpu佔用率將近50%,看來cgroups關於cpu的控制起了效果

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                         30142 root      20   0  105m 2884 1024 R 49.4  0.2  23:32.53 sh 

cpu控制組foo下面還有其他的控制,還可以做更多其他的關於cpu的控制

[root@localhost ~]# ls /cgroup/cpu/foo/
cgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  notify_on_release  tasks

 

 

cgroups管理程式記憶體資源

跑一個耗記憶體的指令碼,記憶體不斷增長

x="a"
while [ True ];do
    x=$x$x
done;

top看記憶體佔用穩步上升

  PID USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND                                                                                         30215 root      20   0  871m 501m 1036 R 99.8 26.7   0:38.69 sh  
30215 root      20   0 1639m 721m 1036 R 98.7 38.4   1:03.99 sh 
30215 root      20   0 1639m 929m 1036 R 98.6 49.5   1:13.73 sh

下面用cgroups控制這個程式的記憶體資源

mkdir -p /cgroup/memory/foo
echo 1048576 >  /cgroup/memory/foo/memory.limit_in_bytes   #分配1MB的記憶體給這個控制組
echo 30215 > /cgroup/memory/foo/tasks  

發現之前的指令碼被kill掉

[root@localhost ~]# sh /home/test.sh 
已殺死

因為這是強硬的限制記憶體,當程式試圖佔用的記憶體超過了cgroups的限制,會觸發out of memory,導致程式被kill掉。

實際情況中對程式的記憶體使用會有一個預估,然後會給這個程式的限制超配50%比如,除非發生記憶體洩露等異常情況,才會因為cgroups的限制被kill掉。

也可以通過配置關掉cgroups oom kill程式,通過memory.oom_control來實現(oom_kill_disable 1),但是儘管程式不會被直接殺死,但程式也進入了休眠狀態,無法繼續執行,仍讓無法服務。

關於記憶體的控制,還有以下配置檔案,關於虛擬記憶體的控制,以及權值比重式的記憶體控制等

複製程式碼
[root@localhost /]# ls /cgroup/memory/foo/
cgroup.event_control  memory.force_empty         memory.memsw.failcnt             
memory.memsw.usage_in_bytes memory.soft_limit_in_bytes memory.usage_in_bytes tasks cgroup.procs memory.limit_in_bytes memory.memsw.limit_in_bytes
memory.move_charge_at_immigrate memory.
stat memory.use_hierarchy memory.failcnt memory.max_usage_in_bytes memory.memsw.max_usage_in_bytes
memory.oom_control memory.swappiness notify_on_release
複製程式碼

cgroups管理程式io資源

跑一個耗io的指令碼

 dd if=/dev/sda of=/dev/null &

通過iotop看io佔用情況,磁碟速度到了284M/s

30252 be/4 root      284.71 M/s    0.00 B/s  0.00 %  0.00 % dd if=/dev/sda of=/dev/null  

下面用cgroups控制這個程式的io資源

mkdir -p /cgroup/blkio/foo

echo '8:0   1048576' >  /cgroup/blkio/foo/blkio.throttle.read_bps_device
#8:0對應主裝置號和副裝置號,可以通過ls -l /dev/sda檢視
echo 30252 > /cgroup/blkio/foo/tasks

再通過iotop看,確實將讀速度降到了1M/s

30252 be/4 root      993.36 K/s    0.00 B/s  0.00 %  0.00 % dd if=/dev/sda of=/dev/null  

對於io還有很多其他可以控制層面和方式,如下

複製程式碼
[root@localhost ~]# ls /cgroup/blkio/foo/
blkio.io_merged         blkio.io_serviced      blkio.reset_stats                
blkio.throttle.io_serviced blkio.throttle.write_bps_device blkio.weight cgroup.procs blkio.io_queued blkio.io_service_time blkio.sectors
blkio.throttle.read_bps_device blkio.throttle.write_iops_device blkio.weight_device notify_on_release blkio.io_service_bytes blkio.io_wait_time blkio.throttle.io_service_bytes
blkio.throttle.read_iops_device blkio.
time cgroup.event_control tasks
複製程式碼

 

參考博文:

Cgroup與LXC簡介

用cgroups管理cpu資源

百度社群私有云經驗分享


結束語

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




相關文章