Libcontainer 是Docker中用於容器管理的包,它基於Go語言實現,通過管理namespaces
、cgroups
、capabilities
以及文件系統來進行容器控制。你可以使用Libcontainer創建容器,並對容器進行生命周期管理。
容器是一個可管理的執行環境,與主機系統共享內核,可與系統中的其他容器進行隔離。
在2013年Docker剛發布的時候,它是一款基於LXC的開源容器管理引擎。把LXC復雜的容器創建與使用方式簡化為Docker自己的一套命令體系。隨着Docker的不斷發展,它開始有了更為遠大的目標,那就是反向定義容器的實現標准,將底層實現都抽象化到Libcontainer的接口。這就意味着,底層容器的實現方式變成了一種可變的方案,無論是使用namespace、cgroups技術抑或是使用systemd等其他方案,只要實現了Libcontainer定義的一組接口,Docker都可以運行。這也為Docker實現全面的跨平台帶來了可能。
1. Libcontainer 特性
目前版本的Libcontainer,功能實現上涵蓋了包括namespaces使用、cgroups管理、Rootfs的配置啟動、默認的Linux capability權限集、以及進程運行的環境變量配置。內核版本最低要求為2.6
,最好是3.8
,這與內核對namespace的支持有關。
目前除user namespace不完全支持以外,其他五個namespace( ipc/mnt/net/pid/uts/user)都是默認開啟的,通過clone
系統調用進行創建。
1.1 建立文件系統
文件系統方面,容器運行需要rootfs
。所有容器中要執行的指令,都需要包含在rootfs
中。所有掛載在容器銷毀時都會被卸載,因為mount namespace會在容器銷毀時一同消失。為了容器可以正常執行命令,以下文件系統必須在容器運行時掛載到rootfs
中。
【bootfs(boot file system)主要包含 bootloader和kernel,bootloader主要是引導加載kernel,當kernel被加載到內存中后 bootfs就被umount了。 rootfs (root file system) 包含的就是典型 Linux 系統中的/dev,/proc,/bin,/etc等標准目錄和文件。】
路徑 | 類型 | 參數 | 權限及數據 |
/proc | proc | MS_NOEXEC,MS_NOSUID,MS_NODEV | |
/dev | tmpfs | MS_NOEXEC,MS_STRICTATIME | mode=755 |
/dev/shm | shm | MS_NOEXEC,MS_NOSUID,MS_NODEV | mode=1777,size=65536k |
/dev/mqueue | mqueue | MS_NOEXEC,MS_NOSUID,MS_NODEV | |
/dev/pts | devpts | MS_NOEXEC,MS_NOSUID | newinstance,ptmxmode=0666, mode=620,gid5 |
/sys | sysfs | MS_NOEXEC,MS_NOSUID,MS_NODEV, MS_RDONLY |
當容器的文件系統剛掛載完畢時,/dev
文件系統會被一系列設備節點所填充,所以rootfs
不應該管理/dev
文件系統下的設備節點,Libcontainer會負責處理並正確啟動這些設備。設備及其權限模式如下。
路徑 |
模式 |
權限 |
/dev/null |
0666 |
rwm |
/dev/zero |
0666 |
rwm |
/dev/full |
0666 |
rwm |
/dev/tty |
0666 |
rwm |
/dev/random |
0666 |
rwm |
/dev/urandom |
0666 |
rwm |
/dev/fuse |
0666 |
rwm |
容器支持偽終端TTY
,當用戶使用時,就會建立/dev/console
設備。其他終端支持設備,如/dev/ptmx
則是宿主機的/dev/ptmx
鏈接。容器中指向宿主機 /dev/null
的IO也會被重定向到容器內的 /dev/null
設備。當/proc
掛載完成后,/dev/
中與IO相關的鏈接也會建立,如下表。
源地址 |
目的地址 |
/proc/1/fd |
/dev/fd |
/proc/1/fd/0 |
/dev/stdin |
/proc/1/fd/1 |
/dev/stdout |
/proc/1/fd/2 |
/dev/stderr |
pivot_root
則用於改變進程的根目錄,這樣可以有效的將進程控制在我們建立的rootfs
中。如果rootfs
是基於ramfs
的(不支持pivot_root
),那么會在mount
時使用MS_MOVE
標志位加上chroot
來頂替。
當文件系統創建完畢后,umask
權限被重新設置回0022
。
1.2 資源管理
在《Docker背后的內核知識:cgroups資源隔離》一文中已經提到,Docker使用cgroups進行資源管理與限制,包括設備、內存、CPU、輸入輸出等。
目前除網絡外所有內核支持的子系統都被加入到Libcontainer的管理中,所以Libcontainer使用cgroups原生支持的統計信息作為資源管理的監控展示。
容器中運行的第一個進程init
,必須在初始化開始前放置到指定的cgroup目錄中,這樣就能防止初始化完成后運行的其他用戶指令逃逸出cgroups的控制。父子進程的同步則通過管道來完成,在隨后的運行時初始化中會進行展開描述。
1.3 可配置的容器安全
容器安全一直是被廣泛探討的話題,使用namespace對進程進行隔離是容器安全的基礎,遺憾的是,usernamespace由於設計上的復雜性,還沒有被Libcontainer完全支持。
Libcontainer目前可通過配置capabilities
、selinux
、apparmor
以及seccomp
進行一定的安全防范,目前除seccomp
以外都有一份默認的配置項提供給用戶作為參考。
在本系列的后續文章中,我們將對容器安全進行更深入的探討,敬請期待。
1.4 運行時與初始化進程
在容器創建過程中,父進程需要與容器的init
進程進行同步通信,通信的方式則通過向容器中傳入管道來實現。當init
啟動時,他會等待管道內傳入EOF
信息,這就給父進程完成初始化,建立uid/gid映射,並把新進程放進新建的cgroup一定的時間。
在Libcontainer中運行的應用(進程),應該是事先靜態編譯完成的。Libcontainer在容器中並不提供任何類似Unix init這樣的守護進程,用戶提供的參數也是通過exec
系統調用提供給用戶進程。通常情況下容器中也沒有長進程存在。
如果容器打開了偽終端,就會通過dup2
把console作為容器的輸入輸出(STDIN, STDOUT, STDERR)對象。
除此之外,以下四個文件也會在容器運行時自動生成。
- /etc/hosts
- /etc/resolv.conf
- /etc/hostname
- /etc/localtime
1.5 在運行着的容器中執行新進程
用戶也可以在運行着的容器中執行一條新的指令,就是我們熟悉的docker exec
功能。同樣,執行指令的二進制文件需要包含在容器的rootfs
之內。
通過這種方式運行起來的進程會隨容器的狀態變化,如容器被暫停,進程也隨之暫停,恢復也隨之恢復。當容器進程不存在時,進程就會被銷毀,重啟也不會恢復。
1.6 容器熱遷移(Checkpoint & Restore)
目前libcontainer已經集成了CRIU作為容器檢查點保存與恢復(通常也稱為熱遷移)的解決方案,應該在不久之后就會被Docker使用。也就是說,通過libcontainer你已經可以把一個正在運行的進程狀態保存到磁盤上,然后在本地或其他機器中重新恢復當前的運行狀態。這個功能主要帶來如下幾個好處。
- 服務器需要維護(如系統升級、重啟等)時,通過熱遷移技術把容器轉移到別的服務器繼續運行,應用服務信息不會丟失。
- 對於初始化時間極長的應用程序來說,容器熱遷移可以加快啟動時間,當應用啟動完成后就保存它的檢查點狀態,下次要重啟時直接通過檢查點啟動即可。
- 在高性能計算的場景中,容器熱遷移可以保證運行了許多天的計算結果不會丟失,只要周期性的進行檢查點快照保存就可以了。
要使用這個功能,需要保證機器上已經安裝了1.5.2或更高版本的criu
工具。不同Linux發行版都有criu
的安裝包,你也可以在CRIU官網上找到從源碼安裝的方法。我們將會在nsinit
的使用中介紹容器熱遷移的使用方法。
CRIU(Checkpoint/Restore In Userspace)由OpenVZ項目於2005年發起,因為其涉及的內核系統繁多、代碼多達數萬行,其復雜性與向后兼容性都阻礙着它進入內核主線,幾經周折之后決定在用戶空間實現,並在2012年被Linus加並入內核主線,其后得以快速發展。
你可以在CRIU官網查看其原理,簡單描述起來可以分為兩部分,一是檢查點的保存,其中分為3步。
- 收集進程與其子進程構成的樹,並凍結所有進程。
- 收集任務(包括進程和線程)使用的所有資源,並保存。
- 清理我們收集資源的相關寄生代碼,並與進程分離。
第二部分自然是恢復,分為4步。
- 讀取快照文件並解析出共享的資源,對多個進程共享的資源優先恢復,其他資源則隨后需要時恢復。
- 使用fork恢復整個進程樹,注意此時並不恢復線程,在第4步恢復。
- 恢復所有基礎任務(包括進程和線程)資源,除了內存映射、計時器、證書和線程。這一步主要打開文件、准備namespace、創建socket連接等。
- 恢復進程運行的上下文環境,恢復剩下的其他資源,繼續運行進程。
至此,libcontainer的基本特性已經預覽完畢,下面我們將從使用開始,一步步深入libcontainer的原理。
2. nsinit
與Libcontainer的使用
俗話說,了解一個工具最好的入門方式就是去使用它,nsinit
就是一個為了方便不通過Docker就可以直接使用libcontainer
而開發的命令行工具。它可以用於啟動一個容器或者在已有的容器中執行命令。使用nsinit
需要有 rootfs 以及相應的配置文件。
2.1 nsinit
的構建
使用nsinit
需要rootfs
,最簡單最常用的是使用Docker busybox
,相關配置文件則可以參考sample_configs
目錄,主要配置的參數及其作用將在配置參數一節中介紹。拷貝一份命名為container.json
文件到你rootfs
所在目錄中,這份文件就包含了你對容器做的特定配置,包括運行環境、網絡以及不同的權限。這份配置對容器中的所有進程都會產生效果。
具體的構建步驟在官方的README
文檔中已經給出,在此為了節省篇幅不再贅述。
最終編譯完成后生成nsinit
二進制文件,將這個指令加入到系統的環境變量,在busybox目錄下執行如下命令,即可使用,需要root權限。
nsinit exec --tty --config container.json /bin/bash
執行完成后會生成一個以容器ID命名的文件夾,上述命令沒有指定容器ID,默認名為”nsinit”,在“nsinit”文件夾下會生成一個state.json
文件,表示容器的狀態,其中的內容與配置參數中的內容類似,展示容器的狀態。
2.2 nsinit
的使用
目前nsinit
定義了9個指令,使用nsinit -h
就可以看到,對於每個單獨的指令使用--help
就能獲得更詳細的使用參數,如nsinit config --help
。
nsinit
這個命令行工具是通過cli.go
實現的,cli.go
封裝了命令行工具需要做的一些細節,包括參數解析、命令執行函數構建等等,這就使得nsinit
本身的代碼非常簡潔明了。具體的命令功能如下。
- config:使用內置的默認參數加上執行命令時用戶添加的部分參數,生成一份容器可用的標准配置文件。
- exec:啟動容器並執行命令。除了一些共有的參數外,還有如下一些獨有的參數。
- –tty,-t:為容器分配一個終端顯示輸出內容。
- –config:使用配置文件,后跟文件路徑。
- –id:指定容器ID,默認為
nsinit
。 - –user,-u:指定用戶,默認為“root”.
- –cwd:指定當前工作目錄。
- –env:為進程設置環境變量。
- init:這是一個內置的參數,用戶並不能直接使用。這個命令是在容器內部執行,為容器進行namespace初始化,並在完成初始化后執行用戶指令。所以在代碼中,運行
nsinit exec
后,傳入到容器中運行的實際上是nsinit init
,把用戶指令作為配置項傳入。 - oom:展示容器的內存超限通知。
- pause/unpause:暫停/恢復容器中的進程。
- stats:顯示容器中的統計信息,主要包括cgroup和網絡。
- state:展示容器狀態,就是讀取
state.json
文件。 - checkpoint:保存容器的檢查點快照並結束容器進程。需要填
--image-path
參數,后面是檢查點保存的快照文件路徑。完整的命令示例如下。nsinit checkpoint --image-path =/tmp/criu
-
restore:從容器檢查點快照恢復容器進程的運行。參數同上。
總結起來,nsinit
與Docker execdriver進行的工作基本相同,所以在Docker的源碼中並不會涉及到nsinit
包的調用,但是nsinit
為Libcontainer自身的調試和使用帶來了極大的便利。
3. 配置參數解析
no_pivot_root
:這個參數表示用rootfs
作為文件系統掛載點,不單獨設置pivot_root
。parent_death_signal
: 這個參數表示當容器父進程銷毀時發送給容器進程的信號。pivot_dir
:在容器root
目錄中指定一個目錄作為容器文件系統掛載點目錄。rootfs
:容器根目錄位置。readonlyfs
:設定容器根目錄為只讀。mounts
:設定額外的掛載,填充的信息包括原路徑,容器內目的路徑,文件系統類型,掛載標識位,掛載的數據大小和權限,最后設定共享掛載還是非共享掛載(獨立於mount_label
的設定起作用)。devices
:設定在容器啟動時要創建的設備,填充的信息包括設備類型、容器內設備路徑、設備塊號(major,minor)、cgroup文件權限、用戶編號、用戶組編號。mount_label
:設定共享掛載還是非共享掛載。hostname
:設定主機名。namespaces
:設定要加入的namespace,每個不同種類的namespace都可以指定,默認與父進程在同一個namespace中。capabilities
:設定在容器內的進程擁有的capabilities
權限,所有沒加入此配置項的capabilities
會被移除,即容器內進程失去該權限。networks
:初始化容器的網絡配置,包括類型(loopback、veth)、名稱、網橋、物理地址、IPV4地址及網關、IPV6地址及網關、Mtu大小、傳輸緩沖長度txqueuelen
、Hairpin Mode設置以及宿主機設備名稱。routes
:配置路由表。cgroups
:配置cgroups資源限制參數,使用的參數不多,主要包括允許的設備列表、內存、交換區用量、CPU用量、塊設備訪問優先級、應用啟停等。apparmor_profile
:配置用於selinux的apparmor文件。process_label
:同樣用於selinux的配置。rlimits
:最大文件打開數量,默認與父進程相同。additional_groups
:設定gid
,添加同一用戶下的其他組。uid_mappings
:用於User namespace的uid映射。gid_mappings
:用戶User namespace的gid映射。readonly_paths
:在容器內設定只讀部分的文件路徑。MaskPaths
:配置不使用的設備,通過綁定/dev/null
進行路徑掩蓋。
4. Libcontainer實現原理
在Docker中,對容器管理的模塊為execdriver
,目前Docker支持的容器管理方式有兩種,一種就是最初支持的LXC方式,另一種稱為native
,即使用Libcontainer進行容器管理。在孫宏亮的《Docker源碼分析系列》中,Docker Deamon啟動過程中就會對execdriver進行初始化,會根據驅動的名稱選擇使用的容器管理方式。
雖然在execdriver
中只有LXC和native兩種選擇,但是native(即Libcontainer
)通過接口的方式定義了一系列容器管理的操作,包括處理容器的創建(Factory)、容器生命周期管理(Container)、進程生命周期管理(Process)等一系列接口,相信如果Docker的熱潮一直像如今這般洶涌,那么不久的將來,Docker必將實現其全平台通用的宏偉藍圖。本節也將從Libcontainer的這些抽象對象開始講解,與你一同解開Docker容器管理之謎。在介紹抽象對象的具體實現過程中會與Docker execdriver聯系起來,讓你充分了解整個過程。
4.1 Factory 對象
Factory對象為容器創建和初始化工作提供了一組抽象接口,目前已經具體實現的是Linux系統上的Factory對象。Factory抽象對象包含如下四個方法,我們將主要描述這四個方法的工作過程,涉及到具體實現方法則以LinuxFactory為例進行講解。
- Create():通過一個
id
和一份配置參數創建容器,返回一個運行的進程。容器的id
由字母、數字和下划線構成,長度范圍為1~1024。容器ID為每個容器獨有,不能沖突。創建的最終返回一個Container類,包含這個id
、狀態目錄(在root目錄下創建的以id
命名的文件夾,存state.json
容器狀態文件)、容器配置參數、初始化路徑和參數,以及管理cgroup的方式(包含直接通過文件操作管理和systemd管理兩個選擇,默認選cgroup文件系統管理)。 - Load():當創建的
id
已經存在時,即已經Create
過,存在id
文件目錄,就會從id
目錄下直接讀取state.json
來載入容器。其中的參數在配置參數部分有詳細解釋。 - Type():返回容器管理的類型,目前可能返回的有libcontainer和lxc,為未來支持更多容器接口做准備。
- StartInitialization():容器內初始化函數。
- 這部分代碼是在容器內部執行的,當容器創建時,如果
New
不加任何參數,默認在容器進程中運行的第一條命令就是nsinit init
。在execdriver
的初始化中,會向reexec
注冊初始化器,命名為native
,然后在創建Libcontainer以后把native
作為執行參數傳遞到容器中執行,這個初始化器創建的Libcontainer就是沒有參數的。 - 傳入的參數是一個管道文件描述符,為了保證在初始化過程中,父子進程間狀態同步和配置信息傳遞而建立。
- 不管是純粹新建的容器還是已經創建的容器執行新的命令,都是從這個入口做初始化。
- 第一步,通過管道獲取配置信息。
- 第二步,從配置信息中獲取環境變量並設置為容器內環境變量。
- 若是已經存在的容器執行新命令,則只需要配置cgroup、namespace的Capabilities以及AppArmor等信息,最后執行命令。
- 若是純粹新建的容器,則還需要初始化網絡、路由、namespace、主機名、配置只讀路徑等等,最后執行命令。
- 這部分代碼是在容器內部執行的,當容器創建時,如果
至此,容器就已經創建和初始化完畢了。
4.2 Container 對象
Container對象主要包含了容器配置、控制、狀態顯示等功能,是對不同平台容器功能的抽象。目前已經具體實現的是Linux平台下的Container對象。每一個Container進程內部都是線程安全的。因為Container有可能被外部的進程銷毀,所以每個方法都會對容器是否存在進行檢測。
- ID():顯示Container的ID,在Factor對象中已經說過,ID很重要,具有唯一性。
- Status():返回容器內進程是運行狀態還是停止狀態。通過執行“SIG=0”的KILL命令對進程是否存在進行檢測。
- State():返回容器的狀態,包括容器ID、配置信息、初始進程ID、進程啟動時間、cgroup文件路徑、namespace路徑。通過調用
Status()
判斷進程是否存在。 - Config():返回容器的配置信息,可在“配置參數解析”部分查看有哪些方面的配置信息。
- Processes():返回cgroup文件
cgroup.procs
中的值,在Docker背后的內核知識:cgroups資源限制部分的講解中我們已經提過,cgroup.procs
文件會羅列所有在該cgroup中的線程組ID(即若有線程創建了子線程,則子線程的PID不包含在內)。由於容器不斷在運行,所以返回的結果並不能保證完全存活,除非容器處於“PAUSED”狀態。 - Stats():返回容器的統計信息,包括容器的cgroups中的統計以及網卡設備的統計信息。Cgroups中主要統計了cpu、memory和blkio這三個子系統的統計內容,具體了解可以通過閱讀“cgroups資源限制”部分對於這三個子系統統計內容的介紹來了解。網卡設備的統計則通過讀取系統中,網絡網卡文件的統計信息文件
/sys/class/net/<EthInterface>/statistics
來實現。 - Set():設置容器cgroup各子系統的文件路徑。因為cgroups的配置是進程運行時也會生效的,所以我們可以通過這個方法在容器運行時改變cgroups文件從而改變資源分配。
- Start():構建ParentProcess對象,用於處理啟動容器進程的所有初始化工作,並作為父進程與新創建的子進程(容器)進行初始化通信。傳入的Process對象可以幫助我們追蹤進程的生命周期,Process對象將在后文詳細介紹。
- 啟動的過程首先會調用
Status()
方法的具體實現得知進程是否存活。 - 創建一個管道(詳見Docker初始化通信——管道)為后期父子進程通信做准備。
- 配置子進程
cmd
命令模板,配置參數的值就是從factory.Create()
傳入進來的,包括命令執行的工作目錄、命令參數、輸入輸出、根目錄、子進程管道以及KILL
信號的值。 - 根據容器進程是否存在確定是在已有容器中執行命令還是創建新的容器執行命令。若存在,則把配置的命令構建成一個
exec.Cmd
對象、cgroup路徑、父子進程管道及配置保留到ParentProcess對象中;若不存在,則創建容器進程及相應namespace,目前對user namespace有了一定的支持,若配置時加入user namespace,會針對配置項進行映射,默認映射到宿主機的root用戶,最后同樣構建出相應的配置內容保留到ParentProcess對象中。通過在cmd.Env
寫入環境變量_LIBCONTAINER_INITTYPE
來告訴容器進程采用的哪種方式啟動。 - 執行ParentProcess中構建的
exec.Cmd
內容,即執行ParentProcess.start()
,具體的執行過程在Process部分介紹。 - 最后如果是新建的容器進程,還會執行狀態更新函數,把
state.json
的內容刷新。
- 啟動的過程首先會調用
- Destroy():首先使用cgroup的freezer子系統暫停所有運行的進程,然后給所有進程發送
SIGKIL
信號(如果沒有使用pid namespace
就不對進程處理)。最后把cgroup及其子系統卸載,刪除cgroup文件夾。 - Pause():使用cgroup的freezer子系統暫停所有運行的進程。
- Resume():使用cgroup的freezer子系統恢復所有運行的進程。
- NotifyOOM():為容器內存使用超界提供只讀的通道,通過向
cgroup.event_control
寫入eventfd
(用作線程間通信的消息隊列)和cgroup.oom_control
(用於決定內存使用超限后的處理方式)來實現。 - Checkpoint():保存容器進程檢查點快照,為容器熱遷移做准備。通過使用CRIU的SWRK模式來實現,這種模式是CRIU另外兩種模式CLI和RPC的結合體,允許用戶需要的時候像使用命令行工具一樣運行CRIU,並接受用戶遠程調用的請求,即傳入的熱遷移檢查點保存請求,傳入文件形式以Google的protobuf協議保存。
- Restore():恢復檢查點快照並運行,完成容器熱遷移。同樣通過CRIU的SWRK模式實現,恢復的時候可以傳入配置文件設置恢復掛載點、網絡等配置信息。
至此,Container對象中的所有函數及相關功能都已經介紹完畢,包含了容器生命周期的全部過程。
TIPs: Docker初始化通信——管道
Libcontainer創建容器進程時需要做初始化工作,此時就涉及到使用了namespace隔離后的兩個進程間的通信。我們把負責創建容器的進程稱為父進程,容器進程稱為子進程。父進程clone
出子進程以后,依舊是共享內存的。但是如何讓子進程知道內存中寫入了新數據依舊是一個問題,一般有四種方法。
- 發送信號通知(signal)
- 對內存輪詢訪問(poll memory)
- sockets通信(sockets)
- 文件和文件描述符(files and file-descriptors)
對於Signal而言,本身包含的信息有限,需要額外記錄,namespace帶來的上下文變化使其不易理解,並不是最佳選擇。顯然通過輪詢內存的方式來溝通是一個非常低效的做法。另外,因為Docker會加入network namespace,實際上初始時網絡棧也是完全隔離的,所以socket方式並不可行。
Docker最終選擇的方式就是打開的可讀可寫文件描述符——管道。
Linux中,通過pipe(int fd[2])
系統調用就可以創建管道,參數是一個包含兩個整型的數組。調用完成后,在fd[1]
端寫入的數據,就可以從fd[0]
端讀取。
// 需要加入頭文件: #include <unistd.h> // 全局變量: int fd[2]; // 在父進程中進行初始化: pipe(fd); // 關閉管道文件描述符 close(checkpoint[1]);
調用pipe
函數后,創建的子進程會內嵌這個打開的文件描述符,對fd[1]
寫入數據后可以在fd[0]
端讀取。通過管道,父子進程之間就可以通信。通信完畢的奧秘就在於EOF
信號的傳遞。大家都知道,當打開的文件描述符都關閉時,才能讀到EOF
信號,所以libcontainer
中父進程先關閉自己這一端的管道,然后等待子進程關閉另一端的管道文件描述符,傳來EOF
表示子進程已經完成了初始化的過程。
4.3 Process 對象
Process 主要分為兩類,一類在源碼中就叫Process
,用於容器內進程的配置和IO的管理;另一類在源碼中叫ParentProcess
,負責處理容器啟動工作,與Container對象直接進行接觸,啟動完成后作為Process
的一部分,執行等待、發信號、獲得pid
等管理工作。
ParentProcess對象,主要包含以下六個函數,而根據”需要新建容器”和“在已經存在的容器中執行”的不同方式,具體的實現也有所不同。
-
已有容器中執行命令
- pid(): 啟動容器進程后通過管道從容器進程中獲得,因為容器已經存在,與Docker Deamon在不同的pid namespace中,從進程所在的namespace獲得的進程號才有意義。
- start(): 初始化容器中的執行進程。在已有容器中執行命令一般由
docker exec
調用,在execdriver包中,執行exec
時會引入nsenter
包,從而調用其中的C語言代碼,執行nsexec()
函數,該函數會讀取配置文件,使用setns()
加入到相應的namespace,然后通過clone()
在該namespace中生成一個子進程,並把子進程通過管道傳遞出去,使用setns()
以后並沒有進入pid namespace,所以還需要通過加上clone()
系統調用。- 開始執行進程,首先會運行
C
代碼,通過管道獲得進程pid,最后等待C
代碼執行完畢。 - 通過獲得的pid把cmd中的Process替換成新生成的子進程。
- 把子進程加入cgroup中。
- 通過管道傳配置文件給子進程。
- 等待初始化完成或出錯返回,結束。
- 開始執行進程,首先會運行
-
新建容器執行命令
- pid():啟動容器進程后通過
exec.Cmd
自帶的pid()
函數即可獲得。 - start():初始化及執行容器命令。
- 開始運行進程。
- 把進程pid加入到cgroup中管理。
- 初始化容器網絡。(本部分內容豐富,將從本系列的后續文章中深入講解)
- 通過管道發送配置文件給子進程。
- 等待初始化完成或出錯返回,結束。
- pid():啟動容器進程后通過
-
實現方式類似的一些函數
- terminate() :發送
SIGKILL
信號結束進程。 - startTime() :獲取進程的啟動時間。
- signal():發送信號給進程。
- wait():等待程序執行結束,返回結束的程序狀態。
- terminate() :發送
Process對象,主要描述了容器內進程的配置以及IO。包括參數Args
,環境變量Env
,用戶User
(由於uid、gid映射),工作目錄Cwd
,標准輸入輸出及錯誤輸入,控制終端路徑consolePath
,容器權限Capabilities
以及上述提到的ParentProcess對象ops
(擁有上面的一些操作函數,可以直接管理進程)。
5. 總結
本文主要介紹了Docker容器管理的方式Libcontainer,從Libcontainer的使用到源碼實現方式。我們深入到容器進程內部,感受到了Libcontainer較為全面的設計。總體而言,Libcontainer本身主要分為三大塊工作內容,一是容器的創建及初始化,二是容器生命周期管理,三則是進程管理,調用方為Docker的execdriver
。容器的監控主要通過cgroups的狀態統計信息,未來會加入進程追蹤等更豐富的功能。另一方面,Libcontainer在安全支持方面也為用戶盡可能多的提供了支持和選擇。遺憾的是,容器安全的配置需要用戶對系統安全本身有足夠高的理解,user namespace也尚未支持,可見Libcontainer依舊有很多工作要完善。但是Docker社區的火熱也自然帶動了大家對Libcontainer的關注,相信在不久的將來,Libcontainer就會變得更安全、更易用。
2016-05-11
【參考】http://www.infoq.com/cn/articles/docker-container-management-libcontainer-depth-analysis