C++17/Linux:子线程阻塞网络调用无法被信号解除问题咨询
问题解答
1. 原设计(主线程做服务器)信号处理每次生效的原因
Linux内核递送进程级信号时,会优先选择处于可中断阻塞状态(如accept()/recv()等系统调用)且未屏蔽该信号的线程接收信号。
在你的原设计中:
- 主线程作为服务器线程,长期阻塞在
accept()或recv()这类可中断系统调用中; - 其他子线程要么处于计算状态,要么阻塞在非可中断操作(或未进入阻塞状态);
- 所有线程默认继承主线程的信号掩码,均未屏蔽
SIGTERM。
内核每次递送SIGTERM时,都会优先选择正在阻塞可中断系统调用的主线程,使其accept()/recv()返回EINTR,触发退出逻辑。这就是数百次测试均生效的核心原因——内核信号调度逻辑天然倾向于唤醒阻塞线程,而非让信号被空闲或计算中的线程接收。
2. 子线程做服务器的问题修复方案
方案2a:独立信号捕获线程 + 线程间信号唤醒(完全适配你的场景)
该方案无需修改套接字阻塞模式,是单连接服务器的轻量最优解,具体实现步骤如下:
核心思路
- 让所有业务子线程(服务器线程、串口IO线程等)屏蔽
SIGTERM,仅由专门的信号捕获线程处理该信号; - 信号捕获线程收到
SIGTERM后设置全局终止标志,再通过线程间自定义信号主动唤醒阻塞在系统调用上的业务线程。
具体实现步骤
全局变量与信号掩码初始化
#include <atomic> #include <pthread.h> #include <signal.h> #include <errno.h> #include <unistd.h> std::atomic<bool> g_terminate_flag{false}; pthread_t g_server_thread; // 存储服务器线程ID,用于后续发送唤醒信号 // pthread_t g_serial_thread; // 若有串口线程,同样存储其ID在创建任何子线程前,设置进程信号掩码屏蔽
SIGTERM和自定义唤醒信号(如SIGUSR1),后续子线程会继承该掩码:sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGUSR1); pthread_sigmask(SIG_BLOCK, &mask, nullptr);信号捕获线程实现
该线程仅负责等待SIGTERM,收到信号后设置终止标志并唤醒业务线程:void* signal_handler_thread(void* arg) { sigset_t wait_mask; sigemptyset(&wait_mask); sigaddset(&wait_mask, SIGTERM); // 解除当前线程对SIGTERM的屏蔽,仅让它接收该信号 pthread_sigmask(SIG_UNBLOCK, &wait_mask, nullptr); int sig; // 阻塞等待SIGTERM,无需注册传统信号处理函数 sigwait(&wait_mask, &sig); // 设置终止标志,用memory_order保证线程可见性 g_terminate_flag.store(true, std::memory_order_release); // 给服务器线程发送SIGUSR1,中断其阻塞的系统调用 pthread_kill(g_server_thread, SIGUSR1); // 若有串口线程,同样发送信号唤醒 // pthread_kill(g_serial_thread, SIGUSR1); return nullptr; }服务器线程实现
服务器线程解除对SIGUSR1的屏蔽,允许被唤醒,同时在系统调用返回后检查终止标志:void* server_thread(void* arg) { int sockfd = *(int*)arg; sigset_t unblock_mask; sigemptyset(&unblock_mask); sigaddset(&unblock_mask, SIGUSR1); // 解除当前线程对SIGUSR1的屏蔽,允许接收唤醒信号 pthread_sigmask(SIG_UNBLOCK, &unblock_mask, nullptr); while (!g_terminate_flag.load(std::memory_order_acquire)) { struct sockaddr_in client_addr; socklen_t addr_len = sizeof(client_addr); int connfd = accept(sockfd, (struct sockaddr*)&client_addr, &addr_len); if (connfd == -1) { // 检查是否为信号中断导致的错误 if (errno == EINTR) { continue; // 再次检查终止标志 } // 处理其他错误 break; } // 处理客户端连接逻辑 char buf[1024]; ssize_t recv_len; while (!g_terminate_flag.load(std::memory_order_acquire)) { recv_len = recv(connfd, buf, sizeof(buf), 0); if (recv_len == -1) { if (errno == EINTR) { continue; } break; } else if (recv_len == 0) { // 客户端断开连接 break; } // 处理接收的数据 } close(connfd); } close(sockfd); return nullptr; }串口IO线程适配
对于阻塞串口IO的线程,同样解除SIGUSR1屏蔽,当收到信号时,阻塞的read()会返回EINTR,此时检查终止标志即可退出:void* serial_thread(void* arg) { int serial_fd = *(int*)arg; sigset_t unblock_mask; sigemptyset(&unblock_mask); sigaddset(&unblock_mask, SIGUSR1); pthread_sigmask(SIG_UNBLOCK, &unblock_mask, nullptr); char buf[256]; while (!g_terminate_flag.load(std::memory_order_acquire)) { ssize_t read_len = read(serial_fd, buf, sizeof(buf)); if (read_len == -1) { if (errno == EINTR) { continue; } break; } // 处理串口数据 } close(serial_fd); return nullptr; }
方案2b:非阻塞套接字 + 多路复用(可行但冗余)
该方案确实能解决问题,但对单连接服务器属于过度设计:
- 需要将套接字设为非阻塞模式,用
select()/poll()/epoll()监听事件,同时将全局终止标志作为退出条件; - 串口IO线程若用阻塞
read(),仍会遇到类似问题,要么也改成非阻塞加多路复用,要么还是需要信号唤醒。
因此方案2b并非当前场景的最优解,仅适合后续计划扩展为多连接服务器的情况。
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

