这篇文章上次修改于 1008 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

背景

系统调用被信号打断后, linux 默认不会重启系统调用,当然我们可以设置一些选项来重启一部分系统调用,但并不是所有的系统调用都可以被重启,比如我们经常使用的多路 I/O 复用模型 epoll 中的 epoll_wait 就是一个典型例子。

信号

信号可能会对程序的一般流程造成很大的破坏,因为它们本质上是异步的。当您在系统调用中被阻止(导致它们失败)或在执行用户空间指令(可能导致竞争条件)时,它们可能会发生。您可能必须仔细地为您的程序建模以补偿信号的异步性质,这是有经验的 Unix 程序员始终考虑的事情。

信号注意事项

以下是一些信号相关注意事项

  1. 对于体面的程序,您不能取消信号。它们是 Linux 下的现实。
  2. 您必须注意从信号处理程序中调用了哪些函数。您调用的函数必须是异步信号安全的。signal-safety(7)中列举了信号安全相关的函数
  3. 不应该从信号处理程序中调用不可重入函数,因为它们可以从主代码中调用,在执行过程中被中断。如果信号处理程序调用相同的函数,就会出现问题。
  4. 当程序被系统调用阻塞时出现信号时,系统调用会返回错误EINTR。但是,可以通过使用sigaction(2)函数设置信号处理程序时指定SA_RESTART标志 来自动重新启动许多系统调用。
  5. 不幸的是,有很多系统调用(如 epoll_wait、epoll_pwait、poll、ppoll、select、pselect、recv、send、和 nanosleep) 尽管SA_RESTART已指定,但永远不会重新启动。
  6. 如果在设置信号处理程序和调用pause等待信号发生之间发生信号,则很容易“丢失”或错过信号,从而陷入等待信号的状态。

不可信号重启的系统调用

以下接口在被信号打断后永远不会重新启动,无论是否使用 SA_RESTART ,他们总是以错误 EINTR 失败:

  • “输入”套接字接口,当已使用 setsockopt 在套接字上设置超时 (SO_RCVTIMEO):accept、recv、 recvfrom, recvmmsg (也有非空超时参数) 和 recvmsg。
  • “输出”套接字接口,当已使用 setsockopt 在套接字上设置超时 (SO_RCVTIMEO):connect, send、sendto 和 sendmsg。
  • 用于等待信号的接口:pause, sigsuspend, sigtimedwait 和 sigwaitinfo。
  • 文件描述符复用接口:epoll_wait, epoll_pwait、poll、ppoll、select 和 pselect。
  • SystemV IPC 接口:msgrcv、msgsnd、semop 和 semtimedop。
  • sleep 接口:clock_nanosleep、nanosleep 和 usleep。
  • io_getevents。
  • 如果被中断, sleep 函数也永远不会重新启动处理程序,但成功返回还剩下的睡眠秒数。

设置自动重启标志

简单总结一下设置系统调用被信号中断时自动重启标志的方法 :

在安装信号捕捉函数时

struct sigaction sa;
sigemptyset(&sa.sa_mask);//清空信号掩码集    sigemptyset简单地将 初始化signalmask为空,这样就可以保证不会屏蔽任何信号。(也就是说,所有的信号都会被接收到)
sa.sa_flags = SA_RESTART;//设置重启标志
sa.sa_handler = sigalrm_handler;//指定信号捕捉函数名称
if (sigaction(SIGALRM, &sa, NULL) == -1) handle_error("sigaction");//注册信号捕捉函数

被信号打断的epoll_wait

#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <unistd.h>
#define handle_error(msg)   \
    do                      \
    {                       \
        perror(msg);        \
        exit(EXIT_FAILURE); \
    } while (0)
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd;
/*
 * Our signal handler simply prints a message and returns.
 * */
void sigalrm_handler(int signo) { printf("Received SIGALRM\n"); }
/*
 * This function is responsible for setting up the
 * listening socket for the socket-based echo
 * functionality. Pretty standard stuff.
 * */
