Linux 進程間通訊系列之 信號

信號(Signal)linux

信號是比較複雜的通訊方式,用於通知接受進程有某種事件發生,除了用於進程間通訊外,進程還能夠發送信號給進程自己;Linux除了支持Unix早期信號
語義函數sigal外,還支持語義符合Posix.1標準的信號函數sigaction(實際上,該函數是基於BSD的,BSD爲了實現可靠信號機制,又
可以統一對外接口,用sigaction函數從新實現了signal函數)session

信號種類數據結構

 

每種信號類型都有對應的信號處理程序(也叫信號的操做),就好像每一箇中斷都有一箇中斷服務例程同樣。大多數信號的默認操做是結束接收信號的進程;然而,一個進程一般能夠請求系統採起某些代替的操做,各類代替操做是:異步

  • 忽略信號。隨着這一選項的設置,進程將忽略信號的出現。有兩個信號  不能夠被忽略:SIGKILL,它將結束進程;SIGSTOP,它是做業控制機制的一部分,將掛起做業的執行。
  • 恢復信號的默認操做。
  • 執行一個預先安排的信號處理函數。進程能夠登記特殊的信號處理函數。當進程收到信號時,信號處理函數將像中斷服務例程同樣被調用,當從該信號處理函數返回時,控制被返回給主程序,而且繼續正常執行。

可是,信號和中斷有所不一樣。中斷的響應和處理都發生在內核空間,而信號的響應發生在內核空間,信號處理程序的執行卻發生在用戶空間。函數

那麼,何時檢測和響應信號呢?一般發生在兩種狀況下:spa

  • 當前進程因爲系統調用、中斷或異常而進入內核空間之後,從內核空間返回到用戶空間前夕;
  • 當前進程在內核中進入睡眠之後剛被喚醒的時候,因爲檢測到信號的存在而提早返回到用戶空間。

 

信號本質指針

信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一箇中斷請求能夠說是同樣的。信號是異步的,一個進程沒必要經過任何操做來等待信號的到達,事實上,進程也不知道信號到底何時到達。code

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

信號來源blog

信號事件的發生有兩個來源:硬件來源(好比咱們按下了鍵盤或者其它硬件故障);軟件來源,最經常使用發送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來源還包括一些非法運算等操做。接口

 

關於信號處理機制的原理(內核角度)

內核給一個進程發送軟中斷信號的方法,是在進程所在的進程表項的信號域設置對應於該信號的位。這裏要補充的是,若是信號發送給一個正在睡眠的進程,那麼要 看該進程進入睡眠的優先級,若是進程睡眠在可被中斷的優先級上,則喚醒進程;不然僅設置進程表中信號域相應的位,而不喚醒進程。這一點比較重要,由於進程檢查是否收到信號的時機是:一個進程在即將從內核態返回到用戶態時;或者,在一個進程要進入或離開一個適當的低調度優先級睡眠狀態時。    

內核處理一個進程收到的信號的時機是在一個進程從內核態返回用戶態時。因此,當一個進程在內核態下運行時,軟中斷信號並不當即起做用,要等到將返回用戶態時才處理。進程只有處理完信號纔會返回用戶態(上面的例子程序中,在步驟5中,解除阻塞後,先打印caught SIGQUIT,再打印SIGQUIT unblocked,即在sigprocmask返回前,信號處理程序先執行),進程在用戶態下不會有未處理完的信號。    

內核處理一個進程收到的軟中斷信號是在該進程的上下文中,所以,進程必須處於運行狀態。若是進程收到一個要捕捉的信號,那麼進程從內核態返回用戶態時執行用戶定義的函數。並且執行用戶定義的函數的方法很巧妙,內核是在用戶棧上建立一個新的層,該層中將返回地址的值設置成用戶定義的處理函數的地址,這樣進程從內核返回彈出棧頂時就返回到用戶定義的函數處,從函數返回再彈出棧頂時,才返回原先進入內核的地方,接着原來的地方繼續運行。這樣作的緣由是用戶定義的處理函數不能且不容許在內核態下執行(若是用戶定義的函數在內核態下運行的話,用戶就能夠得到任何權限)。

在信號的處理方法中有幾點特別要引發注意。    

