【APUE】Chapter10 Signals

Signal主要分兩大部分:html

  A. 什麼是Signal,有哪些Signal,都是幹什麼使的。linux

  B. 列舉了很是多不正確(不可靠)的處理Signal的方式,以及怎麼樣設計來避免這些錯誤出現。shell

 

10.2 Signal Concepts安全

  1. Signal的實體就是在頭文件中定義的正整數(在我使用的linux系統中在/usr/include/bits/signum.h中),以下:數據結構

    

  2. 列舉了可能會產生Signal的條件:app

    (1)終端的user的案件操做:如,Ctrl+c,Ctrl+\;由terminal發出。異步

    (2)硬件異常拋出的Signal:如,除0,非法內存引用;由kernel發出。函數

    (3)kill(2) kill(1) :向process或process group發送Signal;由process發出。post

    (4)軟件執行時產生的Signal :好比後面要提到的SIGALRM,定點到時信號。ui

  3. Signal是典型的異步驅動時事件,當Signal出現的時候,能夠設定程序來告知kernal去作什麼事情:

    (1)忽略Signal。這裏有兩個Signal不能忽略:分別是SIGKILL和SIGSTOP。不能忽略的緣由是必須讓kernal或root權限能夠強制終止進程。若是遇到了hardware exception狀況,不去處理這類異常的話,進程後續如何執行就不肯定了。

    (2)捕獲Signal。這裏的作法是預先告訴kernal「若是出現了這個信號,用哪一個函數去處理」。另,SIGKILL和SIGSTOP是不能被捕獲的。

    (3)默認處理策略。系統預先指定了幾個Signal默認的處理策略。在signum.h中:

        

      具體能夠對照Figure10.1中每一個Signal的默認處理策略去查看;有些默認策略是terminal+core類型的,可用於系統down了以後的debug。可是,也不必定保證core必定會產生;若是權限不足的話,極可能沒法產生core。這個也要留意。

  4. 這裏插播一個Signal的解釋。SIGTSTP,當執行終端命令交互的程序到時候,按下ctrl+z能夠觸發這個信號,向foreground process group全部的。這個信號名字裏雖然叫stop,可是並非真的給停了。這裏與stop相對的是continue(即SIGCONT),只是給暫停的意思。這個信號若是隻是望文生義,就容易理解誤差。不只僅是這個信號,後面還有更沒法望文生義的名詞解釋。

 

 

10.3 signal function

  signal這個函數的含義就是告訴系統用什麼函數處理什麼信號

  1. 看一個例子:

 1 #include "apue.h"
 2 #include <stdio.h>
 3 #include <signal.h>
 4 
 5 static void sig_usr(int signo)
 6 {
 7     if (signo==SIGUSR1)
 8     {
 9         printf("received SIGUSR1\n");
10     }
11     else if (signo==SIGUSR2)
12     {
13         printf("received SIGUSR2\n");
14     }
15     else
16     {
17     }
18 }
19 
20 int main(void)
21 {
22     if (signal(SIGUSR1, sig_usr) == SIG_ERR)
23     {
24         err_sys("can't catch SIGUSR1");
25     }
26     if (signal(SIGUSR2, sig_usr) == SIG_ERR)
27     {
28         err_sys("can't catch SIGUSR2");
29     }
30     for (;;)
31     {
32         pause();
33     }
34 }

  執行結果以下:

    

  初步分析以下:

  (1)main中給SIGUSR1和SIGUSR2都註冊了signal handler

  (2)pause()函數的做用是阻塞進程,並一直等着signal到來。

  (3)kill跟它叫kill沒有關係,純粹是unix系統的一個misnomer,它的做用就是向process或process group發送信號。所以就是發送SIGUSR1和SIGUSR2信號。

  (4)kill默認狀況下發送的信號是SIGTERM,即終止進程。

  若是連續兩次發送SIGUSR1信號會怎樣?以下:

  

  經過上面的運行結果可知,用signal給某個信號註冊一次handler,只能管一次。即,在信號發生跳轉到自定的 handler 處理函數執行後, 系統會自動將此處理函數換回原來系統預設的處理方式。那麼有沒有註冊一次,可以處理屢次信號的方法呢?有,改用sigaction函數去註冊signal handler。代碼以下:

 1 #include "apue.h"
 2 #include <stdio.h>
 3 #include <signal.h>
 4 
 5 static void sig_usr(int signo)
 6 {
 7     if (signo==SIGUSR1)
 8     {
 9         printf("received SIGUSR1\n");
10     }
11     else if (signo==SIGUSR2)
12     {
13         printf("received SIGUSR2\n");
14     }
15     else
16     {
17     }
18 }
19 
20 int main(void)
21 {
22     struct sigaction sa1, sa2;
23     sa1.sa_handler = sig_usr;
24     sa2.sa_handler = sig_usr;
25     sigemptyset(&sa1.sa_mask);
26     sigemptyset(&sa2.sa_mask);
27     sigaddset(&sa1.sa_mask, SIGUSR1);
28     sigaddset(&sa2.sa_mask, SIGUSR2);
29     sigaction(SIGUSR1, &sa1, NULL);
30     sigaction(SIGUSR2, &sa2, NULL);
31     /* 
32     if (signal(SIGUSR1, sig_usr) == SIG_ERR)
33     {
34         err_sys("can't catch SIGUSR1");
35     }
36     if (signal(SIGUSR2, sig_usr) == SIG_ERR)
37     {
38         err_sys("can't catch SIGUSR2");
39     }
40     */
41     for (;;) pause();
42 }

    運行結果以下:

    

  Program Start-Up

  這裏面提到了一種狀況,若是是由某個程序經過執行exec產生的新的Program,進而替代原來的Process(雖然進程號不變,可是即便是執行原來的程序,程序的地址也變化了);那麼,由原來的Porcess註冊的各類Signal處理相關的內容,在新的Program中是無效的,由於原來的signal-handler中註冊的信號處理函數的地址失效了,不是原來的函數了。

 

