淺談Linux信號機制

一、信號列表

root@ubuntu:# kill -l

 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP

 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1

11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP

21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ

26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR

31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3

38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8

43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13

48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12

53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7

58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2

63) SIGRTMAX-1 64) SIGRTMAX

其中最常見的:

  • Ctrl + C 觸發的是 SIGINT;
  • Ctrl + \ 觸發的是SIGQUIT;
  • 但是特別說明下 Ctrl + D 並不是觸發信號,而是產生一個 EOF,這也是為什麼在 Python 交互模式按下這個組合會退出 Python 的原因。

1.1、實時信號非實時信號

如上,kill列舉出所有信號。實時信號與非實時信號又叫做可靠信號與不可靠信號。SIGRTMIN 及其以後的是實時信號,之前的是非實時信號。區別是實時信號支持重復排隊,但是非實時信號不支持。非實時信號在排隊時候會默認隻出現一次,意思就是即使多次發送也終將隻收到一個。在隊列的取出順序上也有區別,即最先取出的信號一定是實時信號。

PS:

  • kill、killall 默認發送SIGTERM 信號。
  • linux下 SIGKILL不能被阻塞、或忽略。
  • 默認情況下 SIGCHLD 不被忽略,編程時候需要註意這個(要麼設置 SIG_IGN 或者主動 wait)。
  • 所有未定義處理函數的信號,默認退出進程。
  • 信號被設置block後仍存在於隊列中隻是不被處理,如果放開屏蔽將會被處理。
  • 信號可以中斷sleep調用引起睡眠的進程。

1.2、信號狀態

信號的”未決“是一種狀態,指的是從信號的產生到信號被處理前的這一段時間;信號的”阻塞“是一個開關動作,指的是阻止信號被處理,但不是阻止信號產生。

例如在sleep前用 sigprocmask 阻塞瞭退出信號,然後sleep,然後在sleep的過程中產生一個退出信號,但是此時退出信號被阻塞過,(中文的”阻塞”在這裡容易被誤解為一種狀態,實際上是一種類似於開關的動作,所以說“被阻塞過”,而不是“被阻塞”)所以處於“未決”狀態,在 sleep後又用sigprocmask關掉退出信號的阻塞開關,因為之前產生的退出信號一直處於未決狀態,當關上阻塞開關後,馬上退出“未決”狀態,得到處理,這一切發生在sigprocmask返回之前。

1.3、信號生命周期

對於一個完整的信號生命周期(從信號發送到相應的處理函數執行完畢)來說,可以分為三個重要的階段,這三個階段由四個重要事件來刻畫:

1.信號誕生;

2. 信號在進程中註冊完畢;

3.信號在進程中的註銷完畢;

4.信號處理函數執行完畢。相鄰兩個事件的時間間隔構成信號生命周期的一個階段。

下面闡述四個事件的實際意義:

  • 信號”誕生”。信號的誕生指的是觸發信號的事件發生(如檢測到硬件異常、定時器超時以及調用信號發送函數kill()或sigqueue()等)。
  • 信號在目標進程中”註冊”;進程的task_struct結構中有關於本進程中未決信號的數據成員:
struct sigpending pending;
struct sigpending
{
    struct sigqueue *head, **tail;
    sigset_t signal;
};

信號在進程中註冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),並且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。隻要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

1.信號在進程中的註銷。在目標進程執行過程中,會檢測是否有信號等待處理(每次從系統空間返回到用戶空間時都做這樣的檢查)。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中占有的結構卸掉。是否將信號從進程未決信號集中刪除對於實時與非實時信號是不同的。對於非實時信號來說,由於在未決信號信息鏈中最多隻占用一個sigqueue結構,因此該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);而對於實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用gqueue結構的數目區別對待:如果隻占用一個sigqueue結構(進程隻收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號註銷完畢)。否則,不在進程的未決信號集中刪除該信號(信號註銷完畢)。進程在執行信號相應處理函數之前,首先要把信號在進程中註銷。

2.信號生命終止。進程註銷信號後,立即執行相應的信號處理函數,執行完畢後,信號的本次發送對進程的影響徹底結束。

1.4、信號的執行和註銷

