signal()函数

来源:互联网 发布:mysql 数据库文件类型 编辑:程序博客网 时间:2024/05/22 04:57

UNIX系统的信号机制最简单的接口是signal函数。signal函数的功能:为指定的信号安装一个新的信号处理函数。

#include <signal.h>void (*signal(int signo, void (*func)(int)))(int);
返回值:若成功则返回信号以前的处理配置,若出错则返回SIG_ERR

复杂原型分开看:

void (* signal( int signo, void (*func)(int) )  )(int);

函数名      :signal

函数参数   :int signo, void (*func)(int)

返回值类型:void (*)(int);


func的值是常量SIG_IGN、常量SIG_DFL或当接到此信号后要调用的函数的地址。如果指定SIG_IGN,则向内核表示忽略此信号(记住有两个信号SIGKILL和SIGSTOP不能忽略)。如果指定SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为“捕捉”该信号。称此函数为信号处理程序或信号捕捉函数。

signal函数原型说明此函数需要两个参数,返回一个函数指针,而该指针所指向的函数无返回值(void)。第一个参数signo一个整数,第二个参数是函数指针,它所指向的函数需要一个整型参数,无返回值。signal的返回值是一个函数地址,该函数有一个整型参数(即i最后的(int))。用自然语言描述也就是要向信号处理程序传递一个整型参数,而它却无返回值。但调用signal设置信号处理程序时,第二个参数是指向该函数(也就是信号处理程序)的指针。signal的返回值则是指向之前的信号处理程序的指针。

如果使用下面的typedef可以简化上面的写法:

原型简化:

typedef void (Sigfunc)(int);Sigfunc *signal(int , Sigfunc *);

说明:
Sigfunc是信号处理函数,Sigfunc*为指向该函数的指针类型。
signal函数输入两个参数:int和Sigfunc*
返回一个指针:Sigfunc*,这个指针指向上一次传入的Sigfunc*
自然语言表述之,就是捕获一个信号,需要信号的名字(int型),以及捕获后怎么处理(Sigfunc*),处理完毕后返回上一次处理这个信号的函数地址;如果信号处理函数注册过程中遇到错误将返回SIG_ERR


如果查系统的头文件<signal.h>,则很可能会找到下列形式的声明:

#define SIG_ERR (void(*)())-1#define SIG_DFL (void(*)())0#define SIG_IGN (void(*)())1

这些常量可以用于代替“指向函数的指针,该函数需要一个整型参数,而且无返回值“。signal的第二个参数及其返回值就可以用它们表示。这些常量所使用的三个值不一定是-1、0和1.它们必须是三个值而决不能是任一可声明函数的地址。

三种信号动作:

  • 默认动作
  • 忽略信号
  • 捕获信号

    注意:SIGKILL和SIGSTOP不可捕获

什么是信号:

简而言之,信号是一种软件中断,提供了一种处理异步异步的方法。
所谓异步事件,是指相对软件的运行来说,其发生是随机的。例如键盘输入中断按键(^C),它的发生在程序执行过程中是不可预测的。
硬件异常也能产生信号,例如被零除、无效内存引用等。这些条件通常先由内核硬件检测到,然后通知内核。内核将决定产生什么样的信号

signal()函数的不可靠性及缺陷:
所谓不可靠,是指信号可能会丢失,这主要是由于signal()实现时存在的缺陷造成的:

  1. 每次接到信号后,该信号复位成默认动作(需要再次设置信号捕捉函数,不然又是以默认的方式处理了)
  2. 不改变信号的处理方式就无法确定当前的信号处理方式
  3. 无法避免地导致系统调用的中断

    例如进程正在读一个低速设备,当没有读完时被一个信号中断,当信号处理函数返回时,进程不会继续刚才的系统调用,而是执行下一条语句。

  4. 进程不能关闭某些不想捕获的信号

    进程只能忽略它们,但有时候这并不是我们希望的样子;另外如果不忽略它们,可能会导致进程在执行临介区代码时,被意外中断,从而引发其他问题。

我们将先探讨这些缺陷,并提供一些解决的方法。这些解决方法可能仍然不可靠,这只是为了说明问题。

缺陷1:自动复位成默认动作

我们观察下面的代码:

#include "apue.h"   // for err_sys()#include <signal.h> static voidsig_int(int signo){    /* Uncomment this area to solve this issue    if(signal(SIGINT, sig_int) == SIG_ERR)            err_sys("sig_int: can't catch SIGINT");    */    printf("SIGINT caught!\n");} int main(){    if(signal(SIGINT, sig_int) == SIG_ERR)        err_sys("main: can't catch SIGINT");     while(1)        pause();    // wait for signal happen }

运行结果如下:

$ ./a.out^CSIGINT caught!^C

结论:
可见,signal函数默认将复位信号动作。我们可以通过在运行信号处理函数的时候再次注册来解决这个问题。
但是这会引发新的问题:从调用信号处理函数到信号处理函数执行signal()之间存在时间窗口,另一个SIGINT信号可能在此之间产生并导致程序退出。下面我们来验证这一点:

