Linux進程信號詳解

信號是什麼

一個信號就是一條小消息,它通知進程系統中發生了一個某種類型的事件

信號是多種多樣的,並且一個信號對應一個事件,這樣才能做到收到一個信號後,知道到底是一個什麼事件,應該如何處理(但是要保證必須識別這個信號)

信號的種類

使用kill-l命令查看信號種類

查看信號種類

一共62種,其中131是非可靠信號,3464是可靠信號(非可靠信號是早期Unix系統中的信號,後來又添加了可靠信號方便用戶自定義信號,這二者之間具體的區別在下文中會提到)

信號的生命週期

產生》》進程中的註冊》》進程中的註銷》》捕獲處理

信號的產生

硬件事件舉例:

  • 如果一個進程試圖除以0,那麼內核就發送給它一個SIGFPE信號(序號8)
  • 如果一個進程執行一條非法指令,那麼內核就發送給它一個SIGILL信號(序號4)
  • 如果進程進行非法存儲器引用(野指針、段錯誤),內核就發送給它一個SIGSEGV信號(序號11)

軟件事件舉例:

  • ctrl+c 中斷信號——20) SIGTSTP

  • ctrl+| 退出信號——3) SIGQUIT

  • ctrl+z 停止信號——2) SIGINT

  • kill命令:kill -signum pid

    當kill命令不帶-signum參數時(kill pid),默認的信號是15) SIGTERM

    kill -9 pid則是一個強大的“強殺”命令,能殺死kill pid殺不掉的處於T狀態的進程

  • int kill(pid_t pid, int sig);

    kill命令的系統調用接口(在代碼中使用kill)

    #include <sys/types.h>
    #include <signal.h>
    int kill(pid_t pid, int sig);
    //kill()系統調用可用於將任何信號發送到任何進程組或進程
    //第一個參數是一個進程的PID,第二個參數則是信號編號
    

    示例:

    #include <stdio.h>
    #include <unistd.h>
    #include <signal.h>
    int main(){ 
      kill(getpid(),SIGQUIT); //可以通過getpid()的方式獲取自身的PID然後發信號給自己
      printf("hello/n");
      return 0;
    }
    

    運行結果:

    ubuntu@VM-0-7-ubuntu:/home/zeno/c_practice$ ./217_kill
    Quit (core dumped)
    
  • int raise(int signum);

    raise是一個庫函數(#include <signal.h>),作用是發送信號到調用這個函數的進程/線程

    在單線程程序中,它等效於kill(getpid(), sig);(也就和上面的示例一樣)

    在多線程程序中,它等效於pthread_kill(pthread_self(), sig);

  • void abort();

    abort是一個庫函數(#include <stdlib.h>),作用是造成進程異常中止

    在進程中調用abort()就相當於調用了raise(3)

  • unsigned int alarm(unsigned int seconds);

    alarm是一個系統調用接口(#include <unistd.h>),在seconds秒後會將SIGALRM信號傳遞到調用進程

信號的註冊

在pcb中有一個未決(pending)信號集合(未決(pending)的意思是信號產生了但還沒有決定怎麼做),信號的註冊就是指在這個pending集合中標記對應信號數值的二進制位爲1

上面的話有些難以理解,我們先來看看在linux內核源碼裏一個進程的信號是如何保存的

在linux內核源碼sched.h中的task_struct結構體裏有這樣一段關於信號的內容:

/* signal handlers */
        struct signal_struct *signal;
        struct sighand_struct *sighand;

        sigset_t blocked, real_blocked;
        sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
        struct sigpending pending;

上面最後一行的sigpending結構體定義在signal.h中:

struct sigpending {
        struct list_head list;
        sigset_t signal;
};

這裏的signal就是用來做信號標記的,給一個進程發送一個信號說白了就是在signal裏標記一下這個信號曾經來過

那麼signal是如何進行標記的呢?還得繼續瞭解一下sigset_t這個結構體

在bits/sigset.h中進行了以下定義:

/* A `sigset_t' has a bit for each signal.  */
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

注:這裏的__sigset_t其實就是sigset_t,只是一個類型名的重定義

在這個結構體中只有一個數組成員,這個數組裏存放着一些數作爲位圖,位圖的每一個二進制位就代表了一種信號,0表示未曾收到這個信號,1表示已經收到這個信號

這裏需要注意的是,真正存放信號的是數組中某個數的某個二進制位,數組的存在只是因爲單獨一個數的二進制位存不下這麼多種類的信號

現在我們就可以理解,當使用上述方式對某一個進程發送一個信號時,操作系統就會將該進程對應的pending集合中表示相應信號的位圖的二進制位由0改爲1

但是非可靠信號和可靠信號的註冊還有一點區別

爲了理解這種區別我們還應該瞭解一下list_head鏈表和signal.h中的sigqueue結構體

list_head是linux內核提供的一個用來創建雙向循環鏈表的結構,由於這個結構是沒有數據域的所以較爲複雜,在這裏不做深究,有興趣可以通過這篇博客詳細瞭解

我們需要知道的是,內核通過一個以list爲表頭的鏈表將所有產生的信號都串在了一起,鏈表中的每個節點的結構是一個sigqueue:

/*
 * Real Time signals may be queued.
 */
struct sigqueue {
        struct list_head list;
        int flags;
        siginfo_t info;
        struct user_struct *user;
};

這個結構體保存信號所攜帶的信息

現在我們就可以對非可靠信號和可靠信號的區別有一定的瞭解了

  • 1~31非可靠信號的註冊:

    當試圖對一個進程發送一個非可靠信號時,若發現位圖上對應的位爲0,則置爲1,並在list_head鏈表里加入一個sigqueue節點;若發現位圖上對應的位已經爲1,則直接返回。簡單地說就是若信號還未註冊,則註冊一下,若已經註冊,則什麼都不做

  • 34~64可靠信號的註冊:

    當試圖對一個進程發送一個可靠信號時,若發現位圖上對應的位爲0,則置爲1,並在list_head鏈表里加入一個sigqueue節點;若發現位圖上對應的位已經爲1,對該位不進行操作但依舊在鏈表里加入一個節點。也就是說,每次對進程發送一個可靠信號時,不管該進程之前是否收到過相同的信號,總是會在list_head鏈表里加入sigqueue節點

對於信號來說,位圖只是用來標記有沒有待處理信號的,而節點纔是信號真正註冊的信息

信號的註銷

看上文中信號的生命週期會發現,在處理信號之前,會先銷燬信號的信息

信號註銷存在的目的就是爲了抹除信號存在的痕跡,防止對同一個信號進行多次處理

刪除要處理的信號sigqueue節點:

  • 若信號是非可靠信號,則直接將位圖置0(非可靠信號在沒有處理之前只會註冊一次)
  • 若信號是可靠信號,則刪除後需要判斷是否還有相同節點,沒有的話纔會重置位圖爲0

信號的捕獲處理

在學習信號的捕獲和處理之前我們還需要了解一下信號的阻塞

信號的阻塞

信號的阻塞就是阻止一個信號的抵達,當一種信號被阻塞時,它仍可以被髮送,但是產生的待處理信號不會被接收,直到進程取消對這種信號的阻塞

在pcb中,有一個阻塞信號集合(blocked位圖,實現方式與pending相同),凡是添加到這個集合中的信號,都表示需要阻塞,暫時不處理

那麼該如何實現對一個進程的某個信號進行阻塞呢?

我們可以通過sigprocmask函數顯式地阻塞和取消阻塞選擇的信號:

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

參數中的how表示了當前要對blocked集合進行的操作,它的值可從下面三個宏定義中選擇一個填入:

  • SIG_BLOCK:添加set中的信號到blocked中(blocked = blocked | set)
  • SIG_UNBLOCK:從blocked中刪除set中的信號(blocked = blocked & ~set)
  • SIG_SETMASK:blocked = set

在這個函數中,如果oldset非空,blocked位圖以前的值會保存在oldset中

捕獲信號與處理信號

接着我們就可以來研究一下捕獲信號

當內核準備將控制傳遞給一個進程時,它會檢查該進程的未被阻塞的待處理信號的集合,也就是存在於pending集合中同時又不存在於blocked集合中(pending & ~blocked),如果這個集合爲空(通常情況下),那麼內核將控制傳遞到該進程中的下一條指令裏

然而,如果該集合是非空的,那麼內核會選擇集合中的某個信號(通常是序號最小的信號),並且強制該進程接收該信號,收到這個信號會觸發進程的某種行爲。一旦進程完成了這個行爲,那麼控制就傳遞迴該進程中的下一條指令

這裏的“行爲”,就是進程對信號的處理

處理的實現是調用一個信號處理函數signal:

#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

這裏的sighandler_t是一個函數指針類型,signal函數的第一個參數就是信號的序號,我們就可以通過第二個參數來改變處理信號signum的方式:

  • 如果handler是SIG_IGN,那麼忽略類型爲signum的信號
  • 如果handler是SIG_DFL,那麼類型爲signum的信號行爲恢復爲默認行爲
  • 在其它情況下,handler是一個用戶定義的函數的地址,也就是指向一個信號處理程序的函數指針,只要進程收到一個類型爲signum的信號,就會調用這個函數

需要注意的是,在所有信號中,有兩個信號不可被阻塞,不可被自定義修改處理方式,也不可被忽略,這兩個信號分別是9) SIGKILL19) SIGSTOP

一般情況下對於信號的捕獲和處理都是一起被提到的,上文中對“捕獲”和“處理”的分界可能並不是特別準確,在《深入理解計算機系統》中對捕獲信號和處理信號的定義如下:

調用信號處理程序稱爲捕獲信號,執行信號處理程序稱爲處理信號

現在我們通過一個具體的例子來感受一下信號的阻塞與接觸阻塞的操作和信號的捕獲處理以及可靠信號與非可靠信號的區別:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sigcb(int signum){ 
  printf("receive a signal:%d\n",signum);
}
int main(){ 
  signal(SIGINT, sigcb);//修改停止信號(序號2,鍵盤ctrl+z)的處理方式爲調用sigcb函數
  signal(40, sigcb);//修改信號40的處理方式爲調用sigcb函數

  //阻塞所有信號
  sigset_t set, old;
  sigemptyset(&set);//清空信號集合
  sigemptyset(&old);//清空信號集合

  //sigaddset(int signum, sigset_t *set)將指定信號添加到集合
  sigfillset(&set);//將所有的信號都添加到set集合中
  sigprocmask(SIG_BLOCK, &set, &old);//將set中的信號添加到blocked中造成信號阻塞

  printf("presse enter to continue:\n");
  getchar();//在按下回車之前,程序卡在這裏

  sigprocmask(SIG_UNBLOCK, &set, NULL);//解除阻塞
  return 0;
}

運行程序並分別通過ctrl+c和kill命令多次發送2號和40號信號:

zeno@VM-0-7-ubuntu:~$ ./mask
presse enter to continue:
^C^C^C^C^C^C^C^C^C^C
zeno@VM-0-7-ubuntu:~$ ps -ef | grep mask | grep -v grep
zeno     29043 27880  0 14:09 pts/10   00:00:00 ./mask
zeno@VM-0-7-ubuntu:~$ kill -40 29043
zeno@VM-0-7-ubuntu:~$ kill -40 29043
zeno@VM-0-7-ubuntu:~$ kill -40 29043
zeno@VM-0-7-ubuntu:~$ kill -40 29043

可以看到對於該進程,不論是非可靠信號(2)還是可靠信號(40),都被阻塞導致無法處理,但是接下來按下回車,所有阻塞都會被解除:

zeno@VM-0-7-ubuntu:~$ ./mask 
presse enter to continue:
^C^C^C^C^C^C^C^C^C^C
receive a signal:40
receive a signal:40
receive a signal:40
receive a signal:40
receive a signal:2

在這裏我們就可以發現,雖然信號2和信號40都曾發送多次,但是隻有信號40也就是可靠信號被處理了多次,而信號2也就是可靠信號只調用了一次信號處理函數,這也就印證了我們上文中所提到的可靠信號與非可靠信號的區別

至此我們就已經大致瞭解了什麼是進程信號和信號的工作過程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章