在unix/linux系統中,正常情況下,子程序是通過父程序fork建立的。子程序的結束和父程序的運作是一個異步過程,即父程序永遠無法預測子程序到底什麼時候結束。 當一個程序完成它的工作終止之後,它的父程序需要調用wait()或者waitpid()系統調用取得子程序的終止狀态。
父程序先于子程序退出,那麼子程序将成為孤兒程序。孤兒程序将被init程序(程序号為1)接管,并由init程序對它完成狀态收集(wait/waitpid)工作。
運作結果如圖: 父程序退出後,子程序的父程序(ppid)變為1,被init程序接管

子程序退出,而父程序并沒有調用wait或waitpid擷取子程序的狀态資訊,那麼子程序的程序描述符仍然儲存在系統中,這種程序稱之為僵屍程序
運作結果如圖:子程序(pid=2158)成為了僵屍程序
在每個程序退出的時候,核心釋放該程序所有的資源,包括打開的檔案,占用的記憶體等。 但是仍然為其保留一定的資訊(包括程序号、退出狀态、運作時間等)。直到父程序通過wait / waitpid來取時才釋放。 如果父程序不調用wait / waitpid的話, 那麼保留的那段資訊就不會釋放,其程序号就會一直被占用,系統所能使用的程序号是有限的,如果大量的産生僵屍程序,可能導緻系統不能産生新的程序.
在docker容器中運作的程序,一般是沒有init程序的。可以進入容器使用 ps 檢視,會發現 pid 為 1 的程序并不是 init,而是容器的主程序。如果容器中産生了孤兒程序,誰來接管這個程序?
找到相同線程組裡其它可用線程
沿着它的程序樹向祖先程序找一個最近的child_subreaper并且運作着的程序
該namespace下程序号為1的程序
docker daemon從1.11版後從架構上發生了比較大的變化,由原來的一個子產品拆分為4個獨立的子產品:engine、containerd、runc、containerd-shim,将容器的生命周期管理交給containerd, containerd再使用runc運作容器。
架構上的變化也改變了docker容器運作時的程序樹的結構,這裡運作一個簡單的docker鏡像,并通過<code>ps xf -o pid,ppid,stat,args</code>檢視程序樹,從程序樹中也可以看出docker daemon架構的變化
docker 1.11之後
docker 1.11之前
準備兩個檔案parent.sh、child.sh
運作docker,此時sleep程序的為容器首程序,pid為1
進入容器,并運作parent.sh
在容器中通過<code>ps xf -o pid,ppid,stat,args</code>檢視程序樹可以看到程序結構如下, sleep作為容器啟動指令,它的程序号為1,根據上一節關于linux接收孤兒程序的描述,當沒有其他符合條件的程序接收時,該程序就會成為孤兒程序的接收者
接下來通過<code>kill -9</code>殺死運作parent.sh的程序,此時運作child.sh的程序就成為了孤兒程序,這個時候docker容器是如何處理孤兒程序的接收的呢?docker 1.11之前和之後版本的處理是有所差別的
先來看下docker 1.11版之前容器内的程序樹(如下圖),可以看到運作child.sh的程序的父程序變為了1(sleep程序)
再來看下docker 1.11版之後版本容器内的程序樹(如下圖),可以看到child.sh程序的父程序變成了0,與sleep處于同一個層級,那麼是誰接收了這個孤兒程序呢?
此時需要檢視主機的程序樹才能确定孤兒程序到底是被誰接收了,在主機上運作<code>ps xf -o pid,ppid,stat,args</code>,結果如下圖
docker1.11版本之前孤兒程序是由容器内pid為1的程序接收,而1.11版本後是由docker-containerd-shim程序接收
關于僵屍程序的概念以及産生的原因上面已經闡述過了,僵屍程序是指子程序退出,而父程序并沒有調用wait或waitpid擷取子程序的狀态資訊,那麼子程序的程序描述符仍然儲存在系統中。我們這裡隻讨論docker中的孤兒程序機制是否會導緻僵屍程序的産生,這個也是docker早期版本被诟病的問題。
1.11版本前,孤兒程序是被容器内pid為1的程序所接收。上面關于孤兒程序的實驗中,容器中pid為1的程序為sleep程序,而sleep程序是不會對子程序退出進行wait/waitpid操作的,是以我們kill掉child.sh程序就會産生僵屍程序(如下圖)
上圖可以看到運作child.sh的程序和sleep程序都成為了僵屍程序,這裡sleep程序成為僵屍程序是由于sleep程序是child.sh的子程序,當child.sh退出時,sleep程序成為了孤兒程序并被pid為1的sleep程序所接收,當sleep運作結束時(這裡運作的是sleep 10)退出,pid為1的sleep程序不進行wait/waitpid操作,就使得sleep程序成為僵屍程序
1.11版本後,孤兒程序是被docker-containerd-shim程序接收,如果docker-containerd-shim在子程序退出時調用wait/waitpid就不會産生僵屍程序,反之就會産生僵屍程序。這裡也進行相同的操作,kill掉運作child.sh的程序,結果如下圖
從結果上看child.sh和sleep(child.sh的子程序)程序都正常退出(程序樹上看不到),并沒有産生僵屍程序。是以docker-containerd-shim會在子程序退出時調用wait/waitpid。從源碼中看下docker-containerd-shim的處理
docker1.11之前的版本,孤兒程序是否有可能成為僵屍程序取決于容器内pid為1的程序是否在子程序退出時調用wait/waitpid, docker1.11版本之後孤兒程序不會成為僵屍程序