static voidsig_int(int signo){    volatile unsigned long i, j;    printf("SIGINT caught!\n");    for(i = 0; i < 10000; i++)        for(j = 0; j < 10000; j++)            ;    if(signal(SIGINT, sig_int) == SIG_ERR)            err_sys("sig_int: can't catch SIGINT"); }

我们只修改了信号处理函数,使信号在注册前延时一段时间,以便我们有时间再发送一个SIGINT信号。这里的volatile属性用于避免编译器对延时循环的优化处理,这种优化会导致这个循环无效。运行结果如下:

$ ./a.out^CSIGINT caught!^C

结论:
将时间窗口放大后,我们看到进程不幸被终止了。在实际运行过程中,我们很难保证从信号处理函数到再次注册之间不被这个信号再次终止。即使这种情况非常罕见,我们也不应该忽略这个问题。

缺陷2:不改变信号的处理方式就无法确定当前的信号处理方式

假设我们坚持使用具有缺陷的signal()函数来写,以确定当前的信号动作,那么必须包含以下代码:

1
2
if(signal(SIGINT, SIG_IGN) != SIG_IGN))
    signal(SIGINT, sig_int);

这段代码将判断当前信号是否被忽略,如果没有才注册这个信号。问题是这两个signal()函数之间存在时间窗口,如果在检测到当前信号不被忽略,但在这个信号注册前,发生了一个SIGINT信号,而且这个信号只发生一次,那么我们将丢失这个信号。

缺陷3:无法避免地导致系统调用的中断

进程正在读一个低速设备,且没有读完时被一个信号中断,当信号处理函数返回时,进程不会继续刚才的系统调用,而是执行下一条语句。
这个缺陷也许不会导致信号丢失,但是它使得内核不能完整地完成一个系统调用。我们来看一个例子:

#include "apue.h"   // for err_sys()#include <fcntl.h>    // for open(), read()#include <errno.h>    // for errno#include <signal.h> static voidsig_int(int signo){    if(signal(SIGINT, SIG_IGN) == SIG_IGN)        signal(SIGINT, sig_int);    printf("SIGINT caught!\n");} #define BUFSIZE     4096 int main(){    int fd;    char buf[BUFSIZE];     if(signal(SIGINT, sig_int) == SIG_ERR)        err_sys("main: can't catch SIGINT");     if((fd = open("/dev/random", O_RDONLY)) < 0) // without O_NONBLOCK        err_sys("main: can't open device");     errno = 0;    if(read(fd, buf, BUFSIZE) < 0)    {        if(errno == EINTR)            fprintf(stderr, "Read Interrupted by Signal\n");        else            perror("read");        exit(0);    }    printf("Read finished\n");    exit(0);}

这个程序将以阻塞方式打开/dev/random设备,这个设备收集并产生随机数,这个过程需要一定时间,因此被视为一个低速设备。
我们对它进行读操作,并在此过程中引发一个中断,以观察系统调用read()被打断之后,是继续下面的语句,还是自动恢复read()调用。
如果自动恢复,将会输出Read finished,否则将输出read()调用失败的原因。
运行:

$ ./a.outRead finished$ ./a.out^CSIGINT caught!Read Interrupted by Signal

分析及结论:
我们需要执行a.out程序两次,第一次将消耗random设备缓存里的随机数,然后立即运行第二次,这时候read()发生了阻塞。
我们立即发送SIGINT信号,输出结果为Read Interrupted by Signal
这表明:signal()确实中断了系统调用。能够被中断的系统调用还有:ioctl, read, readv, write, writev, wait, waitpid。

不可靠性:进程不能关闭某些不想捕获的信号

进程不希望某种信号发生时,它不能关闭信号,只能忽略该信号。而有时我们希望通知系统“阻止信号的发生,如果它确实发生了,则记住它们”。以下是一个经典的例子:

#include "apue.h"   // for err_sys()#include <signal.h> int sig_int_flag = 0; static voidsig_int(int signo){    sig_int_flag = 1;    signal(SIGINT, sig_int);    printf("SIGINT caught!\n");} int main(){    if(signal(SIGINT, sig_int) == SIG_ERR)        err_sys("main: can't catch SIGINT");     while(sig_int_flag == 0)        pause();    // wait for signal happen      }

当信号发生后,信号处理函数将标志位置位,以便通知进程。在这里,进程继续运行。遗憾的是判断标志位与pause()之间存在一个时间窗口,信号可能在判断之后、pause()之前发生。假如这个信号仅发生一次,那么该进程将一直阻塞下去,并且丢失这个信号。

总结:

从这些缺陷的例子中我们可以看到,使用已经过时的、存在语义不完整性的signal()函数将会导致一些不易察觉的问题。
这仅仅是Ubuntu(Linux 2.6.32)下signal存在的问题,而不同unix系统之间又存在一些差异,因此,使用signal()也会降低程序的可移植性。

1 0
原创粉丝点击