2011年12月20日 星期二

Linux環境進程間通信(二): 信號(下)


一、信號生命週期

從信號發送到信號處理函數的執行完畢

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

下面闡述四個事件的實際意義:

  1. 信號"誕生"。信號的誕生指的是觸發信號的事件發生(如檢測到硬體異常、計時器超時以及調用信號發送函數kill()sigqueue()等)。
  2. 信號在目標進程中"註冊";進程的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結構(一個非即時信號誕生後,(1)、如果發現相同的信號已經在目標結構中註冊,則不再註冊,對於進程來說,相當於不知道本次信號發生,信號丟失;(2)、如果進程的未決信號中沒有相同信號,則在進程中註冊自己)。

  1. 信號在進程中的登出。在目標進程執行過程中,會檢測是否有信號等待處理(每次從系統空間返回到使用者空間時都做這樣的檢查)。如果存在未決信號等待處理且該信號沒有被進程阻塞,則在運行相應的信號處理函數前,進程會把信號在未決信號鏈中佔有的結構卸掉。是否將信號從進程未決信號集中刪除對於即時與非即時信號是不同的。對於非即時信號來說,由於在未決信號資訊鏈中最多只佔用一個sigqueue結構,因此該結構被釋放後,應該把信號在進程未決信號集中刪除(信號登出完畢);而對於即時信號來說,可能在未決信號資訊鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:如果只佔用一個sigqueue結構(進程只收到該信號一次),則應該把信號在進程的未決信號集中刪除(信號登出完畢)。否則,不應該在進程的未決信號集中刪除該信號(信號登出完畢)。
    進程在執行信號相應處理函數之前,首先要把信號在進程中登出。
  2. 信號生命終止。進程登出信號後,立即執行相應的信號處理函數,執行完畢後,信號的本次發送對進程的影響徹底結束。

注:
1)信號註冊與否,與發送信號的函數(如kill()sigqueue()等)以及信號安裝函數(signal()sigaction())無關,只與信號值有關(信號值小於SIGRTMIN的信號最多只註冊一次,信號值在SIGRTMINSIGRTMAX之間的信號,只要被進程接收到就被註冊)。
2)在信號被登出到相應的信號處理函數執行完畢這段時間內,如果進程又收到同一信號多次,則對即時信號來說,每一次都會在進程中註冊;而對於非即時信號來說,無論收到多少次信號,都會視為只收到一個信號,只在進程中註冊一次。



二、信號程式設計注意事項

  1. 防止不該丟失的信號丟失。如果對八中所提到的信號生命週期理解深刻的話,很容易知道信號會不會丟失,以及在哪裡丟失。
  2. 程式的可攜性
    考慮到程式的可攜性,應該儘量採用POSIX信號函數,POSIX信號函數主要分為兩類:
    • POSIX 1003.1信號函數: Kill()sigaction()sigaddset()sigdelset()sigemptyset()sigfillset()sigismember()sigpending()sigprocmask()sigsuspend()
    • POSIX 1003.1b信號函數。POSIX 1003.1b在信號的即時性方面對POSIX 1003.1做了擴展,包括以下三個函數: sigqueue()sigtimedwait()sigwaitinfo()。其中,sigqueue主要針對信號發送,而sigtimedwaitsigwaitinfo()主要用於取代sigsuspend()函數,後面有相應實例。

#include <signal.h>
int sigwaitinfo(sigset_t *set, siginfo_t *info).


該函數與sigsuspend()類似,阻塞一個進程直到特定信號發生,但信號到來時不執行信號處理函數,而是返回信號值。因此為了避免執行相應的信號處理函數,必須在調用該函數前,使進程遮罩掉set指向的信號,因此調用該函數的典型代碼是:

sigset_t newmask;
int rcvd_sig;
siginfo_t info;
sigemptyset(&newmask);
sigaddset(&newmask, SIGRTMIN);
sigprocmask(SIG_BLOCK, &newmask, NULL);
rcvd_sig = sigwaitinfo(&newmask, &info)
if (rcvd_sig == -1) {
        ..
}


調用成功返回信號值,否則返回-1sigtimedwait()功能相似,只不過增加了一個進程等待的時間。

  1. 程式的穩定性。
    為了增強程式的穩定性,在信號處理函數中應使用可重入函數。

