昨天我們討論到 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,他的紀錄也會被移除掉而不再可見了。
到這邊,我們可以來調整一下昨天的那張圖:
看來每個 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 給大家參考:
根據上表,可以看到此時作為 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,他也會負擔起一樣的重責大任嗎?