簡介: 在訊號(上)中,討論了linux訊號種類、來源、如何安裝一個訊號以及對訊號集的操作。本部分則首先討論從訊號的生命週期上認識訊號,或者宏觀上看似簡單的訊號機制(進程收到訊號後,作相應的處理,看上去再簡單不過了),在微觀上究竟是如何?的,也是在更深層次上理解訊號。接下來還討論了訊號編程的一些注意事項,最後給出了訊號編程的一些執行個體。
一、訊號生命週期
從訊號發送到訊號處理函數的執行完畢
對於一個完整的訊號生命週期(從訊號發送到相應的處理函數執行完畢)來說,可以分為三個重要的階段,這三個階段由四個重要事件來刻畫:訊號誕生;訊號在進程中註冊完畢;訊號在進程中的登出完畢;訊號處理函數執行完畢。相鄰兩個事件的時間間隔構成訊號生命週期的一個階段。
下面闡述四個事件的實際意義:
- 訊號"誕生"。訊號的誕生指的是觸發訊號的事件發生(如檢測到硬體異常、定時器逾時以及調用訊號發送函數kill()或sigqueue()等)。
- 訊號在目標進程中"註冊";進程的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)、如果進程的未決訊號中沒有相同訊號,則在進程中註冊自己)。
- 訊號在進程中的登出。在目標進程執行過程中,會檢測是否有訊號等待處理(每次從系統空間返回到使用者空間時都做這樣的檢查)。如果存在未決訊號等待處理且該訊號沒有被進程阻塞,則在運行相應的訊號處理函數前,進程會把訊號在未決訊號鏈中佔有的結構卸掉。是否將訊號從進程未決訊號集中刪除對於即時與非即時訊號是不同的。對於非即時訊號來說,由於在未決訊號資訊鏈中最多隻佔用一個sigqueue結構,因此該結構被釋放後,應該把訊號在進程未決訊號集中刪除(訊號登出完畢);而對於即時訊號來說,可能在未決訊號資訊鏈中佔用多個sigqueue結構,因此應該針對佔用sigqueue結構的數目區別對待:如果只佔用一個sigqueue結構(進程只收到該訊號一次),則應該把訊號在進程的未決訊號集中刪除(訊號登出完畢)。否則,不應該在進程的未決訊號集中刪除該訊號(訊號登出完畢)。
進程在執行訊號相應處理函數之前,首先要把訊號在進程中登出。
- 訊號生命終止。進程登出訊號後,立即執行相應的訊號處理函數,執行完畢後,訊號的本次發送對進程的影響徹底結束。
註:
1)訊號註冊與否,與發送訊號的函數(如kill()或sigqueue()等)以及訊號安裝函數(signal()及sigaction())無關,只與訊號值有關(訊號值小於SIGRTMIN的訊號最多隻註冊一次,訊號值在SIGRTMIN及SIGRTMAX之間的訊號,只要被進程接收到就被註冊)。
2)在訊號被登出到相應的訊號處理函數執行完畢這段時間內,如果進程又收到同一訊號多次,則對即時訊號來說,每一次都會在進程中註冊;而對於非即時訊號來說,無論收到多少次訊號,都會視為只收到一個訊號,只在進程中註冊一次。
回頁首
二、訊號編程注意事項
- 防止不該丟失的訊號丟失。如果對八中所提到的訊號生命週期理解深刻的話,很容易知道訊號會不會丟失,以及在哪裡丟失。
- 程式的可移植性
考慮到程式的可移植性,應該盡量採用POSIX訊號函數,POSIX訊號函數主要分為兩類:
- 程式的穩定性。
為了增強程式的穩定性,在訊號處理函數中應使用可重新進入函數。訊號處理常式中應當使用可再入(可重新進入)函數(註:所謂可重新進入函數是指一個可以被多個任務調用的過程,任務在調用時不必擔心資料是否會出錯)。因為進程在收到訊號後,就將跳轉到訊號處理函數去接著執行。如果訊號處理函數中使用了不可重新進入函數,那麼訊號處理函數可能會修改原來進程中不應該被修改的資料,這樣進程從訊號處理函數中返回接著執行時,可能會出現不可預料的後果。不可再入函數在訊號處理函數中被視為不安全函數。
滿足下列條件的函數多數是不可再入的:(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下的訊號應用並沒有想象的那麼恐怖,程式員所要做的最多隻有三件事情:
- 安裝訊號(推薦使用sigaction());
- 實現三參數訊號處理函數,handler(int signal,struct siginfo *info, void *);
- 發送訊號,推薦使用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。
執行個體二:訊號傳遞附加資訊
主要包括兩個執行個體:
- 向進程本身發送訊號,並傳遞指標參數;
#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);} |
這個例子中,訊號實現了附加資訊的傳遞,訊號究竟如何對這些資訊進行處理則取決於具體的應用。
- 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 pid,SIGRTMIN+10為42),查看結果可以看出幾個關鍵函數的運行機制,訊號集相關操作比較簡單。
註:在上面幾個執行個體中,使用了printf()函數,只是作為診斷工具,pringf()函數是不可重新進入的,不應在訊號處理函數中使用。
回頁首
結束語:
系統地對linux訊號機制進行分析、總結使我受益匪淺!感謝王小樂等網友的支援!
Comments and suggestions are greatly welcome!
回頁首
附錄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);} |
參考資料
- linux核心原始碼情景分析(上),毛德操、胡希明著,浙江大學出版社,當要驗證某個結論、想法時,最好的參考資料;
- UNIX環境進階編程,作者:W.Richard Stevens,譯者:尤晉元等,機械工業出版社。對訊號機制的發展過程闡述的比較詳細。
- signal、sigaction、kill等手冊,最直接而可靠的參考資料。
- http://www.linuxjournal.com/modules.php?op=modload&name=NS-help&file=man提供了許多系統調用、庫函數等的線上指南。
- http://www.opengroup.org/onlinepubs/007904975/可以在這裡對許多關鍵函數(包括系統調用)進行查詢,非常好的一個網址。
- http://unix.org/whitepapers/reentrant.html對函數可重新進入進行了闡述。
- http://www.uccs.edu/~compsvcs/doc-cdrom/DOCS/HTML/APS33DTE/DOCU_006.HTM對即時訊號給出了相當好的描述。
關於作者