|
在信號(上)中,討論了linux信號種類,、來源,、如何安裝一個信號以及對信號集的操作。本部分則首先討論從信號的生命周期上認識信號,,或者宏觀上看似簡單的信號機制(進程收到信號后,,作相應的處理,看上去再簡單不過了),,在微觀上究竟是如何實現(xiàn)的,,也是在更深層次上理解信號。接下來還討論了信號編程的一些注意事項,最后給出了信號編程的一些實例,。
一,、信號生命周期
從信號發(fā)送到信號處理函數(shù)的執(zhí)行完畢
對于一個完整的信號生命周期(從信號發(fā)送到相應的處理函數(shù)執(zhí)行完畢)來說,可以分為三個重要的階段,,這三個階段由四個重要事件來刻畫:信號誕生,;信號在進程中注冊完畢;信號在進程中的注銷完畢,;信號處理函數(shù)執(zhí)行完畢,。相鄰兩個事件的時間間隔構成信號生命周期的一個階段。
下面闡述四個事件的實際意義:
- 信號"誕生",。信號的誕生指的是觸發(fā)信號的事件發(fā)生(如檢測到硬件異常,、定時器超時以及調(diào)用信號發(fā)送函數(shù)kill()或sigqueue()等)。
- 信號在目標進程中"注冊",;進程的task_struct結構中有關于本進程中未決信號的數(shù)據(jù)成員:
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結構中,。只要信號在進程的未決信號集中,,表明進程已經(jīng)知道這些信號的存在,但還沒來得及處理,,或者該信號被進程阻塞,。
注: 當一個實時信號發(fā)送給一個進程時,不管該信號是否已經(jīng)在進程中注冊,,都會被再注冊一次,,因此,信號不會丟失,,因此,,實時信號又叫做"可靠信號"。這意味著同一個實時信號可以在同一個進程的未決信號信息鏈中占有多個sigqueue結構(進程每收到一個實時信號,,都會為它分配一個結構來登記該信號信息,,并把該結構添加在未決信號鏈尾,即所有誕生的實時信號都會在目標進程中注冊),; 當一個非實時信號發(fā)送給一個進程時,,如果該信號已經(jīng)在進程中注冊,則該信號將被丟棄,,造成信號丟失,。因此,,非實時信號又叫做"不可靠信號"。這意味著同一個非實時信號在進程的未決信號信息鏈中,,至多占有一個sigqueue結構(一個非實時信號誕生后,(1),、如果發(fā)現(xiàn)相同的信號已經(jīng)在目標結構中注冊,,則不再注冊,對于進程來說,,相當于不知道本次信號發(fā)生,,信號丟失;(2),、如果進程的未決信號中沒有相同信號,,則在進程中注冊自己)。
- 信號在進程中的注銷,。在目標進程執(zhí)行過程中,,會檢測是否有信號等待處理(每次從系統(tǒng)空間返回到用戶空間時都做這樣的檢查)。如果存在未決信號等待處理且該信號沒有被進程阻塞,,則在運行相應的信號處理函數(shù)前,,進程會把信號在未決信號鏈中占有的結構卸掉。是否將信號從進程未決信號集中刪除對于實時與非實時信號是不同的,。對于非實時信號來說,,由于在未決信號信息鏈中最多只占用一個sigqueue結構,因此該結構被釋放后,,應該把信號在進程未決信號集中刪除(信號注銷完畢),;而對于實時信號來說,可能在未決信號信息鏈中占用多個sigqueue結構,,因此應該針對占用sigqueue結構的數(shù)目區(qū)別對待:如果只占用一個sigqueue結構(進程只收到該信號一次),,則應該把信號在進程的未決信號集中刪除(信號注銷完畢)。否則,,不應該在進程的未決信號集中刪除該信號(信號注銷完畢),。
進程在執(zhí)行信號相應處理函數(shù)之前,首先要把信號在進程中注銷,。
- 信號生命終止,。進程注銷信號后,立即執(zhí)行相應的信號處理函數(shù),,執(zhí)行完畢后,,信號的本次發(fā)送對進程的影響徹底結束。
注: 1)信號注冊與否,,與發(fā)送信號的函數(shù)(如kill()或sigqueue()等)以及信號安裝函數(shù)(signal()及sigaction())無關,,只與信號值有關(信號值小于SIGRTMIN的信號最多只注冊一次,,信號值在SIGRTMIN及SIGRTMAX之間的信號,只要被進程接收到就被注冊),。 2)在信號被注銷到相應的信號處理函數(shù)執(zhí)行完畢這段時間內(nèi),,如果進程又收到同一信號多次,則對實時信號來說,,每一次都會在進程中注冊,;而對于非實時信號來說,無論收到多少次信號,,都會視為只收到一個信號,,只在進程中注冊一次。
|
二,、信號編程注意事項
- 防止不該丟失的信號丟失,。如果對八中所提到的信號生命周期理解深刻的話,很容易知道信號會不會丟失,,以及在哪里丟失,。
- 程序的可移植性
考慮到程序的可移植性,應該盡量采用POSIX信號函數(shù),,POSIX信號函數(shù)主要分為兩類:
- POSIX 1003.1信號函數(shù): Kill(),、sigaction()、sigaddset(),、sigdelset(),、sigemptyset()、sigfillset(),、sigismember(),、sigpending()、sigprocmask(),、sigsuspend(),。
- POSIX 1003.1b信號函數(shù)。POSIX 1003.1b在信號的實時性方面對POSIX 1003.1做了擴展,,包括以下三個函數(shù): sigqueue(),、sigtimedwait()、sigwaitinfo(),。其中,,sigqueue主要針對信號發(fā)送,而sigtimedwait及sigwaitinfo()主要用于取代sigsuspend()函數(shù),,后面有相應實例,。
#include <signal.h>
int sigwaitinfo(sigset_t *set, siginfo_t *info).
|
該函數(shù)與sigsuspend()類似,阻塞一個進程直到特定信號發(fā)生,,但信號到來時不執(zhí)行信號處理函數(shù),,而是返回信號值,。因此為了避免執(zhí)行相應的信號處理函數(shù),必須在調(diào)用該函數(shù)前,,使進程屏蔽掉set指向的信號,,因此調(diào)用該函數(shù)的典型代碼是:
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) {
..
}
|
調(diào)用成功返回信號值,否則返回-1,。sigtimedwait()功能相似,,只不過增加了一個進程等待的時間。
- 程序的穩(wěn)定性,。
為了增強程序的穩(wěn)定性,在信號處理函數(shù)中應使用可重入函數(shù),。
信號處理程序中應當使用可再入(可重入)函數(shù)(注:所謂可重入函數(shù)是指一個可以被多個任務調(diào)用的過程,,任務在調(diào)用時不必擔心數(shù)據(jù)是否會出錯)。因為進程在收到信號后,,就將跳轉到信號處理函數(shù)去接著執(zhí)行,。如果信號處理函數(shù)中使用了不可重入函數(shù),那么信號處理函數(shù)可能會修改原來進程中不應該被修改的數(shù)據(jù),,這樣進程從信號處理函數(shù)中返回接著執(zhí)行時,,可能會出現(xiàn)不可預料的后果。不可再入函數(shù)在信號處理函數(shù)中被視為不安全函數(shù),。
滿足下列條件的函數(shù)多數(shù)是不可再入的:(1)使用靜態(tài)的數(shù)據(jù)結構,,如getlogin(),gmtime(),,getgrgid(),,getgrnam(),getpwuid()以及getpwnam()等等,;(2)函數(shù)實現(xiàn)時,,調(diào)用了malloc()或者free()函數(shù);(3)實現(xiàn)時使用了標準I/O函數(shù)的,。The Open Group視下列函數(shù)為可再入的:
_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(),。
即使信號處理函數(shù)使用的都是"安全函數(shù)",,同樣要注意進入處理函數(shù)時,首先要保存errno的值,,結束時,,再恢復原值。因為,,信號處理過程中,,errno值隨時可能被改變。另外,,longjmp()以及siglongjmp()沒有被列為可再入函數(shù),,因為不能保證緊接著兩個函數(shù)的其它調(diào)用是安全的。
|
三,、深入淺出:信號應用實例
linux下的信號應用并沒有想象的那么恐怖,,程序員所要做的最多只有三件事情:
- 安裝信號(推薦使用sigaction()),;
- 實現(xiàn)三參數(shù)信號處理函數(shù),handler(int signal,struct siginfo *info, void *),;
- 發(fā)送信號,,推薦使用sigqueue()。
實際上,,對有些信號來說,,只要安裝信號就足夠了(信號處理方式采用缺省或忽略)。其他可能要做的無非是與信號集相關的幾種操作,。
實例一:信號發(fā)送及處理 實現(xiàn)一個信號接收程序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);
}
|
說明,命令行參數(shù)為信號值,,后臺運行sigreceive signo &,,可獲得該進程的ID,假設為pid,,然后再另一終端上運行kill -s signo pid驗證信號的發(fā)送接收及處理,。同時,可驗證信號的排隊問題,。 注:可以用sigqueue實現(xiàn)一個命令行信號發(fā)送程序sigqueuesend,,見 附錄1。
實例二:信號傳遞附加信息 主要包括兩個實例:
- 向進程本身發(fā)送信號,,并傳遞指針參數(shù),;
#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;//三參數(shù)信號處理函數(shù)
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);//向本進程發(fā)送信號,并傳遞附加信息
}
}
void new_op(int signum,siginfo_t *info,void *myact)//三參數(shù)信號處理函數(shù)的實現(xiàn)
{
int i;
for(i=0;i<10;i++)
{
printf("%c\n ",(*( (char*)((*info).si_ptr)+i)));
}
printf("handle signal %d over;",signum);
}
|
這個例子中,,信號實現(xiàn)了附加信息的傳遞,,信號究竟如何對這些信息進行處理則取決于具體的應用。
- 2,、 不同進程間傳遞整型參數(shù):把1中的信號發(fā)送和接收放在兩個程序中,,并且在發(fā)送過程中傳遞整型參數(shù)。
信號接收程序:
#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);
}
|
信號發(fā)送程序:命令行第二個參數(shù)為信號值,,第三個參數(shù)為接收進程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下有一些,但傳遞的基本上都是關于傳遞一個整數(shù),,傳遞指針的我還沒看到,。我一直沒有實現(xiàn)不同進程間的指針傳遞(實際上更有意義),也許在實現(xiàn)方法上存在問題吧,請實現(xiàn)者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);
}
|
編譯該程序,,并以后臺方式運行。在另一終端向該進程發(fā)送信號(運行kill -s 42 pid,,SIGRTMIN+10為42),,查看結果可以看出幾個關鍵函數(shù)的運行機制,信號集相關操作比較簡單,。
注:在上面幾個實例中,,使用了printf()函數(shù),只是作為診斷工具,,pringf()函數(shù)是不可重入的,,不應在信號處理函數(shù)中使用。
結束語:
系統(tǒng)地對linux信號機制進行分析,、總結使我受益匪淺,!感謝王小樂等網(wǎng)友的支持! Comments and suggestions are greatly welcome!
附錄1:
用sigqueue實現(xiàn)的命令行信號發(fā)送程序sigqueuesend,,命令行第二個參數(shù)是發(fā)送的信號值,,第三個參數(shù)是接收該信號的進程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內(nèi)核源代碼情景分析(上),,毛德操,、胡希明著,浙江大學出版社,,當要驗證某個結論、想法時,,最好的參考資料,;
- UNIX環(huán)境高級編程,作者:W.Richard Stevens,,譯者:尤晉元等,,機械工業(yè)出版社。對信號機制的發(fā)展過程闡述的比較詳細,。
- signal,、sigaction、kill等手冊,,最直接而可靠的參考資料,。
- http://www./modules.php?op=modload&name=NS-help&file=man提供了許多系統(tǒng)調(diào)用、庫函數(shù)等的在線指南,。
- http://www./onlinepubs/007904975/可以在這里對許多關鍵函數(shù)(包括系統(tǒng)調(diào)用)進行查詢,,非常好的一個網(wǎng)址。
- http:///whitepapers/reentrant.html對函數(shù)可重入進行了闡述。
- http://www./~compsvcs/doc-cdrom/DOCS/HTML/APS33DTE/DOCU_006.HTM對實時信號給出了相當好的描述,。
|