

很多 docker 初學者,在運行容器的時候,或者是寫第一個 dockerfile 的時候,問題最多的就是容器啟動后就停了,怎么看都覺得命令沒有問題,容器也沒有錯誤日志,dockerfile 也就那么幾條……其實你沒有錯,錯的是 docker,它執(zhí)行的太快了
這話怎么說呢,我拿 nginx 官方的 dockerfile 給你解釋下。

上面是 nginx 官方的 dockerfile 文件,我把set部分刪掉了,其他沒啥,主要看下CMD
為什么這里不是systemctl nginx start,或者/etc/init.d/nginx start,再或者 nginx 直接啟動,而是用 daemon off 的方式啟動?這是因為如果 nginx 用后臺模式運行,啟動的命令執(zhí)行完之后,這個啟動的命令就退出了,這個時候,容器也就跟著退出了。
又為什么命令執(zhí)行完,容器就退出了?這個要從 Linux 內(nèi)核說起。
在 Linux 操作系統(tǒng)中,當內(nèi)核初始化完畢之后,會啟動一個 init 進程,這個進程是整個操作系統(tǒng)的第一個用戶進程,所以它的進程 ID 為 1,也就是我們常說的 PID1 進程,然后所有的用戶態(tài)進程,都是這個進程的子進程,所以,整個系統(tǒng)的用戶進程,都是由init進程作為根進程的。Linux 內(nèi)核程序通過進程表對進程進行管理, 每個進程在進程表中占有一項,稱為進程表項,它記錄了進程的狀態(tài),打開的文件描述符等等一系統(tǒng)信息。當一個進程結束了運行或在半途中終止了運行,那么內(nèi)核就需要釋放該進程所占用的系統(tǒng)資源。這包括進程運行時打開的文件,申請的內(nèi)存等。但是,這里要注意的是,進程表項并沒有隨著進程的退出而被清除,它會一直占用內(nèi)核的內(nèi)存。為什么會有這么奇怪的行為呢?這是因為在某些程序中,我們必須明確地知道進程的退出狀態(tài)等信息,而這些信息的獲取是由父進程調(diào)用wait/waitpid而獲取的。設想這樣一種場景,如果子進程在退出的時候直接清除文件表項的話,那么父進程就很可能沒有地方獲取進程的退出狀態(tài)了,因此操作系統(tǒng)就會將文件表項一直保留至wait/waitpid 系統(tǒng)調(diào)用結束。僵尸進程指的是:進程退出后,到其父進程還未對其調(diào)用wait/waitpid之間的這段時間所處的狀態(tài)。一般來說,這種狀態(tài)持續(xù)的時間很短,所以我們一般很難在系統(tǒng)中捕捉到。但是,一些粗心的程序員可能會忘記調(diào)用wait/waitpid,或者由于某種原因未執(zhí)行該調(diào)用等等,那么這個時候就會出現(xiàn)長期駐留的僵尸進程了。如果大量的產(chǎn)生僵尸進程,其進程號就會一直被占用,可能導致系統(tǒng)不能產(chǎn)生新的進程。然后還有我們經(jīng)常會見到的一種情況,就是父進程先于子進程結束,這種情況多見于手動kill某個父進程的情況,這種情況就是下面要說到的。父進程先于子進程退出,那么子進程將成為孤兒進程。孤兒進程將被init進程(進程號為1)接管,并由init進程對它完成狀態(tài)收集(wait/waitpid)工作。PID1負責清理那些被拋棄的進程所留下來的痕跡,有效的回收的系統(tǒng)資源,保證系統(tǒng)長時間穩(wěn)定的運行了解了Linux 的 PID1,接著來看下容器中的 PID1 進程。熟悉docker都知道,docker容器并不是一個完整的linux的操作系統(tǒng),它也沒什么內(nèi)核初始化過程,更沒有像init(1)這樣的初始化過程。在docker容器中被標志為PID1的進程實際上就是一個普通的用戶進程,我們還拿nginx官方的鏡像起的容器來看。我用docker run -d nginx直接啟動的

可以看到,就是Dockerfile中指定的CMD那個進程,注意:如果你啟動容器的時候,指定了命令,會覆蓋CMD,也就是CMD是條默認啟動的命令參數(shù),如果啟動容器時指定了命令,會覆蓋,當Dockerfile中有多條CMD時,執(zhí)行最后一條
這個進程其實在宿主機上有一個普通的用戶進程ID

之所以在容器中PID變成1,是因為linux內(nèi)核提供的PID namespaces功能,如果宿主機上所有用戶進程構成了一個完整的樹形結構,那么PID namespaces實際上就是將這個CMD或ENTRYPOINT進程及其子進程作為另外一個分支,很顯然這部分也是一個樹形結構
當我們在宿主機上kill掉這個進程ID,那么整個容器便會處于退出狀態(tài)
這也就解釋了上面為什么命令執(zhí)行完之后,容器就退出了
認真的小伙伴從上面圖中看到了,我上面說linux中PID1進程為所有用戶進程的父進程,但是在容器里面,通過ps命令看到的進程的父進程都是“0”,這又是為什么呢?
前面提到,容器中的進程樹實際上是宿主機進程樹的一棵子樹,或者說分支,那么我們在宿主機上就可以找到這顆子樹的父進程。

我們可以看到,這個docker容器中PID 0的進程應該就是這個containerd-shim
我們結合docker的結構圖看一下

從架構圖中,我們可以看到 containerd-shim 進程下還有一個 runC 進程,但是我們在上面過程中,并沒有發(fā)現(xiàn) runC 這個進程。
runC 是 OCI 標準的一個參考實現(xiàn),而 OCI Open Container Initiative,是由多家公司共同成立的項目,并由 Linux 基金會進行管理,致力于 container runtime 的標準的制定和 runc 的開發(fā)等工作。runc,是對于 OCI 標準的一個參考實現(xiàn),是一個可以用于創(chuàng)建和運行容器的 CLI(command-line interface)工具。runc直接與容器所依賴的 cgroup/linux kernel 等進行交互,負責為容器配置cgroup/namespace 等啟動容器所需的環(huán)境,創(chuàng)建啟動容器的相關進程。事實上,Docker 容器的創(chuàng)建過程是這樣子的 docker-containerd-shim –> runC –> entrypoint,而我們看到的最終狀態(tài)是 docker-containerd-shim –> entrypoint,而runc進程創(chuàng)建完容器之后,自己就先退出去了,所以我們上面的過程中一直沒有出現(xiàn)。看到這里你應該了解,為什么你啟動容器或?qū)懞玫膁ockerfile,總是剛啟動就退出,而且沒有任何錯誤了吧!
來源:公眾號運維研習社
Linux學習指南
有收獲,點個在看 