信號處理常式中應當使用可再入(可重入)函數(注:所謂可重入函數是指一個可以被多個任務調用的過程,任務在調用時不必擔心資料是否會出錯)。因為進程在收到信號後,就將跳轉到信號處理函數去接著執行。如果信號處理函數中使用了不可重入函數,那麼信號處理函數可能會修改原來進程中不應該被修改的資料,這樣進程從信號處理函數中返回接著執行時,可能會出現不可預料的後果。不可再入函數在信號處理函數中被視為不安全函數。

滿足下列條件的函數多數是不可再入的:(1)使用靜態的資料結構,如getlogin()gmtime()getgrgid()getgrnam()getpwuid()以及getpwnam()等等;(2)函數實現時,調用了malloc()或者free()函數;(3)實現時使用了標準I/O函數的。The Open Group視下列函數為可再入的:

_exit()、access()、alarm()、cfgetispeed()、cfgetospeed()、cfsetispeed()、cfsetospeed()、chdir()、chmod()、chown()、close()、creat()、dup()、dup2()、execle()、execve()、fcntl()、fork()、fpathconf()、fstat()、fsync()、getegid()、 geteuid()、getgid()、getgroups()、getpgrp()、getpid()、getppid()、getuid()、kill()、link()、lseek()、mkdir()、mkfifo()、 open()、pathconf()、pause()、pipe()、raise()、read()、rename()、rmdir()、setgid()、setpgid()、setsid()、setuid()、 sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、signal()、sigpending()、sigprocmask()、sigsuspend()、sleep()、stat()、sysconf()、tcdrain()、tcflow()、tcflush()、tcgetattr()、tcgetpgrp()、tcsendbreak()、tcsetattr()、tcsetpgrp()、time()、times()、 umask()、uname()、unlink()、utime()、wait()、waitpid()、write()。

即使信號處理函數使用的都是"安全函數",同樣要注意進入處理函數時,首先要保存errno的值,結束時,再恢復原值。因為,信號處理過程中,errno值隨時可能被改變。另外,longjmp()以及siglongjmp()沒有被列為可再入函數,因為不能保證緊接著兩個函數的其它調用是安全的。




三、深入淺出:信號應用實例

linux下的信號應用並沒有想像的那麼恐怖,程式師所要做的最多只有三件事情:

  1. 安裝信號(推薦使用sigaction());
  2. 實現三參數信號處理函數,handler(int signal,struct siginfo *info, void *)
  3. 發送信號,推薦使用sigqueue()

實際上,對有些信號來說,只要安裝信號就足夠了(信號處理方式採用缺省或忽略)。其他可能要做的無非是與信號集相關的幾種操作。

實例一:信號發送及處理
實現一個信號接收程式sigreceive(其中信號安裝由sigaction())。

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
        struct sigaction act; 
        int sig;
        sig=atoi(argv[1]);
       
        sigemptyset(&act.sa_mask);
        act.sa_flags=SA_SIGINFO;
        act.sa_sigaction=new_op;
       
        if(sigaction(sig,&act,NULL) < 0)
        {
               printf("install sigal error\n");
        }
       
        while(1)
        {
               sleep(2);
               printf("wait for the signal\n");
        }
}
void new_op(int signum,siginfo_t *info,void *myact)
{
        printf("receive signal %d", signum);
        sleep(5);
}




說明,命令列參數為信號值,後臺運行sigreceive signo &,可獲得該進程的ID,假設為pid,然後再另一終端上運行kill -s signo pid驗證信號的發送接收及處理。同時,可驗證信號的排隊問題。
注:可以用sigqueue實現一個命令列信號反射程式sigqueuesend,見 附錄1

實例二:信號傳遞附加資訊
主要包括兩個實例:

  1. 向進程本身發送信號,並傳遞指標參數;

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
        struct sigaction act; 
        union sigval mysigval;
        int i;
        int sig;
        pid_t pid;            
        char data[10];
        memset(data,0,sizeof(data));
        for(i=0;i < 5;i++)
               data[i]='2';
        mysigval.sival_ptr=data;
       
        sig=atoi(argv[1]);
        pid=getpid();
       
        sigemptyset(&act.sa_mask);
        act.sa_sigaction=new_op;//三參數信號處理函數
        act.sa_flags=SA_SIGINFO;//資訊傳遞開關
        if(sigaction(sig,&act,NULL) < 0)
        {
               printf("install sigal error\n");
        }
        while(1)
        {
               sleep(2);
               printf("wait for the signal\n");
               sigqueue(pid,sig,mysigval);//向本進程發送信號,並傳遞附加資訊
        }
}
void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實現
{
        int i;
        for(i=0;i<10;i++)
        {
               printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));
        }
        printf("handle signal %d over;",signum);
}



