You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用epoll_pwait处理中断信号,实现多线程程序优雅退出?

多线程epoll_pwait优雅处理SIGINT退出问题

我有一个多线程应用,用epoll_pwait等待流读取而非忙轮询,需要收到SIGINT时优雅退出。当前的epoll_pwait用法如下(伪代码):

// SIGMASK = sigemptyset + sigaddset(SIGINT)
num = epoll_pwait(epoll_fd, &event_data, -1, &SIGMASK);
if (num == -1) {
    if (errno == EINTR) {
        print("interrupt occurred");
    }
}
// 读取流数据
// ...

问题在于:如果SIGINT在调用epoll_pwait之前发生,进程进入epoll_pwait后会错过这个信号。我尝试用标志位判断,但存在竞态(检查标志位后、进入epoll_pwait前收到信号,依然会错过):

static bool INTERRUPTED = false;
sigaction(SIGINT, /* 设置INTERRUPTED=true的处理函数 */, NULL);

// SIGMASK = sigemptyset + sigaddset(SIGINT)
if (INTERRUPTED) {
    print("interrupt occurred");
}
// 此处收到SIGINT会错过
num = epoll_pwait(epoll_fd, &event_data, -1, &SIGMASK);
if (num == -1) {
    if (errno == EINTR) {
        print("interrupt occurred");
    }
}
// 读取流数据
// ...

解决方案1:使用signalfd将信号转为文件描述符(推荐)

把SIGINT通过signalfd包装成一个文件描述符,然后将这个fd加入epoll的监听列表。这样不管信号在什么时候触发,都会被epoll检测到,完全避免竞态问题,而且符合epoll的事件驱动模型。

步骤:

  1. 阻塞SIGINT,确保信号不会被默认处理,而是通过signalfd接收
  2. 创建signalfd,指定监听SIGINT
  3. 将signalfd加入epoll实例,监听可读事件
  4. 主循环调用普通的epoll_wait(无需epoll_pwait),当signalfd可读时,读取信号数据并触发退出逻辑

示例代码:

#include <sys/signalfd.h>
#include <signal.h>
#include <unistd.h>
#include <sys/epoll.h>

int main() {
    sigset_t sigset;
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGINT);
    // 阻塞SIGINT,让signalfd接收
    pthread_sigmask(SIG_BLOCK, &sigset, NULL);

    // 创建signalfd
    int sigfd = signalfd(-1, &sigset, SFD_NONBLOCK);

    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    struct epoll_event ev;
    // 监听signalfd的可读事件
    ev.events = EPOLLIN;
    ev.data.fd = sigfd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sigfd, &ev);

    // 添加你的流fd到epoll...

    struct epoll_event events[10];
    while (1) {
        int num = epoll_wait(epoll_fd, events, 10, -1);
        if (num == -1) {
            // 这里一般不会收到EINTR,因为SIGINT已被阻塞
            perror("epoll_wait");
            break;
        }

        for (int i = 0; i < num; i++) {
            if (events[i].data.fd == sigfd) {
                // 处理SIGINT,优雅退出
                struct signalfd_siginfo siginfo;
                read(sigfd, &siginfo, sizeof(siginfo));
                printf("Received SIGINT, exiting...\n");
                // 清理资源、退出
                close(sigfd);
                close(epoll_fd);
                return 0;
            } else {
                // 处理你的流数据读取
                // ...
            }
        }
    }

    return 0;
}

解决方案2:修复标志位的竞态问题(无需signalfd)

如果不想用signalfd,可以通过信号掩码的原子操作解决竞态,核心思路是:在检查标志位和调用epoll_pwait的过程中,临时阻塞SIGINT,确保这段时间内收到的SIGINT会处于pending状态,直到epoll_pwait临时解除阻塞时触发EINTR。

注意:因为是多线程,标志位必须用原子变量,避免线程竞争。

示例代码:

#include <signal.h>
#include <sys/epoll.h>
#include <stdatomic.h>

_Atomic bool interrupted = false;

void sigint_handler(int sig) {
    interrupted = true;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigint_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);

    int epoll_fd = epoll_create1(EPOLL_CLOEXEC);
    // 添加你的流fd到epoll...

    sigset_t sigint_mask;
    sigemptyset(&sigint_mask);
    sigaddset(&sigint_mask, SIGINT);

    while (1) {
        // 1. 先阻塞SIGINT
        pthread_sigmask(SIG_BLOCK, &sigint_mask, NULL);

        // 2. 检查中断标志
        if (interrupted) {
            printf("Received SIGINT, exiting...\n");
            break;
        }

        // 3. 调用epoll_pwait,临时解除SIGINT的阻塞
        // 这里传入的sigmask是空集,意味着调用期间允许SIGINT
        struct epoll_event event_data;
        int num = epoll_pwait(epoll_fd, &event_data, 1, -1, NULL);

        // 4. 恢复SIGINT的阻塞(可选,epoll_pwait会自动恢复原掩码)
        pthread_sigmask(SIG_UNBLOCK, &sigint_mask, NULL);

        if (num == -1) {
            if (errno == EINTR) {
                // 被SIGINT打断,设置标志位
                interrupted = true;
                continue;
            }
            perror("epoll_pwait");
            break;
        }

        // 处理流数据读取
        // ...
    }

    close(epoll_fd);
    return 0;
}

多线程注意事项

  • 确保只有主线程处理SIGINT:在其他工作线程中调用pthread_sigmask(SIG_BLOCK, &sigint_mask, NULL),阻塞SIGINT,避免信号被随机线程处理。
  • 原子变量必须用_Atomic修饰(C11标准),或者用pthread_mutex保护标志位,避免多线程读写冲突。

内容的提问来源于stack exchange,提问作者Jonathan Woollett-light

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 22:40:37