第一,在一些系統中,當一個進程處理完中斷信號返回用戶態以前,內核清除用戶區中設定的對該信號的處理例程的地址,即下一次進程對該信號的處理方法又改成默認值,除非在下一次信號到來以前再次使用signal系統調用。這可能會使得進程在調用signal以前又得 到該信號而致使退出。在BSD中,內核再也不清除該地址。但不清除該地址可能使得進程由於過多過快的獲得某個信號而致使堆棧溢出。爲了不出現上述狀況。在 BSD系統中,內核模擬了對硬件中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。    

第二個要引發注意的是,若是要捕捉的信號發生於進程正在一個系統調用中時,而且該進程睡眠在可中斷的優先級上(若系統調用未睡眠而是在運行,根據上面的分 析,等該系統調用運行完畢後再處理信號),這時該信號引發進程做一次longjmp,跳出睡眠狀態,返回用戶態並執行信號處理例程。當從信號處理例程返回 時,進程就象從系統調用返回同樣,但返回了一個錯誤如-1,並將errno設置爲EINTR,指出該次系統調用曾經被中斷。這要注意的是,BSD系統中內 核能夠自動地從新開始系統調用,或者手如上面所述手動設置重啓。    

第三個要注意的地方:若進程睡眠在可中斷的優先級上,則當它收到一個要忽略的信號時,該進程被喚醒,但不作longjmp,通常是繼續睡眠。但用戶感受不 到進程曾經被喚醒,而是象沒有發生過該信號同樣。因此可以使pause、sleep等函數從掛起態返回的信號必需要有信號處理函數,若是沒有什麼動做,可 以將處理函數設爲空。    

第四個要注意的地方:內核對子進程終止(SIGCLD)信號的處理方法與其餘信號有所區別。當進程正常或異常終止時,內核都向其父進程發一個SIGCLD 信號,缺省狀況下,父進程忽略該信號,就象沒有收到該信號似的,若是父進程但願得到子進程終止的狀態,則應該事先用signal函數爲SIGCLD信號設 置信號處理程序,在信號處理程序中調用wait。

SIGCLD信號的做用是喚醒一個睡眠在可被中斷優先級上的進程。若是該進程捕捉了這個信號,就象普通訊號處理同樣轉處處理例程。若是進程忽略該信號,則 什麼也不作。其實wait不必定放在信號處理函數中,但這樣的話由於不知道子進程什麼時候終止,在子進程終止前,wait將使父進程掛起休眠。

信號生命週期

 

 

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。

傳遞給信號處理例程的整數參數是信號值,這樣可使得一個信號處理例程處理多個信號

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sigroutine(int dunno)

{ /* 信號處理例程,其中dunno將會獲得信號的值 */

    switch (dunno) {
    case 1:
        printf("Get a signal -- SIGHUP ");
        break;
    case 2:
        printf("Get a signal -- SIGINT ");
        break;
    case 3:
        printf("Get a signal -- SIGQUIT ");
        break;
    }
    return;
}

int main() {
    printf("process id is %d ", getpid());
    signal(SIGHUP, sigroutine); //* 下面設置三個信號的處理方法
    signal(SIGINT, sigroutine);
    signal(SIGQUIT, sigroutine);

    for (;;);
}

 

其中信號SIGINT由按下Ctrl-C發出,信號SIGQUIT由按下Ctrl-發出。該程序執行的結果以下:

 

localhost:~$ ./sig_test
process id is 463
Get a signal -SIGINT //按下Ctrl-C獲得的結果
Get a signal -SIGQUIT //按下Ctrl-獲得的結果
//按下Ctrl-z將進程置於後臺
 [1]+ Stopped ./sig_test
localhost:~$ bg
 [1]+ ./sig_test &
localhost:~$ kill -HUP 463 //向進程發送SIGHUP信號
localhost:~$ Get a signal – SIGHUP
kill -9 463 //向進程發送SIGKILL信號,終止進程

 

2. 信號的發送

發送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。

 

2.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,並設置相應的錯誤代碼errno。下面是一些可能返回的錯誤代碼:

EINVAL:指定的信號sig無效。

ESRCH:參數pid指定的進程或進程組不存在。注意,在進程表項中存在的進程,多是一個尚未被wait收回,但已經終止執行的僵死進程。

EPERM: 進程沒有權力將這個信號發送到指定接收信號的進程。由於,一個進程被容許將信號發送到進程pid時,必須擁有root權力,或者是發出調用的進程的UID 或EUID與指定接收的進程的UID或保存用戶ID(savedset-user-ID)相同。若是參數pid小於-1,即該信號發送給一個組,則該錯誤表示組中有成員進程不能接收該信號。

 

2.2    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指定的信息會拷貝到對應sig 註冊的3參數信號處理函數的siginfo_t結構中,這樣信號處理函數就能夠處理這些信息了。因爲sigqueue系統調用支持發送帶參數信號,因此比kill()系統調用的功能要靈活和強大得多。

 

2.3    alarm()

#include <unistd.h>
unsigned int alarm(unsigned int seconds)

系統調用alarm安排內核爲調用進程在指定的seconds秒後發出一個SIGALRM的信號。若是指定的參數seconds爲0,則再也不發送 SIGALRM信號。後一次設定將取消前一次的設定。該調用返回值爲上次定時調用到發送之間剩餘的時間,或者由於沒有前一次定時調用而返回0。

 

注意,在使用時,alarm只設定爲發送一次信號,若是要屢次發送,就要屢次使用alarm調用。

 

2.4    setitimer()

如今的系統中不少程序再也不使用alarm調用,而是使用setitimer調用來設置定時器,用getitimer來獲得定時器的狀態,這兩個調用的聲明格式以下:

int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);

在使用這兩個調用的進程中加入如下頭文件:

#include <sys/time.h>

 

該系統調用給進程提供了三個定時器,它們各自有其獨有的計時域,當其中任何一個到達,就發送一個相應的信號給進程,並使得計時器從新開始。三個計時器由參數which指定,以下所示:

TIMER_REAL:按實際時間計時,計時到達將給進程發送SIGALRM信號。

ITIMER_VIRTUAL:僅當進程執行時才進行計時。計時到達將發送SIGVTALRM信號給進程。

ITIMER_PROF:當進程執行時和系統爲該進程執行動做時都計時。與ITIMER_VIR-TUAL是一對,該定時器常常用來統計進程在用戶態和內核態花費的時間。計時到達將發送SIGPROF信號給進程。

 

定時器中的參數value用來指明定時器的時間,其結構以下:

struct itimerval {
        struct timeval it_interval; /* 下一次的取值 */
        struct timeval it_value; /* 本次的設定值 */

};

該結構中timeval結構定義以下:

struct timeval {
       long tv_sec; /**/
      long tv_usec; /* 微秒,1秒 = 1000000 微秒*/

};

在setitimer 調用中,參數ovalue若是不爲空,則其中保留的是上次調用設定的值。定時器將it_value遞減到0時,產生一個信號,並將it_value的值設定爲it_interval的值,而後從新開始計時,如此往復。當it_value設定爲0時,計時器中止,或者當它計時到期,而it_interval 爲0時中止。調用成功時,返回0;錯誤時,返回-1,並設置相應的錯誤代碼errno:

EFAULT:參數value或ovalue是無效的指針。

EINVAL:參數which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一個。

下面是關於setitimer調用的一個簡單示範,在該例子中,每隔一秒發出一個SIGALRM,每隔0.5秒發出一個SIGVTALRM信號:

#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>
int sec;
void sigroutine(int signo) {
    switch (signo) {
    case SIGALRM:
        printf("Catch a signal -- SIGALRM ");
        break;
    case SIGVTALRM:
        printf("Catch a signal -- SIGVTALRM ");
        break;
    }
    return;
}
int main()
{
    struct itimerval value, ovalue, value2;
    sec = 5;
    printf("process id is %d ", 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);
    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);

    for (;;);
}

該例子的屏幕拷貝以下:

localhost:~$ ./timer_test
process id is 579
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGVTALRM
Catch a signal – SIGALRM
Catch a signal –GVTALRM

2.5    abort()

#include <stdlib.h>

void abort(void);

向進程發送SIGABORT信號,默認狀況下進程會異常退出,固然可定義本身的信號處理函數。即便SIGABORT被進程設置爲阻塞信號,調用abort()後,SIGABORT仍然能被進程接收。該函數無返回值。

2.6    raise()

#include <signal.h>

int raise(int signo)

向進程自己發送信號,參數爲即將發送的信號值。調用成功返回 0;不然,返回 -1。

3.信號集及信號集操做函數:

  信號集用來描述信號的集合,每一個信號佔用一位。Linux所支持的全部信號能夠所有或部分的出如今信號集中,主要與信號阻塞相關函數配合使用

信號集被定義爲一種數據類型:

typedef struct {
                       unsigned long sig[_NSIG_WORDS];
} sigset_t

下面是爲信號集操做定義的相關函數:

#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指向的信號集中

4.信號阻塞與信號未決:

每一個進程都有一個用來描述哪些信號遞送到進程時將被阻塞的信號集,該信號集中的全部信號在遞送到進程後都將被阻塞。

下面是與信號阻塞相關的幾個函數:

#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來實現對信號集的操做,操做主要有三種:

SIG_BLOCK 在進程當前阻塞信號集中添加set指向信號集中的信號

SIG_UNBLOCK 若是進程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞

SIG_SETMASK 更新進程阻塞信號集爲set指向的信號集

 

sigpending(sigset_t *set))得到當前已遞送到進程,卻被阻塞的全部信號,在set指向的信號集中返回結果。

 

sigsuspend(const sigset_t *mask))用於在接收到某個信號以前, 臨時用mask替換進程的信號掩碼, 並暫停進程執行,直到收到信號爲止。

    是阻塞函數,對參數信號屏蔽,對沒有指定的參數不屏蔽,但當沒有屏蔽信號處理函數調用sigsuspend 返回

sigsuspend 返回後將恢復調用以前的信號掩碼。

sigsuspend 和sigprocmask是有區別的

sigprocmask的整個過程不能被中斷,例如系統備份整個過程不能被中斷

sigsuspend 是在某一時候不能被中斷,例如拷貝一個文件夾下的電影,在考完一部以後,是能夠中斷的

sigsuspend返回條件

        1.信號發送,而且信號是非屏蔽信號

         2.信號必需要處理,並且處理函數放回後,sigsuspend才返回

信號處理函數完成後,進程將繼續執行。該系統調用始終返回-1,並將errno設置爲EINTR。

 

#include <stdio.h>     
#include <signal.h>     
void checkset();     

void func();     
void main()     
{     
     sigset_tblockset,oldblockset,pendmask;     
     printf("pid:%ld\n",(long)getpid());     

     signal(SIGINT,func); //信號量捕捉函數,捕捉到SIGINT,跳轉到函數指針func處執行     

    sigemptyset(&blockset); //初始化信號量集     
    sigaddset(&blockset,SIGTSTP); //將SIGTSTP添加到信號量集中     
    sigaddset(&blockset,SIGINT);//將SIGINT添加到信號量集中     

   sigprocmask(SIG_SETMASK,&blockset,&oldblockset); //將blockset中的SIGINT,SIGTSTP阻塞掉,並保存當前信號屏蔽字     

     /*執行如下程序時,不會被信號打攪*/ 
    checkset();     
    sleep(5);     
     sigpending(&pendmask); //檢查信號是懸而未決的     
     if(sigismember(&pendmask,SIGINT)) //SIGINT是懸而未決的。所謂懸而未決,是指SIGQUIT被阻塞尚未被處理     
         printf("SIGINTpending\n");     

     /*免打攪結束*/ 

     sigprocmask(SIG_SETMASK,&oldblockset,NULL); //恢復被屏蔽的信號SIGINT SIGTSTP     
     printf("SIGINTunblocked\n");     
     sleep(6);     
}     

void checkset()     
{     
     sigset_tset;     
     printf("checksetstart:\n");     
     if(sigprocmask(0,NULL,&set)<0)     
     {     
     printf("checksetsigprocmask error!!\n");     
     exit(0);     
     }     
     if(sigismember(&set,SIGINT))     
     printf("sigint\n");     
         
     if(sigismember(&set,SIGTSTP))     
     printf("sigtstp\n");     

     if(sigismember(&set,SIGTERM))     
     printf("sigterm\n");     

     printf("checksetend\n"); 

}     
void func()     
{     
     printf("hellofunc\n");     
}
相關文章
相關標籤/搜索