內核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處於運行狀態。當其由於被信號喚醒或者正常調度重新獲得CPU時,在其從內核空間返回到用戶空間時會檢測是否有信號等待處理。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中占有的結構卸掉。當所有未被屏蔽的信號都處理完畢後,即可返回用戶空間。對於被屏蔽的信號,當取消屏蔽後,在返回到用戶空間時會再次執行上述檢查處理的一套流程。

處理信號有三種類型:進程接收到信號後退出;進程忽略該信號;進程收到信號後執行用戶設定用系統調用signal的函數。當進程接收到一個它忽略的信號時,進程丟棄該信號,就象沒有收到該信號似的繼續運行。如果進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。而且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上創建一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,才返回原先進入內核的地方。這樣做的原因是用戶定義的處理函數不能且不允許在內核態下執行(如果用戶定義的函數在內核態下運行的話,用戶就可以獲得任何權限)。

eg:

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

void myHandler(int num)
{
    int ret = 0;
    
    if (SIGUSR1 == num)
    {
        sigset_t set;
        ret = sigemptyset(&set);
        assert(!(-1 == ret));
        ret = sigaddset(&set, SIGINT);
        assert(!(-1 == ret));
        ret = sigaddset(&set, SIGRTMIN);
        assert(!(-1 == ret));
        ret = sigprocmask(SIG_UNBLOCK, &set, NULL);
        assert(!(-1 == ret));
        printf("解除阻塞 recv sig num: %d\n", num);
    }
    else if (num == SIGINT || num == SIGRTMIN)
    {
        printf("recv sig num: %d\n", num);
    }
    else
    {
        printf(" 其他信號recv sig num: %d\n", num);
    }
}

int main(void)
{
    pid_t pid;
    int ret = 0;
    // 設置回調函數
    struct sigaction act;
    act.sa_handler = myHandler;
    act.sa_flags = SA_SIGINFO;
    // 註冊非實時信號的處理函數
    ret = sigaction(SIGINT, &act, NULL);
    assert(!(-1 == ret));
    // 註冊實時信號的處理函數
    ret = sigaction(SIGRTMIN, &act, NULL);
    assert(!(-1 == ret));
    // 註冊用戶自定義信號
    ret = sigaction(SIGUSR1, &act, NULL);
    assert(!(-1 == ret));
    
    // 把 SIGINT  SIGRTMIN 軍添加到阻塞狀態字中
    sigset_t set;
    ret = sigemptyset(&set);
    assert(!(-1 == ret));
    ret = sigaddset(&set, SIGINT);
    assert(!(-1 == ret));
    ret = sigaddset(&set, SIGRTMIN);
    assert(!(-1 == ret));
    ret = sigprocmask(SIG_BLOCK, &set, NULL);
    assert(!(-1 == ret));
    
    pid = fork();
    assert(!(-1 == ret));
    if (0 == pid)
    {
        union sigval value;
        value.sival_int = 10;
        int i = 0;
        // 發三次不穩定信號
        for (i = 0; i < 3; i++)
        {
            ret = sigqueue(getppid(), SIGINT, value);
            assert(!(-1 == ret));
            printf("發送不可靠信號 ok\n");
        }
        
        // 發三次穩定信號
        value.sival_int = 20;
        for (i = 0; i < 3; i++)
        {
            ret = sigqueue(getppid(), SIGRTMIN, value);
            assert(!(-1 == ret));
            printf("發送可靠信號ok\n");
        }
        // 向父進程發送 SIGUSR1 解除阻塞
        ret = kill(getppid(), SIGUSR1);
        assert(!(-1 == ret));
    }
    while (1)
    {
        sleep(1);
    }
    return 0;
}

二、信號掩碼和信號處理函數的繼承

2.1、信號處理函數的繼承

信號處理函數是進程屬性,所以進程裡的每個線程的信號處理函數是相同的。通過fork創建的子進程會繼承父進程的信號處理函數。execve 後設置為處理的信號處理函數會被重置為默認函數,設置為忽略的信號保持不變。意思是如果父進程裡信號設置處理為SIG_IGN,那麼等到子進程被exec瞭,這個信號的處理還是被忽略,不會重置為默認函數。

eg:

// test.c --> test
#include <stdlib.h>
  
typedef void (*sighandler_t)(int);
static sighandler_t old_int_handler;
  
static sighandler_t old_handlers[SIGSYS + 1];
  