從下面的部分開始,更多的從反面出發,分析signal處理上經歷的各類不靠譜設計,從而理解爲何要設計各類靠譜的機制。

10.4 Unreliable Signals

  1. 不靠譜僞代碼(1)

int sig_int(); /*signal處理函數*/

...

signal(SIGINT, sig_int); /*註冊signal handler*/

...

sig_int()
{
    signal(SIGINT, sig_int); /*從新註冊signal handler以便處理下一次信號*/    
    ...
}

  瞭解過signal函數以後,能夠知道,上述代碼的意思大概是:每次出現SIGINT信號,都會觸發sig_int()函數來處理信號;因爲signal函數只能管處理一次SIGINT,所以爲了可以連續處理SIGINT信號,在每次進入sig_int()函數時,都從新用signal函數註冊一下sig_int。

  上面的代碼看似是沒有問題的,可是卻隱含着比較大的漏洞。

  考慮以下的狀況:若是以前已經來了一個SIGINT信號,系統開始調用註冊的sig_int()進行信號處理;若是偏偏在進入sig_int()函數以前,又來了一個SIGINT信號;因爲此時系統已經沒有用於處理SIGINT信號的函數了,則第二個到來的SIGINT信號就被採用默認的信號處理策略(對於SIGINT這個信號來講,就是直接將進程terminates了),每每達不到咱們預期的信號處理效果。這種漏洞的可怕之處還在於,大部分時間程序都是工做正確的,偶爾會出現問題,這種bug是最難排除的。

  2. 不靠譜僞代碼(2)

int sig_int(); /*SIGINT信號處理函數*/
int sig_int_flag; /*SIGINT信號出現時候將其賦值爲零*/

main()
{
    signal(SIGINT, sig_int); /*註冊信號處理函數*/
    ...
    while(sig_int_flay == 0)
    {
        pause();  /*等着信號到來*/
    }  
}   

sig_int()
{
    signal(SIGINT, sig_int); /*從新註冊信號處理函數*/
    sig_int_flag = 1; /*修改環境變量*/
}

  上述的僞代碼的本意是:main函數中的while循環就要等着SIGINT信號的到來,而且處理完sig_int_flag標誌標量,才往下進行。

  代碼的本意是好的,可是仍是存在漏洞。

  考慮以下的狀況:若是已經註冊完了sig_int函數,首次進入while循環的判斷條件,變量爲0;而偏偏在執行while循環體中的pause()以前,來了一個SIGINT信號;開始執行sig_int的過程當中已經把sig_int_flag設置爲1了;sig_int執行完畢,開始執行pause()函數;若是之後不再來SIGINT信號了,那麼pause()就一直等下去了。仍是跟上一個不靠譜的代碼問題同樣,這樣的代碼大部分時間能夠正常運行,偶爾出現問題,很是難debug。

  總結一下上面兩個不靠譜的代碼,其核心問題我認爲是:signal是異步出現的,隨時都能打斷當前執行的程序;而上述代碼的設計思路都是傳統的同步順序執行的,因此會遇到各類細節問題

  (1)第一種狀況是signal「該來的時候來了,不應來的時候也來了」,即信號處理函數失效。

  (2)第二種狀況是signal「該來的時候不來,不應來的時候也來了」,即信號丟失。

 