int setup_listening_socket(int port)
{
    int sock;
    struct sockaddr_in srv_addr;
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) handle_error("socket()");
    int enable = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
        handle_error("setsockopt(SO_REUSEADDR)");
    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(port);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* We bind to a port and turn this socket into a listening
     * socket.
     * */
    if (bind(sock, (const struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0)
        handle_error("bind()");
    if (listen(sock, 10) < 0) handle_error("listen()");
    return (sock);
}
/*
 * Reads 1kb from the "in" file descriptor and
 * writes it to the "out" file descriptor.
 */
void copy_fd(int in, int out)
{
    char buff[1024];
    int bytes_read;
    bzero(buff, sizeof(buff));
    bytes_read = read(in, buff, sizeof(buff) - 1);
    if (bytes_read == -1)
        handle_error("read");
    else if (bytes_read == 0)
        return;
    write(out, buff, strlen(buff));
}
/*
 * If a parsable number is passed as the 1st argument to the
 * program, sets up SIGALRM to be sent to self the specified
 * seconds later.
 * */
void setup_signals(int argc, char **argv)
{
    struct sigaction sa;
    /* No argument passed, and so no signal will be delivered */
    if (argc < 2)
    {
        printf("No alarm set. Will not be interrupted.\n");
        return;
    }
    /*  if a proper number is passed, we setup the alarm,
     * else we return without setting one up.
     * */
    errno = 0;
    long alarm_after = strtol(argv[1], NULL, 10);
    if (errno) handle_error("strtol()");
    if (alarm_after)
        printf("Alarm after %ld seconds.\n", alarm_after);
    else
    {
        printf("No alarm set. Will not be interrupted.\n");
        return;
    }
    /*
     * Setup a signal handler for the SIGALRM signal
     * */
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sa.sa_handler = sigalrm_handler;
    if (sigaction(SIGALRM, &sa, NULL) == -1) handle_error("sigaction");
    /*
     * Let's send ourselves a SIGALRM signal specified
     * seconds later.
     * */
    alarm(alarm_after);
}
/**
 * Helper function to setup epoll
 */
void setup_epoll()
{
    epollfd = epoll_create1(0);
    if (epollfd == -1) handle_error("epoll_create1()");
}
/**
 * Adds the file descriptor passed to be monitored by epoll
 * */
void add_fd_to_epoll(int fd)
{
    /* Add fd to be monitored by epoll */
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) handle_error("epoll_ctl");
}
int main(int argc, char *argv[])
{
    /* Setup sigalrm if a number is passed as the first argument */
    setup_signals(argc, argv);
    /* Let's setup epoll */
    setup_epoll();
    /* Add stdin-based echo server to epoll's monitoring list */
    add_fd_to_epoll(STDIN_FILENO);
    /* Setup a socket to listen on port 5000 */
    listen_sock = setup_listening_socket(5000);
    /* Add socket-based echo server to epoll's monitoring list */
    add_fd_to_epoll(listen_sock);
    while (1)
    {
        /* Let's wait for some activity on either stdin or on the socket */
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) handle_error("epoll_wait()");
        /*
         * For each of the file descriptors epoll says are ready,
         * check which one it is the echo read data back.
         * */
        for (int n = 0; n < nfds; n++)
        {
            if (events[n].data.fd == STDIN_FILENO)
            {
                printf("stdin ready..\n");
                copy_fd(STDIN_FILENO, STDOUT_FILENO);
            }
            else if (events[n].data.fd == conn_sock)
            {
                printf("socket data ready..\n");
                copy_fd(conn_sock, conn_sock);
            }
            else if (events[n].data.fd == listen_sock)
            {
                /* Listening socket is ready, meaning
                 * there's a new client connection */
                printf("new connection ready..\n");
                conn_sock = accept(listen_sock, NULL, NULL);
                if (conn_sock == -1) handle_error("accept()");
                /* Add the connected client to epoll's monitored FDs list */
                add_fd_to_epoll(conn_sock);
            }
        }
    }
    return 0;
}

运行:

gcc a.c
./a.out  5

image-20210726205425376

当5秒后定时信号 SIGALRM 被信号捕捉函数处理之后, 返回 epoll_wait 时报错被中断的系统调用,程序结束运行。

这显然是对于服务器来说不能忍受的。

epoll_wait脆弱到被任何信号捕捉函数中断后,无法继续运行下去

注: 上面的代码启用了 自动重启 标志,但从结果来看并不能对 epoll_wait 生效。

因篇幅原因,故不对没有设置自动重启标志的 epoll_wait 测试,结果显而易见 epoll_wait 被中断了。

