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

C++17/Linux:子线程阻塞网络调用无法被信号解除问题咨询

问题解答

1. 原设计(主线程做服务器)信号处理每次生效的原因

Linux内核递送进程级信号时,会优先选择处于可中断阻塞状态(如accept()/recv()等系统调用)且未屏蔽该信号的线程接收信号。

在你的原设计中:

  • 主线程作为服务器线程,长期阻塞在accept()或recv()这类可中断系统调用中;
  • 其他子线程要么处于计算状态,要么阻塞在非可中断操作(或未进入阻塞状态);
  • 所有线程默认继承主线程的信号掩码,均未屏蔽SIGTERM。

内核每次递送SIGTERM时,都会优先选择正在阻塞可中断系统调用的主线程,使其accept()/recv()返回EINTR,触发退出逻辑。这就是数百次测试均生效的核心原因——内核信号调度逻辑天然倾向于唤醒阻塞线程,而非让信号被空闲或计算中的线程接收。

2. 子线程做服务器的问题修复方案

方案2a:独立信号捕获线程 + 线程间信号唤醒(完全适配你的场景)

该方案无需修改套接字阻塞模式,是单连接服务器的轻量最优解,具体实现步骤如下:

核心思路

  • 让所有业务子线程(服务器线程、串口IO线程等)屏蔽SIGTERM,仅由专门的信号捕获线程处理该信号;
  • 信号捕获线程收到SIGTERM后设置全局终止标志,再通过线程间自定义信号主动唤醒阻塞在系统调用上的业务线程。

具体实现步骤

  1. 全局变量与信号掩码初始化

    #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);
    
  2. 信号捕获线程实现
    该线程仅负责等待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;
    }
    
  3. 服务器线程实现
    服务器线程解除对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;
    }
    
  4. 串口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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:12:17