信号

进程有一个非常简洁,有时也是非常有用的方法‘骚扰’另一个进程:信号。简单地说,一个进程可以“产生”一个信号,将它发送给另一个进程。目标进程的信号处理器(只是一个函数)被激活并处理它。

举例来说,一个进程可能想要停止另一进程,这可以通过给它发出 SIGSTOP 信号来完成。如果要继续执行的话,它必须收到 SIGCONT 信号。当进程收到某一信号时,它怎么知道该做什么呢?好的, 许多信号已被预先定义,进程也有缺省的信号处理器来处理它。

缺省的处理器?对。以 SIGINT 为例,这是当用户按下了 ^C 时进程收到的中断信号。缺省的处理 SIGINT 的信号处理器将使进程退出。听起来很熟悉?正如你想象的那样,你可以重载 SIGINT 来做你想做的(或者什么也不做)。你可以让进程 printf() “中断?!没门,Jose!”,并继续干它乐意的事情。

现在你知道你可以让你的进程用你的方式响应任何信号。自然地,也有一些例外,要不然这就太容易理解了。看看曾经流行的 SIGKILL ,信号#9。你曾经敲入 kill -9 nnnn 来杀掉一个正在运行的进程吗?就是发出了一个 SIGKILL。现在你可能也想起了没有任何进程可以在 “kill -9” 下逃脱,你是对的。SIGKILL 是一个你不能加入你自己的信号处理器的信号之一。前面提到的 SIGSTOP 也是其中的一个。

(另外:你经常使用 Unix "Kill" 命令而不指明发送哪个信号……究竟是什么信号?答案:SIGTERM 。你可以写出自己的SIGTERM 处理器,这样不会对通常的 "kill" 响应,用户必须使用 "kill -9" 来杀掉进程。)

是否所有的信号都已预定义了?如果你想给进程发出一个只有自己知道意义的信号该怎么办?有两个被保留的信号:SIGUSR1SIGUSR2。你可以用自己的方式自由地使用和处理它们。(例如,我的 cd 播放器可能响应 SIGUSR1 来跳到下一首歌曲。用这种方法,我可以在命令行敲入 "kill -SIGUSR1 nnnn" 来控制它。

你不能 SIGKILL 总统!

你可以想到 Unix "kill" 命令是向进程发出信号的一种方法。完全是纯粹的,难以置信的巧合,有一个叫做 kill() 的系统调用完成同样的功能,参数为信号编码(在 signal.h 中定义)和进程号。同样,有一个库函数叫做 raise() 可以用来在本进程中发出信号。

问题依旧:怎样捕获稍纵即逝的 SIGTERM ?你需要使用 signal() 调用,并传递一个指向函数的指针作为你的信号处理器。从来没用过指向函数的指针?(有时间你必需看看 qsort() 过程)不要着急,非常简单:如果 "foo("hi!");" 是对函数 foo() 的调用,那么 "foo" 就是指向此函数的指针。甚至你用不着取址操作符。

无论如何,这样的 signal() 令人困惑:

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

好的,最基本的解决办法:我们将要处理的信号和信号处理器的地址作为参数传递给 signal() 调用。你定义的信号处理器函数以一个 int 作为参数,返回 void。 现在,signal() 调用返回不是一个错误,就是指向原信号处理器函数的指针。因此,我们的调用 signal() 以信号和处理器的指针作为参数,返回原处理器的指针。上面的代码仅仅只是定义了这些。

幸运的是,它使用起来要比看上去容易的多。你要做的所有的事情就是一个处理函数,它用一个 int 作参数,返回 void。然后调用 signal() 使其运作。容易吗?让我们设计一个简单的程序来处理 SIGINT,阻止用户通过 ^C 退出。程序名为 sigint.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <errno.h>
    #include <signal.h>

    int main(void)
    {
        void sigint_handler(int sig); /* 原型 */
        char s[200];
    
                /* 设置处理器 */
        if (signal(SIGINT, sigint_handler) == SIG_ERR) {
            perror("signal");
            exit(1);
        }
    
        printf("Enter a string:\n");
    
        if (gets(s) == NULL)
            perror("gets");
        else 
            printf("You entered: \"%s\"\n", s);
    
        return 0;
    }
    
        /* 这里是处理器 */
    void sigint_handler(int sig)
    {
        printf("Not this time!\n");
    }

本程序有两个函数:main() 设置信号处理器(通过调用 signal() ),sigint_handler() 是信号处理器其自身。

看看你运行时发生了什么?如果你在输入字符串时中途按下了 ^C,gets() 调用失败,并设置全局变量 errnoEINTR。另外,sigint_handler() 被调用执行,事实上,你看到:

    Enter a string:
    the quick brown fox jum^CNot this time!
    gets: Interrupted system call

有个很重要的信息我先前忘了提起:当信号处理器被调用后,该信号的信号处理器被重置为缺省的处理器!这样,实际的情况是我们的 sigint_handler() 会捕获 敲入的第一个 ^C,以后则再也不会。最迅速和马虎的处理方法是在处理器中重置它,像这样:

    void sigint_handler(int sig)
    {
        signal(SIGINT, sigint_handler); /* reset it to this function */
        printf("Not this time!\n");
    }

这样设置带来的问题是:如果产生了一个中断,处理器被调用,然后在第一个重置处理器之前发生了第二个中断,这时将会调用缺省的处理器。如果你处理了大量的信号,这是你可能要注意的一个问题。

你知道的所有的东西都是错误的

signal() 系统调用是传统的设置信号的方法。 POSIX 标准定义了完整的一批新函数来筛选哪个是你想收到的的信号,检查哪个信号紧急,设置信号处理器。由于这些调用中的大多数将信号分组或是 集合操作,因此有更多的函数来处理信号集操作。

简而言之,新的信号处理方法摒弃了旧的方式。时间允许的话,我将在本文的下一版本中加入对它的描述。

使你受欢迎的一些信号

这里有一个信号的列表(根据你自己的需要舍弃):

信号 描述
SIGABRT 处理退出信号。
SIGALRM 报警时钟。
SIGFPE 错误的算术操作。
SIGHUP 挂起。
SIGILL 非法指令。
SIGINT 中断信号。
SIGKILL Kill (不能忽略)。
SIGPIPE 向管道写,没有程序读。
SIGQUIT 退出信号。
SIGSEGV 非法内存引用。
SIGTERM 终止信号。
SIGUSR1 用户定义信号1。
SIGUSR2 用户定义信号2。
SIGCHLD 子进程终止或停止。
SIGCONT 如果停止,继续执行。
SIGSTOP 停止执行(不能忽略)。
SIGTSTP 停止信号。
SIGTTIN 后台进程尝试读。
SIGTTOU 后台进程尝试写。
SIGBUS 总线错误。
SIGPOLL Pollable event.
SIGPROF 剖析定时器到时。
SIGSYS 非法系统调用。
SIGTRAP 跟踪/断点陷阱。
SIGURG 在 socket 中高带宽数据可用。
SIGVTALRM 徐拟定时器到时。
SIGXCPU 超过CPU时间限制。
SIGXFSZ 超过文件大小限制。
表一、通用信号。

每个信号都有自己缺省的信号处理器,在你本地机器上的手册上定义了他们的行为。


Back to the IPC main page (http://www.ecst.csuchico.edu/~beej/guide/ipc/)

Copyright © 1997 by Brian "Beej" Hall. This guide may be reprinted in any medium provided that its content is not altered, it is presented in its entirety, and this copyright notice remains intact. Contact beej@ecst.csuchico.edu for more information.