引入signalfd

如何避免循环 while(1){epoll_wait()} ,这里引出我们这篇文章的主角 signalfd

Linux 有一种机制可以将异步信号转换为可由epoll 监听的描述符。如果信号可以通过文件描述符传递,就像数据一样,那么现有的 I/O 多路复用机制epoll可以用来处理信号,就像我们处理代表本地文件或套接字的其他文件描述符一样。

如何将 signal 的异步中断转化为 signalfd 的一个事件,是我们接下来要考虑的问题

解决办法

#include <errno.h>
#include <netinet/in.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/time.h>
#include <unistd.h>
#define handle_error(msg)   \
    do                      \
    {                       \
        perror(msg);        \
        exit(EXIT_FAILURE); \
    } while (0)
#define MAX_EVENTS 10
struct epoll_event ev, events[MAX_EVENTS];
int listen_sock, conn_sock, nfds, epollfd, sfd;
/**
 * This function is responsible for setting up the
 * listening socket for the socket-based echo
 * functionality. Pretty standard stuff.
 * */
int setup_listening_socket(int port)
{
    int sock;
    struct sockaddr_in srv_addr;
    sock = socket(PF_INET, SOCK_STREAM, 0);
    if (sock == -1) handle_error("socket()");
    int enable = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
        handle_error("setsockopt(SO_REUSEADDR)");
    memset(&srv_addr, 0, sizeof(srv_addr));
    srv_addr.sin_family = AF_INET;
    srv_addr.sin_port = htons(port);
    srv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    /* We bind to a port and turn this socket into a listening
     * socket.
     * */
    if (bind(sock, (const struct sockaddr *)&srv_addr, sizeof(srv_addr)) < 0)
        handle_error("bind()");
    if (listen(sock, 10) < 0) handle_error("listen()");
    return (sock);
}
/**
 * Reads 1kb from the "in" file descriptor and
 * writes it to the "out" file descriptor.
 */
void copy_fd(int in, int out)
{
    char buff[1024];
    int bytes_read;
    bzero(buff, sizeof(buff));
    bytes_read = read(in, buff, sizeof(buff) - 1);
    if (bytes_read == -1)
        handle_error("read");
    else if (bytes_read == 0)
        return;
    write(out, buff, strlen(buff));
}
/**
 * If a parsable number is passed as the 1st argument to the
 * program, sets up SIGALRM to be sent to self the specified
 * seconds later.
 * */
void setup_signals(int argc, char **argv)
{
    sigset_t mask;
    long alarm_after = 0;
    /* No argument passed. Let's set a default interval of 5. */
    if (argc < 2)
    {
        printf("No alarm set. Will default to 5 seconds.\n");
        alarm_after = 5;
    }
    /*  if a proper number is passed, we setup the alarm,
     * else we return without setting one up.
     * */
    errno = 0;
    if (alarm_after == 0)
    {
        alarm_after = strtol(argv[1], NULL, 10);
        if (errno) handle_error("strtol()");
        if (alarm_after)
            printf("Alarm set every %ld seconds.\n", alarm_after);
        else
        {
            printf("No alarm set. Will default to 5 seconds.\n");
            return;
        }
    }
    /*
     * Setup SIGALRM to be delivered via SignalFD
     * */
    sigemptyset(&mask);
    sigaddset(&mask, SIGALRM);
    sigaddset(&mask, SIGQUIT);
    /*
     * Block these signals so that they are not handled
     * in the usual way. We want them to be handled via
     * SignalFD.
     * */
    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) handle_error("sigprocmask");
    sfd = signalfd(-1, &mask, 0);
    if (sfd == -1) handle_error("signalfd");
    /*
     * Let's send ourselves a SIGALRM signal every specified
     * seconds continuously.
     * */
    struct itimerval itv;
    itv.it_interval.tv_sec = alarm_after;
    itv.it_interval.tv_usec = 0;
    itv.it_value = itv.it_interval;
    if (setitimer(ITIMER_REAL, &itv, NULL) == -1) handle_error("setitimer()");
}
/**
 * Helper function to setup epoll
 */
void setup_epoll()
{
    epollfd = epoll_create1(0);
    if (epollfd == -1) handle_error("epoll_create1()");
}
/**
 * Adds the file descriptor passed to be monitored by epoll
 * */
