Unix Shell无法传递CTRL+C信号给高负载多进程C++程序的问题
问题描述
我开发了一个多线程C++程序,主线程完成初始化后创建多个子进程处理RabbitMQ的消息,主线程采用如下方式实现忙等待:
while (rmqbag_active) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); }
我已为SIGINT注册信号处理函数:
std::signal(SIGINT, signal_handler);
void signal_handler([[maybe_unused]] int signal) { rmqbag_active = false; rmqbag::log_debug("Received stopping signal"); }
当子进程负载较低时,程序运行正常;但当消息量过高(4个子进程每秒接收约1750条消息)时,按下CTRL+C无任何反应,程序未收到信号(无日志输出)。
手动通过HTOP向进程发送SIGINT时,程序可正常处理并退出。我已尝试多个终端模拟器及bash、zsh、fish shell,现象一致;也试过用nanosleep替代this_thread::sleep_for实现忙等待,还为子进程注册了信号处理函数,但均无效。子进程无日志输出,不会造成终端消息刷屏。
请问为何终端无法向进程发送停止信号?
问题分析与解决方案
核心原因
终端按下CTRL+C时,会向前台进程组的所有进程发送SIGINT,而非单个进程。高负载下出现无响应的情况,通常是以下原因导致:
- 进程组前台状态丢失:高负载时,某个子进程或主线程的系统调用(如RabbitMQ客户端的IO操作)可能意外导致整个进程组脱离前台状态,终端的
SIGINT无法送达。 - 信号被意外屏蔽:RabbitMQ客户端库在高负载处理消息时,可能调用底层IO接口(如
epoll_wait、recv),这些接口可能临时修改进程的信号掩码,屏蔽SIGINT。 - 全局变量的线程/进程安全问题:
rmqbag_active未声明为原子类型,信号处理函数的修改无法被主线程及时感知,但这只会导致退出延迟,不会完全阻止信号接收,不过高负载会放大该问题。
修复步骤
- 确保全局变量的原子性:将
rmqbag_active声明为std::atomic<bool>,避免缓存一致性导致的主线程无法及时读取修改:std::atomic<bool> rmqbag_active = true; - 检查并恢复进程组前台状态:在主线程的循环中定期检查当前前台进程组是否与自身进程组一致,若不一致则尝试恢复:
#include <unistd.h> #include <sys/ioctl.h> while (rmqbag_active) { pid_t current_pgrp = tcgetpgrp(STDIN_FILENO); if (current_pgrp != getpgrp()) { tcsetpgrp(STDIN_FILENO, getpgrp()); } std::this_thread::sleep_for(std::chrono::milliseconds(500)); } - 替换忙等待为条件变量:用
std::condition_variable替代睡眠等待,信号处理函数直接唤醒主线程,避免睡眠期间的信号延迟:std::atomic<bool> rmqbag_active = true; std::mutex mtx; std::condition_variable cv; void signal_handler([[maybe_unused]] int signal) { rmqbag_active = false; cv.notify_one(); rmqbag::log_debug("Received stopping signal"); } // 主线程等待逻辑 int main() { // 初始化、注册信号、创建子进程... std::unique_lock<std::mutex> lk(mtx); cv.wait(lk, []{ return !rmqbag_active; }); // 清理资源、退出 return 0; } - 排查RabbitMQ客户端的信号干扰:检查客户端库是否有默认信号处理逻辑,或在高负载下修改信号掩码。可在信号注册后,用
sigprocmask检查SIGINT是否被屏蔽:#include <signal.h> sigset_t mask; sigprocmask(SIG_BLOCK, nullptr, &mask); if (sigismember(&mask, SIGINT)) { rmqbag::log_debug("SIGINT is masked"); } - 避免子进程重复注册信号:信号是发送给进程组的,仅需在主线程注册一次信号处理函数即可,子进程重复注册可能导致信号处理逻辑混乱。
内容的提问来源于stack exchange,提问作者Lasse
相关产品推荐
相关产品推荐

