進程間通訊使用信號

1、什麼是信號
用過Windows的咱們都知道,當咱們沒法正常結束一個程序時,能夠用任務管理器強制結束這個進程,但這實際上是怎麼實現的呢?一樣的功能在Linux上是經過生成信號和捕獲信號來實現的,運行中的進程捕獲到這個信號而後做出必定的操做並最終被終止。

信號是UNIX和Linux系統響應某些條件而產生的一個事件,接收到該信號的進程會相應地採起一些行動。一般信號是由一個錯誤產生的。但它們還能夠做爲進程間通訊或修改行爲的一種方式,明確地由一個進程發送給另外一個進程。一個信號的產生叫生成,接收到一個信號叫捕獲。

2、信號的種類
信號的名稱是在頭文件signal.h中定義的,信號都以SIG開頭,經常使用的信號並很少,經常使用的信號以下:

更多的信號類型可查看附錄表。

3、信號的處理——signal函數
程序可用使用signal函數來處理指定的信號,主要經過忽略和恢復其默認行爲來工做。signal函數的原型以下:
  1. #include <signal.h>  
  2. void (*signal(int sig, void (*func)(int)))(int);  
這是一個至關複雜的聲明,耐心點看能夠知道signal是一個帶有sig和func兩個參數的函數,func是一個類型爲void (*)(int)的函數指針。該函數返回一個與func相同類型的指針,指向先前指定信號處理函數的函數指針。準備捕獲的信號的參數由sig給出,接收到的指定信號後要調用的函數由參數func給出。其實這個函數的使用是至關簡單的,經過下面的例子就能夠知道。注意信號處理函數的原型必須爲void func(int),或者是下面的特殊值:
    SIG_IGN:忽略信號
    SIG_DFL:恢復信號的默認行爲

說了這麼多,仍是給出一個例子來講明一下吧,源文件名爲signal1.c,代碼以下:
  1. #include <signal.h>  
  2. #include <stdio.h>  
  3. #include <unistd.h>  
  4.   
  5. void ouch(int sig)  
  6. {  
  7.     printf("\nOUCH! - I got signal %d\n", sig);  
  8.     //恢復終端中斷信號SIGINT的默認行爲  
  9.     (void) signal(SIGINT, SIG_DFL);  
  10. }  
  11.   
  12. int main()  
  13. {  
  14.     //改變終端中斷信號SIGINT的默認行爲,使之執行ouch函數  
  15.     //而不是終止程序的執行  
  16.     (void) signal(SIGINT, ouch);  
  17.     while(1)  
  18.     {  
  19.         printf("Hello World!\n");  
  20.         sleep(1);  
  21.     }  
  22.     return 0;  
  23. }  
運行結果以下:


能夠看到,第一次按下終止命令(ctrl+c)時,進程並無被終止,面是輸出OUCH! - I got signal 2,由於SIGINT的默認行爲被signal函數改變了,當進程接受到信號SIGINT時,它就去調用函數ouch去處理,注意ouch函數把信號SIGINT的處理方式改變成默認的方式,因此當你再按一次ctrl+c時,進程就像以前那樣被終止了。

4、信號處理——sigaction函數
前面咱們看到了signal函數對信號的處理,可是通常狀況下咱們可使用一個更加健壯的信號接口——sigaction函數。它的原型爲:
  1. #include <signal.h>  
  2. int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);  
該函數與signal函數同樣,用於設置與信號sig關聯的動做,而oact若是不是空指針的話,就用它來保存原先對該信號的動做的位置,act則用於設置指定信號的動做。

sigaction結構體定義在signal.h中,可是它至少包括如下成員:
void (*) (int) sa_handler;處理函數指針,至關於signal函數的func參數。
sigset_t sa_mask; 指定一個。信號集,在調用sa_handler所指向的信號處理函數以前,該信號集將被加入到進程的信號屏蔽字中。信號屏蔽字是指當前被阻塞的一組信號,它們不能被當前進程接收到
int sa_flags;信號處理修改器;

