信號及信號來源
什么是信號
信號是UNIX和Linux系統響應某些條件而產生的一個事件,接收到該信號的進程會相應地采取一些行動。通常信號是由一個錯誤產生的。但它們還可以作為進程間通信或修改行為的一種方式,明確地由一個進程發送給另一個進程。一個信號的產生叫生成,接收到一個信號叫捕獲。
信號本質
信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。
信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。
信號是進程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進程有哪些事情發生了。信號機制經過POSIX實時擴展后,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。
信號來源
信號事件的發生有兩個來源
-
硬件來源(比如我們按下了鍵盤或者其它硬件故障);
-
軟件來源,最常用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操作。
信號可以直接進行用戶空間進程和內核進程之間的交互,內核進程也可以利用它來通知用戶空間進程發生了那些系統事件。
如果該進程當前並未處於執行態,則該信號就由內核保存起來,直到該進程恢復執行再傳遞個它;如果一個信號被進程設置為阻塞,則該信號的傳遞被延遲,直到其阻塞取消時才被傳遞給進程。
linux產生信號的條件
-
當用戶按某些終端鍵時,將產生信號。
終端上按“Ctrl+c”組合鍵通常產生中斷信號 SIGINT,終端上按“Ctrl+\”鍵通常產生中斷信號 SIGQUIT,終端上按“Ctrl+z”鍵通常產生中斷信號 SIGSTOP 等。 -
硬件異常將產生信號。
比如數據運算時,除數為0;或者無效的存放訪問等.這些條件通常由硬件檢測到,並通知內核,然后內核為該條件發生時正在運行的進程產生適當的信號.。 -
軟件異常將產生信號。
當檢測到某種軟件條件已發生,並將其通知有關進程時,產生信號。 -
調用 kill() 函數將發送信號。
注意:接收信號進程和發送信號進程的所有者必須相同,或發送信號進程的所有者必須是超級用戶。 -
運行 kill 命令將發送信號。
此程序實際上是使用 kill 函數來發送信號。也常用此命令終止一個失控的后台進程。
信號的捕獲和處理
若內核(空間)向用戶空間(進程)發出某個信號時,用戶空間(進程)可按照下列3中方式來面對:
-
忽略信號,即對信號不做任何處理
大多數信號都可以使用這種方式處理,但信號SIGKILL和SIGSTOP絕不能被忽略.因為它們向超級用戶提供了一種使進程終止的可靠方法. -
缺省動作,執行信號的默認動作.大多數信號的系統默認動作是終止在進程.
-
捕捉信號,定義信號處理函數,當信號發生時,執行相應的處理函數;
注意,進程對實時信號的缺省反應是進程終止。
Linux究竟采用上述三種方式的哪一個來響應信號,取決於傳遞給相應API函數的參數。
信號是一種軟件中斷機制,即當信號發生時,必須用中斷的方法告訴內核”請執行下列操作”.
在linux終端內輸入kill -l可以查看系統所支持的信號.可以看出,每個信號的名字都是以SIG開頭.

在頭文件signal.h(/usr/include/bits/signum.h)中,這些信號都被定義為正整數,即每個信號和一個數字編碼相對應.
不同的架構,文件存儲路徑可能不同可以使用sudo find /usr/include -name signum.h查找
我的位於/usr/include/x86_64-linux-gnu/bits/signum.h

其中SIGRTMIN,SIGRTMAX定義如下
#define SIGRTMIN (__libc_current_sigrtmin ()) #define SIGRTMAX (__libc_current_sigrtmax ()) /* These are the hard limits of the kernel. These values should not be used directly at user level. */ #define __SIGRTMIN 32 #define __SIGRTMAX (_NSIG - 1)

