/ 编程

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 这个文件描述符。