進程最後的遺言
進程最後的遺言
前言
在本篇文章當中主要給大家介紹父子進程之間的關係,以及他們之間的交互以及可能造成的狀態,幫助大家深入理解父子進程之間的關係,以及他們之間的交互。
殭屍進程和孤兒進程
殭屍進程
在 Unix 作業系統和類 Unix 作業系統當中,當子進程退出的時候,父進程可以從子進程當中獲取子進程的退出資訊,因此在 類 Unix 作業系統當中只有父進程通過 wait 系統調用讀取子進程的退出狀態資訊之後,子進程才會完全退出。那麼子進程在程式執行完成之後(調用 _exit 系統調用之後),到父進程執行 wait 系統調用獲取子進程退出狀態資訊之前,這一段時間的進程的狀態是殭屍進程的狀態。
正式定義:在 Unix 或者類 Unix 作業系統當中,殭屍進程就是哪些已經完成程式的執行(完成_exit 系統調用退出了程式),但是在內核當中還有屬於這個進程的進程表項。這個表項的作用主要是讓父進程讀取子進程的退出狀態資訊 (exit status)。
在後文當中我們有一個例子詳細分析這個退出狀態的相關資訊。一旦父進程通過 wait 系統調用讀取完子進程的 exit statis 資訊之後,殭屍進程的進程表項就會從進程表(process table)當中被移除,這個進程就算是徹底消亡了(reaped)。
如果系統當中有很多處於殭屍狀態的進程而父進程又沒有使用 wait 系統調用去得到子進程的退出狀態,那麼系統當中就會有很多記憶體沒有被釋放,這就會導致資源泄露。
下面是一個殭屍進程的例子,對應的程式碼名稱為 Z.c:
#include <stdio.h>
#include <unistd.h>
int main() {
printf("parent pid = %d\n", getpid());
if(fork()) {
while(1);
}
printf("child process pid = %d\n", getpid());
return 0;
}
上面C語言對應的python程式碼如下:
import os
if __name__ == "__main__":
print(f"parent pid = {os.getpid()}")
pid = os.fork()
if pid != 0:
# parent process will never exit
while True:
pass
# child process will exit
print(f"child process pid = {os.getpid()}")
現在執行上面的程式,得到的結果如下所示:
從上圖當中我們可以看到父進程一直在進行死循環的操作,而子進程退出了程式,而現在應該處於殭屍進程狀態。而我們通過 ps 命令得到的進程狀態的結果,根據進程號得到子進程的狀態為 Z+
,這個狀態就表示這個進程就是一個殭屍進程。我們在這裡再簡要談一下命令 ps 對進程的狀態的各種表示:
STAT
當中字母的含義表:
條目 | 含義 |
---|---|
D | 表示不能夠被中斷的睡眠操作,比如說IO操作 |
I | 內核當中的空閑執行緒 |
R | 正在執行或者處於就緒隊列當中的進程 |
S | 可以被中斷的睡眠,一般是等待某個事件觸發 |
T | 被其他的進程發送的訊號給停下來了 |
t | 被調試或者tracing中 |
Z | 表示這個進程是一個殭屍進程 |
< | 表示高優先順序 |
N | 表示低優先順序 |
L | 有頁面被所在記憶體當中,也就是說這個頁面不會被作業系統換出道對換區當中 |
s | 表示這個進程是一個 session leader |
l | 是一個多執行緒程式 |
+ | 表示在前台進程組當中 |
大家可以根據上表當中的內容對應一下程式的狀態,就發現子進程目前處於殭屍狀態。
孤兒進程
孤兒進程:當一個進程還在執行,但是他的父進程已經退出了,那麼這個進程就變成了一個孤兒進程,然後他會被 init 進程(進程ID=1)”收養”,然後 init 進程會調用 wait 系統調用回收這個進程的資源。
下面是一個孤兒進程的例子,我們可以看看子進程的父進程的輸出是什麼:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
if(fork()) {
sleep(1);
// 父進程退出
exit(0);
}
while(1) {
printf("pid = %d parent pid = %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
對應的python程式碼如下:
import os
import time
if __name__ == "__main__":
pid = os.fork()
if pid == 0:
while True:
print(f"pid = {os.getpid()} parent pid = {os.getppid()}")
time.sleep(1)
程式執行結果如下所示:
可以看到子進程的父進程發生了變化,當父進程退出之後,子進程的父進程變成了 init 進程,進程號等於1。
wait系統調用
waitpid 及其參數分析
在前文當中我們主要談到了兩種比較不一樣的進程,其中更是主要談到了 wait 系統調用對於殭屍進程的重要性。在 linux 當中與 wait 相關的主要有下面兩個系統調用:
pid_t waitpid(pid_t pid, int *wstatus, int options);
pid_t wait(int *wstatus);
其中 wait 系統調用是 waitpid 系統調用的一個特例,我們首先解釋一下 waipit 系統調用。上面兩個系統調用主要是用於等待子進程的狀態變化的,並且從子進程當中取出額外的狀態資訊(status information)。只有當子進程的狀態發生變化了 wait 系統調用才能返回,主要有以下幾種狀態:
- 子進程結束了。
- 子進程被別的進程發送的訊號停止運行了(SIGSTOP和SIGTSTP可以讓一個進程被掛起停止執行)。
- 停止運行的進程被訊號喚醒繼續執行了(SIGCONT可以喚醒進程繼續執行)。
當有子進程出現上面的狀態的時候 wait 或者 waitpid 系統調用會馬上返回,否則 wait 或者 waitpid 系統調用就會一致阻塞。waitpid 的幾個參數:
- pid:
- pid < -1 表示等待任何進程組 -pid 當中的該進程的子進程。
- pid == -1 表示等待任何一個子進程。
- pid == 0 表示等待子進程,這些子進程的進程組號(process group id) 等於這個進程(調用 waitpid 函數的這個進程)的進程號。
- pid > 0 表示等待進程號等於 pid 的子進程。
- options:
- WNOHANG:如果 options 等於這個值的話,表示如果還沒有子進程結束執行就立即返回,不進行等待。
- WUNTRACED:如果子進程被其他進程發送的訊號 stop 了,wait 函數也返回。
- WCONTINUED:如果子進程被其他進程發送的訊號(SIGCONT)恢復執行了,wait 函數也返回。
- 根據上面的分析, waitpid(-1, &wstatus, 0) == wait( &wstatus)
- wstatus:是我們傳入給 wait 或者 waitpid 函數的一個指針,系統調用會將子進程的很多狀態資訊放入到 wstatus 指針指向的數據當中,我們可以使用下面的一些宏去判斷一些資訊。
- WIFEXITED(wstatus):如果子進程正常退出(使用 exit 、_exit或者直接從main函數返回)這行程式碼就返回為 true。
- WEXITSTATUS(wstatus):這個宏主要是返回程式退出的退出碼,關於退出碼的內容,可以參考這篇文章Shell揭秘——程式退出狀態碼。
- WIFSIGNALED(wstatus):表示子進程是否是其他進程發送訊號導致程式退出的。
- WTERMSIG(wstatus):如果子進程是其他進程發送訊號導致程式退出的話,我們可以使用這個宏去得到具體的訊號值。
- WCOREDUMP(wstatus):表示子進程是否發生 core dump 然後退出的。
- WIFSTOPPED(wstatus):當子進程接收到了一個其他進程發送的訊號導致程式被掛起,這個宏就返回 true。
- WSTOPSIG(wstatus):返回掛起訊號的具體的訊號值。
- WIFCONTINUED(wstatus): 返回 true 如果子進程接收到了一個SIGCONT訊號恢復程式的執行。
示例說明
下面是一個幾乎綜合了上面所有的資訊的一個例子,我們仔細看看這個程式的輸出:
#include <sys/wait.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t cpid, w;
int wstatus;
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { /* Code executed by child */
printf("Child PID is %jd\n", (intmax_t) getpid());
if (argc == 1)
pause(); // 子進程會在這裡等待接收訊號 直到訊號的處理函數返回 /* Wait for signals */
_exit(atoi(argv[1]));
} else { /* Code executed by parent */
do {
w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED);
if (w == -1) {
perror("waitpid");
exit(EXIT_FAILURE);// EXIT_FAILURE 是一個宏 等於 1
}
// 程式是否是正常退出
if (WIFEXITED(wstatus)) {
printf("exited, status=%d\n", WEXITSTATUS(wstatus));
} else if (WIFSIGNALED(wstatus)) { // 是否是被訊號殺死
printf("killed by signal %d\n", WTERMSIG(wstatus));
} else if (WIFSTOPPED(wstatus)) { // 是否被 stop
printf("stopped by signal %d\n", WSTOPSIG(wstatus));
} else if (WIFCONTINUED(wstatus)) { // 是否處於 stop 狀態再被喚醒
printf("continued\n");
}
// 判斷程式是否退出 正常退出和因為訊號退出 如果程式是退出了 父進程就退出 while 循環
} while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus));
exit(EXIT_SUCCESS); // EXIT_SUCCESS 是一個宏 等於 0
}
}
在上圖的例子當中,我們在上面的終端先執行 wait.out 程式,然後在下面一個終端我們首先發送一個 SIGSTOP 訊號給子進程,先讓進程停下來,然後在發送一個 SIGCONT 訊號讓進程繼續執行,最後發送了一個 SIGKILL 訊號讓子進程退出,可以看到我們上面提到的宏操作都一一生效了,在父進程當中訊號和退出狀態都一一接受了。
我們再來演示一個 coredump 的例子:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
if(fork()) {
int s;
wait(&s);
if(WCOREDUMP(s)) {
printf("core dump true\n");
}
}else{
int a = *(int*)NULL;
}
}
子執行緒解引用NULL會造成segmentation fault (core dump),然後父進程接收子進程的退出狀態,然後對狀態進行判斷,是否是 coredump 導致的退出:
我們在父進程對子進程的退出碼進行了判斷,如果子進程退出的原因是 core dump 的話就進行列印輸出,而在上面的程式輸出當中我們看到了程式進行了輸出,因此父進程可以判斷子進程是因為 core dump 而退出程式的。
從子進程獲取系統資源資訊
出了上面所談到的等在子進程退出的方法之外,我們還有可以獲取子進程執行時候的狀態資訊,比如說運行時佔用的最大的記憶體空間,進程有多少次上下文切換等等。主要有下面兩個系統調用:
pid_t wait3(int *wstatus, int options,
struct rusage *rusage);
pid_t wait4(pid_t pid, int *wstatus, int options,
struct rusage *rusage);
其中 3 和 4 表示對應的函數的參數的個數。在上面的兩個函數當中有一個比較重要的數據類型 struct rusage
,我們看一下這個結構體的內容和對應欄位的含義:
struct rusage {
struct timeval ru_utime; /* user CPU time used */ // 程式在用戶態的時候使用了多少的CPU時間
struct timeval ru_stime; /* system CPU time used */ // 程式在內核態的時候使用了多少CPU時間
long ru_maxrss; /* maximum resident set size */ // 使用的記憶體的峰值 單位 kb
long ru_ixrss; /* integral shared memory size */ // 暫時沒有使用
long ru_idrss; /* integral unshared data size */ // 暫時沒有使用
long ru_isrss; /* integral unshared stack size */ // 暫時沒有使用
long ru_minflt; /* page reclaims (soft page faults) */ // 沒有IO操作時候 page fault 的次數
long ru_majflt; /* page faults (hard page faults) */ // 有 IO 操作的時候 page fault 的次數
long ru_nswap; /* swaps */ // 暫時沒有使用
long ru_inblock; /* block input operations */ // 文件系統寫操作次數
long ru_oublock; /* block output operations */ // 文件系統讀操作次數
long ru_msgsnd; /* IPC messages sent */// 暫時沒有使用
long ru_msgrcv; /* IPC messages received */ // 暫時沒有使用
long ru_nsignals; /* signals received */ // 暫時沒有使用
long ru_nvcsw; /* voluntary context switches */ // 主動上下文切換的次數
long ru_nivcsw; /* involuntary context switches */ // 非主動上下文切換的次數
};
下面我們用一個例子看看是如何從子進程當中獲取子進程執行時候的一些詳細數據資訊的:
下面是子進程的程式碼,我們在 fork 之後使用 execv 載入下面的程式:
#include <stdio.h>
int main() {
printf("hello world\n");
return 0;
}
父進程程式碼:
#include <sys/time.h>
#include <stdio.h>
#include <sys/resource.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char* argv[]) {
if(fork() != 0) {
struct rusage usage; // 定義一個統計資源的結構體
int pid = wait4(-1, NULL, 0, &usage); // 將這個結構體的地址傳入 好讓內核能講對應的資訊存在指針所指向的地方
// 列印記憶體使用的峰值
printf("pid = %d memory usage peek = %ldkb\n", pid, usage.ru_maxrss);
}else {
execv("./getrusage.out", argv);
}
return 0;
}
上面程式執行結果如下圖所示:
可以看出我們得到了記憶體使用的峰值,其實我們還可以使用 time 命令去查看一個進程執行時候的這些數據值。
從上面的結果可以看到使用 time 命令得到的結果和我們自己使用程式得到的結果是一樣的,這也從側面驗證了我們的程式。如果要達到上面的效果的話,需要注意使用絕對地址命令,因為 time 是shell的保留字。
總結
在本篇文章當中主要給大家詳細介紹了殭屍進程、孤兒進程、父進程從子進程獲取進程退出資訊,以及他們形成的原因,並且使用實際的例子進行了驗證,這一部分知識練習比較緊密,希望大家有所收穫!
以上就是本篇文章的所有內容了,我是LeHung,我們下期再見!!!更多精彩內容合集可訪問項目://github.com/Chang-LeHung/CSCore
關注公眾號:一無是處的研究僧,了解更多電腦(Java、Python、電腦系統基礎、演算法與數據結構)知識。