如何使用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的事件驱动模型。
步骤:
- 阻塞SIGINT,确保信号不会被默认处理,而是通过signalfd接收
- 创建signalfd,指定监听SIGINT
- 将signalfd加入epoll实例,监听可读事件
- 主循环调用普通的
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
相关产品推荐
相关产品推荐

