Manjusaka

Manjusaka

簡單聊聊進程中的信號處理

最近在某個技術群裡幫人分析了 Linux 編程下信號處理的一段代碼。我自己覺得這段代碼是挺不錯的一個例子,所以寫個簡單的水文,用這段代碼聊聊 Linux 中的信號處理

正文#

我們首先來看一看這一段代碼

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

void deletejob(pid_t pid) { printf("delete task %d\n", pid); }

void addjob(pid_t pid) { printf("add task %d\n", pid); }

void handler(int sig) {
  int olderrno = errno;
  sigset_t mask_all, prev_all;
  pid_t pid;
  sigfillset(&mask_all);
  while ((pid = waitpid(-1, NULL, 0)) > 0) {
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    deletejob(pid);
    sigprocmask(SIG_SETMASK, &prev_all, NULL);
  }
  if (errno != ECHILD) {
    printf("waitpid error");
  }
  errno = olderrno;
}

int main(int argc, char **argv) {
  int pid;
  sigset_t mask_all, prev_all;
  sigfillset(&mask_all);
  signal(SIGCHLD, handler);
  while (1) {
    if ((pid = fork()) == 0) {
      execve("/bin/date", argv, NULL);
    }
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    addjob(pid);
    sigprocmask(SIG_SETMASK, &prev_all, NULL);
  }
}

實際上這段代碼是比較典型的信號處理的代碼,為了引出後續的內容,我們先來復習一下,這段代碼中幾個關鍵的 syscall

  1. signal1: 信號處理函數,使用者可以通過這個函數為當前進程指定具體信號的 Handler。當信號觸發時,系統會調用具體的 Handler 進行對應的邏輯處理。
  2. sigfillset2: 用於操作 signal sets(信號集)的函數之一,這裡的含義是將系統所有支持的信號量添加進一個信號集中
  3. fork3: 大家比較熟悉的一個 API 了,創建一個新的進程,並返回 pid 。如果是在父進程中,返回的 pid 是對應子進程的 pid。如果子進程中,pid 為 0
  4. execve4: 執行一個特定的可執行文件
  5. sigprocmask5:設置進程的信號屏蔽集。當傳入第一個參數為 SIG_BLOCK 時,函數會將當前進程的信號屏蔽集保存在第三個參數傳入的信號集變量中,並將當前進程的信號屏蔽集設置為第二個參數傳入的信號屏蔽集。當第一個參數為 SIG_SETMASK 時,函數會將當前進程的信號屏蔽集設置為第二個參數設置的值。
  6. wait_pid6: 做一個不精確的概括,回收並釋放已終止的子進程的資源。

OK 了解完這樣一些關鍵的 syscall 後,這段代碼那麼基本上不難理解了。但是要吃透這段代碼,我們還需要去復習一下一些 Linux 或者說 POSIX 中的機制:

  1. fork 創建出來的子進程,會繼承父進程中的很多東西。就本文中聊的信號一部分來說,子進程會繼承父進程的信號屏蔽集和信號處理函數的相關設置
  2. execve 執行後,會重設當前進程的程序段與堆棧。所以在上面的代碼中我們執行 /bin/date 後,子進程會被重設。信號處理函數等設置也會被重設
  3. 每個進程都有信號屏蔽集,在信號屏蔽集中的信號被觸發時,會進入一個隊列,暫時不會觸發進程的信號處理,此時信號處於 pending 狀態。在取消對應信號的屏蔽與阻塞後,再次觸發進程的信號處理機制。如果進程顯式聲明忽略信號,那麼不會觸發信號的處理。(Tips:關於信號隊列這一點,這是一個 POSIX 1. 的約定。在 POSIX 中將這種機制稱為可靠信號,當阻塞期間,有多個信號發生時,會進入一個可靠隊列確保信號能被妥投。 Linux 支持可靠信號,其餘 Unix / 類 Unix 不一定支持)
  4. 子進程退出後,會給所屬的父進程傳遞一個 SIGCHLD1 信號,父進程在接收到這種信號後,需要調用 wait_pid6 函數對子進程進行處理。否則未被回收的子進程,會成為一個殭屍進程,也就是通常說的 Z 進程

OK,到現在,大家在掌握這些東西後,對於上面的代碼應該能完整明白了。不過可能大家還有一個疑惑,為什麼在這段代碼中需要調用 sigprocmask5 設置進程的信號屏蔽集來阻塞信號呢?這涉及到另一個問題。

如前面所說,信號在觸發時,進程會 " 跳轉 “對應的信號處理函數進行處理。但是信號處理函數處理完後的行為會怎麼樣呢?依照 Linux 中的設計,可能會出現兩種情況

  1. 對於可重入函數而言,信號處理函數返回後會繼續處理
  2. 對於不可重入函數而言,會返回 EINTR1

OK 大家這裡應該對我們為什麼會在這裡使用 sigprocmask5 有具體的了解了,實際上是為了保證我們的一些函數能夠正常的執行完,不會被信號處理所打斷。當然這裡也有其餘的問題,如果信號觸發特別密集的情況下,這裡的處理會帶來額外的 cost。所以還是需要根據不同的場景做 trade-off 了。

好了。差不多就這樣吧,福報久了真沒力氣寫文章,💊。下一篇文章應該就是我最近做內核協議棧監控的一些吃屎記錄了(flag++(逃。

參考資料#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。