void sig_handler(int signo)
{
    printf("receive signo %d\n",signo);
    old_handlers[signo](signo);
}
  
int main(int argc, char **argv)
{
    old_handlers[SIGINT] = signal(SIGINT, SIG_IGN);
    old_handlers[SIGTERM] = signal(SIGTERM, sig_handler);
  
    int ret;
  
    ret = fork();
    if (ret == 0) {
        //child
        // 這裡execlp將運行 test2 作為子進程。
        execlp("/tmp/test2", "/tmp/test2",(char*)NULL);
    }else if (ret > 0) {
        //parent
        while(1) {
            sleep(1);
        }
    }else{
        perror("");
        abort();
    }
  
}
  
================================================
test2.c --> test2
#include <stdio.h>
int main(int argc, char **argv)
{
    while(1) {
        sleep(1);
    }
    return 0;
}

結論:test換成test2後,SIGINT的處理方式還是忽略,SIGTERM被重置為默認的方式。

2.2、信號掩碼的繼承

信號掩碼有以下規則:

1.每個線程可以有自己信號掩碼。

2.fork出來的子進程會繼承父進程的信號掩碼,exec後信號掩碼保持不變。如果父進程是多線程,那麼子進程隻繼承主線程的掩碼。

3.針對進程發送的信號,會被任意的沒有屏蔽該信號的線程接收,註意隻有一個線程會隨機收到。linux下如果都可以所有線程都可以接收信號,那麼信號將默認發送到主線程,posix系統是隨機發送。

4.fork之後子進程裡pending的信號集初始化為空,exec會保持pending信號集。

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
  
typedef void (*sighandler_t)(int);
  
static void *thread1(void *arg)
{
    sigset_t set;
      
    printf("in thread1\n");
  
    sigemptyset(&set);
    sigaddset(&set, SIGTERM);
    pthread_sigmask(SIG_BLOCK, &set, NULL);

    while(1) {
        sleep(1);
    }
}
  
static void sigset_print(sigset_t *set)
{
    int i;
  
    for (i = 1; i <= SIGSYS; i++) {
        if (sigismember(set, i)) {
            printf("signal %d is in set\n",i);
        }
    }
}
  
int main(int argc, char **argv)
{
    int ret;
    sigset_t set;
    pthread_t pid;
  
    pthread_create(&pid, NULL, thread1, NULL);
    sleep(1);
  
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    pthread_sigmask(SIG_BLOCK, &set, NULL);
  
    ret = fork();
    if (ret == 0) {
        //child
        pthread_sigmask(SIG_BLOCK, NULL, &set);
        sigset_print(&set);
  
        while(1) {
            sleep(1);
        }
    }else if (ret > 0) {
        //parent
        while(1) {
            sleep(1);
        }
    }else{
        perror("");
        abort();
    }
  
}

結論:隻有在主線程裡設置的掩碼才被子進程繼承瞭。這裡面的原因在於linux裡的fork隻是復制瞭調用fork()的那個線程,因此在子進程裡隻有父進程的主線程被拷貝瞭,當然信號掩碼就是父進程的主線程的信號掩碼的復制瞭。再次驗證證明,如果是在thread1裡調用fork,那麼子進程的信號掩碼就會是thread1的拷貝瞭。

2.3、sigwait 與多線程

sigwait函數:sigwait等一個或者多個指定信號發生。

它所做的工作隻有兩個:

第一,監聽被阻塞的信號;

第二,如果所監聽的信號產生瞭,則將其從未決隊列中移出來。sigwait並不改變信號掩碼的阻塞與非阻塞狀態。

在POSIX標準中,當進程收到信號時,如果是多線程的情況,我們是無法確定是哪一個線程處理這個信號。而sigwait是從進程中pending的信號中,取走指定的信號。這樣的話,如果要確保sigwait這個線程收到該信號,那麼所有線程含主線程以及這個sigwait線程則必須block住這個信號,因為如果自己不阻塞就沒有未決狀態(阻塞狀態)信號,別的所有線程不阻塞就有可能當信號過來時,被其他的線程處理掉。

PS:

在多線程代碼中,總是使用sigwait或者sigwaitinfo或者sigtimedwait等函數來處理信號。而不是signal或者sigaction等函數。因為在一個線程中調用signal或者sigaction等函數會改變所以線程中的信號處理函數,而不是僅僅改變調用signal/sigaction的那個線程的信號處理函數。

