一般來說,容器技術主要包括Cgroup和Namespace這兩個內核特性。
對於Linux容器的最小組成,除了上面兩個抽象的技術概念還不夠,完整的容器可以用以下公示描述:
容器=Cgroup+Namespace+rootfs+容器引擎(用戶態工具)。
其中各項功能分別為:
| Cgroup: | 資源控制 |
|---|---|
| Namespace: | 訪問隔離 |
| rootfs: | 文件系統隔離 |
| 容器引擎: | 生命周期控制 |
Cgroup
Cgroup是control group,又稱為控制組,它主要是做資源控制。原理是將一組進程放在放在一個控制組里,通過給這個控制組分配指定的可用資源,達到控制這一組進程可用資源的目的。

為什么需要Cgroup
在 Linux 里,一直以來就有對進程進行分組的概念和需求,比如 session group, progress group 等,后來隨着人們對這方面的需求越來越多,比如需要追蹤一組進程的內存和 IO 使用情況等,於是出現了 cgroup,用來統一將進程進行分組,並在分組的基礎上對進程進行監控和資源控制管理等。
Cgroups最初的目標是為資源管理提供的一個統一的框架,既整合現有的cpuset等子系統,也為未來開發新的子系統提供接口。現在的cgroups適用於多種應用場景,從單個進程的資源控制,到實現操作系統層次的虛擬化(OS Level Virtualization)。Cgroups提供了以下功能:
- 1.限制進程組可以使用的資源數量(Resource limiting )。比如:memory子系統可以為進程組設定一個memory使用上限,一旦進程組使用的內存達到限額再申請內存,就會觸發OOM(out of memory)。
- 2.進程組的優先級控制(Prioritization )。比如:可以使用cpu子系統為某個進程組分配特定cpu share。
- 3.記錄進程組使用的資源數量(Accounting )。比如:可以使用cpuacct子系統記錄某個進程組使用的cpu時間
- 4.進程組隔離(Isolation)。比如:使用ns子系統可以使不同的進程組使用不同的namespace,以達到隔離的目的,不同的進程組有各自的進程、網絡、文件系統掛載空間。
- 5.進程組控制(Control)。比如:使用freezer子系統可以將進程組掛起和恢復。
Cgroup 是透過階層式的方式來管理的,和程序、子群組相同,都會由它們的 parent 繼承部份屬性。然而,這兩個模型之間有所不同。
Cgroup 是將任意進程進行分組化管理的 Linux 內核功能。Cgroup 本身是提供將進程進行分組化管理的功能和接口的基礎結構,I/O 或內存的分配控制等具體的資源管理功能是通過這個功能來實現的。這些具體的資源管理功能稱為 Cgroup 子系統或控制器。Cgroup 子系統有控制內存的 Memory 控制器、控制進程調度的 CPU 控制器等。運行中的內核可以使用的 Cgroup 子系統由/proc/cgroup 來確認。
Cgroup 提供了一個 CGroup 虛擬文件系統,作為進行分組管理和各子系統設置的用戶接口。要使用 Cgroup,必須掛載 CGroup 文件系統。這時通過掛載選項指定使用哪個子系統。
Cgroup的子系統
cgroup子系統目前有下列幾種:
- devices 進程范圍設備權限
- cpuset 分配進程可使用的 CPU數和內存節點
- cpu 控制CPU占有率
- cpuacct 統計CPU使用情況,例如運行時間,throttled時間
- memory 限制內存的使用上限
- freezer 暫停 Cgroup 中的進程
- net_cls 配合 tc(traffic controller)限制網絡帶寬
- net_prio 設置進程的網絡流量優先級
- huge_tlb 限制 HugeTLB 的使用
- perf_event 允許 Perf 工具基於 Cgroup 分組做性能檢測
Cgroup支持的文件種類
-
Release_agent 刪除分組時執行的命令,這個文件只存在於根分組
-
Notify_on_release 設置是否執行 release_agent。為 1 時執行
-
Tasks 屬於分組的線程 TID 列表
-
Cgroup.procs 屬於分組的進程 PID 列表。僅包括多線程進程的線程 leader 的 TID,這點與 tasks 不同
-
Cgroup.event_control 監視狀態變化和分組刪除事件的配置文件
相互關系
每次在系統中創建新層級時,該系統中的所有任務都是那個層級的默認 cgroup(我們稱之為 root cgroup,此 cgroup 在創建層級時自動創建,后面在該層級中創建的 cgroup 都是此 cgroup 的后代)的初始成員;
一個子系統最多只能附加到一個層級;
一個層級可以附加多個子系統;
一個任務可以是多個 cgroup 的成員,但是這些 cgroup 必須在不同的層級;
系統中的進程(任務)創建子進程(任務)時,該子任務自動成為其父進程所在 cgroup 的成員。然后可根據需要將該子任務移動到不同的 cgroup 中,但開始時它總是繼承其父任務的 cgroup。

