信号处理问题

注:本文摘自《深入理解计算机系统》第8章 --- 异常控制流。本文不适于不了解信号的人,在此也不对信号做过多解释,只是个人需要记录相关的信号处理问题而已,想了解更详细的请自行查阅相关资料。

程序只捕获一个信号时是简单直接的,但当要捕获多个信号时,就可能产生一些细微的问题。
1、待处理信号被阻塞。unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。比如,假设一个进程捕获了SIGCHLD信号(注:每个子进程终止或退出时,内核都会发送一个SIGCHLD信号给父进程),并且当前正在运行它的SIGCHLD处理程序,如果另一个 SIGCHLD 信号传递到这个进程,那么这个信号将变成待处理的,但不会被接收,直到处理程序返回。

2、待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为k的信号传送到一个目的进程,而由于目的进程当前正在执行信号k的处理程序,所以信号k是阻塞的,那么第二个信号就被简单地丢弃,它不会排队等待。关键思想是存在一个待处理的信号仅仅表明至少已经有一个信号到达了。

3、系统调用可以被中断。像read、wait 和 accpet 这样的系统调用潜在地会阻塞一段较长的时间,称为慢系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误条件,并将error设置为EINTR。

我知道,要看懂这几句话可能不用例子只怕很难理解,所以想了下,还是把例子抄上来好些。
void handler1(int sig){
    pid_t pid;
    if((pid=waitpid(-1, NULL, 0)) < 0)
        unix_error("waitpid error");
    printf("Handler reaped child %d\n", (int)pid);
    Sleep(2);
    return;
}

int main(){
    int i, n;
    char buf[MAXBUF];
    if(signal(SIGCHLD, handler1) == SIG_ERR)
        unix_error("signal error");
    for(i=0; i<3; i++){
        if(Fork() == 0){
            printf("Hello from child %d\n", (int)getpid());
            Sleep(1);
            exit(0);
        }
    }
    if((n=read(SIGIN_FILENO, buf, sizeof(buf))) < 0)
        unix_error("read");
    printf("Parent processing input\n");
    while(1){
        ;
    }
    exit(0);
}

解释:父进程设置了一个SIGCHLD处理程序,然后创建了三个子进程,其中每个子进程运行1秒,然后终止。同时,父进程等待来自中端的一个输入行,随后处理它。这个处理被模型化为一个无限循环,当每个子进程终止时,内核通过发送一个SIGCHLD信号通知父进程,父进程捕获这个信号,回收一个子进程,做一些其它的清除工作,然后返回。
但当在linux上运行时,输出是类似这样的:
linux> ./signal1
Hello from child 10320
Hello from child 10321
Hello from child 10322
Handler reaped child 10320
Handler reaped child 10322
<cr>
Parent processing input

从中可看出,尽管发送了3个SIGCHLD信号给父进程,但是其中只有两个信号被接收了,因此父进程只是回收了两个子进程。此时若挂起父进程,用ps命令查看一下,可看到进程10321没有被回收,而是成为了一个僵尸进程。
为何会这样?尽管这个程序看似很正常,但其实它是有缺陷的,因为它无法处理信号阻塞、信号不排队等待和系统调用被中断这些情况。它实际发生的情况是:父进程接收并捕获了第一个信号,当处理程序还在处理第一个信号时,第二个信号就传送并添加到了待处理信号集合里。但因为信号SIGCHLD信号被SIGCHLD处理程序阻塞了,所以第二个信号就不会被接收。此后不久,就在处理程序还在处理第一个信号时,第三个信号到达了。因为已经有了一个待处理的SIGCHLD,所以第三个SIGCHLD信号会被丢弃。一段时间后,处理程序返回,内核注意到有一个待处理的SIGCHLD信号,就迫使父进程接收该信号。父进程捕获它,并第二次执行处理程序。在处理程序完成对第二个信号的处理后,已经没有待处理的SIGCHLD信号了,而且也绝不会再有,因为第三个SIGCHLD的所有信息都已经丢失了。所以教训是绝不可以用信号来对其它进程中发生的事件计数!

为了修正这个问题,必须想到存在一个待处理的信号只是暗示自进程最后一次收到一个信号以来,至少已经有一个这种类型的信号被发送了,所以必须修改SIGCHLD处理程序,使得每次SIGCHLD处理程序被调用时,回收尽可能多的僵尸子进程。在此只需修改SIGCHLD处理程序,其余的保持原样:
void handler2(int sig){
    pid_t pid;
    while((pid = waitpid(-1, NULL, 0)) > 0){
        printf("Handler reaped child %d\n", (int)pid);
    }
    if(error != ECHILD)
        unix_error("waitpid error");
    Sleep(2);
    return;
}

现在再次运行,结果就能回收所有僵尸子进程了,在此偷个懒就不贴运行结果了。
然而,革命尚未成功!本程序能够正确解决信号会阻塞和不会排队等待的情况,但它没有考虑系统调用被中断的可能性。在有些系统上,在从键盘上进行输入之前,被阻塞的read系统调用就提前返回一个错误:
solaris> ./signal2
Hello from child 18906
Hello from child 18907
Hello from child 18908
Handler reaped child 18906
Handler reaped child 18908
Handler reaped child 18907
read: Interrupted system call

出现这个问题是因为在特定的Solaris系统上,诸如read这样的慢速系统调用在被信号发送中断后,是不会自动重启的。相反,和linux系统自动重启被中断的系统调用不同,它们会提前返回给调用程序一个错误条件。修改时只要它能手动地重启被终止的read调用即可,本次需要修改主函数:
int main(){
    int i, n;
    char buf[MAXBUF];
    pid_t pid;

    if(signal(SIGCHLD, handler1) == SIG_ERR)
        unix_error("signal error");

    for(i=0; i<3; i++){
        if((pid=Fork()) == 0){
            printf("Hello from child %d\n", (int)getpid());
            Sleep(1);
            exit(0);
        }
    }
    whileda((n=read(SIGIN_FILENO, buf, sizeof(buf))) < 0){
        if(errno != EINTR)
            unix_error("read");
    }
    printf("Parent processing input\n");
    while(1){
        ;
    }
    exit(0);
}

猜你喜欢

转载自aisxyz.iteye.com/blog/2302821