在信号处理函数中调用 longjmp 几乎必然导致死锁,因其会跳过 std::mutex 解锁析构、破坏 RAII 安全性,并使 futex 锁处于“已锁定但所有者死亡”状态。

在信号处理函数中调用 longjmp 会导致死锁,这不是“可能”,而是**几乎必然发生**——尤其当信号在持有 std::mutex 的线程上下文中被触发时。
为什么 signal handler + longjmp 会卡住 mutex
信号是异步的,可能在任意指令点中断线程执行。如果此时线程正处在 std::mutex::lock() 的临界区内(比如刚获取了内核锁、但还没完成 RAII 对象构造),longjmp 会直接跳过析构逻辑,导致互斥量处于“已加锁但无人释放”的状态。后续任何线程尝试获取该锁,都会永久阻塞。
-
std::mutex不是 async-signal-safe 的:C++ 标准明确禁止在信号处理函数中调用它及其相关操作(如lock_guard析构) -
longjmp不会触发栈上对象的析构:RAII 安全彻底失效,std::unique_lock或std::lock_guard的自动解锁行为完全丢失 - glibc 的
pthread_mutex_t实现通常依赖 futex,而 futex 在被longjmp中断时不会回滚内部状态,锁永远停留在 “locked and owner dead” 状态
std::signal 与 std::raise 不是安全组合
即使你用 std::signal(SIGUSR1, handler) 注册了处理函数,并在另一处调用 std::raise(SIGUSR1),也不能规避风险:只要目标线程正在持锁,longjmp 就会把它钉死。
- 不要假设“我只在无锁路径 raise 信号”——调度不可控,线程可能在
lock()返回前被中断 -
sigprocmask屏蔽信号仅降低概率,不能根除;且多线程下每个线程需单独设置,极易遗漏 -
std::signal在多线程中行为未定义(POSIX 要求使用sigaction),C++ 标准对此不作保证
替代方案:用 self-pipe 或 signalfd 配合 event loop
真正可移植、安全的做法,是把信号“转成普通 I/O 事件”,由主循环统一处理,完全避开信号处理函数上下文。
立即学习“C++免费学习笔记(深入)”;
- Linux 下优先用
signalfd():创建一个文件描述符,把信号写入其中,然后用epoll_wait()等待,全程在用户态可控上下文中处理 - 跨平台可用 self-pipe 技巧:开一对
socketpair(PF_UNIX, SOCK_STREAM, 0),信号处理函数里只做write(pipe_fd[1], "x", 1)(async-signal-safe 操作),主循环监听pipe_fd[0] - 处理逻辑(比如恢复状态、重试、退出)放在主循环或工作线程中,此时可安全使用
std::mutex、std::condition_variable等全部 C++ 并发设施
如果你必须保留 longjmp,至少禁用它对锁的影响
这不是推荐做法,而是“实在无法重构时的止损手段”。核心是确保信号绝不在锁区内到达。
- 所有涉及
std::mutex的代码段前后,用pthread_sigmask(SIG_BLOCK, &set, nullptr)临时屏蔽信号(注意:需用原生 pthread 接口,std::signal不支持) - 绝对不要在 RAII 锁对象生命周期内调用
sigwait()或类似同步等待信号的函数 - 验证:用
gdbattach 卡死进程,执行info threads+thread apply all bt,确认阻塞点是否为futex_wait或__lll_lock_wait
最危险的不是你不了解 longjmp,而是你以为“只跳一次就不会出事”。一旦锁状态损坏,整个进程的并发逻辑就不可信了——哪怕其他线程看起来还在跑,数据也早已错乱。信号 + 非局部跳转 + C++ 锁,这三者叠加,本质上就是主动关闭了所有安全护栏。


