sa_mask的值一般是經過使用信號集函數來設置的,關於信號集函數,我將會在個人下一篇文章—— Linux進程間通訊——信號集函數,詳細講述。
sa_flags,一般能夠取如下的值:


此外,如今有一個這樣的問題,咱們使用signal或sigaction函數來指定處理信號的函數,可是若是這個信號處理函數創建以前就接收到要處理的信號的話,進程會有怎樣的反應呢?它就不會像咱們想像的那樣用咱們設定的處理函數來處理了。sa_mask就能夠解決這樣的問題,sa_mask指定了一個信號集,在調用sa_handler所指向的信號處理函數以前,該信號集將被加入到進程的信號屏蔽字中,設置信號屏蔽字能夠防止信號在它的處理函數還未運行結束時就被接收到的狀況,即便用sa_mask字段能夠消除這一競態條件。

承接上面的例子,下面給出用sigaction函數重寫的例子代碼,源文件爲signal2.c,代碼以下:
  1. #include <unistd.h>  
  2. #include <stdio.h>  
  3. #include <signal.h>  
  4.   
  5. void ouch(int sig)  
  6. {  
  7.     printf("\nOUCH! - I got signal %d\n", sig);  
  8. }  
  9.   
  10. int main()  
  11. {  
  12.     struct sigaction act;  
  13.     act.sa_handler = ouch;  
  14.     //建立空的信號屏蔽字,即不屏蔽任何信息  
  15.     sigemptyset(&act.sa_mask);  
  16.     //使sigaction函數重置爲默認行爲  
  17.     act.sa_flags = SA_RESETHAND;  
  18.   
  19.     sigaction(SIGINT, &act, 0);  
  20.   
  21.     while(1)  
  22.     {  
  23.         printf("Hello World!\n");  
  24.         sleep(1);  
  25.     }  
  26.     return 0;  
  27. }  
運行結果與前一個例子中的相同。注意sigaction函數在默認狀況下是不被重置的,若是要想它重置,則sa_flags就要爲SA_RESETHAND。

5、發送信號
上面說到的函數都是一些進程接收到一個信號以後怎麼對這個信號做出反應,即信號的處理的問題,有沒有什麼函數能夠向一個進程主動地發出一個信號呢?咱們能夠經過兩個函數kill和alarm來發送一個信號。

一、kill函數
先來看看kill函數,進程能夠經過kill函數向包括它自己在內的其餘進程發送一個信號,若是程序沒有發送這個信號的權限,對kill函數的調用就將失敗,而失敗的常見緣由是目標進程由另外一個用戶所擁有。想想也是容易明白的,你總不能控制別人的程序吧,固然超級用戶root,這種上帝般的存在就除外了。

kill函數的原型爲:
  1. #include <sys/types.h>  
  2. #include <signal.h>  
  3. int kill(pid_t pid, int sig);  
它的做用把信號sig發送給進程號爲pid的進程,成功時返回0。

kill調用失敗返回-1,調用失敗一般有三大緣由:
一、給定的信號無效(errno = EINVAL)
二、發送權限不夠( errno = EPERM )
三、目標進程不存在( errno = ESRCH )

二、alarm函數
這個函數跟它的名字同樣,給咱們提供了一個鬧鐘的功能,進程能夠調用alarm函數在通過預約時間後向發送一個SIGALRM信號。

alarm函數的型以下:
  1. #include <unistd.h>  
  2. unsigned int alarm(unsigned int seconds);  
alarm函數用來在seconds秒以後安排發送一個SIGALRM信號,若是seconds爲0,將取消全部已設置的鬧鐘請求。alarm函數的返回值是之前設置的鬧鐘時間的餘留秒數,若是返回失敗返回-1。