10.5 Interrupted System Calls

  這個部分闡述與System Call相關的signal處理問題。我沒太理解深刻,暫時記錄如下兩點:

  (1)早期的unix系統中,若是某個process正在被「a slow system call」給阻塞;那麼這個時候來個信號,原來正在執行的「a slow system call」就被打斷了,返回一個error而且errno被設置爲EINTR。

  (2)爲了不這樣的問題,有的系統提供了處理上述問題的system call restart的機制。可能的作法就是每次執行slow system call的時候,都去檢查error的返回值,是不是EINTR,來決定是否啓動restart。

  有個僞代碼以下:

again:
    if ( (n = read(fd, buf, BUFFSIZE)) < 0 )
    {
        if ( error == EINTR )
            goto again; /*被interrupted的system call*/
        
         ...  /*處理其餘問題*/
    
    }    

  在後續的14.4節中,select()和poll()函數的時候還會細說interrupted system calls

  

10.6 Reentrant Functions (可重入函數)

  這部分說的是signal處理中安全問題,是否可重入。(能夠查閱這個bloghttp://particle128.com/posts/2014/05/reentrant.html

  所謂的安全問題,是指signal到來與處理,打斷了原來執行的程序;在執行完信號處理函數後,原來正在執行的程序可能就受到影響了,與預想的結果不太同樣,這就是我理解的signal安全。  

  書上舉了兩個例子,來講明因爲重入某些函數可能帶來的signal不安全狀況:

  (1)若是原進程正執行malloc分配內存呢,分配到一半的時候來個signal信號;而後進入到信號處理函數中,又用到malloc函數動態分配內存了。

  (2)若是原進程正執行getpwnam函數呢(簡單理解getpwnam往一個static的存數據),正執行到一半忽然來個signal信號;而後進入到signal信號處理函數中,又調用getpwnam;結果就是上次存一半的結果被新的覆蓋了;再次回到原來的進程順序執行的時候,getpwnam得到結果就亂套了。

  上述的例子只是一個熱身,系統些來講,判斷一個signal handler函數是否是reentrant的,通常從signal handler function以下的幾個方面進行考慮:

  (1)是否用到了static data:若是信號發生時正調用getwpnam,而且在signal handler中也調用了getwpnam,則在handler中新獲取的值就會覆蓋以前進程中獲取的值。

  (2)是否調用了free或者malloc函數:若是信號發生時正調用malloc(修改堆上的存儲空間鏈接表),而且信號處理程序又調用malloc,會破壞內核的數據結構。

  (3)是否調用了standard IO:由於好多standard I/O函數都使用了全局的數據結構(如,printf中用到的文件偏移量是全局的

  (4)是否用了longjmp這類的函數:信號發生時候程序正在修改一個數據結構,longjmp這種徹底推倒重來的函數在信號處理中一旦出現,就容易出現改一半就沒改完的狀況

  類unix系統中,若是對於signal是安全的,有兩種要求:

  (1)首先函數必須是reentrant fucntion標準的(上述的4點),即從自身設計上不要出現signal不安全的漏洞

  (2)這些函數執行時已經block各種signal,即從外部影響上主動避開signal出現帶來的問題

  另外,書上還提醒了一點:即便是調用Figure10.4中的reentrant function,還須要檢驗errno這個變量值;緣由是,可能在信號處理函數中改變了error的實際值。好比,若是信號處理函數中調用了read(參考interrupted system call的內容),就有可能在信號處理完成後改變error的值。

  所以,即便在signal handler中調用的是Figure10.4中的reentrant function,也須要在信號處理函數中檢驗errno的值。

  例如,在Figure10.4中有fork函數,它的做用是分叉產生一個child process;當child process執行完了以後,就會產生一個SIGCHLD信號,返回給主進程;而這個信號的signal handler通常都有一個wait function,而wait function改變errno值。

  總結一下,之後再設計signal處理函數的時候,必定要注意是不是reentrant的,以及信號處理安全問題。

 

10.7 SIGCLD Semantics

   SIGCLD和SIGCHLD都是與child process結束狀態有關的信號。

   這重點介紹的是SIGCLD這個信號,有一些類unix系統對於這個SIGCLD信號的處理是比較特殊的。

   (1)有時候,咱們不想關心一些child process的運行情況,就會把SIGCLD信號的處理方式設置爲SIG_IGN。這樣作的好處就是,不會產生殭屍進程,「the status of these child processes is discarded」;可是,若是父進程中不當心有wait()函數正等着這個child process,那就會一直阻塞了。

   (2)若是註冊signal handler來處理SIGCLD信號,在調用signal函數的時候,kernel會檢測是否已經有child process正在等待被回收處理。這點特性,也帶來了以下的問題。

   有問題的示例代碼以下:#include "apue.h#include <sys/wait.hstatic void sig_cld(int);

int main()
{
    pid_t pid;
    signal(SIGCLD, sig_cld);
    if ( (pid=fork()) ==0 ) /*child process*/
    {
        sleep(2);
        _exit(0);
    }
    pause();
    exit(0);
}

static void sig_cld(int signo)
{
int status; signal(SIGCLD, sig_cld);
/*reestablish handler*/
...
   pid = wait(&status); }

  main()中的sleep(2)是爲了保證parent process先執行pause(),而後child process再執行_exit(0)。sig_cld()函數中在開始就調用signal(SIGCLD, sig_cld)是爲了可以連續處理SIGCLD信號。

  不考慮其餘代碼漏洞,上述代碼在正常邏輯順序上有較大的問題:每次調用signal(SIGCLD, XXX)的時候,kernel就會去check是否有child process等待被回收;因爲此時child process尚未被回收呢,所以kernel認爲還有child process等着被處理,所以再調用sig_cld函數;整個代碼陷入了循環。

  總結一下,凡是涉及到SIGCLD以及SIGCHLD的信號處理問題,須要參考具體所在系統對於SIGCLD以及SIGCHLD的實現分析。

 

10.8 10.9 10.10 kill和alarm函數

  三個部分放在一塊兒,用一個例子綜合在一塊兒。

  1. kill函數:kill(pid_t pid, int signo),向某個進程發送信號。

    這裏注意兩點:

    (1)調用kill函數的進程是否有權限給另外一個進程發信號

    (2)kill(pid, 0) 這種形式能夠用來驗證pid進程號是否存在;可是考慮到類Unix系統,即便是一個進程的資源已經釋放了,進程號pid仍然會被佔用一段時間,所以這種經過檢測pid號的方式來檢測進程是否存在也不必定是徹底準確的,得看具體的系統實現。

  2. alarm函數:alarm(unsigned int seconds),啓動一個倒計時器,到達seconds的時間後,會觸發SIGALRM的信號;若是沒有設定信號處理函數,則默認的行爲就是將進程結束。

    這裏注意兩點:

    (1)一個進程只能同時有一個有效的alarm函數,若是在一個進程中屢次使用alarm函數:後一次的alarm時間會替代上一次的alarm的時間。具體示例以下:    

#include <unistd.h>
#include <stdio.h> 
#include <stdlib.h> 
#include <signal.h>
#include <sys/time.h> 

    
struct timeval start, end;

void sig_int()
{
    unsigned int seconds = 0;
    seconds = alarm(5);
    printf("left seconds: %u\n", seconds);
    gettimeofday(&start, NULL);
    pause();
}

void sig_alm()
{
    float time_interval;
    gettimeofday(&end, NULL);
    time_interval = 1000000*(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
    time_interval = time_interval/1000000;
    printf("Pasue time: %f\n", time_interval);
}

int main()
{
    signal(SIGINT, sig_int);
    signal(SIGALRM, sig_alm);
    alarm(100);
    pause();
}

  程序執行結果如圖:

  

  分析以下:main中執行倒計時100秒;大概過了10秒以後在終端ctrl+c觸發interrupt信號,進入SIGINT信號處理函數;在SIGINT信號處理函數中,修改alarm的倒計時爲5秒。最後從執行結果看到,第二次執行的alarm(5)替代了前一次的alarm(100)。

  有時候爲了不以前設定的alarm無效了,也能夠檢查相似上述代碼中的seconds的值,來保證等夠100秒。

  (2)在某些狀況下,若是產生了資源競爭,調用pause()以前,alarm就執行完了,pasue()就會一直在等着了。爲了不這種狀況,書上給出了以下的sleep函數的設計。我在書上代碼的基礎上稍加修改,爲的就是在代碼中更好的體現資源競爭。

#include "apue.h"
#include <signal.h>
#include <unistd.h>
#include <setjmp.h> 

static jmp_buf env_alrm;

static void sig_alm(int signo)
{
    longjmp(env_alrm, 1);
}


unsigned int sleep2(unsigned int second)
{
    if (signal(SIGALRM, sig_alm) == SIG_ERR){ 
        return second; 
    }
    if ( setjmp(env_alrm)==0 )
    {
        alarm(second);
        kill(getpid(),SIGINT);
        pause();
    }
    return alarm(0);
} 

static void  sig_int(int signo)
{
    int i,j;
    volatile int k;
    printf("sig_int starting\n");
    for ( i=0; i<300000; i++)
    {
        for (j=0; j<40000; j++) k += i*j;
    }
    printf("sig_int finished\n");
}

int main()
{
    unsigned int unslept;
    if (signal(SIGINT, sig_int)==SIG_ERR) { 
        err_sys("signal(SIGINT) error"); 
    }  
    unslept = sleep2(5);
    printf("sleep2 returned : %u\n", unslept);
    exit(0);
}

  程序的執行結果以下:

  

  分析以下:

  a. 上述代碼體現了setjmp longjmp可以在資源競爭條件下對alarm和pause對進行保護

  上述代碼在main()中調用sleep2()函數。

  進入到sleep2()函數以後,先註冊一個SIGALRM的信號處理函數 → 再在setjmp保護下,alarm(second)設定倒計時 → 隨後立刻執行kill命令,向進程自身發送一個SIGINT信號,觸發sig_int函數 → sig_int函數中執行的任務遠超過alarm(second)中second的限制,爲的就是模擬資源競爭時alarm已經倒計時完畢的時候,pause()還沒開始。

  因爲sleep2()中保護機制的存在,若是因爲資源競爭致使「alarm已經執行完畢,可是pause還沒開始」,longjmp就直接跳到setjmp的地方了,而且返回的值爲1;這樣就跳過了pause()的語句,天然也就不會無限期的等待了。

  b. 上述代碼也體現了setjmp longjmp這種機制的隱患

  加入不是人爲產生資源競爭,而是進程真的在執行某些任務;這個時候因爲alarm到時,強制longjmp結束,頗有可能形成其餘任務還沒處理完成,就直接longjmp結束了。

  總結一下,要慎用setjmp和longjmp這樣的長跳起色制。 

 

10.11  10.12  10.13  sigset_t & sigprocmask & sigpending

  1. sigset_t

  有時候咱們須要設定,一個進程要屏蔽哪些信號、要接受哪些信號、記錄進程原來對信號的設定情況等。這個時候,就須要一種信號集合的數據結構,以及圍繞其的一些周邊函數來完成。其中數據結構就是sigset_t

  2. int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)

  設定信號mask。

  how : 修改信號mask的方式,SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK

  ret : 信號mask被設定成設麼樣

  oret : 保留原來的信號mask

  返回值表示函數執行成功或失敗。

  3. int sigpending(sigset_t *set)

  得到當前進程有哪些信號被pending了。

  看一個例子:

#include "apue.h"


static void sig_quit(int signo)
{
    printf("caught SIGQUIT\n");
    signal(SIGQUIT, SIG_DFL);
}

int main()
{
    sigset_t newmask, oldmask, pendmask;

    signal(SIGQUIT, sig_quit); /*捕捉退出信號*/
    sigemptyset(&newmask); /*初始化新的信號mask*/
    sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信號*/
    sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信號*/

    sleep(5);

    sigpending(&pendmask); /*得到有當前進程被阻塞的信號*/
    if (sigismember(&pendmask, SIGQUIT)) { 
        printf("\nSIGQUIT pending\n");
    }
    sigprocmask(SIG_SETMASK, &oldmask, NULL); /*釋放被阻塞的信號*/
    printf("SIGQUIT unblocked\n");
    sleep(5);
    exit(0);
}

  代碼執行結果以下:

  

  分析以下:

  (1)上述代碼首先用sigprocmask阻塞了SIGQUIT信號;通過第一個sleep(5)以後,立刻執行sigpending,得到了當前被阻塞的信號,而且獲得了驗證SIGQUIT確實在被阻塞的信號集合pendmask中。

  (2)緊接着再用sigprocmask函數釋從新恢復了信號mask的值,即釋放被阻塞的信號;隨後,以前被阻塞的SIGQUIT信號立刻就進來了,而且觸發了sig_quit信號處理函數;注意,在信號處理函數中恢復了對SIGQUIT的默認處理方式。

  (3)即便第一個sleep(5)的時候輸入了多個ctrl+\輸入信號,最終被處理的SIGQUIT信號也只有一個(最起碼在我使用的Linux系統上是這樣的)。

  在上述代碼基礎上再擴展一下,若是多個不一樣的信號被pending住,當unblock的以後,會有什麼效果呢?將書上的代碼修改以下:

#include "apue.h"


static void sig_quit(int signo)
{
    printf("caught SIGQUIT\n");
    signal(SIGQUIT, SIG_DFL);
}

static void sig_int(int signo)
{
    printf("caught SIGINT\n");
    signal(SIGINT, SIG_DFL);
}

int main()
{
    sigset_t newmask, oldmask, pendmask;

    signal(SIGQUIT, sig_quit); /*捕捉退出信號*/
    signal(SIGINT, sig_int); /*捕捉中斷信號*/
    sigemptyset(&newmask); /*初始化新的信號mask*/
    sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信號*/
    sigaddset(&newmask, SIGINT); /*阻塞interrupt信號*/
    sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信號*/

    sleep(5);

    sigpending(&pendmask); /*得到有當前進程被阻塞的信號*/
    if (sigismember(&pendmask, SIGQUIT)) { 
        printf("\nSIGQUIT pending\n");
    }

    if (sigismember(&pendmask, SIGINT)) { 
        printf("\nSIGINT pending\n");
    }
    sigprocmask(SIG_SETMASK, &oldmask, NULL); /*釋放被阻塞的信號*/
    printf("SIGQUIT unblocked\n");
    printf("SIGINT unblocked\n");
    sleep(5);
    exit(0);
}

  不只阻塞了SIGQUIT信號,並且還阻塞SIGINT信號。再執行代碼以下:

  

  分析以下:

  (1)能夠看到,對於同類的信號被阻塞信號,只獲取一個;對於不一樣類的阻塞信號,能夠把不一樣類等待的信號分別處理了。

  (2)對比兩次運行程序,雖然輸入信號的順序是不一樣的,可是執行信號處理函數的順序卻沒有改變。

  針對以上兩點內容,google了一下相關資料:這個blog的解釋很是好http://galex.cn/【apue】信號/

  

  上述的信號是經典信號,不支持排隊,並且信號的響應順序和信號到來的順序根本沒有關係。恩,暫時這樣理解。

 

10.14 10.15  sigaction Function & sigsetjmp siglongjmp Function

  1. int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact) 

  本質上用於信號處理函數的註冊,signal(2)的增強改進版。執行成功返回0,出錯返回-1。

  signo : 要處理的信號

  act : 對信號處理函數的新設定

  oact : 以前註冊的signo信號處理函數的信息

  其中sigaction是結構體變量,裏面不只存放了信號處理函數,並且還有須要屏蔽掉的信號集合,以及其餘信息。

  struct sigaction{

    void (*sa_handler) (int); /*信號處理函數*/

    sigset_t sa_mask; /*須要屏蔽的信號*/

    ...

  }

  這裏的屏蔽信號指的是執行信號處理函數前但願屏蔽掉的信號;當信號處理函數執行完並return的時候,信號mask就會恢復到以前的狀態。

  回想以前在signal handler中從新調用signal的例子,應用sigaction函數就能夠作到「Hence, we are guaranteed that whenever we are processing a given signal,  another occurrence of that same signal is blocked until we're finished processing the first occurrence

  另外,用sigaction函數註冊的信號處理函數,只要註冊一次就一直生效;除非顯式修改該信號的處理函數。

  

  2. sigsetjmp & siglongjmp

  int sigsetjmp(sigjmp_buf env, int savemask)

    返回0表明是設置jmp點;返回非零值表明從別處跳回來,具體是什麼值在跳的時候決定。

    env : 目前還不詳

    savemask : 執行跳轉以前的信號mask值

  void siglongjmp(sigjmp_buf env, int val)

    跳到sigsetjmp的地方,其中val要求是非零值,即sigsetjmp的返回值。

  以前已經有setjmp和longjmp這種終極長跳轉函數對了,爲何還要有sigsetjmp和siglongjmp函數對呢?

  考慮下面這種狀況:

  a. 假設進入到了信號處理函數中(此時,假設同類信號已經被blocked了),該類信號已經在mask中設置爲blocked了;

  b. 若是signal handler被順利執行完,這時信號mask就恢復到執行signal handler以前的狀態了

  c. 可是,若是signal handler執行到中間,執行到了longjmp語句了,極可能就把信號mask恢復這個環節就個跳過去了

  d. 信號mask沒有恢復的問題就是,原本同類信號能夠再繼續被處理,可是因爲沒有恢復信號mask,就不能被處理了

  上面的狀況,也是sigsetjmp和siglongjmp的設計初衷。

  

下面看一個例子(基於書上Figure 10.20的示例改造的),實際體會一下sigsetjmp和siglongjmp的函數對的做用:

#include "apue.h"
#include <setjmp.h>
#include <time.h>
#include <errno.h> 

static sigjmp_buf jmpbuf;
static volatile sig_atomic_t canjmp;

/*得到當前進程是否mask了某種信號*/
void pr_mask(const char *str)
{
    sigset_t sigset;
    int errno_save;
    errno_save = errno;

    sigemptyset(&sigset);
    if ( sigprocmask(0,NULL,&sigset)<0 ) //得到當前的signal set狀況
    {
        err_ret("sigprocmask error");
        errno = errno_save;
        return;
    }
    printf("%s",str); //打印調用信息title
    if ( sigismember(&sigset, SIGINT) ) printf(" SIGINT");
    if ( sigismember(&sigset, SIGQUIT) ) printf(" SIGQUIT");
    if ( sigismember(&sigset, SIGUSR1) ) printf(" SIGUSR1");
    if ( sigismember(&sigset, SIGALRM) ) printf(" SIGALRM"); 
    printf("\n");
    errno = errno_save;
}

/*SIGUSR1的信號處理函數*/
void sig_usr1(int signo)
{
    time_t starttime;
    if (canjmp==0) return; 
    
    pr_mask("starting sig_usr1: ");
    alarm(3);
    starttime = time(NULL);
    for (; ;) /*busy等待5秒*/
        if (time(NULL)>starttime+5)
            break;
    pr_mask("finishing sig_usr1: ");
    canjmp = 0;
    siglongjmp(jmpbuf,1);
    /*longjmp(jmpbuf,1);*/
}
/*SIGALRM信號處理函數*/
void sig_alarm(int signo)
{
    pr_mask("in sig_alrm: ");
}

int main()
{
    struct sigaction siga1,siga2;

    siga1.sa_handler = sig_usr1;
    sigemptyset(&siga1.sa_mask);
    sigaddset(&siga1.sa_mask, SIGUSR1);

    siga2.sa_handler = sig_alarm;
    sigemptyset(&siga2.sa_mask);
    sigaddset(&siga2.sa_mask, SIGALRM);

    sigaction(SIGUSR1, &siga1, NULL);
    sigaction(SIGALRM, &siga2, NULL);
    
    pr_mask("staring main: ");

    if (/*setjmp(jmpbuf)*/sigsetjmp(jmpbuf,1)) {
        pr_mask("ending main: ");
        exit(0); 
    } 
    canjmp = 1;
    for(; ;)
        pause();
}

  程序運行結果以下:

  (1)用sigsetjmp和siglongjmp函數對的運行結果:

    

  (2)用setjmp和longjmp函數對的運行結果:

    

  對比以上兩個運行結果能夠看到,sigsetjmp和siglongjmp函數對確實很好地保護了信號mask現場,不會由於jmp的動做就致使某些信號mask位沒有被恢復過來。(另,在我運行的系統上,若是直接用signal還不太行,必須用sigaction函數才能夠得到上述的結果;緣由就是系統的signal沒有實現同類信號屏蔽

 


10.18 system Function

  這個部分主要說system這個函數與signal相關的內容:POSIX.1標準要求system必須忽略SIGINT和SIGQUIT信號,屏蔽SIGCHLD信號。

  下面用正反兩個例子來講明爲何有上述的要求。

  例子一

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

int system(const char *cmdstring)    /* version without signal handling */
{
    pid_t    pid;
    int        status;
    if (cmdstring == NULL)
        return(1);        /* always a command processor with UNIX */

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {                /* child */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);        /* execl error */
    } else {                            /* parent */
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }
        }
    }
    return(status);
}
static void sig_int(int signo)
{
    printf("caught SIGINT\n");
}