Cgroup 特點
在 cgroups 中,任務就是系統的一個進程。
控制族群(control group)。控制族群就是一組按照某種標准划分的進程。Cgroups 中的資源控制都是以控制族群為單位實現。一個進程可以加入到某個控制族群,也從一個進程組遷移到另一個控制族群。一個進程組的進程可以使用 cgroups 以控制族群為單位分配的資源,同時受到 cgroups 以控制族群為單位設定的限制。
層級(hierarchy)。控制族群可以組織成 hierarchical 的形式,既一顆控制族群樹。控制族群樹上的子節點控制族群是父節點控制族群的孩子,繼承父控制族群的特定的屬性。
子系統(subsytem)。一個子系統就是一個資源控制器,比如 cpu 子系統就是控制 cpu 時間分配的一個控制器。子系統必須附加(attach)到一個層級上才能起作用,一個子系統附加到某個層級以后,這個層級上的所有控制族群都受到這個子系統的控制。
Cgroup 設計原理分析
CGroups 的源代碼較為清晰,我們可以從進程的角度出發來剖析 cgroups 相關數據結構之間的關系。在 Linux 中,管理進程的數據結構是 task_struct,其中與 cgroups 有關的代碼如下所示:
#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 的結構,代碼如以下所示:
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;
};
其中 cgroups 指針指向了一個 css_set 結構,而 css_set 存儲了與進程有關的 cgroups 信息。cg_list 是一個嵌入的 list_head 結構,用於將連到同一個 css_set 的進程組織成一個鏈表。下面我們來看 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 結構如下所示:
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 結構如下所示:
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 的結構如下所示:
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版本
Cgroups最初由Paul Menage和Rohit Seth編寫,並於2007年進入Linux內核主線。此后稱為cgroups版本1。
然后由Tejun Heo接管了cgroup的開發和維護。Tejun Heo重新設計並重寫了cgroup。現在,此重寫稱為版本2,cgroups-v2的文檔首次出現在2016年3月14日發布的Linux內核4.5中。
與v1不同,cgroup v2僅具有單個進程層次結構,並且在進程之間進行區分,而不對線程進行區分。
在cgroup v2中,所有已安裝的控制器都位於一個統一的層次結構中。盡管(不同的)控制器可以同時安裝在v1和v2層次結構下,但是不可能同時在v1和v2層次結構下同時安裝相同的控制器。
- Cgroups v2提供了安裝所有控制器所依據的統一層次結構。
- 不允許“內部”過程。除根cgroup以外,進程只能駐留在葉節點(本身不包含子cgroup的cgroup)中。
- 必須通過文件cgroup.controllers 和cgroup.subtree_control指定活動的cgroup 。
- 該任務的文件已被刪除。此外,cpuset 控制器使用的 cgroup.clone_children文件已被刪除。
- cgroup.events文件提供了一種用於通知空cgroup的改進機制 。
在cgroups v1中,能夠針對不同的層次結構安裝不同的控制器的功能旨在為應用程序設計提供極大的靈活性。但是,實際上,靈活性的用途比預期的要少,並且在許多情況下增加了復雜性。因此,在cgroups v2中,所有可用的控制器都是按單個層次結構安裝的。可用的控制器會自動掛載,這意味着使用以下命令掛載cgroup v2文件系統時不必(或不可能)指定控制器:
mount -t cgroup2 none /mnt/cgroup2
僅當當前未通過針對cgroup v1層次結構的安裝使用cgroup v2控制器時,該控制器才可用。或者,換句話說,不可能針對v1層次結構和統一的v2層次結構使用同一控制器。
Namespace
Namespace又稱為命名空間,它主要做訪問隔離。其原理是針對一類資源進行抽象,並將其封裝在一起提供給一個容器使用,對於這類資源,因為每個容器都有自己的抽象,而他們彼此之間是不可見的,所以就可以做到訪問隔離。
什么是Namespace
namespace即“命名空間”,也稱“名稱空間” 。VS.NET中的各種語言使用的一種代碼組織的形式 通過名稱空間來分類,區別不同的代碼功能 同時也是VS.NET中所有類的完全名稱的一部分。
命名空間是用來組織和重用代碼的。如同名字一樣的意思,NameSpace(名字空間),之所以出來這樣一個東西,是因為人類可用的單詞數太少,並且不同的人寫的程序不可能所有的變量都沒有重名現象,對於庫來說,這個問題尤其嚴重,如果兩個人寫的庫文件中出現同名的變量或函數(不可避免),使用起來就有問題了。為了解決這個問題,引入了名字空間這個概念,通過使用 namespace xxx;你所使用的庫函數或變量就是在該名字空間中定義的,這樣一來就不會引起不必要的沖突了。
通常來說,命名空間是唯一識別的一套名字,這樣當對象來自不同的地方但是名字相同的時候就不會含糊不清了。使用擴展標記語言的時候,XML的命名空間是所有元素類別和屬性的集合。元素類別和屬性的名字是可以通過唯一XML命名空間來唯一。
在XML里,任何元素類別或者屬性因此分為兩部分名字,一個是命名空間里的名字另一個是它的本地名。在XML里,命名空間通常是一個統一資源識別符(URI)的名字。而URI只當名字用。主要目的是為了避免名字的沖突。
為什么要使用Namespace
例如,在同一個班中有兩個相同名字的人,他們都叫Tony,那么他們一定也有其他的一些信息方便人們去分辨它們。Namespace能實現輕量級虛擬化(容器)服務。在同一個 namespace 下的進程可以感知彼此的變化,而對外界的進程一無所知。這樣就可以讓容器中的進程產生錯覺,認為自己置身於一個獨立的系統中,從而達到隔離的目的。也就是說 linux 內核提供的 namespace 技術為 docker 等容器技術的出現和發展提供了基礎條件。
Namespace的定義
namespace 是 Linux 內核用來隔離內核資源的方式。通過 namespace 可以讓一些進程只能看到與自己相關的一部分資源,而另外一些進程也只能看到與它們自己相關的資源,這兩撥進程根本就感覺不到對方的存在。具體的實現方式是把一個或多個進程的相關資源指定在同一個 namespace 中。
Linux namespaces 是對全局系統資源的一種封裝隔離,使得處於不同 namespace 的進程擁有獨立的全局系統資源,改變一個 namespace 中的系統資源只會影響當前 namespace 里的進程,對其他 namespace 中的進程沒有影響。
6種在Linux 內核中實現的Namespace
| IPC | 隔離 System V IPC 和 POSIX 消息隊列 |
|---|---|
| Network | 隔離網絡資源 |
| Mount | 隔離文件系統掛載點 |
| PID | 隔離進程ID |
| UTS | 隔離主機名和域名 |
| User | 隔離用戶和用戶組 |
與命名空間相關的三個系統調用:
clone創建全新的Namespace,由clone創建的新進程就位於這個新的namespace里。創建時傳入 flags參數,可選值有 CLONE_NEWIPC, CLONE_NEWNET, CLONE_NEWNS, CLONE_NEWPID, CLONE_NEWUTS, CLONE_NEWUSER, 分別對應上面六種namespace。
unshare為已有進程創建新的namespace。
setns把某個進程放在已有的某個namespace里。
6種命名空間
UTS namespace
UTS namespace 對主機名和域名進行隔離。為什么要隔離主機名?因為主機名可以代替IP來訪問。如果不隔離,同名訪問會出沖突。
IPC namespace
Linux 提供很多種進程通信機制,IPC namespace 針對 System V 和 POSIX 消息隊列,這些 IPC 機制會使用標識符來區別不同的消息隊列,然后兩個進程通過標識符找到對應的消息隊列。
IPC namespace 使得 相同的標識符在兩個 namespace 代表不同的消息隊列,因此兩個namespace 中的進程不能通過 IPC 來通信。
PID namespace
隔離進程號,不同namespace 的進程可以使用相同的進程號。
當創建一個 PID namespace 時,第一個進程的PID 是1,即 init 進程。它負責回收所有孤兒進程的資源,所有發給 init 進程的信號都會被屏蔽。
Mount namespace
隔離文件掛載點,每個進程能看到的文件系統都記錄在/proc/$$/mounts里。在一個 namespace 里掛載、卸載的動作不會影響到其他 namespace。
Network namespace
隔離網絡資源。每個 namespace 都有自己的網絡設備、IP、路由表、/proc/net 目錄、端口號等。網絡隔離可以保證獨立使用網絡資源,比如開發兩個web 應用可以使用80端口。
新創建的 Network namespace 只有 loopback 一個網絡設備,需要手動添加網絡設備。
User namespace
隔離用戶和用戶組。它的厲害之處在於,可以讓宿主機上的一個普通用戶在 namespace 里成為 0 號用戶,也就是 root 用戶。這樣普通用戶可以在容器內“隨心所欲”,但是影響也僅限在容器內。
Namespace發展歷史
Linux 在很早的版本中就實現了部分的 namespace,比如內核 2.4 就實現了 mount namespace。大多數的 namespace 支持是在內核 2.6 中完成的,比如 IPC、Network、PID、和 UTS。還有個別的 namespace 比較特殊,比如 User,從內核 2.6 就開始實現了,但在內核 3.8 中才宣布完成。同時,隨着 Linux 自身的發展以及容器技術持續發展帶來的需求,也會有新的 namespace 被支持,比如在內核 4.6 中就添加了 Cgroup namespace。
