程序最後的遺言

2022-10-28 06:00:48

程序最後的遺言

前言

在本篇文章當中主要給大家介紹父子程序之間的關係,以及他們之間的互動以及可能造成的狀態,幫助大家深入理解父子程序之間的關係,以及他們之間的互動。

殭屍程序和孤兒程序

殭屍程序

在 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,我們下期再見!!!更多精彩內容合集可存取專案:https://github.com/Chang-LeHung/CSCore

關注公眾號:一無是處的研究僧,瞭解更多計算機(Java、Python、計算機系統基礎、演演算法與資料結構)知識。