void add_fd_to_epoll(int fd)
{
    /* Add fd to be monitored by epoll */
    ev.events = EPOLLIN;
    ev.data.fd = fd;
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev) == -1) handle_error("epoll_ctl");
}
/**
 * This is not a signal handler in the traditional sense.
 * Signal handlers are invoked by the kernel asynchronously.
 * Meaning, we have no control over when it's invoked and
 * if we need to restart any system calls.
 * This function is invoked from our epoll based event loop
 * synchronously. Meaning, we have full control over when we
 * invoke this function call. And we do not interrupt any
 * system calls. This makes error handling much simpler in
 * our programs.
 */
void handle_signals()
{
    struct signalfd_siginfo sfd_si;
    if (read(sfd, &sfd_si, sizeof(sfd_si)) == -1) handle_error("read()");
    if (sfd_si.ssi_signo == SIGALRM)
        printf("Got SIGALRM via SignalFD.\n");
    else if (sfd_si.ssi_signo == SIGQUIT)
    {
        printf("Got SIGQUIT. Will exit.\n");
        exit(0);
    }
    else
        printf("Got unexpected signal!\n");
}
int main(int argc, char *argv[])
{
    /* Let's setup epoll */
    setup_epoll();
    /* Add stdin-based echo server to epoll's monitoring list */
    add_fd_to_epoll(STDIN_FILENO);
    /* Setup a socket to listen on port 5000 */
    listen_sock = setup_listening_socket(5000);
    /* Add socket-based echo server to epoll's monitoring list */
    add_fd_to_epoll(listen_sock);
    /* Setup sigalrm+sigquit if a number is passed as the first argument */
    setup_signals(argc, argv);
    /* Add the SignalFD file descriptor to epoll's monitoring list */
    add_fd_to_epoll(sfd);
    while (1)
    {
        /* Let's wait for some activity on either stdin or on the socket */
        nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (nfds == -1) handle_error("epoll_wait()");
        /*
         * For each of the file descriptors epoll says are ready,
         * check which one it is the echo read data back.
         * */
        for (int n = 0; n < nfds; n++)
        {
            if (events[n].data.fd == STDIN_FILENO)
            {
                printf("stdin ready..\n");
                copy_fd(STDIN_FILENO, STDOUT_FILENO);
            }
            else if (events[n].data.fd == conn_sock)
            {
                printf("socket data ready..\n");
                copy_fd(conn_sock, conn_sock);
            }
            else if (events[n].data.fd == listen_sock)
            {
                /* Listening socket is ready, meaning
                 * there's a new client connection */
                printf("new connection ready..\n");
                conn_sock = accept(listen_sock, NULL, NULL);
                if (conn_sock == -1) handle_error("accept()");
                /* Add the connected client to epoll's monitored FDs list */
                add_fd_to_epoll(conn_sock);
            }
            else if (events[n].data.fd == sfd)
            {
                handle_signals();
            }
        }
    }
    return 0;
}

关键步骤


//注册signalfd(仅增加的部分步骤)
sigset_t mask;
int sfd = signalfd(-1, &mask, 0);
if (sfd == -1) handle_error("signalfd");

//将signalfd添加进epoll监听事件
add_fd_to_epoll(sfd);

//epoll对与signalfd的处理函数
void handle_signals()
{
    struct signalfd_siginfo sfd_si;//新的结构体来分辨是何种信号
    if (read(sfd, &sfd_si, sizeof(sfd_si)) == -1) handle_error("read()");
    if (sfd_si.ssi_signo == SIGALRM)
        printf("Got SIGALRM via SignalFD.\n");
    else if (sfd_si.ssi_signo == SIGQUIT)
    {
        printf("Got SIGQUIT. Will exit.\n");
        exit(0);
    }
    else
        printf("Got unexpected signal!\n");
}


//影响了while(1)中的epoll_wait的处理流程,增加了判断
if (events[n].data.fd == sfd)
{
     handle_signals();
}

至此我们学会了如何避免 epoll 被信号处理函数中断,增加了服务器的稳定性, congratulations!

文章来源

文章发自将signalfd加入epoll

读者可以阅读原文---24.5 被信号中断的原语

[hide]微信推送[/hide]