信号本质
信号是在软件层次上对硬件的中断机制的一种模拟,在原理上,一个进程接受到一个信号和处理器接受到中断请求可以说是一样。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号是进程间通信机制中唯一的异步通信机制,可以看作是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。信号事件的发生有两个来源:硬件来源(比如我们按下了键盘或者其它硬件故障);软件来源,最常用发送信号的系统函数是kill, raise, alarm和signal函数,软件来源还包括一些非法运算等操作。
进程可以通过三种方式来响应一个信号:(1)忽略信号(SIG_IGN),即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL及SIGSTOP;(2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数(void (*func)(int));(3)执行缺省操作(SIG_DFL),Linux对每种信号都规定了默认操作。注意,进程对实时信号的缺省反应是进程终止。Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。
信号种类
可以从两个不同的分类角度对信号进行分类:可靠性方面:可靠信号与不可靠信号。与时间的关系上:实时信号与非实时信号。利用kill -l显示当前系统支持的所有信号:
可靠信号与不可靠信号
不可靠信号:Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,因此,把那些建立在早期机制上的信号叫做"不可靠信号",信号值小于SIGRTMIN(Red hat 7.2中,SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是:- 进程每次处理信号后,就将对信号的响应设置为默认动作。在某些情况下,将导致对信号的错误处理;因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号。
- 信号可能丢失,后面将对此详细阐述。
可靠信号:随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现"可靠信号"。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。
信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation以及信号发送函数sigqueue的同时,仍然支持早期的signal信号安装函数,支持信号发送函数kill()。注:不要有这样的误解:由sigqueue()发送、sigaction安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。对于目前linux的两个信号安装函数:signal()及sigaction()来说,它们都不能把SIGRTMIN以前的信号变成可靠信号(都不支持排队,仍有可能丢失,仍然是不可靠信号),而且对SIGRTMIN以后的信号都支持排队。这两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数(对所有信号这一点都成立),而经过signal安装的信号却不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
实时信号与非实时信号
早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
信号生命周期
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:信号诞生;信号在进程中注册完毕;信号在进程中的注销完毕;信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。
下面阐述四个事件的实际意义:
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;
- }
注: 当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册,则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。
3. 信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕)。而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不应该在进程的未决信号集中删除该信号(信号注销完毕)。进程在执行信号相应处理函数之前,首先要把信号在进程中注销。
4. 信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。
注:
1)信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)。
2)在信号被注销到相应的信号处理函数执行完毕这段时间内,如果进程又收到同一信号多次,则对实时信号来说,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次。
相关函数
信号安装
- #include <signal.h>
- typedef void (*sighandler_t)(int);
- sighandler_t signal(int signum, sighandler_t handler);
- #include <signal.h>
- int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
sigaction函数用于改变进程接收到特定信号后的行为。该函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理;第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。
- 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);
- }
2、由_sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;由_sa_sigaction是指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值,参数所指向的结构(简单版本)如下:
- <pre name="code" class="cpp">typedef struct {
- int si_signo;
- int si_errno;
- int si_code;
- union sigval si_value;
- } siginfo_t
- </pre>
- <pre></pre>
- <pre></pre>
- <pre></pre>
- union sigval {
- int sival_int;
- void *sival_ptr;
利用系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。信号参数的传递过程可图示如下:

4、sa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFER及SA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。注: 很多文献在阐述该标志位时都认为,如果设置了该标志位,就必须定义三参数信号处理函数。实际不是这样的,验证方法很简单:自己实现一个单一参数信号处理函 数,并在程序中设置该标志位,可以察看程序的运行结果。实际上,可以把该标志位看成信号是否传递参数的开关,如果设置该位,则传递参数;否则,不传递参 数。
信号发送
- #include <sys/types.h>
- #include <signal.h>
- int kill(pid_t pid, int sig);
- #include <signal.h>
- int raise(int sig);
- #include <unistd.h>
- unsigned int alarm(unsigned int seconds);
- #include <stdlib.h>
- void abort(void);
- #include <sys/time.h>
- int getitimer(int which, struct itimerval *curr_value);
- int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
- ITIMER_REAL: 设定绝对时间;经过指定的时间后,内核将发送SIGALRM信号给本进程;
- ITIMER_VIRTUAL 设定程序执行时间;经过指定的时间后,内核将发送SIGVTALRM信号给本进程;
- ITIMER_PROF 设定进程执行以及内核因本进程而消耗的时间和,经过指定的时间后,内核将发送ITIMER_VIRTUAL信号给本进程;
- struct itimerval {
- <span style="white-space:pre"> </span>struct timeval it_interval; /* next value */
- <span style="white-space:pre"> </span>struct timeval it_value; /* current value */
- };
- struct timeval {
- <span style="white-space:pre"> </span>long tv_sec; /* seconds */
- <span style="white-space:pre"> </span>long tv_usec; /* microseconds */
- };
- #include <signal.h>
- int sigqueue(pid_t pid, int sig, const union sigval value);
- typedef union sigval {
- int sival_int;
- void *sival_ptr;
- }sigval_t;
- typedef struct {
- unsigned long sig[_NSIG_WORDS];
- } sigset_t
- #include <signal.h>
- int sigemptyset(sigset_t *set);//初始化由set指定的信号集,信号集里面的所有信号被清空;
- int sigfillset(sigset_t *set);//调用该函数后,set指向的信号集中将包含linux支持的64种信号;
- int sigaddset(sigset_t *set, int signum);//在set指向的信号集中加入signum信号;
- int sigdelset(sigset_t *set, int signum);//在set指向的信号集中删除signum信号;
- int sigismember(const sigset_t *set, int signum);//判定信号signum是否在set指向的信号集中。
信号阻塞
每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。下面是与信号阻塞相关的几个函数:- #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));
- SIG_BLOCK:在进程当前阻塞信号集中添加set指向信号集中的信号
- SIG_UNBLOCK:如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
- SIG_SETMASK:更新进程阻塞信号集为set指向的信号集
sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。
深入浅出:信号应用实例
- 安装信号(推荐使用sigaction());
- 实现三参数信号处理函数,handler(int signal,struct siginfo *info, void *);
- 发送信号,推荐使用sigqueue()。
- #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验证信号的发送接收及处理。同时,可验证信号的排队问题。
- #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);
- }
- #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()函数是不可重入的,不应在信号处理函数中使用。