linux信號的發展及種類
可以從兩個不同的分類角度對信號進行分類:
-
可靠性方面:可靠信號與不可靠信號;
-
與時間的關系上:實時信號與非實時信號。
可靠信號與不可靠信號
“不可靠信號”
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安裝的信號卻不能向信號處理函數傳遞信息。對於信號發送函數來說也是一樣的。
實時信號與非實時信號
早期Unix系統只定義了32種信號,Ret hat7.2支持64種信號,編號0-63(SIGRTMIN=31,SIGRTMAX=63),將來可能進一步增加,這需要得到內核的支持。
前32種信號已經有了預定義值,每個信號有了確定的用途及含義,並且每種信號都有各自的缺省動作。如按鍵盤的CTRL ^C時,會產生SIGINT信號,對該信號的默認反應就是進程終止。后32個信號表示實時信號,等同於前面闡述的可靠信號。這保證了發送的多個實時信號都被接收。實時信號是POSIX標准的一部分,可用於應用進程。
非實時信號都不支持排隊,都是不可靠信號;實時信號都支持排隊,都是可靠信號。
信號的發送
發送信號的主要函數有:kill()、raise()、sigqueue()、alarm()、setitimer()以及abort()。
kill–傳送信號給指定進程
使用man 2 kill查看幫助信息

函數原型
#include <sys/types.h> #include <signal.h> int kill(pid_t pid,int signo)
參數說明
-
第一個參數pid:指定發送信號的接收線程
-
第二個參數signo:信號的signum
參數pid
| 參數pid的值 | 信號的接收進程 |
|---|---|
| pid>0 | 進程ID為pid的進程 |
| pid=0 | 同一個進程組的進程 |
| pid<0 pid!=-1 | 進程組ID為 -pid的所有進程 |
| pid=-1 | 除發送進程自身外,所有進程ID大於1的進程 |
參數signo
Signo是信號值,當為0時(即空信號),實際不發送任何信號,但照常進行錯誤檢查,因此,可用於檢查目標進程是否存在,以及當前進程是否具有向目標發送信號的權限(root權限的進程可以向任何進程發送信號,非root權限的進程只能向屬於同一個session或者同一個用戶的進程發送信號)。
Kill()最常用於pid>0時的信號發送,調用成功返回 0; 否則,返回 -1。
注
對於pid<0時的情況,對於哪些進程將接受信號,各種版本說法不一,其實很簡單,參閱內核源碼kernal/signal.c即可
/************************************************************************* > File Name: kill.c > Author: GatieMe > Mail: gatieme@163.com > Created Time: 2016年03月27日 星期日 11時07分40秒 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> int main() { int pid; if((pid = fork()) < 0) // 創建新的進程 { perror("Fail to fork"); exit(EXIT_FAILURE); } else if(pid == 0) // 子進程中返回0 { while(1); } else // 父進程中返回子進程的pid { int signum; while(scanf("%d",&signum) == 1) // 用戶輸入帶發送的信號 { kill(pid, signum); // 父進程向子進程發送信號 system("ps -aux | grep ./test"); } } return 0; }

在下面程序中,來父子進程各自每隔一秒打印一句話,3 秒后,父進程通過 kill() 函數給子進程發送一個中斷信號 SIGINT( 2 號信號),最終,子進程結束,剩下父進程在打印信息
/************************************************************************* > File Name: test_kill2.c > Author: GatieMe > Mail: gatieme@163.com > Created Time: 2016年03月27日 星期日 11時23分06秒 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> int main(int argc, char *argv[]) { pid_t pid; int i = 0; pid = fork(); // 創建進程 if( pid < 0 ) { // 出錯 perror("fork"); } if(pid == 0) { // 子進程 while(1) { printf("I am son\n"); sleep(1); } } else if(pid > 0) { // 父進程 while(1) { printf("I am father\n"); sleep(1); i++; if(3 == i) { // 3秒后 kill(pid, SIGINT); // 給子進程 pid ,發送中斷信號 SIGINT // kill(pid, 2); // 等級於kill(pid, SIGINT); } } } return 0; }

raise–向自己發送一信號
向進程本身發送信號,參數為即將發送的信號值。
調用成功返回 0;否則,返回 -1。
#include <signal.h> int raise(int signo)
kill和raise有如下等價關系:
kill(getpid(), xxx)等價於raise(xxx), 意思是, raise函數就是向當前進程發信號的。

我們下面的程序,進程通過raise向自身發送了一個SIGINT信號。
在linux的64個信號中,大多數在默認情況下都是終止當前信號.包括SIGINT,當到了定時時間后,內核發出SIGINT信號,該信號會終止當前進程.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> int main(void) { int i = 0; while(1) { i++; if(i == 3) { printf("I will raise SIGINT to myself...\n"); raise(SIGINT); } printf("I am running now...\n"); sleep(1); } return 0; }

alarm–設置信號傳送鬧鈴
#include <unistd.h> unsigned int alarm(unsigned int seconds)
專門為SIGALRM信號而設,在指定的時間seconds秒后,將向進程本身發送SIGALRM信號,又稱為鬧鍾時間。
進程調用alarm后,任何以前的alarm()調用都將無效。如果參數seconds為零,那么進程內將不再包含任何鬧鍾時間。
返回值,如果調用alarm()前,進程中已經設置了鬧鍾時間,則返回上一個鬧鍾時間的剩余時間,否則返回0。
setitimer–設置更精確的定時信號
在linux下如果對定時要求不太精確的話,使用alarm()和signal()就行了,但是如果想要實現精度較高的定時功能的話,就要使用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信號給本進程; |
* 第一個參數which指定定時器類型(上面三種之一);
-
第二個參數是結構itimerval的一個實例,結構itimerval形式見附錄1。
-
第三個參數可不做處理。
Setitimer()調用成功返回0,否則返回-1。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <time.h> #include <sys/time.h> int main() { struct itimerval value, ovalue, value2; //(1) sec = 5; printf("process id is %d\n", getpid()); signal(SIGALRM, sigroutine); //signal(SIGVTALRM, sigroutine); value.it_value.tv_sec = 1; value.it_value.tv_usec = 0; value.it_interval.tv_sec = 1; value.it_interval.tv_usec = 0; /// 設置絕對時間 setitimer(ITIMER_REAL, &value, &ovalue); //(2) value2.it_value.tv_sec = 0; value2.it_value.tv_usec = 500000; value2.it_interval.tv_sec = 0; value2.it_interval.tv_usec = 500000; /// 設置相對時間 setitimer(ITIMER_VIRTUAL, &value2, &ovalue); while( 1 ) { /// NOP; } }

pause–讓進程暫停直到信號出現

#include <unistd.h> int pause(void);
通過pause可以十當前進程掛起,直至信號出現。
在我們下面的例子中,系統在延遲3s后打印輸出”i am a father process,i will send signal now”,然后結束當前進程.
注意,程序並不會打印輸出”hello i am child process”.
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <signal.h> int main(void) { pid_t pid; pid = fork(); if(pid < 0) { perror("fork"); } else if(pid == 0) { printf("I am child processm, I will PAUSE now\n"); if(pause( ) < 0) { perror("pause"); } while(1) { printf("hello i am child process\n"); sleep(1); } } else { sleep(3); printf("i am a father process,i will send signal now\n"); kill(pid, SIGINT); } return 0; }
abort–終止進程

#include <stdlib.h> void abort(void);
向進程發送SIGABORT信號,默認情況下進程會異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進程設置為阻塞信號,調用abort()后,SIGABORT仍然能被進程接收。該函數無返回值。
#include <stdio.h> #include <stdlib.h> int main(void) { printf("Calling abort()\n"); abort(); /* The next code will never reach... */ printf("after abort...\n"); return 0; }
sigqueue–信號發送函數發送數據
參見 信號發送函數sigqueue和信號安裝函數sigaction
在隊列中向指定進程發送一個信號和數據。
之前學過kill,raise,alarm,abort等功能稍簡單的信號發送函數,現在我們學習一種新的功能比較強大的信號發送函數sigqueue.

#include <sys/types.h> #include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval val)
調用成功返回 0;否則,返回 -1。
sigqueue()是比較新的發送信號系統調用,主要是針對實時信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。
-
第一個參數是指定接收信號的進程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指定的信息會拷貝到參數信號處理函數(參數信號處理函數指的是信號處理函數由sigaction安裝,並設定了sa_sigaction指針)的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。
由於sigqueue系統調用支持發送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。
注
sigqueue()發送非實時信號時,第三個參數包含的信息仍然能夠傳遞給信號處理函數;
sigqueue()發送非實時信號時,仍然不支持排隊,即在信號處理函數執行過程中到來的所有相同信號,都被合並為一個信號。
信號的安裝(設置信號關聯動作)
如果進程要處理某一信號,那么就要在進程中安裝該信號。
安裝信號主要用來確定信號值及進程針對該信號值的動作之間的映射關系,
即進程將要處理哪個信號;該信號被傳遞給進程時,將執行何種操作。
linux主要有兩個函數實現信號的安裝:signal()、sigaction()。
其中signal()在可靠信號系統調用的基礎上實現, 是庫函數。它只有兩個參數,不支持信號傳遞信息,主要是用於前32種非實時信號的安裝;
而sigaction()是較新的函數(由兩個系統調用實現:sys_signal以及sys_rt_sigaction),有三個參數,支持信號傳遞信息,主要用來與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實時信號的安裝。sigaction()優於signal()主要體現在支持信號帶有參數。
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。
例如之前的setitimer精確定時器信號,操作系統的默認處理是終止進程,那么現在我們就可以自己編寫信號處理函數,然后通過signal來安裝。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <time.h> #include <sys/time.h> int sec; void sigroutine(int signo) { switch (signo) { case SIGALRM : { printf("Catch a signal -- SIGALRM \n"); signal(SIGALRM, sigroutine); break; } case SIGVTALRM: { printf("Catch a signal -- SIGVTALRM \n"); signal(SIGVTALRM, sigroutine); break; } } fflush(stdout); return; } int main() { struct itimerval value, ovalue, value2; //(1) sec = 5; printf("process id is %d\n", getpid()); signal(SIGALRM, sigroutine); signal(SIGVTALRM, sigroutine); value.it_value.tv_sec = 1; value.it_value.tv_usec = 0; value.it_interval.tv_sec = 1; value.it_interval.tv_usec = 0; /// 設置絕對時間 setitimer(ITIMER_REAL, &value, &ovalue); //(2) value2.it_value.tv_sec = 0; value2.it_value.tv_usec = 500000; value2.it_interval.tv_sec = 0; value2.it_interval.tv_usec = 500000; /// 設置相對時間 setitimer(ITIMER_VIRTUAL, &value2, &ovalue); while( 1 ) { /// NOP; } }
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不支持它,不應再被使用。
-
聯合數據結構中的兩個元素_sa_handler以及*_sa_sigaction指定信號關聯函數,即用戶指定的信號處理函數。除了可以是用戶自定義的處理函數外,還可以為SIG_DFL(采用缺省的處理方式),也可以為SIG_IGN(忽略信號)。
-
由_sa_handler指定的處理函數只有一個參數,即信號值,所以信號不能傳遞除信號值之外的任何信息;由_sa_sigaction是指定的信號處理函數帶有三個參數,是為實時信號而設的(當然同樣支持非實時信號),它指定一個3參數信號處理函數。第一個參數為信號值,第三個參數沒有使用(posix沒有規范使用該參數的標准),第二個參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構如下:
typedef struct siginfo_t{ int si_signo;//信號編號 int si_errno;//如果為非零值則錯誤代碼與之關聯 int si_code;//說明進程如何接收信號以及從何處收到 pid_t si_pid;//適用於SIGCHLD,代表被終止進程的PID pid_t si_uid;//適用於SIGCHLD,代表被終止進程所擁有進程的UID int si_status;//適用於SIGCHLD,代表被終止進程的狀態 clock_t si_utime;//適用於SIGCHLD,代表被終止進程所消耗的用戶時間 clock_t si_stime;//適用於SIGCHLD,代表被終止進程所消耗系統的時間 sigval_t si_value; int si_int; void * si_ptr; void* si_addr; int si_band; int si_fd; };
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時,該數據結構中的數據就將拷貝到信號處理函數的第二個參數中。這樣,在發送信號同時,就可以讓信號傳遞一些附加信息。信號可以傳遞信息對程序開發是非常有意義的。
-
sa_mask指定在信號處理程序執行過程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發送,除非指定SA_NODEFER或者SA_NOMASK標志位。
注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過程中由sa_mask指定的信號才被阻塞。 -
sa_flags中包含了許多標志位,包括剛剛提到的SA_NODEFER及SA_NOMASK標志位。另一個比較重要的標志位是SA_SIGINFO,當設定了該標志位時,表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該為sigaction結構中的sa_sigaction指定處理函數,而不應該為sa_handler指定信號處理函數,否則,設置該標志變得毫無意義。即使為sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過來的數據,在信號處理函數中對這些信息的訪問都將導致段錯誤(Segmentation fault)。
實例一:利用sigaction安裝SIGINT信號
#include <unistd.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <signal.h> void handler(int sig); /* struct sigaction { void (*sa_handler)(int); //void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; */ int main(int argc, char *argv[]) { struct sigaction act; act.sa_handler = handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; //因為不關心SIGINT上一次的struct sigaction所以,oact為NULL //與signal(handler,SIGINT)相同 if (sigaction(SIGINT, &act, NULL) < 0) { perror("sigaction error\n"); } for (;;) { pause( ); } return 0; } void handler(int sig) { printf("recv a sig = %d\n", sig); }

實例二:利用sigaction實現signal
實際上signal底層實現就是利用sigaction
#include <unistd.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <signal.h> void handler(int sig); __sighandler_t my_signal(int sig, __sighandler_t handler); int main(int argc, char *argv[]) { my_signal(SIGINT, handler); for (;;) pause(); return 0; } __sighandler_t my_signal(int sig, __sighandler_t handler) { struct sigaction act; struct sigaction oldact; act.sa_handler = handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; if (sigaction(sig, &act, &oldact) < 0) return SIG_ERR; return oldact.sa_handler; } void handler(int sig) { printf("recv a sig=%d\n", sig); }

實例三:驗證sigaction.sa_mask效果
#include <unistd.h> #include <sys/stat.h> #include <sys/wait.h> #include <sys/types.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <string.h> #include <signal.h> void handler(int sig); int main(int argc, char *argv[]) { struct sigaction act; act.sa_handler = handler; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, SIGQUIT); act.sa_flags = 0; if (sigaction(SIGINT, &act, NULL) < 0) { perror("sigaction error"); } struct sigaction act2; act2.sa_handler = handler; sigemptyset(&act2.sa_mask); act2.sa_flags = 0; if (sigaction(SIGQUIT, &act2, NULL) < 0) { perror("sigaction error"); } for (;;) { pause(); } return 0; } void handler(int sig) { if(sig == SIGINT){ printf("recv a SIGINT signal\n"); sleep(5); } if (sig == SIGQUIT) { printf("recv a SIGQUIT signal\n"); } }
可知,安裝信號SIGINT時,將SIGQUIT加入到sa_mask阻塞集中,則當SIGINT信號正在執行處理函數時,SIGQUIT信號將被阻塞,只有當SIGINT信號處理函數執行完后才解除對SIGQUIT信號的阻塞,由於SIGQUIT是不可靠信號,不支持排隊,所以只遞達一次

示例四:給自身發送int型數據
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> void sighandler(int signo, siginfo_t *info,void *ctx); //給自身傳遞信息 int main(void) { struct sigaction act; act.sa_sigaction = sighandler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO;//信息傳遞開關 if(sigaction(SIGINT,&act,NULL) == -1) { perror("sigaction error"); exit(EXIT_FAILURE); } sleep(2); union sigval mysigval; mysigval.sival_int = 100; if(sigqueue(getpid(),SIGINT,mysigval) == -1) { perror("sigqueue error"); exit(EXIT_FAILURE); } return 0; } void sighandler(int signo, siginfo_t *info,void *ctx) { //以下兩種方式都能獲得sigqueue發來的數據 printf("receive the data from siqueue by info->si_int is %d\n",info->si_int); printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int); }

示例五:進程間傳遞數據
發送端
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> int main(int argc, char **argv) { if(argc != 2) { fprintf(stderr,"usage:%s pid\n",argv[0]); exit(EXIT_FAILURE); } pid_t pid = atoi(argv[1]); sleep(2); union sigval mysigval; mysigval.sival_int = 100; printf("sending SIGINT signal to %d......\n",pid); if(sigqueue(pid,SIGINT, mysigval) == -1) { perror("sigqueue error"); exit(EXIT_FAILURE); } return 0; }
接收端
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> void sighandler(int signo, siginfo_t *info,void *ctx); //給自身傳遞信息 int main(void) { struct sigaction act; act.sa_sigaction = sighandler; sigemptyset(&act.sa_mask); act.sa_flags = SA_SIGINFO;//信息傳遞開關 if(sigaction(SIGINT, &act, NULL) == -1) { perror("sigaction error"); exit(EXIT_FAILURE); } for(; ;) { printf("waiting a SIGINT signal....\n"); pause(); } return 0; } void sighandler(int signo, siginfo_t *info,void *ctx) { //以下兩種方式都能獲得sigqueue發來的數據 printf("receive the data from siqueue by info->si_int is %d\n",info->si_int); printf("receive the data from siqueue by info->si_value.sival_int is %d\n",info->si_value.sival_int); }

信號進階-信號集
信號生命周期
從信號發送到信號處理函數的執行完畢
對於一個完整的信號生命周期(從信號發送到相應的處理函數執行完畢)來說,可以分為三個重要的階段,這三個階段由四個重要事件來刻畫:
-
信號誕生;
-
信號在進程中注冊完畢;
-
信號在進程中的注銷完畢;
-
信號處理函數執行完畢。
相鄰兩個事件的時間間隔構成信號生命周期的一個階段。

下面闡述四個事件的實際意義:
信號”誕生”
信號的誕生指的是觸發信號的事件發生(如檢測到硬件異常、定時器超時以及調用信號發送函數kill()或sigqueue()等)。
信號在目標進程中”注冊”;進程的task_struct結構中有關於本進程中未決信號的數據成員struct sigpending pending
struct sigpending{ struct sigqueue *head, **tail; sigset_t signal; };
第三個成員是進程中所有未決信號集,第一、第二個成員分別指向一個sigqueue類型的結構鏈(稱之為”未決信號信息鏈”)的首尾,信息鏈中的每個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:
struct sigqueue{ struct sigqueue *next; siginfo_t info; }
信號在進程中注冊
信號在進程中注冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),並且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。 只要信號在進程的未決信號集中,表明進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。
注
當一個實時信號發送給一個進程時,不管該信號是否已經在進程中注冊,都會被再注冊一次,因此,信號不會丟失,因此,實時信號又叫做”可靠信號”。這意味着同一個實時信號可以在同一個進程的未決信號信息鏈中占有多個sigqueue結構(進程每收到一個實時信號,都會為它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中注冊);
當一個非實時信號發送給一個進程時,如果該信號已經在進程中注冊,則該信號將被丟棄,造成信號丟失。
因此,非實時信號又叫做”不可靠信號”。
這意味着同一個非實時信號在進程的未決信號信息鏈中,至多占有一個sigqueue結構
一個非實時信號誕生后,
-
如果發現相同的信號已經在目標結構中注冊,則不再注冊,對於進程來說,相當於不知道本次信號發生,信號丟失;
-
如果進程的未決信號中沒有相同信號,則在進程中注冊自己)。
信號在進程中的注銷
在目標進程執行過程中,會檢測是否有信號等待處理(每次從系統空間返回到用戶空間時都做這樣的檢查)。
如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中占有的結構卸掉。是否將信號從進程未決信號集中刪除對於實時與非實時信號是不同的。
對於非實時信號來說,由於在未決信號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,應該把信號在進程未決信號集中刪除(信號注銷完畢);
而對於實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,因此應該針對占用sigqueue結構的數目區別對待:
如果只占用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號注銷完畢)。
否則,不應該在進程的未決信號集中刪除該信號(信號注銷完畢)。
進程在執行信號相應處理函數之前,首先要把信號在進程中注銷。
信號生命終止
進程注銷信號后,立即執行相應的信號處理函數,執行完畢后,信號的本次發送對進程的影響徹底結束。
注:
信號注冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多只注冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被注冊)。
在信號被注銷到相應的信號處理函數執行完畢這段時間內,如果進程又收到同一信號多次,則對實時信號來說,每一次都會在進程中注冊;而對於非實時信號來說,無論收到多少次信號,都會視為只收到一個信號,只在進程中注冊一次。
信號傳遞過程
信號源為目標進程產生了一個信號,然后由內核來決定是否要將該信號傳遞給目標進程。從信號產生到傳遞給目標進程的流程圖如