這個例子中,信號實現了附加資訊的傳遞,信號究竟如何對這些資訊進行處理則取決於具體的應用。

  1. 2、 不同進程間傳遞整型參數:把1中的信號發送和接收放在兩個程式中,並且在發送過程中傳遞整型參數。
    信號接收程式:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void new_op(int,siginfo_t*,void*);
int main(int argc,char**argv)
{
        struct sigaction act;
        int sig;
        pid_t pid;            
       
        pid=getpid();
        sig=atoi(argv[1]);    
       
        sigemptyset(&act.sa_mask);
        act.sa_sigaction=new_op;
        act.sa_flags=SA_SIGINFO;
        if(sigaction(sig,&act,NULL)<0)
        {
               printf("install sigal error\n");
        }
        while(1)
        {
               sleep(2);
               printf("wait for the signal\n");
        }
}
void new_op(int signum,siginfo_t *info,void *myact)
{
        printf("the int value is %d \n",info->si_int);
}



信號反射程式:命令列第二個參數為信號值,第三個參數為接收進程ID

#include <signal.h>
#include <sys/time.h>
#include <unistd.h>
#include <sys/types.h>
main(int argc,char**argv)
{
        pid_t pid;
        int signum;
        union sigval mysigval;
        signum=atoi(argv[1]);
        pid=(pid_t)atoi(argv[2]);
        mysigval.sival_int=8;//不代表具體含義,只用於說明問題
        if(sigqueue(pid,signum,mysigval)==-1)
               printf("send error\n");
        sleep(2);
}



注:實例2的兩個例子側重點在於用信號來傳遞資訊,目前關於在linux下通過信號傳遞資訊的實例非常少,倒是Unix下有一些,但傳遞的基本上都是關於傳遞一個整數,傳遞指標的我還沒看到。我一直沒有實現不同進程間的指標傳遞(實際上更有意義),也許在實現方法上存在問題吧,請實現者email我。

實例三:信號阻塞及信號集操作

#include "signal.h"
#include "unistd.h"
static void my_op(int);
main()
{
        sigset_t new_mask,old_mask,pending_mask;
        struct sigaction act;
        sigemptyset(&act.sa_mask);
        act.sa_flags=SA_SIGINFO;
        act.sa_sigaction=(void*)my_op;
        if(sigaction(SIGRTMIN+10,&act,NULL))
               printf("install signal SIGRTMIN+10 error\n");
        sigemptyset(&new_mask);
        sigaddset(&new_mask,SIGRTMIN+10);
        if(sigprocmask(SIG_BLOCK, &new_mask,&old_mask))
               printf("block signal SIGRTMIN+10 error\n");
        sleep(10);    
        printf("now begin to get pending mask and unblock SIGRTMIN+10\n");
        if(sigpending(&pending_mask)<0)
               printf("get pending mask error\n");
        if(sigismember(&pending_mask,SIGRTMIN+10))
               printf("signal SIGRTMIN+10 is pending\n");
        if(sigprocmask(SIG_SETMASK,&old_mask,NULL)<0)
               printf("unblock signal error\n");
        printf("signal unblocked\n");
        sleep(10);
}
static void my_op(int signum)
{
        printf("receive signal %d \n",signum);
}



編譯該程式,並以後臺方式運行。在另一終端向該進程發送信號(運行kill -s 42 pidSIGRTMIN+1042),查看結果可以看出幾個關鍵函數的運行機制,信號集相關操作比較簡單。

注:在上面幾個實例中,使用了printf()函數,只是作為診斷工具,pringf()函數是不可重入的,不應在信號處理函數中使用。




附錄1

sigqueue實現的命令列信號反射程式sigqueuesend,命令列第二個參數是發送的信號值,第三個參數是接收該信號的進程ID,可以配合實例一使用:

#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc,char**argv)
{
        pid_t pid;
        int sig;
        sig=atoi(argv[1]);
        pid=atoi(argv[2]);
        sigqueue(pid,sig,NULL);
        sleep(2);
}



沒有留言: