首先謝謝 @小堯弟 這位朋友對我昨天夜裡寫的一篇《淺談Linux中的信號機制(一)》的指正,之前的題目我用的“淺析”一詞,給人一種要剖析內核的感覺。本人自知功力不夠,尚且不能對著Linux內核源碼評頭論足。以後的路還很長,我還是一步一個腳印的慢慢走著吧,Linux內核這座山,我才剛剛抵達山腳下。
好了,言歸正傳,我接著昨天寫下去。如有錯誤還請各位看官指正,先此謝過。
上篇末尾,我們看到了這樣的現象:send進程總共發送了500次SIGINT信號給rcv進程,但是實際過程中rcv只接受/處理了13次SIGINT的信號處理函數(signal-handler function)。究竟是rcv進程接受了500次SIGINT信號只執行了13次信號處理函數,還是rcv進程只接受了13次SIGINT信號然後執行了13次信號處理函數呢。我們不禁要問:信號去了哪兒呢?要搞清這個問題之前,我們還需了解一個叫做做信號集和信號屏蔽的知識點。
信號集在處理信號相關的函數時,我們時常需要一種的特殊的數據結構來表示一組信號的集合,這樣的集合我們稱之為信號集,其數據類型表示為sigset_t,通常是用位掩碼的形式來實現的。我的環境是CentOS7,其定義在/usr/include/bits/sigset.h中,具體如下:
/* A `sigset_t' has a bit for each signal. */
# define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;#endif
在sigset.h同時也提供了一組函數(實際上用宏來實現的,感興趣可以查閱sigset.h),用以實現對sigset_t類型數據的操作。其原型如下:
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);
除此之外Glibc還提供了另外三個非標准規定的函數:
int sigisemptyset(const sigset_t* set);
int sigandset(sigset_t* dest,sigset_t* left,sigset_t* right);
int sigorset(sigset_t* dest,sigset_t* left,sigset_t* right);
基本上看了原型之後這些函數的用法也就一目了然了,不需要浪費篇幅了。除此之外,我覺得的這些函數的實現還是值得一讀的,是C語言中位運算學習的一個不錯的demo。
信號屏蔽在了解了信號集的基本概念之後,我們就可以知道繼續了解其他與信號集相關的概念了,首先是信號屏蔽字。它定義了要阻塞遞送到當前進程的信號集,每一個進程都有一個信號屏蔽字(signal mask)。如果你知道什麼是權限屏蔽(umask)那麼信號屏蔽字也很好理解。sigprocmask()函數可以檢測和更改當前進程的信號屏蔽字。其原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
當oldset是一個非空指針的話,調用sigprocmask之後,oldset便返回了之前的信號屏蔽字。set參數會結合how參數對當前的信號屏蔽字做出修改。(和之前一節提到過的一樣有兩個特殊的信號,你不可以屏蔽它們是:SIGKILL和SIGSTOP)具體規則是:
how 行為 SIG_BLOCK 設置進程的信號屏蔽字為當期信號屏蔽字和set的並集。set是新增的要屏蔽的信號集。 SIG_UNBLOCK 設置當前進程的信號屏蔽字為當前信號屏蔽字和set補集的交集,也就是當前信號屏蔽字減去set中的要解除屏蔽的信號集。set中是要解除屏蔽的信號集。 SIG_SETMASK 設置當前進程的信號屏蔽字為set信號集。然而當set指向一個NULL時,那麼how也就沒有作用了。通常我們讓set設置為NULL時,通過oldset獲取當前的信號屏蔽字。
如果某個或多個信號在進程屏蔽了該信號的期間來到過一次或者多次,我們稱這樣的信號叫做未決的(pending)信號。那麼在調用sigprocmask()解除這個信號屏蔽之後,該信號會在sigprocmask ()返回之前,遞送給(SUSv3 規定至少傳遞一個信號)當前進程。
進程維護了一個數據結構來保存未決的信號,我們可以通過sigpending()來獲取哪些信號是未決的:
int sigpending(sigset_t *set);//return 0 on success,or -1 on error
set參數返回的便是未決的信號集。之後便可以通過使用sigismember()來判斷,set中包含哪些信號。
到這裡我們就可以解釋上一篇末尾的問題了。因為Linux上signal()注冊的信號處理函數在執行時,會自動的將當前的信號添加到進程的信號屏蔽字當中。當信號處理函數返回時,會恢復之前的信號屏蔽字。這意味著,當信號處理函數執行時,它不會遞歸的中斷自身。
實時信號早期Unix系統只定義了32種信號。POSIX.1b定義了一組額外的實時信號(為了兼容之前的應用,而不是修改以前的傳統信號)。實時信號的特點,《Linux系統編程手冊》上有一段總結的很是全面:
根據第二點,我們可以將上篇的博客末尾的SIGINT改成SIGRTMIN+5(當然這裡隨意,只要是實時信號,Linux上kill()也是可以發送實時信號的),然後重復昨天的測試,我們會驚喜的發現,rcv進程“不出意外”地接受並處理了500次信號處理函數。
那麼如何通過發送實時信號時傳遞數據呢?別著急,還得掌握一個系統調用sigaction()。
sigaction()系統調用之前我們已經解除了signal()函數,sigaction()是另外一種選擇,它功能更加強大,兼容性更好,任何時候我們都應優先考慮使用sigaction(),即使signal()更加簡單靈活。其函數原型:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);//Return 0 on success,or -1 on error
與sigprocmask類似地,oldact返回之前的信號設置,act用來設置新的信號處理。signum自然不用解釋,這是要處理的信號。這個函數的關鍵之處就是struct sigaction這個和函數同名的結構體。當然要使用sigaction()還是得從struct sigaction入手,它的定義:
struct sigaction {
union {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);}__sigaction_handler; //Address of handler
sigset_t sa_mask; //Signals blocked during the handler invocation
int sa_flags; //Flags controlling handler invocation
void (*sa_restorer)(void); //Restore,not use
};
sa_mask是一組信號集,當調用信號處理函數之前會將這組信號集添加到進程的信號屏蔽字中,直到信號處理函數返回。利用sa_mask參數,我們可以指定一組信號,讓我們的信號處理函數不被這些信號打斷。與前面的signal()一樣,默認還是會把引發信號處理函數的信號,自動的添加到進程的信號屏蔽字中的。sa_flags參數,如果有經驗的話,我們不難猜到這肯定是一組選項,畢竟身經百戰了嘛。那我們就來看看這組選項是什麼意思:
sa_flags 說明 SA_INTERRUPT 由此信號中斷的系統調用不會自動重啟。 SA_NOCLDSTOP當signum為SIGCHLD時,當因接受一信號的子進程停止或者恢復時,將不會產生此信號(有點繞).但是子進程終止時,仍會產生此信號。
(If sig is SIGCHLD, don’t generate this signal when a child process is stopped or resumed as a consequence of receiving a signal.)
SA_NOCLDWAIT 當signum為SIGCHLD時,子進程終止時不會轉化為僵屍進程。此時調用wait(),則阻塞到所有子進程都終止,才返回-1,errno被視之為ECHILD。 SA_NODEFER 捕獲該信號的時候,不會在執行信號處理函數之前將該信號自動添加到進程的信號屏蔽字中。 SA_ONSTACK 調用信號處理函數時,使用sigaltstack()安裝的備用棧。 SA_RESETHAND 當捕獲該信號時,會在調用信號處理函數之前將信號處理函數設置為默認值SIG_DFL,並清除SA_SIGINFO標志。 SA_RESTART 被此信號中斷的系統調用,會自動重啟。 SA_SIGINFO 調用信號處理函數時附帶了額外的數據要處理,具體見下文。sa_restorer和名字一樣為保留參數,不需要使用。最後我們要看的是__sigaction_handler,這是一個聯合體(當然啦,這是廢話)。sa_handler和sa_sigaction都是信號處理函數的指針,所以一次只能選擇兩者中的一個。如果sa_mask中設置了SA_SIGINFO位那麼就按照void (*sa_sigaction)(int, siginfo_t *, void *)的形式的函數調用信號處理函數,否則使用 void (*sa_handler)(int)這樣的函數。下面我們再來看一看sa_sigaction這個函數:
void sa_sigaction(int signum, siginfo_t* info, void* context);
siginfo_t是一個結構體,其結構和實現相關,我的CentOS7系統上是這樣的:
siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* Signal code */
int si_trapno; /* Trap number that caused hardware-generated signal (unused on most architectures) */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
clock_t si_utime; /* User time consumed */
clock_t si_stime; /* System time consumed */
sigval_t si_value; /* Signal value */
int si_int; /* POSIX.1b signal */
void *si_ptr; /* POSIX.1b signal */
int si_overrun; /* Timer overrun count; POSIX.1b timers */
int si_timerid; /* Timer ID; POSIX.1b timers */
void *si_addr; /* Memory location which caused fault */
long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */
}
每個字段的含義後邊都加了清晰的注釋,但是還有一個參數使我們需要特別注意的,其中si_value字段用來接收伴隨著信號發送過來的數據,其類型是一個sigval_t的聯合體,其定義(我的系統是在路徑/usr/include/bits/siginfo.h 上):
# define __have_sigval_t 1
/* Type for data associated with a signal. */
typedef union sigval
{
int sival_int;
void* sival_ptr;
} sigval_t;
#endif
在實際編程中,到底選擇sival_int還是sival_ptr字段,還是取決於你的應用程序。但是由於指針的作用范圍只能在進程的內部,如果發送一個指針到另一個進程一般沒有什麼實際的意義。
基本上寫到這裡,我們就可以使用sigaction()進行信號處理的demo了,但是這裡我們先不急著寫,留到下一節一並寫了。
使用sigqueue()之前我們提到了發送實時信號時可以附帶數據,kill(),raise()等函數的參數注定他們無法附帶更多的數據,這裡我們要認識一個新的函數sigqueue()專門用於在發送信號的時候,附加傳遞額外的數據。
int sigqueue(pid_t pid, int sig, const union sigval value);//Return 0 on success ,or -1 on error
前兩個參數和kill()一致,但是不同於kill(),這裡不能將pid只能是單個進程,而不像kill()那樣豐富的用法。value的類型便是在上邊提及的sigval_t,於是就清晰了:發送進程在這裡發送的value在接受進程中通過信號處理函數sa_sigaction中的siginfo_t info參數就可以拿到了。
一個處理實時信號信號簡單的demo,處理信號端代碼catch.c:
#include <signal.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> void sighandler(int sig,siginfo_t* info,void* context) { printf("Send process pid = %ld,receive a data :%d\n",info->si_pid,info->si_value.sival_int); } int main() { printf("pid = %ld\n",(long)getpid()); struct sigaction act; act.sa_flags = SA_SIGINFO; sigemptyset(&act.sa_mask); act.sa_sigaction = sighandler; if(sigaction(SIGRTMIN+5,&act,0) == -1) exit(-1); pause(); }
發送信號端send.c:
#include <unistd.h> #include <stdio.h> #include <signal.h> #include <string.h> int main(int argc,char* argv[]) { printf("Send process pid = %ld\n",(long)getpid()); union sigval value; value.sival_int = 5435620; pid_t pid = (pid_t)atol(argv[1]); sigqueue(pid,SIGRTMIN+5,value); }
運行結果如圖所示,在sa_sigaction中成功拿到了發送進程的進程id以及傳送的數據:
當然由於夜深了,這個demo寫的還是比較簡單的,基本我們使用已經沒有任何障礙了。
准備把有關信號的知識點總結完的,一寫出來,才發現信號這部分的知識點真是多,而且牽扯到好多細節方面的東西,看來這個任務今晚完不成了,明天繼續吧。
如果您發現我的博文有錯誤之處,煩請您指正,我先在此謝過!聯系郵箱[email protected]。