在信號(上)中,討論了linux信號種類(lèi)、來(lái)源、如何安裝一個(gè)信號以及對信號集的操作。本部分則首先討論從信號的生命周期上認識信號,或者宏觀(guān)上看似簡(jiǎn)單的信號機制(進(jìn)程收到信號后,作相應的處理,看上去再簡(jiǎn)單不過(guò)了),在微觀(guān)上究竟是如何實(shí)現的,也是在更深層次上理解信號。接下來(lái)還討論了信號編程的一些注意事項,最后給出了信號編程的一些實(shí)例。
對于一個(gè)完整的信號生命周期(從信號發(fā)送到相應的處理函數執行完畢)來(lái)說(shuō),可以分為三個(gè)重要的階段,這三個(gè)階段由四個(gè)重要事件來(lái)刻畫(huà):信號誕生;信號在進(jìn)程中注冊完畢;信號在進(jìn)程中的注銷(xiāo)完畢;信號處理函數執行完畢。相鄰兩個(gè)事件的時(shí)間間隔構成信號生命周期的一個(gè)階段。
下面闡述四個(gè)事件的實(shí)際意義:
信號"誕生"。信號的誕生指的是觸發(fā)信號的事件發(fā)生(如檢測到硬件異常、定時(shí)器超時(shí)以及調用信號發(fā)送函數kill()或sigqueue()等)。
信號在目標進(jìn)程中"注冊";進(jìn)程的task_struct結構中有關(guān)于本進(jìn)程中未決信號的數據成員:
struct sigpending pending: struct sigpending{ struct sigqueue *head, **tail; sigset_t signal; }; 第三個(gè)成員是進(jìn)程中所有未決信號集,第一、第二個(gè)成員分別指向一個(gè)sigqueue類(lèi)型的結構鏈(稱(chēng)之為"未決信號信息鏈")的首尾,信息鏈中的每個(gè)sigqueue結構刻畫(huà)一個(gè)特定信號所攜帶的信息,并指向下一個(gè)sigqueue結構:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信號在進(jìn)程中注冊指的就是信號值加入到進(jìn)程的未決信號集中(sigpending結構的第二個(gè)成員sigset_t signal),并且信號所攜帶的信息被?! ×舻轿礇Q信號信息鏈的某個(gè)sigqueue結構中。只要信號在進(jìn)程的未決信號集中,表明進(jìn)程已經(jīng)知道這些信號的存在,但還沒(méi)來(lái)得及處理,或者該信號被進(jìn)程阻塞。注:
當一個(gè)實(shí)時(shí)信號發(fā)送給一個(gè)進(jìn)程時(shí),不管該信號是否已經(jīng)在進(jìn)程中注冊,都會(huì )被再注冊一次,因此,信號不會(huì )丟失,因此,實(shí)時(shí)信號又叫做"可靠信號"。這意味著(zhù)同一個(gè)實(shí)時(shí)信號可以在同一個(gè)進(jìn)程的未決信號信息鏈中占有多個(gè)sigqueue結構(進(jìn)程每收到一個(gè)實(shí)時(shí)信號,都會(huì )為它分配一個(gè)結構來(lái)登記該信號信息,并把該結構添加在未決信號鏈尾,即所有誕生的實(shí)時(shí)信號都會(huì )在目標進(jìn)程中注冊);
當一個(gè)非實(shí)時(shí)信號發(fā)送給一個(gè)進(jìn)程時(shí),如果該信號已經(jīng)在進(jìn)程中注冊,則該信號將被丟棄,造成信號丟失。因此,非實(shí)時(shí)信號又叫做"不可靠信號"。這意味著(zhù)同一個(gè)非實(shí)時(shí)信號在進(jìn)程的未決信號信息鏈中,至多占有一個(gè)sigqueue結構(一個(gè)非實(shí)時(shí)信號誕生后,(1)、如果發(fā)現相同的信號已經(jīng)在目標結構中注冊,則不再注冊,對于進(jìn)程來(lái)說(shuō),相當于不知道本次信號發(fā)生,信號丟失;(2)、如果進(jìn)程的未決信號中沒(méi)有相同信號,則在進(jìn)程中注冊自己)。
信號在進(jìn)程中的注銷(xiāo)。
在目標進(jìn)程執行過(guò)程中,會(huì )檢測是否有信號等待處理(每次從系統空間返回到用戶(hù)空間時(shí)都做這樣的檢查)。如果存在未決信號等待處理且該信號沒(méi)有被進(jìn)程阻塞,則在運行相應的信號處理函數前,進(jìn)程會(huì )把信號在未決信號鏈中占有的結構卸掉。是否將信號從進(jìn)程未決信號集中刪除對于實(shí)時(shí)與非實(shí)時(shí)信號是不同的。對于非實(shí)時(shí)信號來(lái)說(shuō),由于在未決信號信息鏈中最多只占用一個(gè)sigqueue結構,因此該結構被釋放后,應該把信號在進(jìn)程未決信號集中刪除(信號注銷(xiāo)完畢);而對于實(shí)時(shí)信號來(lái)說(shuō),可能在未決信號信息鏈中占用多個(gè)sigqueue結構,因此應該針對占用sigqueue結構的數目區別對待:如果只占用一個(gè)sigqueue結構(進(jìn)程只收到該信號一次),則應該把信號在進(jìn)程的未決信號集中刪除(信號注銷(xiāo)完畢)。否則,不應該在進(jìn)程的未決信號集中刪除該信號(信號注銷(xiāo)完畢)。
進(jìn)程在執行信號相應處理函數之前,首先要把信號在進(jìn)程中注銷(xiāo)。
信號生命終止。
進(jìn)程注銷(xiāo)信號后,立即執行相應的信號處理函數,執行完畢后,信號的本次發(fā)送對進(jìn)程的影響徹底結束。
注:
1)信號注冊與否,與發(fā)送信號的函數(如kill()或sigqueue()等)以及信號安裝函數(signal()及sigaction())無(wú)關(guān),只與信號值有關(guān)(信號值小于SIGRTMIN的信號最多只注冊一次,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進(jìn)程接收到就被注冊)。
2)在信號被注銷(xiāo)到相應的信號處理函數執行完畢這段時(shí)間內,如果進(jìn)程又收到同一信號多次,則對實(shí)時(shí)信號來(lái)說(shuō),每一次都會(huì )在進(jìn)程中注冊;而對于非實(shí)時(shí)信號來(lái)說(shuō),無(wú)論收到多少次信號,都會(huì )視為只收到一個(gè)信號,只在進(jìn)程中注冊一次。
防止不該丟失的信號丟失。如果對八中所提到的信號生命周期理解深刻的話(huà),很容易知道信號會(huì )不會(huì )丟失,以及在哪里丟失。
程序的可移植性
考慮到程序的可移植性,應該盡量采用POSIX信號函數,POSIX信號函數主要分為兩類(lèi):
POSIX 1003.1信號函數: Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、sigpending()、sigprocmask()、sigsuspend()。
POSIX 1003.1b信號函數。POSIX 1003.1b在信號的實(shí)時(shí)性方面對POSIX 1003.1做了擴展,包括以下三個(gè)函數: sigqueue()、sigtimedwait()、sigwaitinfo()。其中,sigqueue主要針對信號發(fā)送,而sigtimedwait及sigwaitinfo()主要用于取代sigsuspend()函數,后面有相應實(shí)例。
#i nclude <signal.h> int sigwaitinfo(sigset_t *set, siginfo_t *info). 該函數與sigsuspend()類(lèi)似,阻塞一個(gè)進(jìn)程直到特定信號發(fā)生,但信號到來(lái)時(shí)不執行信號處理函數,而是返回信號值。因此為了避免執行相應的信號處理函數,必須在調用該函數前,使進(jìn)程屏蔽掉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) { .. } 調用成功返回信號值,否則返回-1。sigtimedwait()功能相似,只不過(guò)增加了一個(gè)進(jìn)程等待的時(shí)間。 程序的穩定性。
為了增強程序的穩定性,在信號處理函數中應使用可重入函數。
信號處理程序中應當使用可再入(可重入)函數(注:所謂可重入函數是指一個(gè)可以被多個(gè)任務(wù)調用的過(guò)程,任務(wù)在調用時(shí)不必擔心數據是否會(huì )出錯)。因為進(jìn)程在收到信號后,就將跳轉到信號處理函數去接著(zhù)執行。如果信號處理函數中使用了不可重入函數,那么信號處理函數可能會(huì )修改原來(lái)進(jìn)程中不應該被修改的數據,這樣進(jìn)程從信號處理函數中返回接著(zhù)執行時(shí),可能會(huì )出現不可預料的后果。不可再入函數在信號處理函數中被視為不安全函數。
滿(mǎn)足下列條件的函數多數是不可再入的:(1)使用靜態(tài)的數據結構,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;(2)函數實(shí)現時(shí),調用了malloc()或者free()函數;(3)實(shí)現時(shí)使用了標準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()。
即使信號處理函數使用的都是"安全函數",同樣要注意進(jìn)入處理函數時(shí),首先要保存errno的值,結束時(shí),再恢復原值。因為,信號處理過(guò)程中,errno值隨時(shí)可能被改變。另外,longjmp()以及siglongjmp()沒(méi)有被列為可再入函數,因為不能保證緊接著(zhù)兩個(gè)函數的其它調用是安全的。
linux下的信號應用并沒(méi)有想象的那么恐怖,程序員所要做的最多只有三件事情:
安裝信號(推薦使用sigaction());
實(shí)現三參數信號處理函數,handler(int signal,struct siginfo *info, void *);
發(fā)送信號,推薦使用sigqueue()。
實(shí)際上,對有些信號來(lái)說(shuō),只要安裝信號就足夠了(信號處理方式采用缺省或忽略)。其他可能要做的無(wú)非是與信號集相關(guān)的幾種操作。
實(shí)例一:信號發(fā)送及處理
實(shí)現一個(gè)信號接收程序sigreceive(其中信號安裝由sigaction())。
#i nclude <signal.h>#i nclude <sys/types.h>#i nclude <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);}
說(shuō)明,命令行參數為信號值,后臺運行sigreceive signo &,可獲得該進(jìn)程的ID,假設為pid,然后再另一終端上運行kill -s signo pid驗證信號的發(fā)送接收及處理。同時(shí),可驗證信號的排隊問(wèn)題。
注:可以用sigqueue實(shí)現一個(gè)命令行信號發(fā)送程序sigqueuesend,見(jiàn)附錄1。
實(shí)例二:信號傳遞附加信息
主要包括兩個(gè)實(shí)例:
向進(jìn)程本身發(fā)送信號,并傳遞指針參數;
#i nclude <signal.h> #i nclude <sys/types.h> #i nclude <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;//信息傳遞開(kāi)關(guān) 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);//向本進(jìn)程發(fā)送信號,并傳遞附加信息 } } void new_op(int signum,siginfo_t *info,void *myact)//三參數信號處理函數的實(shí)現 { int i; for(i=0;i<10;i++) { printf("%c\n ",(*( (char*)((*info).si_ptr)+i))); } printf("handle signal %d over;",signum); }
這個(gè)例子中,信號實(shí)現了附加信息的傳遞,信號究竟如何對這些信息進(jìn)行處理則取決于具體的應用。
2、 不同進(jìn)程間傳遞整型參數:把1中的信號發(fā)送和接收放在兩個(gè)程序中,并且在發(fā)送過(guò)程中傳遞整型參數。
信號接收程序:
#i nclude <signal.h> #i nclude <sys/types.h> #i nclude <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); }
信號發(fā)送程序:命令行第二個(gè)參數為信號值,第三個(gè)參數為接收進(jìn)程ID。
#i nclude <signal.h> #i nclude <sys/time.h> #i nclude <unistd.h> #i nclude <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;//不代表具體含義,只用于說(shuō)明問(wèn)題 if(sigqueue(pid,signum,mysigval)==-1) printf("send error\n"); sleep(2); }
注:實(shí)例2的兩個(gè)例子側重點(diǎn)在于用信號來(lái)傳遞信息,目前關(guān)于在linux下通過(guò)信號傳遞信息的實(shí)例非常少,倒是Unix下有一些,但傳遞的基本上都是關(guān)于傳遞一個(gè)整數,傳遞指針的我還沒(méi)看到。我一直沒(méi)有實(shí)現不同進(jìn)程間的指針傳遞(實(shí)際上更有意義),也許在實(shí)現方法上存在問(wèn)題吧,請實(shí)現者email我。
實(shí)例三:信號阻塞及信號集操作
#i nclude "signal.h"#i nclude "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);}
編譯該程序,并以后臺方式運行。在另一終端向該進(jìn)程發(fā)送信號(運行kill -s 42 pid,SIGRTMIN+10為42),查看結果可以看出幾個(gè)關(guān)鍵函數的運行機制,信號集相關(guān)操作比較簡(jiǎn)單。
注:在上面幾個(gè)實(shí)例中,使用了printf()函數,只是作為診斷工具,pringf()函數是不可重入的,不應在信號處理函數中使用。
系統地對linux信號機制進(jìn)行分析、總結使我受益匪淺!感謝王小樂(lè )等網(wǎng)友的支持!
Comments and suggestions are greatly welcome!
用sigqueue實(shí)現的命令行信號發(fā)送程序sigqueuesend,命令行第二個(gè)參數是發(fā)送的信號值,第三個(gè)參數是接收該信號的進(jìn)程ID,可以配合實(shí)例一使用:
#i nclude <signal.h>#i nclude <sys/types.h>#i nclude <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);}
| | |Linux環(huán)境進(jìn)程間通信(二):信號(上)
linux信號機制遠遠比想象的復雜,本文力爭用最短的篇幅,對該機制做了深入細致的分析。信號應用實(shí)例將在信號(下)中給出。
信號本質(zhì)
信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個(gè)進(jìn)程收到一個(gè)信號與處理器收到一個(gè)中斷請求可以說(shuō)是一樣的。信號是異步的,一個(gè)進(jìn)程不必通過(guò)任何操作來(lái)等待信號的到達,事實(shí)上,進(jìn)程也不知道信號到底什么時(shí)候到達。
信號是進(jìn)程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進(jìn)程有哪些事情發(fā)生了。信號機制經(jīng)過(guò)POSIX實(shí)時(shí)擴展后,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。
信號來(lái)源
信號事件的發(fā)生有兩個(gè)來(lái)源:硬件來(lái)源(比如我們按下了鍵盤(pán)或者其它硬件故障);軟件來(lái)源,最常用發(fā)送信號的系統函數是kill, raise, alarm和setitimer以及sigqueue函數,軟件來(lái)源還包括一些非法運算等操作。
可以從兩個(gè)不同的分類(lèi)角度對信號進(jìn)行分類(lèi):(1)可靠性方面:可靠信號與不可靠信號;(2)與時(shí)間的關(guān)系上:實(shí)時(shí)信號與非實(shí)時(shí)信號。在《Linux環(huán)境進(jìn)程間通信(一):管道及有名管道》的附1中列出了系統所支持的所有信號。
1、可靠信號與不可靠信號
"不可靠信號"
Linux信號機制基本上是從Unix系統中繼承過(guò)來(lái)的。早期Unix系統中的信號機制比較簡(jiǎn)單和原始,后來(lái)在實(shí)踐中暴露出一些問(wèn)題,因此,把那些建立在早期機制上的信號叫做"不可靠信號",信號值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信號都是不可靠信號。這就是"不可靠信號"的來(lái)源。它的主要問(wèn)題是:
進(jìn)程每次處理信號后,就將對信號的響應設置為默認動(dòng)作。在某些情況下,將導致對信號的錯誤處理;因此,用戶(hù)如果不希望這樣的操作,那么就要在信號處理函數結尾再一次調用signal(),重新安裝該信號。
信號可能丟失,后面將對此詳細闡述。
因此,早期unix下的不可靠信號主要指的是進(jìn)程可能對信號做出錯誤的反應以及信號可能丟失。
Linux支持不可靠信號,但是對不可靠信號機制做了改進(jìn):在調用完信號處理函數后,不必重新調用該信號的安裝函數(信號安裝函數是在可靠機制上的實(shí)現)。因此,Linux下的不可靠信號問(wèn)題主要指的是信號可能丟失。
"可靠信號"
隨著(zhù)時(shí)間的發(fā)展,實(shí)踐證明了有必要對信號的原始機制加以改進(jìn)和擴充。所以,后來(lái)出現的各種Unix版本分別在這方面進(jìn)行了研究,力圖實(shí)現"可靠信號"。由于原來(lái)定義的信號已有許多應用,不好再做改動(dòng),最終只好又新增加了一些信號,并在一開(kāi)始就把它們定義為可靠信號,這些信號支持排隊,不會(huì )丟失。同時(shí),信號的發(fā)送和安裝也出現了新版本:信號發(fā)送函數sigqueue()及信號安裝函數sigaction()。POSIX.4對可靠信號機制做了標準化。但是,POSIX只對可靠信號機制應具有的功能以及信號機制的對外接口做了標準化,對信號機制的實(shí)現沒(méi)有作具體的規定。
信號值位于SIGRTMIN和SIGRTMAX之間的信號都是可靠信號,可靠信號克服了信號可能丟失的問(wèn)題。Linux在支持新版本的信號安裝函數sigation()以及信號發(fā)送函數sigqueue()的同時(shí),仍然支持早期的signal()信號安裝函數,支持信號發(fā)送函數kill()。
注:不要有這樣的誤解:由sigqueue()發(fā)送、sigaction安裝的信號就是可靠的。事實(shí)上,可靠信號是指后來(lái)添加的新信號(信號值位于SIGRTMIN及SIGRTMAX之間);不可靠信號是信號值小于SIGRTMIN的信號。信號的可靠與不可靠只與信號值有關(guān),與信號的發(fā)送及安裝函數無(wú)關(guān)。目前l(fā)inux中的signal()是通過(guò)sigation()函數實(shí)現的,因此,即使通過(guò)signal()安裝的信號,在信號處理函數的結尾也不必再調用一次信號安裝函數。同時(shí),由signal()安裝的實(shí)時(shí)信號支持排隊,同樣不會(huì )丟失。
對于目前l(fā)inux的兩個(gè)信號安裝函數:signal()及sigaction()來(lái)說(shuō),它們都不能把SIGRTMIN以前的信號變成可靠信號(都不支持排隊,仍有可能丟失,仍然是不可靠信號),而且對SIGRTMIN以后的信號都支持排隊。這兩個(gè)函數的最大區別在于,經(jīng)過(guò)sigaction安裝的信號都能傳遞信息給信號處理函數(對所有信號這一點(diǎn)都成立),而經(jīng)過(guò)signal安裝的信號卻不能向信號處理函數傳遞信息。對于信號發(fā)送函數來(lái)說(shuō)也是一樣的。
2、實(shí)時(shí)信號與非實(shí)時(shí)信號
早期Unix系統只定義了32種信號,Ret hat7.2支持64種信號,編號0-63(SIGRTMIN=31,SIGRTMAX=63),將來(lái)可能進(jìn)一步增加,這需要得到內核的支持。前32種信號已經(jīng)有了預定義值,每個(gè)信號有了確定的用途及含義,并且每種信號都有各自的缺省動(dòng)作。如按鍵盤(pán)的CTRL ^C時(shí),會(huì )產(chǎn)生SIGINT信號,對該信號的默認反應就是進(jìn)程終止。后32個(gè)信號表示實(shí)時(shí)信號,等同于前面闡述的可靠信號。這保證了發(fā)送的多個(gè)實(shí)時(shí)信號都被接收。實(shí)時(shí)信號是POSIX標準的一部分,可用于應用進(jìn)程。
非實(shí)時(shí)信號都不支持排隊,都是不可靠信號;實(shí)時(shí)信號都支持排隊,都是可靠信號。
進(jìn)程可以通過(guò)三種方式來(lái)響應一個(gè)信號:(1)忽略信號,即對信號不做任何處理,其中,有兩個(gè)信號不能忽略:SIGKILL及SIGSTOP;(2)捕捉信號。定義信號處理函數,當信號發(fā)生時(shí),執行相應的處理函數;(3)執行缺省操作,Linux對每種信號都規定了默認操作,詳細情況請參考[2]以及其它資料。注意,進(jìn)程對實(shí)時(shí)信號的缺省反應是進(jìn)程終止。
Linux究竟采用上述三種方式的哪一個(gè)來(lái)響應信號,取決于傳遞給相應API函數的參數。
發(fā)送信號的主要函數有:kill()、raise()、 sigqueue()、alarm()、setitimer()以及abort()。
1、kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,int signo)
參數pid的值 信號的接收進(jìn)程 pid>0 進(jìn)程ID為pid的進(jìn)程 pid=0 同一個(gè)進(jìn)程組的進(jìn)程 pid<0 pid!=-1 進(jìn)程組ID為 -pid的所有進(jìn)程 pid=-1 除發(fā)送進(jìn)程自身外,所有進(jìn)程ID大于1的進(jìn)程
Sinno是信號值,當為0時(shí)(即空信號),實(shí)際不發(fā)送任何信號,但照常進(jìn)行錯誤檢查,因此,可用于檢查目標進(jìn)程是否存在,以及當前進(jìn)程是否具有向目標發(fā)送信號的權限(root權限的進(jìn)程可以向任何進(jìn)程發(fā)送信號,非root權限的進(jìn)程只能向屬于同一個(gè)session或者同一個(gè)用戶(hù)的進(jìn)程發(fā)送信號)。
Kill()最常用于pid>0時(shí)的信號發(fā)送,調用成功返回 0; 否則,返回 -1。注:對于pid<0時(shí)的情況,對于哪些進(jìn)程將接受信號,各種版本說(shuō)法不一,其實(shí)很簡(jiǎn)單,參閱內核源碼kernal/signal.c即可,上表中的規則是參考red hat 7.2。
2、raise()
#include <signal.h>
int raise(int signo)
向進(jìn)程本身發(fā)送信號,參數為即將發(fā)送的信號值。調用成功返回 0;否則,返回 -1。
3、sigqueue()
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
調用成功返回 0;否則,返回 -1。
sigqueue()是比較新的發(fā)送信號系統調用,主要是針對實(shí)時(shí)信號提出的(當然也支持前32種),支持信號帶有參數,與函數sigaction()配合使用。
sigqueue的第一個(gè)參數是指定接收信號的進(jìn)程ID,第二個(gè)參數確定即將發(fā)送的信號,第三個(gè)參數是一個(gè)聯(lián)合數據結構union sigval,指定了信號傳遞的參數,即通常所說(shuō)的4字節值。
typedef union sigval { int sival_int; void *sival_ptr; }sigval_t;
sigqueue()比kill()傳遞了更多的附加信息,但sigqueue()只能向一個(gè)進(jìn)程發(fā)送信號,而不能發(fā)送信號給一個(gè)進(jìn)程組。如果signo=0,將會(huì )執行錯誤檢查,但實(shí)際上不發(fā)送任何信號,0值信號可用于檢查pid的有效性以及當前進(jìn)程是否有權限向目標進(jìn)程發(fā)送信號。
在調用sigqueue時(shí),sigval_t指定的信息會(huì )拷貝到3參數信號處理函數(3參數信號處理函數指的是信號處理函數由sigaction安裝,并設定了sa_sigaction指針,稍后將闡述)的siginfo_t結構中,這樣信號處理函數就可以處理這些信息了。由于sigqueue系統調用支持發(fā)送帶參數信號,所以比kill()系統調用的功能要靈活和強大得多。
注:sigqueue()發(fā)送非實(shí)時(shí)信號時(shí),第三個(gè)參數包含的信息仍然能夠傳遞給信號處理函數; sigqueue()發(fā)送非實(shí)時(shí)信號時(shí),仍然不支持排隊,即在信號處理函數執行過(guò)程中到來(lái)的所有相同信號,都被合并為一個(gè)信號。
4、alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds)
專(zhuān)門(mén)為SIGALRM信號而設,在指定的時(shí)間seconds秒后,將向進(jìn)程本身發(fā)送SIGALRM信號,又稱(chēng)為鬧鐘時(shí)間。進(jìn)程調用alarm后,任何以前的alarm()調用都將無(wú)效。如果參數seconds為零,那么進(jìn)程內將不再包含任何鬧鐘時(shí)間。
返回值,如果調用alarm()前,進(jìn)程中已經(jīng)設置了鬧鐘時(shí)間,則返回上一個(gè)鬧鐘時(shí)間的剩余時(shí)間,否則返回0。
5、setitimer()
#include <sys/time.h>
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue));
setitimer()比alarm功能強大,支持3種類(lèi)型的定時(shí)器:
ITIMER_REAL: 設定絕對時(shí)間;經(jīng)過(guò)指定的時(shí)間后,內核將發(fā)送SIGALRM信號給本進(jìn)程;
ITIMER_VIRTUAL 設定程序執行時(shí)間;經(jīng)過(guò)指定的時(shí)間后,內核將發(fā)送SIGVTALRM信號給本進(jìn)程;
ITIMER_PROF 設定進(jìn)程執行以及內核因本進(jìn)程而消耗的時(shí)間和,經(jīng)過(guò)指定的時(shí)間后,內核將發(fā)送ITIMER_VIRTUAL信號給本進(jìn)程;
Setitimer()第一個(gè)參數which指定定時(shí)器類(lèi)型(上面三種之一);第二個(gè)參數是結構itimerval的一個(gè)實(shí)例,結構itimerval形式見(jiàn)附錄1。第三個(gè)參數可不做處理。
Setitimer()調用成功返回0,否則返回-1。
6、abort()
#include <stdlib.h>
void abort(void);
向進(jìn)程發(fā)送SIGABORT信號,默認情況下進(jìn)程會(huì )異常退出,當然可定義自己的信號處理函數。即使SIGABORT被進(jìn)程設置為阻塞信號,調用abort()后,SIGABORT仍然能被進(jìn)程接收。該函數無(wú)返回值。
五、信號的安裝(設置信號關(guān)聯(lián)動(dòng)作)
如果進(jìn)程要處理某一信號,那么就要在進(jìn)程中安裝該信號。安裝信號主要用來(lái)確定信號值及進(jìn)程針對該信號值的動(dòng)作之間的映射關(guān)系,即進(jìn)程將要處理哪個(gè)信號;該信號被傳遞給進(jìn)程時(shí),將執行何種操作。
linux主要有兩個(gè)函數實(shí)現信號的安裝:signal()、sigaction()。其中signal()在可靠信號系統調用的基礎上實(shí)現, 是庫函數。它只有兩個(gè)參數,不支持信號傳遞信息,主要是用于前32種非實(shí)時(shí)信號的安裝;而sigaction()是較新的函數(由兩個(gè)系統調用實(shí)現:sys_signal以及sys_rt_sigaction),有三個(gè)參數,支持信號傳遞信息,主要用來(lái)與 sigqueue() 系統調用配合使用,當然,sigaction()同樣支持非實(shí)時(shí)信號的安裝。sigaction()優(yōu)于signal()主要體現在支持信號帶有參數。
1、signal()
#include <signal.h>
void (*signal(int signum, void (*handler))(int)))(int);
如果該函數原型不容易理解的話(huà),可以參考下面的分解方式來(lái)理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一個(gè)參數指定信號的值,第二個(gè)參數指定針對前面信號值的處理,可以忽略該信號(參數設為SIG_IGN);可以采用系統默認方式處理信號(參數設為SIG_DFL);也可以自己實(shí)現處理方式(參數指定一個(gè)函數地址)。
如果signal()調用成功,返回最后一次為安裝信號signum而調用signal()時(shí)的handler值;失敗則返回SIG_ERR。
2、sigaction()
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
sigaction函數用于改變進(jìn)程接收到特定信號后的行為。該函數的第一個(gè)參數為信號的值,可以為除SIGKILL及SIGSTOP外的任何一個(gè)特定有效的信號(為這兩個(gè)信號定義自己的處理函數,將導致信號安裝錯誤)。第二個(gè)參數是指向結構sigaction的一個(gè)實(shí)例的指針,在結構sigaction的實(shí)例中,指定了對特定信號的處理,可以為空,進(jìn)程會(huì )以缺省方式對信號處理;第三個(gè)參數oldact指向的對象用來(lái)保存原來(lái)對相應信號的處理,可指定oldact為NULL。如果把第二、第三個(gè)參數都設為NULL,那么該函數可用于檢查信號的有效性。
第二個(gè)參數最為重要,其中包含了對指定信號的處理、信號所傳遞的信息、信號處理函數執行過(guò)程中應屏蔽掉哪些函數等等。
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,已過(guò)時(shí),POSIX不支持它,不應再被使用。
1、聯(lián)合數據結構中的兩個(gè)元素_sa_handler以及*_sa_sigaction指定信號關(guān)聯(lián)函數,即用戶(hù)指定的信號處理函數。除了可以是用戶(hù)自定義的處理函數外,還可以為SIG_DFL(采用缺省的處理方式),也可以為SIG_IGN(忽略信號)。
2、由_sa_handler指定的處理函數只有一個(gè)參數,即信號值,所以信號不能傳遞除信號值之外的任何信息;由_sa_sigaction是指定的信號處理函數帶有三個(gè)參數,是為實(shí)時(shí)信號而設的(當然同樣支持非實(shí)時(shí)信號),它指定一個(gè)3參數信號處理函數。第一個(gè)參數為信號值,第三個(gè)參數沒(méi)有使用(posix沒(méi)有規范使用該參數的標準),第二個(gè)參數是指向siginfo_t結構的指針,結構中包含信號攜帶的數據值,參數所指向的結構如下:
siginfo_t { int si_signo; /* 信號值,對所有信號有意義*/ int si_errno; /* errno值,對所有信號有意義*/ int si_code; /* 信號產(chǎn)生的原因,對所有信號有意義*/ union{ /* 聯(lián)合數據結構,不同成員適應不同信號 */ //確保分配足夠大的存儲空間 int _pad[SI_PAD_SIZE]; //對SIGKILL有意義的結構 struct{ ... }... ... ... ... ... //對SIGILL, SIGFPE, SIGSEGV, SIGBUS有意義的結構 struct{ ... }... ... ... } }
注:為了更便于閱讀,在說(shuō)明問(wèn)題時(shí)常把該結構表示為附錄2所表示的形式。
siginfo_t結構中的聯(lián)合數據成員確保該結構適應所有的信號,比如對于實(shí)時(shí)信號來(lái)說(shuō),則實(shí)際采用下面的結構形式:
typedef struct { int si_signo; int si_errno; int si_code; union sigval si_value; } siginfo_t;
結構的第四個(gè)域同樣為一個(gè)聯(lián)合數據結構:
union sigval { int sival_int; void *sival_ptr; }
采用聯(lián)合數據結構,說(shuō)明siginfo_t結構中的si_value要么持有一個(gè)4字節的整數值,要么持有一個(gè)指針,這就構成了與信號相關(guān)的數據。在信號的處理函數中,包含這樣的信號相關(guān)數據指針,但沒(méi)有規定具體如何對這些數據進(jìn)行操作,操作方法應該由程序開(kāi)發(fā)人員根據具體任務(wù)事先約定。
前面在討論系統調用sigqueue發(fā)送信號時(shí),sigqueue的第三個(gè)參數就是sigval聯(lián)合數據結構,當調用sigqueue時(shí),該數據結構中的數據就將拷貝到信號處理函數的第二個(gè)參數中。這樣,在發(fā)送信號同時(shí),就可以讓信號傳遞一些附加信息。信號可以傳遞信息對程序開(kāi)發(fā)是非常有意義的。
信號參數的傳遞過(guò)程可圖示如下:
3、sa_mask指定在信號處理程序執行過(guò)程中,哪些信號應當被阻塞。缺省情況下當前信號本身被阻塞,防止信號的嵌套發(fā)送,除非指定SA_NODEFER或者SA_NOMASK標志位。
注:請注意sa_mask指定的信號阻塞的前提條件,是在由sigaction()安裝信號的處理函數執行過(guò)程中由sa_mask指定的信號才被阻塞。
4、sa_flags中包含了許多標志位,包括剛剛提到的SA_NODEFER及SA_NOMASK標志位。另一個(gè)比較重要的標志位是SA_SIGINFO,當設定了該標志位時(shí),表示信號附帶的參數可以被傳遞到信號處理函數中,因此,應該為sigaction結構中的sa_sigaction指定處理函數,而不應該為sa_handler指定信號處理函數,否則,設置該標志變得毫無(wú)意義。即使為sa_sigaction指定了信號處理函數,如果不設置SA_SIGINFO,信號處理函數同樣不能得到信號傳遞過(guò)來(lái)的數據,在信號處理函數中對這些信息的訪(fǎng)問(wèn)都將導致段錯誤(Segmentation fault)。
注:很多文獻在闡述該標志位時(shí)都認為,如果設置了該標志位,就必須定義三參數信號處理函數。實(shí)際不是這樣的,驗證方法很簡(jiǎn)單:自己實(shí)現一個(gè)單一參數信號處理函數,并在程序中設置該標志位,可以察看程序的運行結果。實(shí)際上,可以把該標志位看成信號是否傳遞參數的開(kāi)關(guān),如果設置該位,則傳遞參數;否則,不傳遞參數。
信號集被定義為一種數據類(lèi)型:
typedef struct { unsigned long sig[_NSIG_WORDS]; } sigset_t
信號集用來(lái)描述信號的集合,linux所支持的所有信號可以全部或部分的出現在信號集中,主要與信號阻塞相關(guān)函數配合使用。下面是為信號集操作定義的相關(guān)函數:
#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指向的信號集中。
每個(gè)進(jìn)程都有一個(gè)用來(lái)描述哪些信號遞送到進(jìn)程時(shí)將被阻塞的信號集,該信號集中的所有信號在遞送到進(jìn)程后都將被阻塞。下面是與信號阻塞相關(guān)的幾個(gè)函數:
#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來(lái)實(shí)現對信號集的操作,操作主要有三種:
參數how 進(jìn)程當前信號集 SIG_BLOCK 在進(jìn)程當前阻塞信號集中添加set指向信號集中的信號 SIG_UNBLOCK 如果進(jìn)程阻塞信號集中包含set指向信號集中的信號,則解除對該信號的阻塞 SIG_SETMASK 更新進(jìn)程阻塞信號集為set指向的信號集
sigpending(sigset_t *set))獲得當前已遞送到進(jìn)程,卻被阻塞的所有信號,在set指向的信號集中返回結果。
sigsuspend(const sigset_t *mask))用于在接收到某個(gè)信號之前, 臨時(shí)用mask替換進(jìn)程的信號掩碼, 并暫停進(jìn)程執行,直到收到信號為止。sigsuspend 返回后將恢復調用之前的信號掩碼。信號處理函數完成后,進(jìn)程將繼續執行。該系統調用始終返回-1,并將errno設置為EINTR。
附錄1:結構itimerval:
struct itimerval { struct timeval it_interval; /* next value */ struct timeval it_value; /* current value */ }; struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ };
附錄2:三參數信號處理函數中第二個(gè)參數的說(shuō)明性描述:
siginfo_t { int si_signo; /* 信號值,對所有信號有意義*/ int si_errno; /* errno值,對所有信號有意義*/ int si_code; /* 信號產(chǎn)生的原因,對所有信號有意義*/ pid_t si_pid; /* 發(fā)送信號的進(jìn)程ID,對kill(2),實(shí)時(shí)信號以及SIGCHLD有意義 */ uid_t si_uid; /* 發(fā)送信號進(jìn)程的真實(shí)用戶(hù)ID,對kill(2),實(shí)時(shí)信號以及SIGCHLD有意義 */ int si_status; /* 退出狀態(tài),對SIGCHLD有意義*/ clock_t si_utime; /* 用戶(hù)消耗的時(shí)間,對SIGCHLD有意義 */ clock_t si_stime; /* 內核消耗的時(shí)間,對SIGCHLD有意義 */ sigval_t si_value; /* 信號值,對所有實(shí)時(shí)有意義,是一個(gè)聯(lián)合數據結構,可以為一個(gè)整數(由si_int標示,也可以為一個(gè)指針,由si_ptr標示)*/ void * si_addr; /* 觸發(fā)fault的內存地址,對SIGILL,SIGFPE,SIGSEGV,SIGBUS 信號有意義*/ int si_band; /* 對SIGPOLL信號有意義 */ int si_fd; /* 對SIGPOLL信號有意義 */
}實(shí)際上,除了前三個(gè)元素外,其他元素組織在一個(gè)聯(lián)合結構中,在聯(lián)合數據結構中,又根據不同的信號組織成不同的結構。注釋中提到的對某種信號有意義指的是,在該信號的處理函數中可以訪(fǎng)問(wèn)這些域來(lái)獲得與信號相關(guān)的有意義的信息,只不過(guò)特定信號只對特定信息感興趣而已。
聯(lián)系客服