Linux 進程間通信系列之 信號


信號(Signal)

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

信號種類

 

每種信號類型都有對應的信號處理程序(也叫信號的操作),就好像每個中斷都有一個中斷服務例程一樣。大多數信號的默認操作是結束接收信號的進程;然而,一個進程通常可以請求系統采取某些代替的操作,各種代替操作是:

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

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

那么,什么時候檢測和響應信號呢?通常發生在兩種情況下:

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

 

信號本質

信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。

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

信號來源

信號事件的發生有兩個來源:硬件來源(比如我們按下了鍵盤或者其它硬件故障);軟件來源,最常用發送信號的系統函數是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");     
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM