孤兒程式與終端的關係

一無是處的研究僧發表於2022-12-05

孤兒程式與終端的關係

孤兒程式

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

首先我們的需要知道什麼是孤兒程式,簡單的來說就是當一個程式還在執行,但是他的父程式已經退出了,這種程式叫做孤兒程式,因為父程式死亡了,因此被叫做孤兒程式。孤兒程式會被程式號等於 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、計算機系統基礎、演算法與資料結構)知識。

相關文章