在上一篇介紹過了docker create
以後,這篇來看看docker start
是怎麼根據create以後的結果運行容器的。html
在這裏咱們先啓動上一篇中建立的那個容器,而後看看docker都幹了些什麼。python
#根據容器名稱啓動容器(也能夠根據容器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_test
docker(client)發送啓動容器命令給dockerdlinux
dockerd收到請求後,準備好rootfs,以及一些其它的配置文件,而後經過grpc的方式通知containerd啓動容器docker
containerd根據收到的請求以及配置文件位置,建立容器運行時須要的bundle,而後啓動shim進程,讓它來啓動容器json
shim進程啓動後,作一些準備工做,而後調用runc啓動容器ubuntu
下面就來詳細的瞭解一下每一步都幹了些什麼。segmentfault
首先來看看dockerd收到客戶端的啓動容器請求後,作了些什麼。bash
dockerd作的第一件事情就是準備好容器運行時須要的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事先爲容器準備好這個目錄。
注意:除了日誌文件外,其它文件在每次容器啓動的時候都會自動生成,因此修改他們的內容後只會在當前容器運行的時候生效,容器重啓後,配置又都會恢復到默認的狀態
在什麼是容器的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" } }
在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啓動容器。
containerd主要功能是啓動並管理運行時的全部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來實現。
按道理來講若是容器裏面的全部進程屬於一個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
啓動容器。
runc會被調用兩次,第一次是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命令乾的活不少,這裏只是介紹了大概的流程和涉及的進程和文件,還有一些其餘東西並無涉及到,好比存儲插件和網絡,後續在專門介紹相關部分的時候再詳細介紹。