第十一章 進程和信號(三)

一個健壯的信號接口 

       我們已經對用 signal 和其相關函數來生成和捕獲信號做了比較深入的介紹,因爲它們在傳統的 Unix 編程中很常見。但 X/Open 與 Unix 規範推薦了一個更新和更健壯的信號編程接口:sigaction 。它的定義如下所示:
#include <signal.h>

int sigaction ( int sig, const struct sigaction *act, struct sigaction *oact );

       sigaction 結構定義在頭文件 signal.h 中,它的作用是定義在接收到參數 sig 指定的信號後應該採取的行動。該結構至少應該包括以下幾個成員:
void ( * ) ( int ) sa_handler     /* function, SIG_DFL or SIG_IGN
sigset_t sa_mask                  /* signals to block in sa_handler
int sa_flags                           /* signal action modifiers
       sigaction 函數設置與 sig 信號關聯的動作。如果 oact 不是空指針,sigaction 將把原先對該信號的動作寫到它指向的位置。如果 act 是空指針,則 sigaction 函數就不需要再做其它設置了,否則將在該參數中設置對指定信號的動作。
       與 signal 函數一樣,sigaction 函數會在成功時返回 0,失敗時返回 -1。如果給出的信號無效或者試圖對一個不允許被捕獲或忽略的信號進行捕獲或忽略,錯誤變量 errno 會被設置 EINVAL。
       在參數 act 所指向的 sigaction 結構中,sa_handler 是一個函數指針,它指向接收到信號 sig 時將被調用的信號處理函數。它相當於前面見到的傳遞給函數 signal 的參數 func 。我們可以將 sa_handler 字段設置爲特殊值 SIG_IGN 和 SIG_DFL,它們分別表示信號將被忽略或把對該信號的處理方式恢復爲默認動作。
       sa_mask 成員指定了一個信號集,在調用 sa_handler 所指向的信號處理函數之前,該信號集將被加入到進程的信號屏蔽字中。這是一組將被阻塞且不會傳遞給該進程的信號。設置信號屏蔽字可以防止前面看到的信號在它的處理函數還未運行結束時就被接收到的情況。使用 sa_mask 字段可以消除這一競態條件。
       但是,由 sigaction 函數設置的信號處理函數在默認情況下是不被重置的,如果希望獲得類似前面用第二次 signal 調用對信號處理進行重置的效果,就必須在 sa_flags 成員中 包含值 SA_RESETHAND。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void ouch(int sig)
{
    printf("OUCH! - I got signal %d\n", sig);
}

int main()
{
    struct sigaction act;

    act.sa_handler = ouch;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGINT, &act, 0);

  while(1) {
    printf("Hello World!\n");
    sleep(1);
  }
}
       運行這個新版的程序時,只要按下按下 Ctrl+C 組合鍵,就可以看到一條消息。因爲 sigaction 函數連續處理到來的 SIGINT 信號。要想終止這個程序,我們只能按下按下 Ctrl+-/ 組合鍵,它在默認情況下產生 SIGQUIT 信號。
$ ./ctrlc2
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
^/
Quit

$

實驗解析

       這個程序用 sigaction 代替 signal 來設置 Ctrl+C 組合鍵(SIGINT信號)的信號處理函數爲 ouch。它首先必須設置一個 sigaction 結構,在該結構中包含信號i處理函數、信號屏蔽字和標誌。在本例中,我們不需要設置任何標誌,並通過調用新的函數 sigemptyset 來創建空的信號屏蔽字。

       運行完這個程序後,你將發現在當前目錄下多了一個 core 文件,你可以安全的刪除它。


信號集合
       頭文件 signal.h 定義了類型 sigset_t 和用來處理信號集的函數。sigaction 和其它函數將用這些信號來修改進程在接收到信號時的行爲。
#include <signal.h>

int sigaddset ( sigset_t *set, int signo );
int sigemptyset ( sigset_t *set );
int sigfillset ( sigset_t *set );
int sigdelset ( sigset_t *set, int signo );

       這些函數執行的操作如它們的名字所示。sigemptyset 將信號集初始化爲空。sigfillset 將信號集初始化爲包含所有已定義的信號。sigaddset 和sigdelset 從信號集中增加或刪除給定的信號(signo)。它們在成功時返回 0 ,失敗時返回 -1並設置 errno。只有一個錯誤代碼被定義,即當給定的信號無效時,errno 將設置爲 EINVAL。

       函數 sigismember 判斷一個給定的信號是否是一個信號集的成員。如果是就返回 1 ;如果不是,它就返回 0;如果給定的信號無效,它就返回 -1並設置 errno 爲 EINVAL。
#include <signal.h>
int sigismember ( sigset_t *set, int signo );

       進程信號屏蔽字的設置或檢查工作由函數 sigprocmask 來完成。信號屏蔽字是指當前被阻塞的一組信號,它們不能被當前進程接收到。
#include <signal.h>
int sigprocmask ( int how, const sigset_t *set, sigset_t *oset );
       sigprocmask 函數可以根據參數 how 指定的方式修改進程的信號屏蔽字。新的信號屏蔽字由參數 set (如果它不爲空)指定,而原先的信號屏蔽字將保存到信號集 oset 中。

      參數 how 的取值可以是下表中的一個:

SIG_BLOCK
把參數 set 中的信號添加到信號屏蔽字中
SIG_SETMASK
把信號屏蔽字設置爲參數 set 中的信號
SIG_UNBLOCK  
從信號屏蔽字中刪除參數 set 中的信號
      

      如果參數 set 是空指針,how 的值就沒有意義了,此時這個調用唯一的目的就是把當前信號屏蔽字的值保存到 oset 中。
       如果 sigprocmask 成功完成,它將返回 0;如果how參數不可用則返回-1,並且將errno設置爲EINVAL。

       如果一個信號被進程阻塞,它就不會傳遞給進程,但會停留在待處理狀態。程序可以通過調用函數 sigpending 來查看它阻塞的信號中有那些正停留在待處理狀態。
#include <signal.h>
int sigpending ( sigset_t *set );
       這個函數的作用是,將被阻塞的信號中停留在待處理狀態的一組信號寫到參數 set 指向的信號集中。成功時它將返回 0,否則返回 -1並設置 errno 以表明錯誤的原因。如果程序需要處理信號,同時又需要控制信號處理函數的調用時間,這個函數就很有用了。

進程可以通過調用 sigsuspend 函數掛起自己執行,直到信號集中的一個信號到達爲止。這是我們前面見到的 pause 函數更通用的一種表現式。

nclude <signal.h>
int sigsuspend ( const sigset_t *sigmask );
      sigsuspend 函數將進程的屏蔽字替換爲由參數 sigmask 給出的信號集,然後掛起程序的執行。程序將在信號處理函數執行完畢後繼續執行。如果接收到的信號終止了程序, sigsuspend 就不會返回;如果接收到的信號沒有終止程序, sigsuspend 就返回 -1並將 errno 設置爲 EINTR。

1、sigaction 標誌
用在 sigaction 函數裏的 sigaction 結構體中的 sa_flags 字段可以包含下表中的取值,它們用於改變信號的行爲:

SA_NOCLDSTOP
子進程停止時不產生 SIGCHLD 信號
SA_RESETHAND 
將對此信號的處理方式在信號處理函數的入口處重置爲 SIG_DFL
SA_RESTART
重啓可中斷的函數而不是給出 EINTR 錯誤
SA_NODEFER 
捕獲到信號時不將它添加到信號屏蔽字中

      當一個信號被捕獲時,SA_RESETHAND 標誌可以用來自動清除它的信號處理函數,就如同我們在前面看到的那樣。
       程序中使用的許多系統調用都是可中斷的。也就是說,當接收到一個信號時,它們將返回一個錯誤並將 errno 設置爲 EINTR ,表明函數是因爲一個信號而返回的。使用了信號的應用應用程序需要特別注意這一行爲。如果 sigaction 調用中的 sa_flags 字段設置了 SA_RESTART標誌,那麼在信號處理函數執行完之後,函數將被重啓而不是被信號中斷。
       一般的做法是,信號處理函數正在執行時,新接收到的信號將在該處理函數的執行期間被添加到進程的信號屏蔽字中。這防止了同一信號的不斷出現引起信號處理函數的再次運行。如果信號處理函數是一個不可重入的函數,在它結束對第一個信號的處理之前又讓另一個信號再次調用它就有可能引起問題。但如果設置了 SA_NODEFER 標誌,當程序接收到這個信號時就不會改變信號屏蔽字。
       信號處理函數可以在其執行期間被中斷並再次被調用。當返回到第一個調用時,它能否繼續正確操作是很關鍵的。這不僅僅是遞歸(調用自身)的問題,而是可重入(可以安全地進入和再次執行)的問題。Linux 內核中,在同一時間負責處理多個設備的中斷服務例程就需要是可重入的,因爲優先級更高的中斷可能會在同一段代碼的執行期間“插入”進來。

2、常用信號參考表

       表中信號的默認動作都是異常終止進程,進程將以 _exit 調用方式退出(它類似 exit,但在返回到內核之前不作任何清理工作)。但進程的結束狀態傳遞到 wait 和 waitpid 函數中去,從而表明進程是因某個特定的信號而異常終止的。

信號名稱 說明
SIGALRM
由 alarm 函數設置的定時器產生
SIGHUP  
由一個處於非連接狀態的終端發送給控制進程,或者由控制進程在自身結束時發送給每個前臺進程
SIGINIT
一般由終端敲入 Ctrl+C 組合鍵或預設置好的中斷字符產生
SIGKILL
因爲這個信號不能被捕獲或忽略,所以一般在 shell 中用它來強制終止異常進程
SIGPIPE  
如果在向管道寫數據時沒有與之對應的讀進程,就會產生這個信號
SIGTERM
作爲一個請求被髮送,要求進程結束運行。Unix在關機時用這個信號要求系統服務停止運行。它是 kill 命令默認發送的信號。
SIGUSR1,SIGUSR2
進程之間可以用這個信號進行通信,例如然進程報告狀態信息等

        默認情況下,下表中的信號也會引起進程的異常終止。但可能還會有一些與具體實現相關的其他動作,比如創建 core 文件等。

信號名稱
說明
SIGFPE
由浮點運算異常產生
SIGILL
處理器執行了一條非法指令。這通常是由一個崩潰的程序或無效的共享內存模塊引起的
SIGQUIT
一般由從終端輸入 Ctrl+/ 組合鍵或預先設置好的退出字符產生
SIGSEGV
段衝突。一般是因爲對內存中的無效地址進行讀寫引起的,例如超越數組邊界或解引用無效指針。當函數返回到一個非法地址時,覆蓋局部數組變量和引起棧崩潰都會引發 SIGSEGV 信號。

      默認情況下,進程接收到下表中的信號時將會被掛起。

信號名稱
說明
SIGSTOP
 停止執行(不能被捕獲或忽略)
SIGTSTP 
終端掛起信號,通常因按下 Ctrl+Z 組合鍵而產生
SIGTTIN,SIGTTOU
shell用這2個信號表明後臺作業因需要從終端讀取輸入或產生輸出而暫停運行

      SIGCONT 信號的作用是重啓被暫停的進程,如果進程沒有暫停,則忽略該信號。SIGCHLD 信號在默認情況下被忽略。

信號名稱
說明
SIGCONT
如果進程被暫停,就繼續執行
SIGCHLD
子進程暫停或退出時產生

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