摘要:首先來看看收到客戶端的啟動容器請求后,做了些什么。待上面的文件都準備好了之后,通過的方式給發送請求,通知啟動容器。主要功能是啟動并管理運行時的所有。包含容器中進程相關的一些屬性信息,后續在這個容器上執行命令時會用到這個文件。
在上一篇介紹過了docker create之后,這篇來看看docker start是怎么根據create之后的結果運行容器的。
啟動容器在這里我們先啟動上一篇中創建的那個容器,然后看看docker都干了些什么。
#根據容器名稱啟動容器(也可以根據容器ID來啟動) root@dev:~# docker start docker_test docker_test #可以看出容器正在后臺運行bash root@dev:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 967438113fba ubuntu "/bin/bash" 38 minutes ago Up 8 seconds docker_teststart的大概流程
docker(client)發送啟動容器命令給dockerd
dockerd收到請求后,準備好rootfs,以及一些其它的配置文件,然后通過grpc的方式通知containerd啟動容器
containerd根據收到的請求以及配置文件位置,創建容器運行時需要的bundle,然后啟動shim進程,讓它來啟動容器
shim進程啟動后,做一些準備工作,然后調用runc啟動容器
下面就來詳細的了解一下每一步都干了些什么。
dockerd首先來看看dockerd收到客戶端的啟動容器請求后,做了些什么。
準備rootfsdockerd做的第一件事情就是準備好容器運行時需要的rootfs,由于在docker create創建容器的時候,容器的所有layer都已經準備好了,現在就差一步將他們合并起來了,對于aufs來說,需要通過mount的方式將所有的layer合并起來,對于其他的文件系統來說,有些可能不需要這一步,/var/lib/docker/aufs/mnt下面已經是合并好的rootfs了。
下面來看看這個容器啟動之后/var/lib/docker/aufs/mnt下的內容。
#init目錄下沒有文件 root@dev:/var/lib/docker/aufs/mnt# tree 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init/ 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init/ 0 directories, 0 files #305226f...目錄下面的內容就是rootfs的內容,包含了大量的文件 root@dev:/var/lib/docker/aufs/mnt# tree 305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281 | tail │?? ├── lastlog │?? └── wtmp ├── mail ├── opt ├── run -> /run ├── spool │?? └── mail -> ../mail └── tmp 692 directories, 4804 files #雖然在容器中,/dev/console,/etc/hosts,/etc/hostname, #/etc/resolv.conf這幾個文件都有內容, #但從外面主機的mount namespace中來看的話,還是空的, #因為bind mount發生在容器中的mount namespace中,所以外面根本就看不到 root@dev:/var/lib/docker/aufs/mnt/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281# ls -l ./dev/console ./etc/hosts ./etc/hostname ./etc/resolv.conf -rwxr-xr-x 1 root root 0 Jun 25 11:25 ./dev/console -rwxr-xr-x 1 root root 0 Jun 25 11:25 ./etc/hostname -rwxr-xr-x 1 root root 0 Jun 25 11:25 ./etc/hosts -rwxr-xr-x 1 root root 0 Jun 25 11:25 ./etc/resolv.conf
和上一篇中create之后的內容相比,唯一的差別就是305226f...目錄下有了內容,而init目錄下還是空的,說明對于aufs文件系統來說,它只需要構造好最上面的一層就可以了,不需要init層和它下面所有層合并之后的結果,大家有興趣的話可以檢查一下/var/lib/docker/aufs/mnt目錄下的其它目錄的內容,會發現其它層的文件夾也全是空的,因為aufs只在運行的時候動態的將容器的最上面一層和下面的所有層進行合并,合并的過程等同于下面的命令:
root@dev:/var/lib/docker/aufs/diff# mkdir /tmp/rootfs root@dev:/var/lib/docker/aufs/diff# mount -t aufs -o br=./305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281=rw:./305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281-init=ro:./7938f2b32c53a9e0d3974f9579dd9dbb450202e1e11fe514e31556d4ea808c4e=ro:./4c10796e21c796a6f3d83eeb3613c566ca9e0fd0a596f4eddf5234b87955b3c8=ro:./fd0ba28a44491fd7559c7ffe0597fb1f95b63207a38a3e2680231fb2f6fe92bd=ro:./b656bf5f0688069cd90ab230c029fdfeb852afcfd0d1733d087474c86a117da3=ro:./1e83d2ea184e08eed978127311cc96498e319426abe2fb5004d4b1454598bd76=ro none /tmp/rootfs root@dev:/var/lib/docker/aufs/diff# tree /tmp/rootfs/ | tail │?? ├── lastlog │?? └── wtmp ├── mail ├── opt ├── run -> /run ├── spool │?? └── mail -> ../mail └── tmp 693 directories, 4820 files #這里mount后的文件夾和文件數量要多于上面的/var/lib/docker/aufs/mnt/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281, #可能跟mount時使用的參數有關,具體情況我沒有仔細研究, #有興趣的話可以參考源代碼docker/daemon/graphdriver/aufs/aufs.go中的aufsMount函數。
準備容器內部需要的文件關于aufs文件系統的使用可以參考:Linux文件系統之aufs
rootfs準備好了之后,dockerd接著就會準備一些容器里面需要用到的配置文件,先看看container目錄下的變化:
root@dev:/var/lib/docker/containers# tree 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/ 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/ ├── 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368-json.log ├── checkpoints ├── config.v2.json ├── hostconfig.json ├── hostname ├── hosts ├── resolv.conf ├── resolv.conf.hash └── shm 2 directories, 7 files
容器啟動后,多了下面這幾個文件,這幾個文件都是docker動態生成的:
967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368-json.log:容器的日志文件,后續容器的stdout和stderr都會輸出到這個目錄。當然如果配置了其它的日志插件的話,日志就會寫到別的地方。
hostname:里面是容器的主機名,來自于config.v2.json,由docker create命令的-h參數指定,如果沒指定的話,就是容器ID的前12位,這里即為967438113fba
resolv.conf:里面包含了DNS服務器的IP,來自于hostconfig.json,由docker create命令的--dns參數指定,沒有指定的話,docker會根據容器的網絡類型生成一個默認的,一般是主機配置的DNS服務器或者是docker bridge的IP。
resolv.conf.hash:resolv.conf文件的校驗碼
shm:為容器分配的一個內存文件系統,后面會綁定到容器中的/dev/shm目錄,可以由docker create的參數--shm-size控制其大小,默認是64M,其本質上就是一個掛載到/dev/shm的tmpfs,由于這個目錄的內容是放在內存中的,所以讀寫速度快,有些程序會利用這個特點而用到這個目錄,所以docker事先為容器準備好這個目錄。
準備OCI需要的bundle注意:除了日志文件外,其它文件在每次容器啟動的時候都會自動生成,所以修改他們的內容后只會在當前容器運行的時候生效,容器重啟后,配置又都會恢復到默認的狀態
在什么是容器的runtime?中,介紹過bundle的概念,它主要包含一個名字叫做config.json的配置文件。
dockerd在生成這個文件前,要做一些準備工作,比如創建好cgroup的相關目錄,準備網絡相關的配置等,然后才生成config.json文件。
cgroup的相關目錄可以直接通過命令find /sys/fs/cgroup/ -name 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368找到.
網絡相關的內容這里不介紹,后續會有專門的文章進行介紹。
bundle被dockerd放在了目錄/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368下,我們這里主要看一下生成的config.json文件中一些比較常見且易懂的字段。
只有當容器在運行的時候,目錄/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368才存在,容器停止執行后該目錄會被刪除掉,下一次啟動的時候會再次被創建。
#這里的只截取了部分輸出,僅供參考 root@dev:/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# cat config.json |python -m json.tool { "hostname": "967438113fba", #主機名 "linux": { "cgroupsPath": "/docker/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368", #cgroup路徑 "namespaces": [ #需要加入的namespace,只有type沒有值表示創建并加入一個新的namespace,這里沒看到user namespace,說明docker默認情況下是不開啟user namespace的。 { "type": "mount" }, { "type": "network" }, { "type": "uts" }, { "type": "pid" }, { "type": "ipc" } ] }, "mounts": [ #需要mount到容器中的文件或者目錄,這里列出來的的幾個文件就是上面介紹的由dockerd進程生成的那幾個文件,它們將通過bind的方式mount到容器中 { "destination": "/etc/resolv.conf", "options": [ "rbind", "rprivate" ], "source": "/var/lib/docker/containers/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/resolv.conf", "type": "bind" }, { "destination": "/etc/hostname", "options": [ "rbind", "rprivate" ], "source": "/var/lib/docker/containers/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/hostname", "type": "bind" }, { "destination": "/etc/hosts", "options": [ "rbind", "rprivate" ], "source": "/var/lib/docker/containers/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/hosts", "type": "bind" }, { "destination": "/dev/shm", "options": [ "rbind", "rprivate" ], "source": "/var/lib/docker/containers/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/shm", "type": "bind" } ], "process": { #這里/bin/bash就是進程啟動后要運行的程序, "args": [ "/bin/bash" ], "cwd": "/", "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "HOSTNAME=967438113fba", "TERM=xterm" ], "terminal": true, "user": { "gid": 0, "uid": 0 } }, "root": { #rootfs的路徑 "path": "/var/lib/docker/aufs/mnt/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281" } }準備IO文件
在bundle目錄里面,除了上面介紹的容器配置文件之外,dockerd還創建了一些跟io相關的命名管道,用來和容器之間進行通信,比如這里的init-stdin文件用來向容器的stdin中寫數據,init-stdout用來接收容器的stdout輸出。
#bundle目錄里面除了config.json之外,還有兩個文件 root@dev:/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# tree . ├── config.json ├── init-stdin └── init-stdout 0 directories, 3 files #這兩個文件是命名管道文件 root@dev:/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# file init-stdin init-stdout init-stdin: fifo (named pipe) init-stdout: fifo (named pipe) #它們被dockerd和docker-containerd-shim兩個進程所打開 root@dev:/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# lsof * COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME dockerd 1218 root 18u FIFO 0,18 0t0 640 init-stdin dockerd 1218 root 21u FIFO 0,18 0t0 641 init-stdout dockerd 1218 root 24w FIFO 0,18 0t0 640 init-stdin dockerd 1218 root 25r FIFO 0,18 0t0 641 init-stdout docker-co 7971 root 7u FIFO 0,18 0t0 640 init-stdin docker-co 7971 root 9u FIFO 0,18 0t0 640 init-stdin docker-co 7971 root 10r FIFO 0,18 0t0 640 init-stdin docker-co 7971 root 12u FIFO 0,18 0t0 641 init-stdout docker-co 7971 root 13w FIFO 0,18 0t0 641 init-stdout docker-co 7971 root 14u FIFO 0,18 0t0 641 init-stdout docker-co 7971 root 15r FIFO 0,18 0t0 641 init-stdout docker-co 7971 root 16w FIFO 0,18 0t0 640 init-stdin #7971是容器進程docker-containerd-shim root@dev:/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# ps -ef|grep 7971|grep docker root 7971 1311 0 17:43 ? 00:00:00 docker-containerd-shim 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368 /var/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368 docker-runc
上面只有init-stdin和init-stdout,沒有init-stderr,那是因為我們創建容器的時候指定了-t參數,意思是讓docker為容器創建一個tty(虛擬的),在這種情況下,stdout和stderr將采用同樣的通道,即容器中進程往stderr中輸出數據時,會寫到init-stdout中。
待上面的文件都準備好了之后,通過grpc的方式給containerd發送請求,通知containerd啟動容器。
containerdcontainerd主要功能是啟動并管理運行時的所有contianer。
準備相關文件containerd會創建目錄/run/docker/libcontainerd/containerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/init并將相關文件放到這里。
只有當容器在運行的時候,目錄/run/docker/libcontainerd/containerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368才存在,容器停止執行后該目錄會被刪除掉,下一次啟動的時候會再次被創建。
root@dev:/run/docker/libcontainerd/containerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368/init# file * control: fifo (named pipe) exit: fifo (named pipe) log.json: empty pid: ASCII text, with no line terminators process.json: ASCII text, with very long lines shim-log.json: empty starttime: ASCII text, with no line terminators
control: 用來往shim發送控制命令,包括關閉stdin和調整終端的窗口大小。
exit:shim進程退出的時候,會關閉該管道,然后containerd就會收到通知,做一些清理工作。
process.json:包含容器中進程相關的一些屬性信息,后續在這個容器上執行docker exec命令時會用到這個文件。
log.json: runc如果運行失敗的話,會寫日志到這個文件
shim-log.json:shim進程執行失敗的話,會寫日志到這個文件
pid:容器啟動后,runc會將容器中第一個進程的pid寫到這個文件中(外面pid namespace中的pid)
starttime:記錄容器的啟動時間
啟動過程contianerd收到啟動容器請求后,就會創建control、exit、process.json這三個文件
然后啟動shim進程,等著runc創建容器并將容器里第一個進程的pid寫入pid文件
如果containerd讀取pid文件失敗,則讀取shim-log.json和log.json,看出了什么異常
如果讀取pid文件成功,說明容器創建成功,則將當前時間作為容器的啟動時間寫入starttime文件
調用runc的start命令啟動容器
監聽容器待容器啟動之后,containerd還需要監聽容器的OOM事件和容器退出事件,以便及時作出響應,OOM事件通過cgroup的內存限制機制進行監聽(通過group.event_control),而容器退出事件通過exit這個命名pipe來實現。
shim按道理來說如果容器里面的所有進程屬于一個pid namespace的話,id為1的進程退出后,容器也就退出了,調用wait函數并傳入容器里第一個進程的pid也能知道容器是否退出,不確定為什么containerd一定要弄個exit來監聽容器的退出,我沒有繼續深入研究,可能是因為pipe的fd可以通過epool來統一監聽并且是異步,處理起來方便。
shim進程被containerd啟動之后,第一步是設置子孫進程成為孤兒進程后由shim進程接管,即shim將變成孤兒進程的父進程,這樣就保證容器里的第一個進程不會因為runc進程的退出而被init進程接管。
從Linux 3.4開始,prctl增加了對PR_SET_CHILD_SUBREAPER的支持,這樣就可以控制孤兒進程可以被誰接管,而不是像以前一樣只能由init進程接管。
接著根據傳入的參數設置好要啟動進程的stdin,stdout,stderr(來自于上面的init-stdin,init-stdout,init-stderr),然后調用runc create命令創建容器,容器創建成功后,runc會將容器的第一個進程的pid寫入上面containerd目錄下的pid文件中,這樣containerd進程就知道容器創建成功了,于是containerd接著就會調用runc start啟動容器。
runcrunc會被調用兩次,第一次是shim調用runc create創建容器,第二次是containerd調用runc start啟動容器。
創建容器runc會根據參數中傳入的bundle目錄名稱以及容器ID,創建容器.
創建容器就是啟動進程/proc/self/exe init,由于/proc/self/exe指向的是自己,所以相當于fork了一個新進程,并且新進程啟動的參數是init,相當于運行了runc init,runc init會根據配置創建好相應的namespace,同時創建一個叫exec.fifo的臨時文件,等待其它進程打開這個文件,如果有其它進程打開這個文件,則啟動容器。
啟動容器啟動容器就是運行runc start,它會打開并讀一下文件exec.fifo,這樣就會觸發runc init進程啟動容器,如果runc start讀取該文件沒有異常,將會刪掉文件exec.fifo,所以一般情況下我們看不到文件exec.fifo。
runc創建的容器都會在在/run/runc下有一個目錄,里面有一個state.json文件(上面說到的exec.fifo這個臨時文件也在這里),包含當前容器詳細的配置及狀態信息。對于本文中的這個容器,相應的目錄為/run/runc/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368。
root@dev:/run/runc/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# ls state.json #通過runc state命令,可以查到指定容器的相關信息 root@dev:/run/runc/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368# docker-runc state 967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368 { "ociVersion": "1.0.0-rc2-dev", "id": "967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368", "pid": 8001, "status": "running", #剛創建時這里的狀態是created,只有運行runc start之后這里才變成running "bundle": "/run/docker/libcontainerd/967438113fba0b7a3005bcb6efae6a77055d6be53945f30389888802ea8b0368", "rootfs": "/var/lib/docker/aufs/mnt/305226f2e0755956ada28b3baf39b18fa328f1a59fd90e0b759a239773db2281", "created": "2017-06-25T04:04:18.830443417Z" }
結束語如果我們平時多帶帶的調用runc命令的話,可以將創建容器和啟動容器這兩步合并成一步,那就是runc run,具體啟動方法可參考“走進docker(03):如何繞過docker運行hello-world?”中關于runc運行bundle的介紹。
docker start命令干的活很多,這里只是介紹了大概的流程和涉及的進程和文件,還有一些其他東西并沒有涉及到,比如存儲插件和網絡,后續在專門介紹相關部分的時候再詳細介紹。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/26957.html
摘要:包含的內容本系列主要介紹三個上的項目由于只介紹核心的東西,所以不會包含下面這些項目使用語言開發,將多個相關的容器配置在一起,從而可以同時創建啟動停止和監控它們。由于本人時間安排發生變化,本系列停止更新,后面不確定是否會繼續,非常抱歉。 本人docker初學者,邊學習邊總結,一方面加深自己的理解,另一方面希望對其他想深入了解docker的同學有所幫助。 由于本人缺乏實戰經驗,錯誤在所難免...
摘要:進程啟動后,就會按照的標準準備好相關運行時環境,然后啟動進程。涉及到標準輸入輸出重定向,這里不細說這里是它們之間的交互流程流程對應的文字描述如下客戶端發送創建容器請求給,收到請求后,發現本地沒有相應的額,于是返回失敗。 在程序員的世界里,hello world是個很特殊的存在,當我們接觸一門新的語言、新的開發庫或者框架時,第一時間想了解的一般都是怎么實現一個hello world,然后...
摘要:結束語命令干的活比較少,主要是準備的和配置文件,配置文件中的項比較多,后續會挑一些常用的項進行專門介紹。 有了image之后,就可以開始創建并啟動容器了,平時我們都是用docker run命令直接創建并運行一個容器,它的背后其實包含獨立的兩步,一步是docker create創建容器,另一步是docker start啟動容器,本篇將先介紹在docker create這一步中,docke...
摘要:當企業的運維團隊去維護一個彈性的容器集群時,傳統的軟件部署方式需要向容器遷移,這個過程中需要有風險預判和規避之道。但是這樣會有些問題,就是大部分鏡像都是基于構建的,這會和樹莓派的很不兼容。多次嘗試后狀態被破壞刪庫重試,重啟大法好。 當前技術世界的發展形勢就是讓開發人員從繁瑣的應用配置和管理中解放出來,使用容器鏡像來處理復雜的程序運行依賴庫的需求,保證代碼運行環境的一致性。既然這樣的好處...
閱讀 1660·2021-11-16 11:41
閱讀 2457·2021-11-08 13:14
閱讀 3106·2019-08-29 17:16
閱讀 3079·2019-08-29 16:30
閱讀 1843·2019-08-29 13:51
閱讀 356·2019-08-23 18:38
閱讀 3223·2019-08-23 17:14
閱讀 630·2019-08-23 15:09