簡介: linux訊號機制遠遠比想象的複雜,本文力爭用最短的篇幅,對該機製做了深入細緻的分析。讀者可以先讀一下訊號應用執行個體(在訊號(下)中),這樣可以對訊號發送直到相應的處理函數執行完畢這一過程有個大致的印象。本文盡量給出了較新函數的應用執行個體,著重說明這些的功能。
一、訊號及訊號來源
訊號本質
訊號是在軟體層次上對中斷機制的一種類比,在原理上,一個進程收到一個訊號與處理器收到一個插斷要求可以說是一樣的。訊號是非同步,一個進程不必通過任何操作來等待訊號的到達,事實上,進程也不知道訊號到底什麼時候到達。
訊號是處理序間通訊機制中唯一的非同步通訊機制,可以看作是非同步通知,通知接收訊號的進程有哪些事情發生了。訊號機制經過POSIX即時擴充後,功能更加強大,除了基本通知功能外,還可以傳遞附加資訊。
訊號來源
訊號事件的發生有兩個來源:硬體來源(比如我們按下了鍵盤或者其它硬體故障);軟體來源,最常用發送訊號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟體來源還包括一些非法運算等操作。
回頁首
二、訊號的種類
可以從兩個不同的分類角度對訊號進行分類:(1)可靠性方面:可靠訊號與不可靠訊號;(2)與時間的關係上:即時訊號與非即時訊號。在《Linux環境處理序間通訊(一):管道及有名管道》的附1中列出了系統所支援的所有訊號。
1、可靠訊號與不可靠訊號
"不可靠訊號"
Linux訊號機制基本上是從Unix系統中繼承過來的。早期Unix系統中的訊號機制比較簡單和原始,後來在實踐中暴露出一些問題,因此,把那些建立在早期機制上的訊號叫做"不可靠訊號",訊號值小於SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的訊號都是不可靠訊號。這就是"不可靠訊號"的來源。它的主要問題是:
- 進程每次處理訊號後,就將對訊號的響應設定為預設動作。在某些情況下,將導致對訊號的錯誤處理;因此,使用者如果不希望這樣的操作,那麼就要在訊號處理函數結尾再一次調用signal(),重新安裝該訊號。
- 訊號可能丟失,後面將對此詳細闡述。
因此,早期unix下的不可靠訊號主要指的是進程可能對訊號做出錯誤的反應以及訊號可能丟失。
Linux支援不可靠訊號,但是對不可靠訊號機製做了改進:在調用完訊號處理函數後,不必重新調用該訊號的安裝函數(訊號安裝函數是在可靠機制上的實現)。因此,Linux下的不可靠訊號問題主要指的是訊號可能丟失。
"可靠訊號"
隨著時間的發展,實踐證明了有必要對訊號的原始機制加以改進和擴充。所以,後來出現的各種Unix版本分別在這方面進行了研究,力圖實現"可靠訊號"。由於原來定義的訊號已有許多應用,不好再做改動,最終只好又新增加了一些訊號,並在一開始就把它們定義為可靠訊號,這些訊號支援排隊,不會丟失。同時,訊號的發送和安裝也出現了新版本:訊號發送函數sigqueue()及訊號安裝函數sigaction()。POSIX.4對可靠訊號機製做了標準化。但是,POSIX只對可靠訊號機制應具有的功能以及訊號機制的對外介面做了標準化,對訊號機制的實現沒有作具體的規定。
訊號值位於SIGRTMIN和SIGRTMAX之間的訊號都是可靠訊號,可靠訊號克服了訊號可能丟失的問題。Linux在支援新版本的訊號安裝函數sigation()以及訊號發送函數sigqueue()的同時,仍然支援早期的signal()訊號安裝函數,支援訊號發送函數kill()。
註:不要有這樣的誤解:由sigqueue()發送、sigaction安裝的訊號就是可靠的。事實上,可靠訊號是指後來添加的新訊號(訊號值位於SIGRTMIN及SIGRTMAX之間);不可靠訊號是訊號值小於SIGRTMIN的訊號。訊號的可靠與不可靠只與訊號值有關,與訊號的發送及安裝函數無關。目前linux中的signal()是通過sigation()函數實現的,因此,即使通過signal()安裝的訊號,在訊號處理函數的結尾也不必再調用一次訊號安裝函數。同時,由signal()安裝的即時訊號支援排隊,同樣不會丟失。
對於目前linux的兩個訊號安裝函數:signal()及sigaction()來說,它們都不能把SIGRTMIN以前的訊號變成可靠訊號(都不支援排隊,仍有可能丟失,仍然是不可靠訊號),而且對SIGRTMIN以後的訊號都支援排隊。這兩個函數的最大區別在於,經過sigaction安裝的訊號都能傳遞資訊給訊號處理函數(對所有訊號這一點都成立),而經過signal安裝的訊號卻不能向訊號處理函數傳遞資訊。對於訊號發送函數來說也是一樣的。
2、即時訊號與非即時訊號
早期Unix系統只定義了32種訊號,Ret hat7.2支援64種訊號,編號0-63(SIGRTMIN=31,SIGRTMAX=63),將來可能進一步增加,這需要得到核心的支援。前32種訊號已經有了預定義值,每個訊號有了確定的用途及含義,並且每種訊號都有各自的預設動作。如按鍵盤的CTRL ^C時,會產生SIGINT訊號,對該訊號的預設反應就是進程終止。後32個訊號表示即時訊號,等同於前面闡述的可靠訊號。這保證了發送的多個即時訊號都被接收。即時訊號是POSIX標準的一部分,可用於應用進程。
非即時訊號都不支援排隊,都是不可靠訊號;即時訊號都支援排隊,都是可靠訊號。
回頁首
三、進程對訊號的響應
進程可以通過三種方式來響應一個訊號:(1)忽略訊號,即對訊號不做任何處理,其中,有兩個訊號不能忽略:SIGKILL及SIGSTOP;(2)捕捉訊號。定義訊號處理函數,當訊號發生時,執行相應的處理函數;(3)執行預設操作,Linux對每種訊號都規定了預設操作,詳細情況請參考[2]以及其它資料。注意,進程對即時訊號的預設反應是進程終止。
Linux究竟採用上述三種方式的哪一個來響應訊號,取決於傳遞給相應API函數的參數。
回頁首
四、訊號的發送
發送訊號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
1、kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)
參數pid的值 |
訊號的接收進程 |
pid>0 |
進程ID為pid的進程 |
pid=0 |
同一個進程組的進程 |
pid<0 pid!=-1 |
進程組ID為 -pid的所有進程 |
pid=-1 |
除發送進程自身外,所有進程ID大於1的進程 |
Sinno是訊號值,當為0時(即空訊號),實際不發送任何訊號,但照常進行錯誤檢查,因此,可用於檢查目標進程是否存在,以及當前進程是否具有向目標發送訊號的許可權(root許可權的進程可以向任何進程發送訊號,非root許可權的進程只能向屬於同一個session或者同一個使用者的進程發送訊號)。
Kill()最常用於pid>0時的訊號發送,調用成功返回 0; 否則,返回 -1。註:對於pid<0時的情況,對於哪些進程將接受訊號,各種版本說法不一,其實很簡單,參閱核心源碼kernal/signal.c即可,上表中的規則是參考red hat 7.2。
2、raise()
#include <signal.h>
int raise(int signo)
向進程本身發送訊號,參數為即將發送的訊號值。調用成功返回 0;否則,返回 -1。
3、sigqueue()
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
調用成功返回 0;否則,返回 -1。
sigqueue()是比較新的發送訊號系統調用,主要是針對即時訊號提出的(當然也支援前32種),支援訊號帶有參數,與函數sigaction()配合使用。
sigqueue的第一個參數是指定接收訊號的進程ID,第二個參數確定即將發送的訊號,第三個參數是一個聯合資料結構union sigval,指定了訊號傳遞的參數,即通常所說的4位元組值。
typedef union sigval { int sival_int; void *sival_ptr; }sigval_t; |
sigqueue()比kill()傳遞了更多的附加資訊,但sigqueue()只能向一個進程發送訊號,而不能發送訊號給一個進程組。如果signo=0,將會執行錯誤檢查,但實際上不發送任何訊號,0值訊號可用於檢查pid的有效性以及當前進程是否有許可權向目標進程發送訊號。
在調用sigqueue時,sigval_t指定的資訊會拷貝到3參數訊號處理函數(3參數訊號處理函數指的是訊號處理函數由sigaction安裝,並設定了sa_sigaction指標,稍後將闡述)的siginfo_t結構中,這樣訊號處理函數就可以處理這些資訊了。由於sigqueue系統調用支援發送帶參數訊號,所以比kill()系統調用的功能要靈活和強大得多。
註:sigqueue()發送非即時訊號時,第三個參數包含的資訊仍然能夠傳遞給訊號處理函數; sigqueue()發送非即時訊號時,仍然不支援排隊,即在訊號處理函數執行過程中到來的所有相同訊號,都被合并為一個訊號。
4、alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
專門為SIGALRM訊號而設,在指定的時間seconds秒後,將向進程本身發送SIGALRM訊號,又稱為鬧鐘時間。進程調用alarm後,任何以前的alarm()調用都將無效。如果參數seconds為零,那麼進程內將不再包含任何鬧鐘時間。
傳回值,如果調用alarm()前,進程中已經設定了鬧鐘時間,則返回上一個鬧鐘時間的剩餘時間,否則返回0。
5、setitimer()
#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
setitimer()比alarm功能強大,支援3種類型的定時器:
- ITIMER_REAL: 設定絕對時間;經過指定的時間後,核心將發送SIGALRM訊號給本進程;
- ITIMER_VIRTUAL 設定程式執行時間;經過指定的時間後,核心將發送SIGVTALRM訊號給本進程;
- ITIMER_PROF 設定進程執行以及核心因本進程而消耗的時間和,經過指定的時間後,核心將發送ITIMER_VIRTUAL訊號給本進程;
Setitimer()第一個參數which指定定時器類型(上面三種之一);第二個參數是結構itimerval的一個執行個體,結構itimerval形式見附錄1。第三個參數可不做處理。
Setitimer()調用成功返回0,否則返回-1。
6、abort()
#include <stdlib.h>
void abort(void);
向進程發送SIGABORT訊號,預設情況下進程會異常退出,當然可定義自己的訊號處理函數。即使SIGABORT被進程設定為阻塞訊號,調用abort()後,SIGABORT仍然能被進程接收。該函數無傳回值。
回頁首
五、訊號的安裝(設定訊號關聯動作)
如果進程要處理某一訊號,那麼就要在進程中安裝該訊號。安裝訊號主要用來確定訊號值及進程針對該訊號值的動作之間的映射關係,即進程將要處理哪個訊號;該訊號被傳遞給進程時,將執行何種操作。
linux主要有兩個函數實現訊號的安裝:signal()、sigaction()。其中signal()在可靠訊號系統調用的基礎上實現, 是庫函數。它只有兩個參數,不支援訊號傳遞資訊,主要是用於前32種非即時訊號的安裝;而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支援訊號傳遞資訊,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支援非即時訊號的安裝。sigaction()優於signal()主要體現在支援訊號帶有參數。
1、signal()
#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);
如果該函數原型不容易理解的話,可以參考下面的分解方式來理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一個參數指定訊號的值,第二個參數指定針對前面訊號值的處理,可以忽略該訊號(參數設為SIG_IGN);可以採用系統預設處理訊號(參數設為SIG_DFL);也可以自己實現處理方式(參數指定一個函數地址)。
如果signal()調用成功,返回最後一次為安裝訊號signum而調用signal()時的handler值;失敗則返回SIG_ERR。
2、sigaction()
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函數用於改變進程接收到特定訊號後的行為。該函數的第一個參數為訊號的值,可以為除SIGKILL及SIGSTOP外的任何一個特定有效訊號(為這兩個訊號定義自己的處理函數,將導致訊號安裝錯誤)。第二個參數是指向結構sigaction的一個執行個體的指標,在結構sigaction的執行個體中,指定了對特定訊號的處理,可以為空白,進程會以預設方式對訊號處理;第三個參數oldact指向的對象用來儲存原來對相應訊號的處理,可指定oldact為NULL。如果把第二、第三個參數都設為NULL,那麼該函數可用於檢查訊號的有效性。
第二個參數最為重要,其中包含了對指定訊號的處理、訊號所傳遞的資訊、訊號處理函數執行過程中應屏蔽掉哪些函數等等。
sigaction結構定義如下:
struct sigaction { union{ __sighandler_t _sa_handler; void (*_sa_sigaction)(int,struct siginfo *, void *); }_u sigset_t sa_mask; unsigned long sa_flags; void (*sa_restorer)(void); } |
其中,sa_restorer,已淘汰,POSIX不支援它,不應再被使用。
1、聯合資料結構中的兩個元素_sa_handler以及*_sa_sigaction指定訊號關聯函數,即使用者指定的訊號處理函數。除了可以是使用者自訂的處理函數外,還可以為SIG_DFL(採用預設的處理方式),也可以為SIG_IGN(忽略訊號)。
2、由_sa_handler指定的處理函數只有一個參數,即訊號值,所以訊號不能傳遞除訊號值之外的任何資訊;由_sa_sigaction是指定的訊號處理函數帶有三個參數,是為即時訊號而設的(當然同樣支援非即時訊號),它指定一個3參數訊號處理函數。第一個參數為訊號值,第三個參數沒有使用(posix沒有規範使用該參數的標準),第二個參數是指向siginfo_t結構的指標,結構中包含訊號攜帶的資料值,參數所指向的結構如下:
siginfo_t { int si_signo; /* 訊號值,對所有訊號有意義*/ int si_errno; /* errno值,對所有訊號有意義*/ int si_code; /* 訊號產生的原因,對所有訊號有意義*/ union{ /* 聯合資料結構,不同成員適應不同訊號 */ //確保分配足夠大的儲存空間 int _pad[SI_PAD_SIZE]; //對SIGKILL有意義的結構 struct{ ... }... ... ... ... ... //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構 struct{ ... }... ... ... } } |
註:為了更便於閱讀,在說明問題時常把該結構表示為附錄2所表示的形式。
siginfo_t結構中的聯合資料成員確保該結構適應所有的訊號,比如對於即時訊號來說,則實際採用下面的結構形式:
typedef struct {int si_signo;int si_errno;int si_code;union sigval si_value;} siginfo_t; |
結構的第四個域同樣為一個聯合資料結構:
union sigval {int sival_int;void *sival_ptr;} |
採用聯合資料結構,說明siginfo_t結構中的si_value要麼持有一個4位元組的整數值,要麼持有一個指標,這就構成了與訊號相關的資料。在訊號的處理函數中,包含這樣的訊號相關資料指標,但沒有規定具體如何對這些資料進行操作,操作方法應該由程式開發人員根據具體任務事先約定。
前面在討論系統調用sigqueue發送訊號時,sigqueue的第三個參數就是sigval聯合資料結構,當調用sigqueue時,該資料結構中的資料就將拷貝到訊號處理函數的第二個參數中。這樣,在發送訊號同時,就可以讓訊號傳遞一些附加資訊。訊號可以傳遞資訊對程式開發是非常有意義的。
訊號參數的傳遞過程可圖示如下:
3、sa_mask指定在訊號處理常式執行過程中,哪些訊號應當被阻塞。預設情況下當前訊號本身被阻塞,防止訊號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標誌位。
註:請注意sa_mask指定的訊號阻塞的前提條件,是在由sigaction()安裝訊號的處理函數執行過程中由sa_mask指定的訊號才被阻塞。
4、sa_flags中包含了許多標誌位,包括剛剛提到的SA_NODEFER及SA_NOMASK標誌位。另一個比較重要的標誌位是SA_SIGINFO,當設定了該標誌位時,表示訊號附帶的參數可以被傳遞到訊號處理函數中,因此,應該為sigaction結構中的sa_sigaction指定處理函數,而不應該為sa_handler指定訊號處理函數,否則,設定該標誌變得毫無意義。即使為sa_sigaction指定了訊號處理函數,如果不設定SA_SIGINFO,訊號處理函數同樣不能得到訊號傳遞過來的資料,在訊號處理函數中對這些資訊的訪問都將導致段錯誤(Segmentation
fault)。
註:很多文獻在闡述該標誌位時都認為,如果設定了該標誌位,就必須定義三參數訊號處理函數。實際不是這樣的,驗證方法很簡單:自己實現一個單一參數訊號處理函數,並在程式中設定該標誌位,可以察看程式的運行結果。實際上,可以把該標誌位看成訊號是否傳遞參數的開關,如果設定該位,則傳遞參數;否則,不傳遞參數。
回頁首
六、訊號集及訊號集操作函數:
訊號集被定義為一種資料類型:
typedef struct {unsigned long sig[_NSIG_WORDS];} sigset_t |
訊號集用來描述訊號的集合,linux所支援的所有訊號可以全部或部分的出現在訊號集中,主要與訊號阻塞相關函數配合使用。下面是為訊號集操作定義的相關函數:
#include <signal.h>int sigemptyset(sigset_t *set);int sigfillset(sigset_t *set);int sigaddset(sigset_t *set, int signum)int sigdelset(sigset_t *set, int signum);int sigismember(const sigset_t *set, int signum);sigemptyset(sigset_t *set)初始化由set指定的訊號集,訊號集裡面的所有訊號被清空;sigfillset(sigset_t *set)調用該函數後,set指向的訊號集中將包含linux支援的64種訊號;sigaddset(sigset_t *set, int signum)在set指向的訊號集中加入signum訊號;sigdelset(sigset_t *set, int signum)在set指向的訊號集中刪除signum訊號;sigismember(const sigset_t *set, int signum)判定訊號signum是否在set指向的訊號集中。 |
回頁首
七、訊號阻塞與訊號未決:
每個進程都有一個用來描述哪些訊號遞送到進程時將被阻塞的訊號集,該訊號集中的所有訊號在遞送到進程後都將被阻塞。下面是與訊號阻塞相關的幾個函數:
#include <signal.h>int sigprocmask(int how, const sigset_t *set, sigset_t *oldset));int sigpending(sigset_t *set));int sigsuspend(const sigset_t *mask)); |
sigprocmask()函數能夠根據參數how來實現對訊號集的操作,操作主要有三種:
參數how |
進程當前訊號集 |
SIG_BLOCK |
在進程當前阻塞訊號集中添加set指向訊號集中的訊號 |
SIG_UNBLOCK |
如果進程阻塞訊號集中包含set指向訊號集中的訊號,則解除對該訊號的阻塞 |
SIG_SETMASK |
更新進程阻塞訊號集為set指向的訊號集 |
sigpending(sigset_t *set))獲得當前已遞送到進程,卻被阻塞的所有訊號,在set指向的訊號集中返回結果。
sigsuspend(const sigset_t *mask))用於在接收到某個訊號之前, 臨時用mask替換進程的訊號掩碼, 並暫停進程執行,直到收到訊號為止。sigsuspend 返回後將恢複調用之前的訊號掩碼。訊號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno設定為EINTR。
附錄1:結構itimerval:
struct itimerval { struct timeval it_interval; /* next value */ struct timeval it_value; /* current value */ }; struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; |
附錄2:三參數訊號處理函數中第二個參數的說明性描述:
siginfo_t {int si_signo; /* 訊號值,對所有訊號有意義*/int si_errno; /* errno值,對所有訊號有意義*/int si_code; /* 訊號產生的原因,對所有訊號有意義*/pid_t si_pid; /* 發送訊號的進程ID,對kill(2),即時訊號以及SIGCHLD有意義 */uid_t si_uid; /* 發送訊號進程的真實使用者ID,對kill(2),即時訊號以及SIGCHLD有意義 */int si_status; /* 退出狀態,對SIGCHLD有意義*/clock_t si_utime; /* 使用者消耗的時間,對SIGCHLD有意義 */clock_t si_stime; /* 核心消耗的時間,對SIGCHLD有意義 */sigval_t si_value; /* 訊號值,對所有即時有意義,是一個聯合資料結構, /*可以為一個整數(由si_int標示,也可以為一個指標,由si_ptr標示)*/void * si_addr; /* 觸發fault的記憶體位址,對SIGILL,SIGFPE,SIGSEGV,SIGBUS 訊號有意義*/int si_band; /* 對SIGPOLL訊號有意義 */int si_fd; /* 對SIGPOLL訊號有意義 */} |
實際上,除了前三個元素外,其他元素組織在一個聯合結構中,在聯合資料結構中,又根據不同的訊號組織成不同的結構。注釋中提到的對某種訊號有意義指的是,在該訊號的處理函數中可以訪問這些域來獲得與訊號相關的有意義的資訊,只不過特定訊號只對特定資訊感興趣而已。
參考資料
- linux核心原始碼情景分析(上),毛德操、胡希明著,浙江大學出版社,當要驗證某個結論、想法時,最好的參考資料;
- UNIX環境進階編程,作者:W.Richard Stevens,譯者:尤晉元等,機械工業出版社。對訊號機制的發展過程闡述的比較詳細。
- signal、sigaction、kill等手冊,最直接而可靠的參考資料。
- http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了許多系統調用、庫函數等的線上指南。
- http://www.opengroup.org/onlinepubs/007904975/可以在這裡對許多關鍵函數(包括系統調用)進行查詢,非常好的一個網址。
- http://unix.org/whitepapers/reentrant.html對函數可重新進入進行了闡述。
- http://www.uccs.edu/~compsvcs/doc-cdrom/DOCS/HTML/APS33DTE/DOCU_006.HTM對即時訊號給出了相當好的描述。
關於作者