孤兒程序與終端的關係

2022-12-05 18:01:43

孤兒程序與終端的關係

孤兒程序

在本篇文章當中主要給大家介紹一下有關孤兒程序和終端之間的關係。

首先我們的需要知道什麼是孤兒程序,簡單的來說就是當一個程式還在執行,但是他的父程序已經退出了,這種程序叫做孤兒程序,因為父程序死亡了,因此被叫做孤兒程序。孤兒程序會被程序號等於 1 的 init 程序收養,也就是說他的新的父程序的程序號等於 1,我們可以使用下面的程式碼進行驗證:


#include <stdio.h>
#include <unistd.h>
#include <err.h>

int main()
{
  pid_t pid = fork();
  if(pid == -1)
  {
    perror("fork:");
  }
  printf("pid = %d ppid = %d\n", getpid(), getppid());
  if(pid == 0)
  {
    do
    {
      sleep(1);
      printf("pid = %d ppid = %d\n", getpid(), getppid());
    } while (1);
    
  }

  return 0;
}

上面的程式的輸出結果如下所示:

➜  daemon git:(master) ✗ ./orphan.out 
pid = 25835 ppid = 25251
pid = 25836 ppid = 25835
➜  daemon git:(master) ✗ pid = 25836 ppid = 1
pid = 25836 ppid = 1
pid = 25836 ppid = 1
pid = 25836 ppid = 1
pid = 25836 ppid = 1

從上面終端的輸出結果我們可以知道當父程序還沒有退出的時候,子程序 25836 的父程序號還是 25835,但是當父程序退出之後,子程序的父程序號已經變成了 1 ,也就是 init 程序,這與我們在上面的分析的結果表現是一致的。

孤兒行程群組和終端的關係

在前面我們提到了當一個程序的父程序退出之後,這個程序就會變成一個孤兒程序,其實與孤兒程序對應的有一個孤兒行程群組,所謂孤兒行程群組就是:一個行程群組當中的程序要麼是孤兒程序,要麼父程序也在這個行程群組當中,要麼父程序在其他的對談當中,滿足上述條件的行程群組就是孤兒行程群組。

事實上核心在一種情況下也會傳送 SIGHUP 訊號給孤兒行程群組,這個情況如下:

  • 父程序程式執行完成退出了,但是子程序還沒有結束,而且被掛起了。
  • 當父程序退出的時候,shell 會在他內部維護的作業列表(job list)將退出的作業刪除掉。子程序被 init 程序收養變成了孤兒程序,根據上文當中提到了孤兒行程群組的概念,這是一個孤兒行程群組,因為這個行程群組當中的所有程序(如果不是多程序程式,只有一個)的父程序要麼退出,要麼父程序也在這個行程群組當中,要麼父程序在其他的對談當中。條件已經滿足,因此這個行程群組是一個孤兒行程群組。
  • 現在有一個問題是,這個被掛起的孤兒程序沒有誰去喚醒啊,理論上來說,在父程序退出之前,子程序被掛起,應該是父程序去收拾好這個爛攤子,但是現在父程序退出了,被掛起的孤兒程序沒有在執行,那麼就一直會佔著他對應的系統資源,如果這樣的程序過多的話,那麼系統的資源將會被消耗殆盡。

因此為了解決這種問題:如果一個行程群組變成了孤兒行程群組並且擁有已停止執行的成員,比如說被掛起來的程序,那麼核心會向行程群組中的所有成員傳送一個 SIGHUP 訊號通知它們已經與對談斷開連線了,之後再傳送一個 SIGCONT 訊號確保它們恢復執行。如果孤兒行程群組不包含被停止的成員,那麼就不會傳送任何訊號。

我們可以使用下面的例子去驗證這一點:

#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

void sig(int no, siginfo_t *info, void* context)
{
  char s[1024];
  int fd = open("text.txt", O_RDWR | O_CREAT, 0644);
  sprintf(s, "No = %d pid = %d\n", no , info->si_pid);
  write(fd, s, strlen(s));
  close(fd);
  sync();
  _exit(0);
}

int main()
{
  struct sigaction action;
  action.sa_sigaction = sig;
  action.sa_flags |= SA_SIGINFO;
  action.sa_flags |= ~(SA_RESETHAND);
  sigaction(SIGHUP, &action, NULL);
  pid_t pid = fork();
  if(pid == -1)
  {
    perror("fork:");
  }
  if(pid != 0)
  {
    kill(pid, SIGSTOP); // 父程序執行 給子程序傳送 SIGSTOP 訊號 讓子程序停止執行
  }
  else
  {
    while(1); // 子程序執行
  }
  sleep(1);
  return 0;
}

在上面的程式當中,我們 fork 出一個子程序,然後子程序不斷的進行死迴圈,父程序會給子程序傳送一個 SIGSTOP 訊號,然後子程序會停止執行,因此父程序退出之後,那麼子程序就會成為孤兒程序,子程序所在的行程群組就會變成孤兒行程群組,在這種情況下核心就會傳送一個 SIGHUP 訊號給這個孤兒程序,同時也會傳送一個 SIGCONT 訊號,保證孤兒程序在執行,而不是被掛起。我們在終端當中執行上面的程式會發現 text.txt 當中的內容如下所示:

No = 1 pid = 0

pid = 0 表示這個訊號是核心傳送的,傳送的訊號為 1 ,對應的訊號名字為 SIGHUP,因此這驗證了我們在上面所談到的內容。

孤兒行程群組和終端的讀和寫

在前面的文章當中我們已經談到了,當後臺程序試圖從控制終端中呼叫 read()時將會收到 SIGTTIN 訊號,當後臺程序試圖向設定了 TOSTOP 標記的控制終端呼叫 write()時會收到 SIGTTOU 訊號。但向一個孤兒行程群組傳送這些訊號毫無意義,因為一旦被停止之後,它將再也無法恢復了。基於此,在進行 read()和 write()呼叫時核心會返回 EIO 的錯誤,而不是傳送 SIGTTIN 或 SIGTTOU 訊號。基於類似的原因,如果 SIGTSTP、SIGTTIN 以及 SIGTTOU 訊號的分送會導致停止孤兒行程群組中的成員,那麼這個訊號會被毫無徵兆地丟棄。這種行為不會因為訊號傳送方式(如訊號可能是由核心產生的或由顯式地呼叫 kill()而傳送)的改變而改變。

總結

在本篇文章當中主要給大家介紹了關於孤兒程序和孤兒行程群組的相關知識,總體來說比之前的兩篇文章相對簡單一點。


以上就是本篇文章的所有內容了,我是LeHung,我們下期再見!!!更多精彩內容合集可存取專案:https://github.com/Chang-LeHung/CSCore

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