Linux环境进程间通讯(二)讯号(下)
一、信号生命周期
从讯号发送到讯号处理函数的执行完毕
对于一个完整的讯号生命周期(从讯号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要风波来描画:讯号诞生;讯号在进程中注册完毕;讯号在进程中的注销完毕;讯号处理函数执行完毕。相邻两个风波的时间间隔构成讯号生命周期的一个阶段。
下边论述四个风波的实际意义:
讯号”诞生”。讯号的诞生指的是触发讯号的风波发生(如测量到硬件异常、定时器超时以及调用讯号发送函数kill()或sigqueue()等)。
讯号在目标进程中”注册”;进程的task_struct结构中有关于本进程中未决讯号的数据成员:
structsigpendingpending:
structsigpending{
structsigqueue*head,**tail;
sigset_tsignal;
};
第三个成员是进程中所有未决讯号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为”未决讯号信息链”)的首尾,信息链中的每位sigqueue结构描画一个特定讯号所携带的信息,并指向下一个sigqueue结构:
structsigqueue{
structsigqueue*next;
siginfo_tinfo;
讯号在进程中注册指的就是讯号值加入到进程的未决讯号集中(sigpending结构的第二个成员sigset_tsignal),而且讯号所携带的信息被保留到未决讯号信息链的某个sigqueue结构中。只要讯号在进程的未决讯号集中,表明进程早已晓得这种讯号的存在,但还没来得及处理,或则该讯号被进程阻塞。
注:
当一个实时讯号发送给一个进程时,不管该讯号是否早已在进程中注册,就会被再注册一次,为此,讯号不会遗失,因而,实时讯号又称作”可靠讯号”。这意味着同一个实时讯号可以在同一个进程的未决讯号信息链中占有多个sigqueue结构(进程每收到一个实时讯号,就会为它分配一个结构来登记该讯号信息,并把该结构添加在未决讯号链尾,即所有诞生的实时讯号就会在目标进程中注册);
当一个非实时讯号发送给一个进程时linux arg,假如该讯号早已在进程中注册,则该讯号将被遗弃,导致讯号遗失。为此,非实时讯号又称作”不可靠讯号”。这意味着同一个非实时讯号在进程的未决讯号信息链中,至多占有一个sigqueue结构(一个非实时讯号诞生后,(1)、如果发觉相同的讯号早已在目标结构中注册,则不再注册,对于进程来说,相当于不晓得本次讯号发生,讯号遗失;(2)、如果进程的未决讯号中没有相同讯号,则在进程中注册自己)。
讯号在进程中的注销。在目标进程执行过程中,会检查是否有讯号等待处理(每次从系统空间返回到用户空间时都做这样的检测)。假如存在未决讯号等待处理且该讯号没有被进程阻塞,则在运行相应的讯号处理函数前,进程会把讯号在未决讯号链中占有的结构卸掉。是否将讯号从进程未决讯号集中删掉对于实时与非实时讯号是不同的。对于非实时讯号来说,因为在未决讯号信息链中最多只占用一个sigqueue结构,因而该结构被释放后,应当把讯号在进程未决讯号集中删掉(讯号注销完毕);而对于实时讯号来说,可能在未决讯号信息链中占用多个sigqueue结构,因而应当针对占用sigqueue结构的数量区别对待:假如只占用一个sigqueue结构(进程只收到该讯号一次),则应当把讯号在进程的未决讯号集中删掉(讯号注销完毕)。否则,不应当在进程的未决讯号集中删掉该讯号(讯号注销完毕)。
进程在执行讯号相应处理函数之前,首先要把讯号在进程中注销。
讯号生命中止。进程注销讯号后,立刻执行相应的讯号处理函数,执行完毕后,讯号的本次发送对进程的影回荡底结束。
注:
1)讯号注册与否,与发送讯号的函数(如kill()或sigqueue()等)以及讯号安装函数(signal()及sigaction())无关,只与讯号值有关(讯号值大于SIGRTMIN的讯号最多只注册一次,讯号值在SIGRTMIN及SIGRTMAX之间的讯号,只要被进程接收到就被注册)。
2)在讯号被注销到相应的讯号处理函数执行完毕这段时间内,假若进程又收到同一讯号多次,则对实时讯号来说,每一次就会在进程中注册;而对于非实时讯号来说,无论收到多少次讯号,就会视为只收到一个讯号,只在进程中注册一次。
二、信号编程注意事项
避免不该遗失的讯号遗失。假如对中学所提及的讯号生命周期理解深刻的话,很容易晓得讯号会不会遗失,以及在那里遗失。
程序的可移植性
考虑到程序的可移植性,应当尽量采用POSIX讯号函数,POSIX讯号函数主要分为两类:
POSIX1003.1讯号函数:Kill()、sigaction()、sigaddset()、sigdelset()、sigemptyset()、sigfillset()、sigismember()、sigpending()、sigprocmask()、sigsuspend()。
POSIX1003.1b讯号函数。POSIX1003.1b在讯号的实时性方面对POSIX1003.1做了扩充redhat linuxredhat linux 下载,包括以下三个函数:sigqueue()、sigtimedwait()、sigwaitinfo()。其中,sigqueue主要针对讯号发送,而sigtimedwait及sigwaitinfo()主要用于替代sigsuspend()函数,旁边有相应实例。
#include
intsigwaitinfo(sigset_t*set,siginfo_t*info).
该函数与sigsuspend()类似,阻塞一个进程直至特定讯号发生,但讯号到来时不执行讯号处理函数,而是返回讯号值。因而为了防止执行相应的讯号处理函数,必须在调用该函数前,使进程屏蔽掉set指向的讯号,因而调用该函数的典型代码是:
sigset_tnewmask;
intrcvd_sig;
siginfo_tinfo;
sigemptyset(&newmask);
sigaddset(&newmask,SIGRTMIN);
sigprocmask(SIG_BLOCK,&newmask,NULL);
rcvd_sig=sigwaitinfo(&newmask,&info)
if(rcvd_sig==-1){
..
调用成功返回讯号值,否则返回-1。sigtimedwait()功能相像,只不过降低了一个进程等待的时间。
程序的稳定性。
为了提高程序的稳定性,在讯号处理函数中应使用可重入函数。
讯号处理程序中应该使用可再入(可重入)函数(注:所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时毋须害怕数据是否会出错)。由于进程在收到讯号后,就将跳转到讯号处理函数去接着执行。假如讯号处理函数中使用了不可重入函数,这么讯号处理函数可能会更改原先进程中不应当被更改的数据,这样进程从讯号处理函数中返回接着执行时,可能会出现不可预想的后果。不可再入函数在讯号处理函数中被视为不安全函数。
满足下述条件的函数多数是不可再入的:(1)使用静态的数据结构,如getlogin(),gmtime(),getgrgid(),getgrnam(),getpwuid()以及getpwnam()等等;(2)函数实现时,调用了malloc()或则free()函数;(3)实现时使用了标准I/O函数的。TheOpenGroup视下述函数为可再入的:
_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(intsignal,structsiginfo*info,void*);
发送讯号,推荐使用sigqueue()。
实际上,对有些讯号来说,只要安装讯号就足够了(讯号处理方法采用缺省或忽视)。其他可能要做的无非是与讯号集相关的几种操作。
实例一:讯号发送及处理
实现一个讯号接收程序sigreceive(其中讯号安装由sigaction())。
#include
#include
#include
voidnew_op(int,siginfo_t*,void*);
intmain(intargc,char**argv)
structsigactionact;
intsig;
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(“installsigalerrorn”);
while(1)
sleep(2);
printf(“waitforthesignaln”);
voidnew_op(intsignum,siginfo_t*info,void*myact)
printf(“receivesignal%d”,signum);
sleep(5);
说明,命令行参数为讯号值linux arg,后台运行sigreceivesigno&,可获得该进程的ID,假定为pid,之后再另一终端上运行kill-ssignopid验证讯号的发送接收及处理。同时,可验证讯号的排队问题。
注:可以用sigqueue实现一个命令行讯号发送程序sigqueuesend,见附表1。
实例二:讯号传递附加信息
主要包括两个实例:
向进程本身发送讯号,并传递表针参数;
#include
#include
#include
voidnew_op(int,siginfo_t*,void*);
intmain(intargc,char**argv)
structsigactionact;
unionsigvalmysigval;
inti;
intsig;
pid_tpid;
chardata[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(“installsigalerrorn”);
while(1)
sleep(2);
printf(“waitforthesignaln”);
sigqueue(pid,sig,mysigval);//向本进程发送讯号,并传递附加信息
voidnew_op(intsignum,siginfo_t*info,void*myact)//三参数讯号处理函数的实现
inti;
for(i=0;i
printf(“%cn”,(*((char*)((*info).si_ptr)+i)));
printf(“handlesignal%dover;”,signum);
这个事例中,讯号实现了附加信息的传递,讯号到底怎样对这种信息进行处理则取决于具体的应用。
2、不同进程间传递整型参数:把1中的讯号发送和接收置于两个程序中,但是在发送过程中传递整型参数。
讯号接收程序:
#include
#include
#include
voidnew_op(int,siginfo_t*,void*);
intmain(intargc,char**argv)
structsigactionact;
intsig;
pid_tpid;
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)
printf(“installsigalerrorn”);
while(1)
sleep(2);
printf(“waitforthesignaln”);
voidnew_op(intsignum,siginfo_t*info,void*myact)
printf(“theintvalueis%dn”,info->si_int);
讯号发送程序:命令行第二个参数为讯号值,第三个参数为接收进程ID。
#include
#include
#include
#include
main(intargc,char**argv)
pid_tpid;
intsignum;
unionsigvalmysigval;
signum=atoi(argv[1]);
pid=(pid_t)atoi(argv[2]);
mysigval.sival_int=8;//不代表具体含意,只用于说明问题
if(sigqueue(pid,signum,mysigval)==-1)
printf(“senderrorn”);
sleep(2);
注:实例2的两个反例优缺在于用讯号来传递信息,目前关于在linux下通过讯号传递信息的实例十分少,倒是Unix下有一些,但传递的基本上都是关于传递一个整数,传递表针的我还没见到。我始终没有实现不同进程间的表针传递(实际上更有意义),似乎在实现方式上存在问题吧,请实现者email我。
实例三:讯号阻塞及讯号集操作
#include”signal.h”
#include”unistd.h”
staticvoidmy_op(int);
main()
sigset_tnew_mask,old_mask,pending_mask;
structsigactionact;
sigemptyset(&act.sa_mask);
act.sa_flags=SA_SIGINFO;
act.sa_sigaction=(void*)my_op;
if(sigaction(SIGRTMIN+10,&act,NULL))
printf(“installsignalSIGRTMIN+10errorn”);
sigemptyset(&new_mask);
sigaddset(&new_mask,SIGRTMIN+10);
if(sigprocmask(SIG_BLOCK,&new_mask,&old_mask))
printf(“blocksignalSIGRTMIN+10errorn”);
sleep(10);
printf(“nowbegintogetpendingmaskandunblockSIGRTMIN+10n”);
if(sigpending(&pending_mask)
printf(“getpendingmaskerrorn”);
if(sigismember(&pending_mask,SIGRTMIN+10))
printf(“signalSIGRTMIN+10ispendingn”);
if(sigprocmask(SIG_SETMASK,&old_mask,NULL)
printf(“unblocksignalerrorn”);
printf(“signalunblockedn”);
sleep(10);
staticvoidmy_op(intsignum)
printf(“receivesignal%dn”,signum);
编译该程序,并以后台形式运行。在另一终端向该进程发送讯号(运行kill-s42pid,SIGRTMIN+10为42),查看结果可以看出几个关键函数的运行机制,讯号集相关操作比较简单。
注:在前面几个实例中,使用了printf()函数,只是作为确诊工具,pringf()函数是不可重入的,不应在讯号处理函数中使用。
结束语:
系统地对linux讯号机制进行剖析、总结使我获益颇丰!谢谢王小乐等网友的支持!
Commentsandsuggestionsaregreatlywelcome!
附表1:
用sigqueue实现的命令行讯号发送程序sigqueuesend,命令行第二个参数是发送的讯号值,第三个参数是接收该讯号的进程ID,可以配合实例一使用:
#include
#include
#include
intmain(intargc,char**argv)
pid_tpid;
intsig;
sig=atoi(argv[1]);
pid=atoi(argv[2]);
sigqueue(pid,sig,NULL);
sleep(2);