linux內核剖析(九)進程間通訊之-信號signal

信號及信號來源


什麼是信號


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

信號本質


信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。session

信號是異步的,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。數據結構

信號是進程間通訊機制中惟一的異步通訊機制,能夠看做是異步通知,通知接收信號的進程有哪些事情發生了。信號機制通過POSIX實時擴展後,功能更增強大,除了基本通知功能外,還能夠傳遞附加信息。架構

信號來源


信號事件的發生有兩個來源app

  • 硬件來源(好比咱們按下了鍵盤或者其它硬件故障);異步

  • 軟件來源,最經常使用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操做。函數

信號能夠直接進行用戶空間進程和內核進程之間的交互,內核進程也能夠利用它來通知用戶空間進程發生了那些系統事件。學習

若是該進程當前並未處於執行態,則該信號就由內核保存起來,直到該進程恢復執行再傳遞個它;若是一個信號被進程設置爲阻塞,則該信號的傳遞被延遲,直到其阻塞取消時才被傳遞給進程。測試

linux產生信號的條件


  1. 當用戶按某些終端鍵時,將產生信號。
    終端上按「Ctrl+c」組合鍵一般產生中斷信號 SIGINT,終端上按「Ctrl+\」鍵一般產生中斷信號 SIGQUIT,終端上按「Ctrl+z」鍵一般產生中斷信號 SIGSTOP 等。ui

  2. 硬件異常將產生信號。
    好比數據運算時,除數爲0;或者無效的存放訪問等.這些條件一般由硬件檢測到,並通知內核,而後內核爲該條件發生時正在運行的進程產生適當的信號.。

  3. 軟件異常將產生信號。
    當檢測到某種軟件條件已發生,並將其通知有關進程時,產生信號。

  4. 調用 kill() 函數將發送信號。
    注意:接收信號進程和發送信號進程的全部者必須相同,或發送信號進程的全部者必須是超級用戶。

  5. 運行 kill 命令將發送信號。
    此程序其實是使用 kill 函數來發送信號。也經常使用此命令終止一個失控的後臺進程。

信號的捕獲和處理


若內核(空間)向用戶空間(進程)發出某個信號時,用戶空間(進程)可按照下列3中方式來面對:

  1. 忽略信號,即對信號不作任何處理
    大多數信號均可以使用這種方式處理,但信號SIGKILL和SIGSTOP毫不能被忽略.由於它們向超級用戶提供了一種使進程終止的可靠方法.

  2. 缺省動做,執行信號的默認動做.大多數信號的系統默認動做是終止在進程.

  3. 捕捉信號,定義信號處理函數,當信號發生時,執行相應的處理函數;

注意,進程對實時信號的缺省反應是進程終止。

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信號的發展及種類


能夠從兩個不一樣的分類角度對信號進行分類:

  1. 可靠性方面:可靠信號與不可靠信號;

  2. 與時間的關係上:實時信號與非實時信號。

可靠信號與不可靠信號


「不可靠信號」


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。

若是把第2、第三個參數都設爲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; };

 

第三個成員是進程中全部未決信號集,第1、第二個成員分別指向一個sigqueue類型的結構鏈(稱之爲」未決信號信息鏈」)的首尾,信息鏈中的每一個sigqueue結構刻畫一個特定信號所攜帶的信息,並指向下一個sigqueue結構:

struct sigqueue{ struct sigqueue *next; siginfo_t info; }

 

信號在進程中註冊


信號在進程中註冊指的就是信號值加入到進程的未決信號集中(sigpending結構的第二個成員sigset_t signal),而且信號所攜帶的信息被保留到未決信號信息鏈的某個sigqueue結構中。 只要信號在進程的未決信號集中,代表進程已經知道這些信號的存在,但還沒來得及處理,或者該信號被進程阻塞。

當一個實時信號發送給一個進程時,無論該信號是否已經在進程中註冊,都會被再註冊一次,所以,信號不會丟失,所以,實時信號又叫作」可靠信號」。這意味着同一個實時信號能夠在同一個進程的未決信號信息鏈中佔有多個sigqueue結構(進程每收到一個實時信號,都會爲它分配一個結構來登記該信號信息,並把該結構添加在未決信號鏈尾,即全部誕生的實時信號都會在目標進程中註冊);

當一個非實時信號發送給一個進程時,若是該信號已經在進程中註冊,則該信號將被丟棄,形成信號丟失。

所以,非實時信號又叫作」不可靠信號」。

這意味着同一個非實時信號在進程的未決信號信息鏈中,至多佔有一個sigqueue結構

一個非實時信號誕生後,

  1. 若是發現相同的信號已經在目標結構中註冊,則再也不註冊,對於進程來講,至關於不知道本次信號發生,信號丟失;

  2. 若是進程的未決信號中沒有相同信號,則在進程中註冊本身)。

信號在進程中的註銷


在目標進程執行過程當中,會檢測是否有信號等待處理(每次從系統空間返回到用戶空間時都作這樣的檢查)。

若是存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。是否將信號從進程未決信號集中刪除對於實時與非實時信號是不一樣的。

對於非實時信號來講,因爲在未決信號信息鏈中最多隻佔用一個sigqueue結構,所以該結構被釋放後,應該把信號在進程未決信號集中刪除(信號註銷完畢);

而對於實時信號來講,可能在未決信號信息鏈中佔用多個sigqueue結構,所以應該針對佔用sigqueue結構的數目區別對待:

若是隻佔用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號註銷完畢)。

不然,不該該在進程的未決信號集中刪除該信號(信號註銷完畢)。

進程在執行信號相應處理函數以前,首先要把信號在進程中註銷。

信號生命終止


進程註銷信號後,當即執行相應的信號處理函數,執行完畢後,信號的本次發送對進程的影響完全結束。

注:

  1. 信號註冊與否,與發送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多隻註冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被註冊)。

  2. 在信號被註銷到相應的信號處理函數執行完畢這段時間內,若是進程又收到同一信號屢次,則對實時信號來講,每一次都會在進程中註冊;而對於非實時信號來講,不管收到多少次信號,都會視爲只收到一個信號,只在進程中註冊一次。

信號傳遞過程


信號源爲目標進程產生了一個信號,而後由內核來決定是否要將該信號傳遞給目標進程。從信號產生到傳遞給目標進程的流程圖如

信號產生、傳遞處處理的流程圖

進程能夠阻塞信號的傳遞。當信號源爲目標進程產生了一個信號以後,內核會執行依次執行下面操做,

  1. 若是目標進程設置了忽略該信號,則內核直接將該信號丟棄。

  2. 若是目標進程沒有阻塞該信號,則內核將該信號傳遞給目標進程,由目標進程執行相對應操做。

  3. 若是目標進程設置阻塞該信號,則內核將該信號放到目標進程的阻塞信號列表中,等待目標進程對該類型信號的下一步設置。

若目標進程後續設置忽略該信號,則內核將該信號從目標進程的阻塞信號列表中移除並丟棄。若目標進程對該信號解除了阻塞,內核將該信號傳遞給目標進程進行相對應的操做。

在信號產生到信號傳遞給目標進程之間的時間間隔內,咱們稱該信號爲未決的(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; } 

 

這裏寫圖片描述

相關文章
相關標籤/搜索