進程可以阻塞信號的傳遞。當信號源為目標進程產生了一個信號之后,內核會執行依次執行下面操作,
-
如果目標進程設置了忽略該信號,則內核直接將該信號丟棄。
-
如果目標進程沒有阻塞該信號,則內核將該信號傳遞給目標進程,由目標進程執行相對應操作。
-
如果目標進程設置阻塞該信號,則內核將該信號放到目標進程的阻塞信號列表中,等待目標進程對該類型信號的下一步設置。
若目標進程后續設置忽略該信號,則內核將該信號從目標進程的阻塞信號列表中移除並丟棄。若目標進程對該信號解除了阻塞,內核將該信號傳遞給目標進程進行相對應的操作。
在信號產生到信號傳遞給目標進程之間的時間間隔內,我們稱該信號為未決的(pending)。
每個進程都有一個信號屏蔽字(signal mask),它規定了當前要阻塞傳遞給該進程的信號集。對於每種可能的信號,信號屏蔽字中都有一位與之對應。
信號集和進程信號屏蔽字
我們已經知道,通過信號實現程序之間的相互通信,我們可以實現如下功能
-
可以通過信號來終止進程
-
可以通過信號來在進程間進行通信
-
程序通過指定信號的關聯處理函數來改變信號的默認處理方式
-
可以通過屏蔽某些信號,使其不能傳遞給進程。
那么我們應該如何設定我們需要處理的信號,我們不需要處理哪些信號等問題呢?
信號集函數就是幫助我們解決這些問題的。
信號集及信號集操作函數
信號集被定義為一種數據類型
typedef struct { unsigned long sig[_NSIG_WORDS]; }sigset_t;
信號集用來描述信號的集合,linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關函數配合使用。
POSIX.1 定義了一個數據類型sigset_t,用於表示信號集。
另外,頭文件 signal.h 提供了下列五個處理信號集的函數。
| 函數 | 功能 |
|---|---|
| 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指向的信號集中。 |
- 函數 sigemptyset 初始化由 set 指向的信號集,清除其中所有信號。
int sigemptyset(sigset_t *set);
返回值:若成功則返回0,若出錯則返回-1
- 函數 sigfillset 初始化由 set 指向的信號集,使其包含所有信號。
int sigfillset(sigset_t *set);
返回值:若成功則返回0,若出錯則返回-1
- 函數 sigaddset 將一個信號 signo 添加到現有信號集 set 中。
int sigaddset(sigset_t *set, int signo);
返回值:若成功則返回0,若出錯則返回-1
- 函數 sigdelset 將一個信號 signo 從信號集 set 中刪除。
int sigdelset(sigset_t *set, int signo);
返回值:若成功則返回0,若出錯則返回-1
- 函數 sigismember 判斷指定信號 signo 是否在信號集 set 中。
int sigismember(const sigset_t *set, int signo);
返回值:若真則返回1,若假則返回0,若出錯則返回-1
信號阻塞與信號未決
每個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的所有信號在遞送到進程后都將被阻塞。
下面是與信號阻塞相關的幾個函數:
#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檢測或設置進程的信號屏蔽字
- sigprocmask()函數能夠根據參數how來實現對信號集的操作,操作主要有三種:
| 參數how | 進程當前信號集 |
|---|---|
| SIG_BLOCK | 在進程當前阻塞信號集中添加set指向信號集中的信號 |
| SIG_UNBLOCK | 如果進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞 |
| SIG_SETMASK | 更新進程阻塞信號集為set指向的信號集 |
在下面的程序文件中先調用 sigprocmask 設置阻塞信號 SIGALRM,然后調用 alarm(2) 設置一個兩秒鍾的鬧鍾(兩秒鍾之后將向當前進程產生一個 SIGALRM 信號)。在睡眠 4 秒鍾之后(此時應該已經產生了 SIGALRM 信號),調用 sigprocmask 函數解除對信號SIGALRM 的阻塞。
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <signal.h> static void sig_alrm(int signo) { printf("received SIGALRM\n"); } int main(void) { sigset_t sigset; // 初始化信號集 sigemptyset(&sigset); // 添加一個鬧鍾信號 sigaddset(&sigset, SIGALRM); if (sigprocmask(SIG_BLOCK, &sigset, NULL) < 0) { printf("sigprocmask error: %s\n", strerror(errno)); exit(-1); } else { printf("signal SIGALARM is in in sigset now...\n"); } if (signal(SIGALRM, sig_alrm) < 0) // 添加信號處理函數 { printf("signal error: %s\n", strerror(errno)); exit(-1); } alarm(2); sleep(4); printf("before unblock sigprocmask\n"); if (sigprocmask(SIG_UNBLOCK, &sigset, NULL) < 0) { printf("sigprocmask SIG_UNBLOCK error: %s\n", strerror(errno)); exit(-1); } else { printf("signal SIGALARM isn't in sigset now...\n"); } return 0; }

從上面的執行輸出,我們看到信號 SIGALRM 是在調用 sigprocmask函數執行 unblock之后才被傳遞給當前進程進行處理的。

如果我們將代碼中的sigprocemask(SIG_BLOCK, &sigset, NULL) 注釋掉,編譯執行,生成如下結果

我們看到由於沒有屏蔽信號 SIGALRM ,程序在2秒后捕獲了SIGALRM直接調用sig_alrm進行了處理。
sigpending 獲取進程未決的信號集
函數 sigpending 獲得當前已遞送到進程,卻被阻塞的所有信號,在set指向的信號集中返回結果。
#include <signal.h> int sigpending(sigset_t *set);
返回值:若成功則返回0,若出錯則返回-1
#include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <signal.h> void alrm_is_pending(const char *str) { sigset_t pendingsigset; printf("%s: ", str); if (sigpending(&pendingsigset) < 0) { printf("sigpending error: %s\n", strerror(errno)); exit(-1); } if (sigismember(&pendingsigset, SIGALRM)) { printf("SIGALRM is pending\n"); } else { printf("SIGALRM is not pending\n"); } } int main(void) { sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGALRM); if (sigprocmask(SIG_BLOCK, &sigset, NULL) < 0) { printf("sigprocmask error: %s\n", strerror(errno)); exit(-1); } alrm_is_pending("before alarm"); alarm(2); sleep(4); alrm_is_pending("after alarm"); return 0; }

從運行結果,我們看到調用 alarm 函數產生信號 SIGALRM 之后,該信號在 sigpending 函數的 set 參數指向的信號集中。
- sigsuspend(const sigset_t *mask))用於在接收到某個信號之前, 臨時用mask替換進程的信號掩碼, 並暫停進程執行,直到收到信號為止。sigsuspend 返回后將恢復調用之前的信號掩碼。信號處理函數完成后,進程將繼續執行。該系統調用始終返回-1,並將errno設置為EINTR。
如果一個信號被進程阻塞,它就不會傳遞給進程,但會停留在待處理狀態,當進程解除對待處理信號的阻塞時,待處理信號就會立刻被處理。
下面以一個例子來說明上述函數的用法,源文件為sigset.c,代碼如下:
#include <unistd.h> #include <signal.h> #include <sys/types.h> #include <stdlib.h> #include <stdio.h> void handler(int sig) { printf("Handle the signal %d\n", sig); } int main() { sigset_t sigset; // 用於記錄屏蔽字 sigset_t ign; // 用於記錄被阻塞的信號集 struct sigaction act; //清空信號集 sigemptyset(&sigset); sigemptyset(&ign); // 向信號集中添加信號SIGINT sigaddset(&sigset, SIGINT); // 設置處理函數和信號集 act.sa_handler = handler; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGINT, &act, 0); printf("Wait the signal SIGINT...\n"); pause(); //掛起進程,等待信號 // 設置進程屏蔽字,在本例中為屏蔽SIGINT sigprocmask(SIG_SETMASK, &sigset, 0); printf("Please press Ctrl+c in 10 seconds...\n"); sleep(10); // 測試SIGINT是否被屏蔽 sigpending(&ign); if(sigismember(&ign, SIGINT)) { printf("The SIGINT signal has ignored\n"); } // 在信號集中刪除信號SIGINT sigdelset(&sigset, SIGINT); printf("Wait the signal SIGINT...\n"); // 將進程的屏蔽字重新設置,即取消對SIGINT的屏蔽 // 並掛起進程 sigsuspend(&sigset); printf("The app will exit in 5 seconds!\n"); sleep(5); return EXIT_SUCCESS; }