快馬加鞭,下面就給合fork、sleep和signal函數,用一個例子來講明kill函數的用法吧,源文件爲signal3.c,代碼以下:
  1. #include <unistd.h>  
  2. #include <sys/types.h>  
  3. #include <stdlib.h>  
  4. #include <stdio.h>  
  5. #include <signal.h>  
  6.   
  7. static int alarm_fired = 0;  
  8.   
  9. void ouch(int sig)  
  10. {  
  11.     alarm_fired = 1;  
  12. }  
  13.   
  14. int main()  
  15. {  
  16.     pid_t pid;  
  17.     pid = fork();  
  18.     switch(pid)  
  19.     {  
  20.     case -1:  
  21.         perror("fork failed\n");  
  22.         exit(1);  
  23.     case 0:  
  24.         //子進程  
  25.         sleep(5);  
  26.         //向父進程發送信號  
  27.         kill(getppid(), SIGALRM);  
  28.         exit(0);  
  29.     default:;  
  30.     }  
  31.     //設置處理函數  
  32.     signal(SIGALRM, ouch);  
  33.     while(!alarm_fired)  
  34.     {  
  35.         printf("Hello World!\n");  
  36.         sleep(1);  
  37.     }  
  38.     if(alarm_fired)  
  39.         printf("\nI got a signal %d\n", SIGALRM);  
  40.   
  41.     exit(0);  
  42. }  
運行結果以下:

在代碼中咱們使用fork調用複製了一個新進程,在子進程中,5秒後向父進程中發送一個SIGALRM信號,父進程中捕獲這個信號,並用ouch函數來處理,變改alarm_fired的值,而後退出循環。從結果中咱們也能夠看到輸出了5個Hello World!以後,程序就收到一個SIGARLM信號,而後結束了進程。

注:若是父進程在子進程的信號到來以前沒有事情可作,咱們能夠用函數pause()來掛起父進程,直到父進程接收到信號。當進程接收到一個信號時,預設好的信號處理函數將開始運行,程序也將恢復正常的執行。這樣能夠節省CPU的資源,由於能夠避免使用一個循環來等待。以本例子爲例,則能夠把while循環改成一句pause();

下面再以一個小小的例子來講明alarm函數和pause函數的用法吧,源文件名爲,signal4.c,代碼以下:
  1. #include <unistd.h>  
  2. #include <sys/types.h>  
  3. #include <stdlib.h>  
  4. #include <stdio.h>  
  5. #include <signal.h>  
  6.   
  7. static int alarm_fired = 0;  
  8.   
  9. void ouch(int sig)  
  10. {  
  11.     alarm_fired = 1;  
  12. }  
  13.   
  14. int main()  
  15. {  
  16.     //關聯信號處理函數  
  17.     signal(SIGALRM, ouch);  
  18.     //調用alarm函數,5秒後發送信號SIGALRM  
  19.     alarm(5);  
  20.     //掛起進程  
  21.     pause();  
  22.     //接收到信號後,恢復正常執行  
  23.     if(alarm_fired == 1)  
  24.         printf("Receive a signal %d\n", SIGALRM);  
  25.     exit(0);  
  26. }  
運行結果以下:

進程在5秒後接收到一個SIGALRM,進程恢復運行,打印信息並退出。

6、信號處理函數的安全問題
試想一個問題,當進程接收到一個信號時,轉到你關聯的函數中執行,可是在執行的時候,進程又接收到同一個信號或另外一個信號,又要執行相關聯的函數時,程序會怎麼執行?

也就是說,信號處理函數能夠在其執行期間被中斷並被再次調用。當返回到第一次調用時,它可否繼續正確操做是很關鍵的。這不只僅是遞歸的問題,而是可重入的(便可以徹底地進入和再次執行)的問題。而反觀Linux,其內核在同一時期負責處理多個設備的中斷服務例程就須要可重入的,由於優先級更高的中斷可能會在同一段代碼的執行期間「插入」進來。

簡言之,就是說,咱們的信號處理函數要是可重入的,即離開後可再次安全地進入和再次執行,要使信號處理函數是可重入的,則在信息處理函數中不能調用不可重入的函數。下面給出可重入的函數在列表,不在此表中的函數都是不可重入的, 可重入函數表以下:


7、附錄——信號表

若是進程接收到上面這些信號中的一個,而事先又沒有安排捕獲它,進程就會終止。

還有其餘的一些信號,以下:



原文:http://blog.csdn.net/ljianhui/article/details/10128731
相關文章
相關標籤/搜索