static void sig_chld(int signo)
{
    printf("caught SIGCHILD\n");
}

static void sig_usr1(int signo)
{
    printf("caught SIGUSR1\n");
}
int main()
{
    signal(SIGINT, sig_int);
    signal(SIGCHLD, sig_chld);
    signal(SIGUSR1, sig_usr1);
    system("/bin/ed");
}

  程序運行結果以下:

  

  分析以下:

  -------------插播一段ed的背景知識-------------

  首先須要補充一下/bin/ed這個程序有關信號的特色:

    (1)ed捕獲SIGINT和SIGQUIT信號

    (2)對於SIGINT信號:prints a question mask

    (3)對於SIGQUIT信號的處理方式:ignore

  爲了驗證ed的特性,我作了以下的試驗:

  

  兩個窗口是tmux開的,左側是調用/bin/ed的窗口;右側的是發送signal用的(另,不知道tmux的,能夠參考個人這篇blog

  能夠看到開啓ed程序後,向其發送SIGINT信號,真的再終端反饋輸出一個?;向其發送SIGQUIT信號,並麼有什麼反應。

  ------------------------插播結束-------------------------

  如今開始切入正題。

  先分析運行結果

  (參考http://blog.csdn.net/windeal3203/article/details/39049291http://blog.csdn.net/ctthuangcheng/article/details/9258715):

  (1)終端輸入ctrl+c,前臺進程都會受到這個信號;這裏的前臺進程包括ed shell a.out三個進程(其中shell自動忽略SIGINT信號,不討論

  (2)ed在收到SIGINT後,天然會輸出一個?;而a.out中註冊了SIGINT的處理函數,所以也會觸發信號處理函數

  (3)輸入q以後,ed退出,發送一個SIGCHLD信號;因爲a.out是ed的父進程(看紅框中的pid和ppid),因此天然收到SIGCHLD信號,並觸發信號處理函數

  請注意,上述的system函數是去除了signal設定的閹割版,並無符合POSIX.1的標準

  咱們須要一直記着:這個/bin/ed是由system函數調用的,這樣帶來的問題有兩個:

  (1)因爲終端正在運行的是/bin/ed程序,輸入的ctrl+c的想法是給ed的,並非給a.out的,a.out錯誤接收到了發給ed的SIGINT了;這個時候若是a.out還有其餘的任務要執行,而且沒有處理SIGINT的機制,就被強制關閉了。

  (2)當ed執行完畢退出的時候,至關於child process結束,所以會給parent process的a.out發送一個SIGCHLD信號;其實這個ed一旦由system函數執行上,就跟a.out沒有太大關係了,a.out錯誤接收了發給ed的SIGCHLD了,認爲是本身的進程執行完畢了。

  我我的理解,system function雖說某種程度上方便了啓動某些程序;但經過上述的分析,system Function也是一個挺彆扭的事情。要想知作別扭的緣由,須要參考一下system的實現(不考慮signal版):

int system(const char *cmdstring)    /* version without signal handling */
{
    pid_t    pid;
    int        status;
    if (cmdstring == NULL)
        return(1);        /* always a command processor with UNIX */

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {                /* child */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);        /* execl error */
    } else {                            /* parent */
        while (waitpid(pid, &status, 0) < 0) {
            if (errno != EINTR) {
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }
        }
    }
    return(status);
}

  (1)顯然system的實現利用fork和exec兩個重要的函數:fork就是分叉生成一個child process;exec的做用是讓child process用shell運行程序;能夠說system利用了fork和exec實現了極大的代碼便利

  (2)可是fork和exec必然會對parent process有影響,好比上面提到的信號問題。

  上述兩個方面(1)簡化了工做,(2)複雜了工做,因此說system Function是個彆扭的函數。

  

  例子二

  給出另外一個system實現(在原書的代碼上作了一些修改,主要是方便顯示),代碼以下:

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

static void sig_int(int signo)
{
    printf("caught SIGINT\n");
}

static void sig_chld(int signo)
{
    printf("caught SIGCHILD\n");
}

int i_system(const char *cmdstring)    /* with appropriate signal handling */
{
    pid_t                pid;
    int                    status;
    struct sigaction    ignore, saveintr, savequit;
    sigset_t            chldmask, savemask;

    if (cmdstring == NULL)
        return(1);        /* always a command processor with UNIX */

    ignore.sa_handler = SIG_IGN;    /* ignore SIGINT and SIGQUIT */
    sigemptyset(&ignore.sa_mask);
    ignore.sa_flags = 0;
    if (sigaction(SIGINT, &ignore, &saveintr) < 0)
        return(-1);
    if (sigaction(SIGQUIT, &ignore, &savequit) < 0)
        return(-1);
    
    sigemptyset(&chldmask);            // now block SIGCHLD
    sigaddset(&chldmask, SIGCHLD);
    if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0)
        return(-1);

    if ((pid = fork()) < 0) {
        status = -1;    /* probably out of processes */
    } else if (pid == 0) {            /* child */
        /* restore previous signal actions & reset signal mask */
        sigaction(SIGINT, &saveintr, NULL);
        sigaction(SIGQUIT, &savequit, NULL);
        sigprocmask(SIG_SETMASK, &savemask, NULL);

        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);        /* exec error */
    } else {
        /* parent */
        while (waitpid(pid, &status, 0) < 0)
            if (errno != EINTR) {
                printf("error\n");
                status = -1; /* error other than EINTR from waitpid() */
                break;
            }       
    }
    printf("ed ends\n");
    /* restore previous signal actions & reset signal mask */
    if (sigaction(SIGINT, &saveintr, NULL) < 0)
        return(-1);
    if (sigaction(SIGQUIT, &savequit, NULL) < 0)
        return(-1);
    
    if (sigprocmask(SIG_SETMASK, &savemask, NULL) < 0)
        return(-1);
    
    printf("return from system\n");
    return(status);
}
int main()
{
    signal(SIGINT, sig_int);
    signal(SIGCHLD, sig_chld);
    i_system("/bin/ed");
    printf("after ed\n");
}

  代碼執行結果:

  

  結果分析:

  (1)當終端輸入q以後,ed進程結束,並向父進程發送SIGCHLD信號  

  (2)可是因爲i_system執行過程當中屏蔽了SIGCHLD信號,所以ed結束後先被主進程waitpid收屍,這個時候父進程a.out是不會收到SIGCHLD信號的

  (3)最後,當全部與ed相關的內容都處理完了以後,最後一個sigprocmask放開SIGCHLD信號的阻塞

  (4)以前因爲ed進程結束帶來的SIGCHLD信號立刻被處理了,觸發sig_chld信號處理函數

  能夠看到,因爲system忽略了SIGINT和SIGQUIT信號,在system的執行過程當中,不會因爲終端發出ctrl+c就會影響父進程a.out的運行;而且因爲屏蔽了SIGCHLD信號,不會使得父進程誤收到SIGCHLD信號;只有當所system中執行的全部內容都結束後,纔會釋放對SIGCHLD的阻塞。

  system這個函數考慮的問題太多,實際用到的時候要把與signal相關的東西考慮清楚。

相關文章
相關標籤/搜索