Docker背后的容器管理 - Libcontainer深度解析

jopen 9年前發布 | 57K 次閱讀 Docker

原文  http://www.infoq.com/cn/articles/docker-container-management-libcontainer-depth-analysis


Libcontainer 是Docker中用于容器管理的包,它基于Go語言實現,通過管理 namespacescgroupscapabilities 以及文件系統來進行容器控制。你可以使用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都是默認開啟的,通過 clone 系統調用進行創建。

1.1 建立文件系統

文件系統方面,容器運行需要 rootfs 。所有容器中要執行的指令,都需要包含在 rootfs 中。所有掛載在容器銷毀時都會被卸載,因為mount namespace會在容器銷毀時一同消失。為了容器可以正常執行命令,以下文件系統必須在容器運行時掛載到 rootfs 中。

路徑 類型 參數 權限及數據
/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步。

  1. 收集進程與其子進程構成的樹,并凍結所有進程。
  2. 收集任務(包括進程和線程)使用的所有資源,并保存。
  3. 清理我們收集資源的相關寄生代碼,并與進程分離。

第二部分自然是恢復,分為4步。

  1. 讀取快照文件并解析出共享的資源,對多個進程共享的資源優先恢復,其他資源則隨后需要時恢復。
  2. 使用fork恢復整個進程樹,注意此時并不恢復線程,在第4步恢復。
  3. 恢復所有基礎任務(包括進程和線程)資源,除了內存映射、計時器、證書和線程。這一步主要打開文件、準備namespace、創建socket連接等。
  4. 恢復進程運行的上下文環境,恢復剩下的其他資源,繼續運行進程。

至此,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為例進行講解。

  1. Create() :通過一個 id 和一份配置參數創建容器,返回一個運行的進程。容器的 id 由字母、數字和下劃線構成,長度范圍為1~1024。容器ID為每個容器獨有,不能沖突。創建的最終返回一個Container類,包含這個 id 、狀態目錄(在root目錄下創建的以 id 命名的文件夾,存 state.json 容器狀態文件)、容器配置參數、初始化路徑和參數,以及管理cgroup的方式(包含直接通過文件操作管理和systemd管理兩個選擇,默認選cgroup文件系統管理)。
  2. Load() :當創建的 id 已經存在時,即已經 Create 過,存在 id 文件目錄,就會從 id 目錄下直接讀取 state.json 來載入容器。其中的參數在配置參數部分有詳細解釋。
  3. Type() :返回容器管理的類型,目前可能返回的有libcontainer和lxc,為未來支持更多容器接口做準備。
  4. StartInitialization() :容器內初始化函數。
    • 這部分代碼是在容器內部執行的,當容器創建時,如果 New 不加任何參數,默認在容器進程中運行的第一條命令就是 nsinit init 。在 execdriver 的初始化中,會向 reexec 注冊初始化器,命名為 native ,然后在創建Libcontainer以后把 native 作為執行參數傳遞到容器中執行,這個初始化器創建的Libcontainer就是沒有參數的。
    • 傳入的參數是一個管道文件描述符,為了保證在初始化過程中,父子進程間狀態同步和配置信息傳遞而建立。
    • 不管是純粹新建的容器還是已經創建的容器執行新的命令,都是從這個入口做初始化。
    • 第一步,通過管道獲取配置信息。
    • 第二步,從配置信息中獲取環境變量并設置為容器內環境變量。
    • 若是已經存在的容器執行新命令,則只需要配置cgroup、namespace的Capabilities以及AppArmor等信息,最后執行命令。
    • 若是純粹新建的容器,則還需要初始化網絡、路由、namespace、主機名、配置只讀路徑等等,最后執行命令。

至此,容器就已經創建和初始化完畢了。

4.2 Container 對象

Container對象主要包含了容器配置、控制、狀態顯示等功能,是對不同平臺容器功能的抽象。目前已經具體實現的是Linux平臺下的 Container對象。每一個Container進程內部都是線程安全的。因為Container有可能被外部的進程銷毀,所以每個方法都會對容器是否存在進行檢測。

  1. ID() :顯示Container的ID,在Factor對象中已經說過,ID很重要,具有唯一性。
  2. Status() :返回容器內進程是運行狀態還是停止狀態。通過執行“SIG=0”的KILL命令對進程是否存在進行檢測。
  3. State() :返回容器的狀態,包括容器ID、配置信息、初始進程ID、進程啟動時間、cgroup文件路徑、namespace路徑。通過調用 Status() 判斷進程是否存在。
  4. Config() :返回容器的配置信息,可在“配置參數解析”部分查看有哪些方面的配置信息。
  5. Processes() :返回cgroup文件 cgroup.procs 中的值,在 Docker背后的內核知識:cgroups資源限制 部分的講解中我們已經提過, cgroup.procs 文件會羅列所有在該cgroup中的線程組ID(即若有線程創建了子線程,則子線程的PID不包含在內)。由于容器不斷在運行,所以返回的結果并不能保證完全存活,除非容器處于“PAUSED”狀態。
  6. Stats() :返回容器的統計信息,包括容器的cgroups中的統計以及網卡設備的統計信息。Cgroups中主要統計了cpu、memory和blkio這三個子系統的統計內容,具體了解可以通過閱讀“cgroups資源限制”部分對于這三個子系統統計內容的介紹來了解。網卡設備的統計則通過讀取系統中,網絡網卡文件的統計信息文件 /sys/class/net/<EthInterface>/statistics 來實現。
  7. Set() :設置容器cgroup各子系統的文件路徑。因為cgroups的配置是進程運行時也會生效的,所以我們可以通過這個方法在容器運行時改變cgroups文件從而改變資源分配。
  8. 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 的內容刷新。
  9. Destroy() :首先使用cgroup的freezer子系統暫停所有運行的進程,然后給所有進程發送 SIGKIL 信號(如果沒有使用 pid namespace 就不對進程處理)。最后把cgroup及其子系統卸載,刪除cgroup文件夾。
  10. Pause() :使用cgroup的freezer子系統暫停所有運行的進程。
  11. Resume() :使用cgroup的freezer子系統恢復所有運行的進程。
  12. NotifyOOM() :為容器內存使用超界提供只讀的通道,通過向 cgroup.event_control 寫入 eventfd (用作線程間通信的消息隊列)和 cgroup.oom_control (用于決定內存使用超限后的處理方式)來實現。
  13. Checkpoint() :保存容器進程檢查點快照,為容器熱遷移做準備。通過使用CRIU的 SWRK模式 來實現,這種模式是CRIU另外兩種模式CLI和RPC的結合體,允許用戶需要的時候像使用命令行工具一樣運行CRIU,并接受用戶遠程調用的請求,即傳入的熱遷移檢查點保存請求,傳入文件形式以Google的protobuf協議保存。
  14. 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對象,主要包含以下六個函數,而根據”需要新建容器”和“在已經存在的容器中執行”的不同方式,具體的實現也有所不同。

  • 已有容器中執行命令

    1. pid() : 啟動容器進程后通過管道從容器進程中獲得,因為容器已經存在,與Docker Deamon在不同的pid namespace中,從進程所在的namespace獲得的進程號才有意義。
    2. start() : 初始化容器中的執行進程。在已有容器中執行命令一般由 docker exec 調用,在execdriver包中,執行 exec 時會引入 nsenter 包,從而調用其中的C語言代碼,執行 nsexec() 函數,該函數會讀取配置文件,使用 setns() 加入到相應的namespace,然后通過 clone() 在該namespace中生成一個子進程,并把子進程通過管道傳遞出去,使用 setns() 以后并沒有進入pid namespace,所以還需要通過加上 clone() 系統調用。
      • 開始執行進程,首先會運行 C 代碼,通過管道獲得進程pid,最后等待 C 代碼執行完畢。
      • 通過獲得的pid把cmd中的Process替換成新生成的子進程。
      • 把子進程加入cgroup中。
      • 通過管道傳配置文件給子進程。
      • 等待初始化完成或出錯返回,結束。
  • 新建容器執行命令

    1. pid() :啟動容器進程后通過 exec.Cmd 自帶的 pid() 函數即可獲得。
    2. start() :初始化及執行容器命令。
      • 開始運行進程。
      • 把進程pid加入到cgroup中管理。
      • 初始化容器網絡。(本部分內容豐富,將從本系列的后續文章中深入講解)
      • 通過管道發送配置文件給子進程。
      • 等待初始化完成或出錯返回,結束。
  • 實現方式類似的一些函數

    • terminate() :發送 SIGKILL 信號結束進程。
    • startTime() :獲取進程的啟動時間。
    • signal() :發送信號給進程。
    • wait() :等待程序執行結束,返回結束的程序狀態。

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就會變得更安全、更易用。

6. 作者簡介

孫健波, 浙江大學SEL實驗室 碩士研究生,目前在云平臺團隊從事科研和開發工作。浙大團隊對PaaS、Docker、大數據和主流開源云計算技術有深入的研究和二次開發經驗,團隊現將部分技術文章貢獻出來,希望能對讀者有所幫助。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!