2.4、多進程下的信號

多進程下鍵盤觸發的信號會同時發送到當前進程組的所有進程。如果一個程序在執行時 fork 瞭多個子進程,那麼按鍵觸發的信號將會被這個程序的所有進程收到。

但是與多線程不一樣,多進程下的信號掩碼和信號處理函數是獨立的。每個進程都可以選擇處理或者不處理,也可以設置自己的信號掩碼。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

int main(int argc, char **argv)
{
    pid_t pid = fork();

    signal(SIGCHLD, SIG_IGN);
    if (pid < 0)
        printf("error fork\n");
    else if (pid == 0)
    {
        signal(SIGINT, SIG_IGN); // 忽略 SIGINT,這樣 ctrl+c 後子進程能活下來; 不設置的話,收到信號將退出
        printf("child gid = %ld\n", getpgid(getpid()));
        do
        {
            sleep(1);
        } while (1);
    }
    else
    {
        printf("parent gid = %ld\n", getpgid(getpid()));
        do
        {
            sleep(1);
        } while (1);
    }

    return 0;
}

如上圖,可以看到,收到SIGINT 後父進程退出,子進程因為設置瞭忽略 SIGINT 所以子進程沒有受到影響。

三、apis

3.1、信號發生函數

1.kill(pid_t pid, int signum);

2.int sigqueue(pid_t pid, int sig, const union sigval value);

3.pthread_kill(pthread_t tid, int signum);

4.raise(int signum);// 發送信號到自己

5.void alarm(void);

6.void abort(void);

7.int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

PS:

sigqueue()比kill()傳遞瞭更多的附加信息,但sigqueue()隻能向一個進程發送信號,而不能發送信號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何信號,0值信號可用於檢查pid的有效性以及當前進程是否有權限向目標進程發送信號。

3.2、信號處理函數

1.signal(int signum, void (*handler)(int signum))

2.sigaction(int signum, struct sigaction* newact, sigaction* oldact)

sigaction act;
act.sa_handler = handler;
act.sa_flags = SA_SIGINFO;
// 註冊信號的處理函數
sigaction(SIGINT, act, NULL);

3.3、信號掩碼函數

1.sigprocmask(int how, struct sigaction* set,struct sigaction* oldset)

2.pthread_sigmask(int how, struct sigaction* set,struct sigaction* oldset)

sigprocmask用於設置進程的信號掩碼,pthread_sigmask用於設置線程的信號掩碼,二者參數相同。第一個參數有SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK

3.4、信號集合變量

sigset_t set

sigemptyset(&set) //清空阻塞信號集合變量

sigfillset(&set)  //添加所有的信號到阻塞集合變量裡

sigaddset(&set,SIGINT) //添加單一信號到阻塞信號集合變量

sigdelset(&set,SIGINT) //從阻塞信號集合變量中刪除單一信號

sigismember(&set,int signum) //測試信號signum是否包含在信號集合set中,如果包含返回1,不包含返回0,出錯返回-1。錯誤代碼也隻有一個EINVAL,表示signum不是有效的信號代碼。

3.5、信號屏蔽函數

1.int sigpending(sigset_t *set); // 返回阻塞的信號集

2.int sigsuspend(const sigset_t *mask);

sigsuspend表示臨時將信號屏蔽字設為mask,並掛起進程直到有信號產生(非屏蔽信號才能喚醒或終止進程),如果信號處理函數返回,那麼siguspend將恢復之前的信號屏蔽字(temporarily)

假設sisuspend阻塞進程時產生瞭信號A,且A不是mask內的屏蔽信號,那麼A的信號處理函數有兩種情形,

一:直接終止進程,此時進程都不存在瞭,那麼sigsuspend當然無須返回瞭(不存在進程瞭sigsuspend也不存在瞭,函數棧嘛);

二:如果信號A的處理函數返回,那麼信號屏蔽字恢復到sigsuspend之前的(sigsuspend調用時將信號屏蔽字設為mask,所以要恢復到sigsuspend調用之前的),然後sigsuspend返回-1並將error置為EINTR.

以上就是淺談Linux信號機制的詳細內容,更多關於Linux信號機制的資料請關註WalkonNet其它相關文章!

推薦閱讀: