Linux 信号处理

使用 sigaction 绑定信号

比较早的时候,使用 signal,现在正在逐渐被抛弃,sigaction 是更好的选择。主要是因为如下原因:

  • signal 在不同系统的行为可能不一致,如果自定义了信号处理函数,进入信号处理函数时,对当前信号的操作可能变为默认,也可能屏蔽该信号。只有设置信号处理方式是 SIG_IGN(忽略)、SIG_DFL(默认)是可移植的。
  • signal 不能设置在信号处理函数执行过程中屏蔽其他信号
  • 在多线程的进程中 signal 的效果不确定

signal 接口最主要的问题还是不可移植,其实现在 Linux 上的 signal 是对 sigaction 的一个封装。sigaction 有更灵活和强大的功能,现在通常使用 sigaction 绑定自己定义的信号处理函数,改变处理信号默认方式,当然用起来比 signal 也要复杂一点。

常见用法如下:

void handler(int sig, siginfo_t *siginfo, void *arg)
{
    printf("recv signal: %d\n", sig);
    sleep(2);   //假装在工作...
    printf("%s\n", "done");
}

int main(int argc, char *argv[])
{
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));

    //sigemptyset(&sa.sa_mask);
    //sigaddset(&sa.sa_mask, SIGQUIT);

    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = handler;

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction error");
        return -1;
    }

    while (1) {
        sleep(10);
    }
    return 0;
}

当进程接收到 SIGINT 信号(CTRL-C),触发 handler 函数。

如果把上面注释的两行打开,在 sa.sa_mask 添加了 SIGQUIT 信号(CTRL-\),在 handler 函数执行过程中,进程屏蔽 SIGQUIT 信号。此外还默认屏蔽与 handler 绑定的 SIGINT 信号,除非设置 sa_flagsSA_NODEFER 改变此默认行为,这样在 handler 执行过程中不会屏蔽触发 handler 的信号。

handler 执行完毕之后,会恢复进程在执行 handler 之前屏蔽的信号集,所以 SIGQUIT 不再被屏蔽。

进程屏蔽信号

更多情况下,我们是要进程/线程主动去屏蔽些信号的。需要使用上面提到的 sigprocmask,多线程环境要使用 pthread_sigmask

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGUSR1);
// sidaddset(&set, xxxx);
sigprocmask(SIG_BLOCK, &set, NULL);

这样进程就屏蔽了 SIGINT, SIGUSR1 信号,通过 sigaddset 添加信号到一个集合里,然后调用 sigprocmask 指定屏蔽该集合里的信号。
因为 SIGINT 信号被屏蔽,也就不会触发 handler 函数。

另外,如果没有屏蔽 SIGINT,然后在 handler 内调用 sigprocmask 屏蔽 SIGINT 信号,要注意出了 handler 函数这个屏蔽作用就失效了,因为 handler 执行完,进程要屏蔽的信号集就复原了。如果要在 handler 执行过程中屏蔽某信号,建议在调用 sagaction 之前通过 sa.sa_mask 设置要在这个过程中屏蔽的信号,而不是在 handler 里通过 sigprocmask 去屏蔽,一是避免产生认为”会在整个进程执行过程中都屏蔽该信号”这样的误会,更重要的是如果进到 handler 里才去屏蔽,有可能跟预期结果不符,例如想在 handler 函数执行过程中屏蔽 SIGUSR1 信号,由于在进到 handler 函数和调用 sigprocmask 之间还有一段时间,这期间 SIGUSR1 信号是没有被屏蔽的。

屏蔽?忽略?

上面说的屏蔽信号,并不是忽略信号(或许叫做“阻塞”信号更合适)。如果一个信号被屏蔽,会被内核放在一个队列里等候着,等到进程不屏蔽它的时候,进程就会收到该信号(不再阻塞)。例如前面通过 sa.sa_mask 设定了在 handler 执行过程中屏蔽的 SIGQUIT 信号,向进程发送 SIGINT 信号触发 handler 之后,立刻又发送 SIGQUIT 信号,在 handler 执行过程中,SIGQUIT 信号被屏蔽,在 handler 执行完之后,进程就会收到该信号。

使用 sigwait 可以等待信号到来,如果队列里有等着的信号,会被取出来(先进先出的顺序)。

sigwaitsigaction 是处理信号的两种截然不同的方式,sigwait 阻塞进程/线程的执行,等待信号。

int sig_num;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigprocmask(SIG_BLOCK, &set, NULL);
while (sigwait(&set, &sig_num) == 0) {
    printf("caught sig: %d\n", sig_num);
    sleep(2);    // 假装在工作...
}

不要忘了用 sigprocmask 屏蔽了 SIGINT 信号,之后再用 sigwait 接收 SIGINT 信号,如果没有屏蔽 SIGINT 信号的话,进程收到 SIGINT,会进行默认操作,一般就是退出程序。

被忽略的

如果在 handler 执行过程中(没设置 SA_NODEFER),发送了很多次 SIGINThandler 执行完之后,进程只会收到一个 SIGINT 信号,而不是发了多少 SIGINT,之后 handler 就被触发多少次。

sigwait 等到 SIGINT 来之后进行处理,处理过程中如果向进程发送了多个 SIGINT,也是只会收到一个 SIGINTsigwait 后的处理也只有一次。

上面两个例子是说,如果进程执行过程中有信号要“等候处理”的时候,多个相同的普通信号只会有一个入队,其余的被忽略。

实时信号

实时信号是不同于普通信号的。

SIGRTMINSIGRTMAX 之间的信号属于实时信号,不会被忽略。

在上面代码段里加一行sigaddset(&set, SIGRTMIN),把 SIGRTMIN 信号的处理也接手过来,然后在 sleep(2) 的过程中,向进程发送多个 SIGINT,然后再试试发送多个 SIGRTMIN 就可以看出区别:

int sig_num;
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGRTMIN);
sigprocmask(SIG_BLOCK, &set, NULL);
while (sigwait(&set, &sig_num) == 0) {
    printf("caught sig: %d\n", sig_num);
    sleep(2);    // 假装在工作...
}
  • "caught sig: 2" (SIGINT) 出现两次,第一次收到时输出一次,剩下多个 SIGINT 只接收到一个又输出一次;
  • "caught sig: 34" 出现多次,发送了多少 SIGRTMIN 信号,就会每隔两秒输出一次。

新东西

sigaction 时,需要小心翼翼地写异步信号安全函数(Async-Signal-Safe Function)。sigwait 处理信号相比 sigaction安全、方便,因为什么时候处理信号在自己掌握之中(同步方式),不会打断进程正在执行的操作,但是 sigwait 阻塞来等待信号可能并不满足一些场景,比如一个工作忙碌的线程,又可能需要处理某个信号。

在 2.6.22 版本的内核之后,Linux 系统提供了 signalfd 接口,一个可以替代sigactionsigwait的新方法,给这个接口一个信号集合作为参数,返回一个文件描述符,调用 read 获取信号。手册里有一个例子:

#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define handle_error(msg) \
    do { perror(msg); exit(EXIT_FAILURE); } while (0)

int main(int argc, char *argv[])
{
    sigset_t mask;
    int sfd;
    struct signalfd_siginfo fdsi;
    ssize_t s;

    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGQUIT);

    /* Block signals so that they aren't handled
       according to their default dispositions */

    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1)
        handle_error("sigprocmask");

    sfd = signalfd(-1, &mask, 0);
    if (sfd == -1)
        handle_error("signalfd");

    for (;;) {
        s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
        if (s != sizeof(struct signalfd_siginfo))
            handle_error("read");

        if (fdsi.ssi_signo == SIGINT) {
            printf("Got SIGINT\n");
        } else if (fdsi.ssi_signo == SIGQUIT) {
            printf("Got SIGQUIT\n");
            exit(EXIT_SUCCESS);
        } else {
            printf("Read unexpected signal\n");
        }
    }
}

使用signalfd很大的优势是,可以把这个文件描述符交给 select, poll, epoll进行检测,就像打开普通文件的普通描述符和网络套接字一样,当select这类函数通知你该文件描述符可读时再去读取、处理信号。

跟使用 sigwait 一样,不要忘记在调用 signalfd 之前用 sigprocmask 阻塞信号让它们不被按照默认方式处理。另外在使用完 signalfd 之后不要忘记 close 这个文件描述符。

Subscribe to Atom

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe