iT邦幫忙

2022 iThome 鐵人賽

DAY 17
0

昨天我們討論到 Linux 上的 process 都是由現有的 process 去 fork 出來的,且這些 processes 之間會有父子關係。而當一個 process 終止時,他會先進入到一個叫做 defunct 的狀態,defunct process 通常也稱為 zombie process,這種狀態下的 process 雖然是終止了,但在 Linux 中仍然可見(所以叫做殭屍,是不是蠻貼切的呢?)這種狀態會持續到這個 process 的 parent process 讀取了他的狀態變化,也就是昨天討論的 parent process 會用 waitpid 在那邊等待 child process 的狀態變化。一但有了變化,parent procss 就會回收掉(reap)這個 child process,他的紀錄也會被移除掉而不再可見了。

到這邊,我們可以來調整一下昨天的那張圖:
https://ithelp.ithome.com.tw/upload/images/20221002/20151857ZWocij7hmQ.png

看來每個 child process 在從紀錄中被移除之前,都會短暫地先成為殭屍,殭屍聽起來有點恐怖,但大部分的時候沒有什麼問題,當他的 parent process 收到狀態變化的通知,然後系統就會把這個殭屍給 reap 掉了。不過,還記得我們昨天問的兩個問題嗎?現在就讓我們來試試看能不能做出昨天提出的那兩種情況。


第一種情境: parent process 不呼叫 waitpid 來等待 child process 的變化並且釋放資源。

為了達成這個情境,我基於昨天的程式碼修改了一下,很簡單,就是把 waitpid 註解掉,然後用一個簡單的 while 加上 sleep,好讓 parent process 一直活著。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>

char* const argv_list[] = {
  "/bin/bash",
  NULL
};

int main(void)
{
  printf("current PID: %d\n", getpid());

  pid_t pid = fork();

  if (pid == -1) {
    // pid == -1: error occurred
    printf("can't fork, error occurred\n");
    exit(EXIT_FAILURE);
  } else if (pid == 0) {
    // pid == 0: child process created
    printf("I am child process with PID: %d\n", getpid());
    execv(argv_list[0], argv_list);
    exit(0);
  } else {
    // pid > 0: a positive number is returned for the pid of parent process
    printf("I am parent process with PID %d and my child is %d\n", getpid(), pid);
    //waitpid(pid, NULL, 0);
    //printf("Parent - container stopped!\n");
    // 不呼叫 waitpid, 且用 sleep 讓 parent 保持執行
    while(1) {
      sleep(1);
    }
  }

  return 0;
}

編譯且執行一下:

ubuntu@ip-xxx:~/fork$ gcc -o zombie-test zombie-example.c

ubuntu@ip-xxx:~/fork$ sudo ./zombie-test
current PID: 74149
I am parent process with PID 74149 and my child is 74150
I am child process with PID: 74150
root@ip-xxx:/home/ubuntu/fork#

先停在這邊觀察一下,目前 parent process 是 74149,child process 是 74150,我們用另外一個 terminal 觀察一下:

ubuntu@ip-xxx:~$ ps -eaf -o pid,ppid,stat,comm
    PID    PPID STAT COMMAND
  ...略
  73330   73329 Ss   bash
  74148   73330 S     \_ sudo
  74149   74148 S         \_ zombie-test
  74150   74149 S+            \_ bash

這次的輸出多加上一個 stat,這邊補充一下 process state codes 給大家參考:
https://ithelp.ithome.com.tw/upload/images/20221002/20151857gScbUwScsA.png

根據上表,可以看到此時作為 parent prcess 的 zombie-test (74149) 或是作為 child procss 的 bash (74150) 都是 S 狀態。

回到執行測試程式的那個 terminal,並且執行 exit:

root@ip-xxx:/home/ubuntu/fork# exit
exit

此時會回到 parent process,但因為 parent process 有 while sleep 在,所以不會結束。到另外一個 termial 去觀察一下:

ubuntu@ip-xxx:~$ ps -eaf -o pid,ppid,stat,comm
    PID    PPID STAT COMMAND
  ...略
  73330   73329 Ss   bash
  74148   73330 S+    \_ sudo
  74149   74148 S+        \_ zombie-test
  74150   74149 Z+            \_ bash <defunct>

這邊可以看到,作為 child process 的 bash 狀態已經變成 Z 了,bash 後面也被加上了 defunct 的註記,而剛剛給的狀態碼上對 Z 的定義正是:

defunct ("zombie") process, terminated but not reaped by its parent

這時候如果我們停掉 parent process,那這個已經變成 zombie 的 child process 也會被 reap 掉。


第二種情境: 在 child process 終止之前,parent process 就先中止了。

一樣我基於昨天的程式碼來做調整,在 child process 被 fork 後,就不透過 execv 去執行程式了,直接給他 sleep,然後在 parent process 這邊,也不呼叫 waitpid,而是直接 return 離開。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>

int main(void)
{
  printf("current PID: %d\n", getpid());

  pid_t pid = fork();

  if (pid == -1) {
    // pid == -1: error occurred
    printf("can't fork, error occurred\n");
    exit(EXIT_FAILURE);
  } else if (pid == 0) {
    // pid == 0: child process created
    printf("I am child process with PID: %d\n", getpid());
    sleep(2000);
    exit(0);
  } else {
    // pid > 0: a positive number is returned for the pid of parent process
    printf("I am parent process with PID %d and my child is %d\n", getpid(), pid);
    //waitpid(pid, NULL, 0);
    printf("Parent - container stopped!\n");
  }

  return 0;
}

編譯且執行:

ubuntu@ip-xxx:~/fork$ sudo ./orphan-test
current PID: 74260
I am parent process with PID 74260 and my child is 74261
Parent - container stopped!
I am child process with PID: 74261
ubuntu@ip-xxx:~/fork$

parent process 的 PID 是 74260, child process 是 74261,程式也如預期的,一執行就離開。此時來用 ps 觀察一下:

ubuntu@ip-xxx:~$ ps -eaf -o pid,ppid,stat,comm
    PID    PPID STAT COMMAND
  ...略
  73330   73329 Ss+  bash
  74261       1 S    orphan-test

這個 74261 是我們剛剛做出來的 child process,他的 PPID 原本應該是 74260,但因為 74260 已經終止了,所以他的 PPID 變成了 1。


那問題又來了,PID 1 到底是何方神聖呢?

當我們啟動 Linux,kernel 初始化完成後第一個啟動的 process 會是一個 init process,他也是作業系統 user space 中的第一個 process,其 PID 為 1,也會被稱為 super process,他會將整個作業系統帶入可以操作的狀態,並且負責建立其他 process,而這些 process 會再去建立其他 process,也就是,這個 PID 1 的 process 會是 linux 上其他所有 process 的祖先。

讓我們看一下目前實驗環境的這台 Ubuntu 20.04 的 PID 1 是誰:

ubuntu@ip-xxx:~$ ps -eaf
UID          PID    PPID  C STIME TTY          TIME CMD
root           1       0  0 Sep18 ?        00:00:16 /sbin/init
...略

ubuntu@ip-xxx:~$ ls -al /sbin/init
lrwxrwxrwx 1 root root 20 Apr 21 12:54 /sbin/init -> /lib/systemd/systemd

根據上述的觀察,我們的 PID 1 是 systemd,這應該是目前常見 linux 發行版的預設 init system 了,有興趣的可以參考 man page 的說明。

回到我們剛剛討論的第二個情境,當一個 child process 尚未執行結束,但其 parent process 已經結束時,這個 child process 會被我們稱為孤兒 orphan,這名稱也依舊很貼切,不過很幸運的是 PID 1 的 init process 作為大家的祖先,他會接管這個 orphan process,成為他的 parent process,那當這個 orphan process 結束時,此時作為 parent 的 init process 也會做出相對應的處理。

第一種情境中的 zombie process 也是如此,當某一個 child process 成為 zombie 後,當其原本的 parent process 結束後,init process 也會接收這個 zombie process,並且好好地 reap 這個 zombie process,使其不再可見。

由上述的討論可知,Linux 作業系統中 PID 1 的這個 process 責任是很重大的,那我們在 container 中觀察到,作為這個 container 啟動程式的 process 在其新的 PID namespace 的中 PID 也會是 1,他也會負擔起一樣的重責大任嗎?


上一篇
Day 16: process 的族譜
下一篇
Day 18: container 中 PID 1 process 的 parent 是誰呢?
系列文
那些關於 docker 你知道與不知道的事32
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言