Docker容器本質上是宿主機上的進程。Docker通過namespace實現了資源隔離,通過cgroups實現了資源限制,通過寫時複製機制(copy-on-write)實現了高效的文件操作。
namespace資源隔離
完成一個基本容器需要六項隔離,Linux內核中提供了這六種隔離的系統調用:
namespace | 系統調用參數 | 隔離內容 |
---|---|---|
UTS | CLONE_NEWUTS | 主機名與域名 |
IPC | CLONE_NEWIPC | 信號量、消息隊列和共享內存 |
PID | CLONE_NEWPID | 進程編號 |
Network | CLONE_NEWNET | 網絡設備、網絡棧、端口等 |
Mount | CLONE_NEWNS | 掛載點(文件系統) |
User | CLONE_NEWUSER | 用戶組和用戶組 |
linux內核實現namespace的一個主要目的,就是爲了實現輕量級虛擬化(容器)技術服務。在同一個namespace下的進程可以感知彼此的變化,而對外界的進程一無所知。這樣就可以讓容器中的進程產生錯覺,彷彿自己置身一個獨立的系統環境中,以達到隔離的目的。(這裏討論的namespace實現針對的是linux內核3.8及以後版本)
namespace API的4種操作
clone()
通過 clone() 在創建新進程的同時創建 namespace
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
Clone() 其實是 linux 系統調用 fork() 的一種更通用的實現方式,它可以通過 flags 來控制使用多少功能。一共有 20 多種 CLONE_ 開頭的 falg(標誌位) 參數用來控制 clone 進程的方方面面(比如是否與父進程共享虛擬內存等),下面我們只介紹與 namespace 相關的 4 個參數:
- fn:指定一個由新進程執行的函數。當這個函數返回時,子進程終止。該函數返回一個整數,表示子進程的退出代碼。
- child_stack:傳入子進程使用的棧空間,也就是把用戶態堆棧指針賦給子進程的 esp 寄存器。調用進程(指調用 clone() 的進程)應該總是爲子進程分配新的堆棧。
- flags:表示使用哪些 CLONE_ 開頭的標誌位,與 namespace 相關的有CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。
- arg:指向傳遞給 fn() 函數的參數。
setns()
通過 setns() 函數可以將當前進程加入到已有的 namespace 中。(在docker中,使用docker exec命令在已經運行的容器中執行新的命令,就需要用到該方法。)
#include <sched.h>
int setns(int fd, int nstype);
和 clone() 函數一樣,C 語言庫中的 setns() 函數也是對 setns() 系統調用的封裝:
- fd:表示要加入 namespace 的文件描述符。它是一個指向 /proc/[pid]/ns 目錄中文件的文件描述符,可以通過直接打開該目錄下的鏈接文件或者打開一個掛載了該目錄下鏈接文件的文件得到。
- nstype:參數 nstype 讓調用者可以檢查 fd 指向的 namespace 類型是否符合實際要求。若把該參數設置爲 0 表示不檢查。
unshare()
通過 unshare 函數可以在原進程上進行 namespace 隔離。也就是創建並加入新的 namespace 。
#include <sched.h>
int unshare(int flags);
和前面兩個函數一樣,C 語言庫中的 unshare() 函數也是對 unshare() 系統調用的封裝。調用 unshare() 的主要作用就是:不啓動新的進程就可以起到資源隔離的效果,相當於跳出原先的 namespace 進行操作。
fork()
系統調用函數fork()並不屬於namespace的API,當程序調用fork()函數時,系統會創建新的進程,爲其分配資源,例如存儲數據和代碼的空間,然後把原來進程的所有值複製到新的進程中,只有少量數值與原來的進程不同,相當於複製了本身。
fork()的神奇之處在於它被調用一次,卻能返回兩次(父進程與子進程各返回一次),通過返回值的不同就可以區分父進程與子進程。他可能有以下3種不同的返回值:
- 在父進程中,fork()返回新創建子進程的進程id;
- 在子進程中,fork()返回0;
- 如果出現問題,fork()返回一個負值。
使用fork()後,父進程有義務監控子進程的運行狀態,並在子進程推出後才能正常退出,否則子進程就會成爲“孤兒”進程。
下面將根據docker內部對namespace資源隔離使用方式分別對6種namespace進行解析。
UTS namespace
UTS (UNIX TIme-sharing System) namespace 提供了主機和域名的隔離,這樣每個Docker容器就可以擁有獨立的主機名和域名,在網絡上可以被視作一個獨立的節點,而不是宿主機上的一個進程。Docker中,每個鏡像基本都以自身提供的服務名稱來命名hostname,且不會對宿主機產生任何影響,其原理就是利用了UTS namespace。
IPC namespace
進程間通信(Inter-Process Communication,IPC)設計的IPC資源包括常見的信號量、消息隊列和共享內存。
申請IPC資源就申請了一個全局唯一的32位ID。所以IPC namespace中實際上包含了系統IPC標識符以及實現POSIX消息隊列的文件系統。在同一個IPC namespace中的進程彼此可見,不同的namespace中的進程則互不可見。
PID namespace
PID namespace的隔離非常實用,他對進程PID重新編號,即兩個不同namespace下的進程可以擁有相同的PID,每個PID namespace都有自己的計數程序。內核爲所有的PID namespace維護了一個樹狀結構,最頂層的是系統初始時創建的,被稱爲root namespace。它創建的新PID namespace被稱爲child namespace,而原先的PID namespace就是新創建的PID namespace的child namespace,而原來的PID namespace就是新創建的PID namespace的 parent namespace。
通過這種方式,不同的PID namespace會形成一個層級體系,所屬的父節點可以看到子節點中的進程,並可以通過信號等方式對子節點中的進程產生影響。但是子節點卻看不到父節點PID namespace中的任何內容。
mount namespace
mount namespace通過隔離文件系統掛載點對隔離文件系統提供支持。隔離後,不同的mount namespace中的文件結構發生變化也互不影響。
network namespace
network namespace主要提供了關於網絡資源的隔離,包括網絡設備,IPv4,IPv6協議棧、IP路由表、防火牆、/proc/net目錄、/sys/class/net目錄、套接字等。
user namespace
user namespace隔離了安裝相關的標識符和屬性
cgroups資源限制
它不但可以限制被namespace隔離起來的資源,還可以爲資源設置權重、計算使用量、操控任務(進程或線程)啓停等。
cgroups 的作用
cgroups 爲不同用戶層面的資源管理提供了一個統一接口,從單個的資源控制到操作系統層面的虛擬化,cgroups提供了4大功能。
- 資源限制
- cgroups可以對任務使用的資源總額進行限制。如設定應用運行時使用的內存上限,一旦超過配額就發出OOM提示
- 優先級分配
- 通過分配的CPU時間片數量以及磁盤IO帶寬大小,實際上就相當於控制了任務運行的優先級
- 資源統計
- cgroups可以統計系統的資源使用量如CPU使用時長,內存用量等,這個功能非常適用於計費
- 任務控制
- cgroups 可以對任務進行掛起、恢復等操作