1. 为什么C++程序员必须亲手写信号处理,而不是依赖框架封装?
“C++信号捕获与处理全解析”——这标题里藏着一个被很多新人忽略的硬核真相:信号不是库函数,而是操作系统直接打在进程脸上的耳光。你写个std::thread启动十个线程,它们再优雅也躲不过SIGSEGV;你用boost::asio封装得再漂亮,SIGINT一来照样中断IO循环;你把std::shared_ptr用得如火纯青,SIGKILL照样不打招呼就把整个进程抹掉。这不是C++语言的问题,而是所有Unix-like系统(Linux/macOS)底层运行时的铁律。
我带过几个刚从Java/Python转过来的开发者,他们第一反应是:“C++有没有像Spring Boot的@ExceptionHandler那样的信号注解?”——没有。C++连运行时异常机制都靠setjmp/longjmp模拟,更别说信号这种比异常更底层、更暴力的异步事件了。信号本质是内核向进程发送的软中断,它可能在任何指令间隙插入,甚至打断malloc内部锁操作、std::vector::push_back的内存重分配,或者你正在写的std::cout << "debug"这一行。这意味着:信号处理函数(signal handler)里能安全调用的函数,整个POSIX标准只列出了不到30个,printf不行,new不行,std::string::append不行,连std::mutex::lock()都不行——因为这些函数内部可能调用非异步信号安全(async-signal-safe)的系统调用,导致死锁或内存损坏。
所以“全解析”的第一个落脚点,不是教你怎么写signal(SIGUSR1, handler),而是让你看清这个战场的物理规则:信号处理不是功能模块,而是和内核搏斗的生存技能。你写的每一行handler代码,都要回答三个问题:
- 这行代码是否在POSIX async-signal-safe函数列表里?(查
man 7 signal-safety) - 它会不会触发隐式内存分配?(比如
std::string构造) - 它会不会访问被其他线程同时修改的全局变量?(信号是进程级的,但handler执行时其他线程照常运行)
我去年帮某嵌入式团队调试一个“偶发崩溃”,现象是程序收到SIGUSR2后5秒内必core dump。他们用的是std::queue存信号数据,handler里直接push()——这违反了两条铁律:std::queue::push()会调用new(非async-signal-safe),且std::queue本身不是线程安全的(信号handler和主线程并发访问)。改成sigqueue()配合signalfd()+epoll轮询后,问题消失。这不是C++的缺陷,而是你没看清信号处理的物理边界。
真正需要信号处理的场景,其实很具体:
- 服务进程优雅退出:
SIGTERM触发资源清理,SIGINT响应Ctrl+C,SIGHUP重载配置; - 崩溃现场捕获:
SIGSEGV/SIGABRT生成堆栈快照,避免“程序闪退无日志”; - 实时性要求高的中断响应:比如工业控制中
SIGALRM触发周期性采样,不能被C++运行时开销阻塞; - 跨进程通信的轻量通道:
SIGUSR1/SIGUSR2传递简单状态,比socket更轻(但别传大数据)。
如果你的项目只是写个命令行工具,按Ctrl+C就退出,那signal(SIGINT, SIG_DFL)就够了;但凡涉及“优雅”“可靠”“诊断”“实时”,你就得亲手拆解信号机制。这不是炫技,是生产环境的生存底线。
2. 信号处理的三重境界:从signal()到sigwaitinfo(),每一步都在填坑
C++没有原生信号支持,所有方案都是对POSIX API的封装。但不同封装层级,解决的是完全不同的问题。我把实践中的方案分成三重境界,对应三种典型踩坑现场:
2.1 第一重:signal()——教科书陷阱,新手速死区
#include <csignal> #include <iostream> void sigint_handler(int sig) { std::cout << "Caught SIGINT!" << std::endl; // ❌ 危险!std::cout非async-signal-safe exit(0); // ✅ exit()在async-signal-safe列表中 } int main() { signal(SIGINT, sigint_handler); while(true) { /* do work */ } }这段代码在多数Linux发行版上“似乎能跑”,但它是定时炸弹。std::cout内部会调用write()系统调用,而write()在async-signal-safe列表里——但仅限于fd为标准输出且未被重定向时。一旦程序被./app > log.txt重定向,std::cout会触发缓冲区刷新,调用malloc分配临时内存,而malloc不在安全列表中。实测在CentOS 7上,重定向后SIGINT触发时有30%概率卡死在malloc锁里。
更隐蔽的坑是signal()的语义模糊性。POSIX规定signal()在不同系统上行为不一致:Linux默认SA_RESTART(系统调用被中断后自动重试),而macOS默认不重试。这意味着同样一段read(fd, buf, len)代码,在Linux上收到SIGUSR1可能继续读,在macOS上直接返回-1并设errno=EINTR。我见过一个网络代理程序,在macOS上因accept()被信号中断后未检查EINTR,导致连接请求永久丢失。
提示:
signal()仅适用于最简单的场景(如exit(0)或_exit(0)),且必须确保handler内只调用async-signal-safe函数。查表方法:man 7 signal-safety,重点记牢write()、read()、close()、_exit()、sigprocmask()这几个核心函数。
2.2 第二重:sigaction()——生产环境入场券,但仍有暗礁
sigaction()解决了signal()的语义不一致问题,并提供精细控制:
#include <csignal> #include <cerrno> #include <unistd.h> volatile sig_atomic_t g_exit_flag = 0; void sigterm_handler(int sig, siginfo_t* info, void* ucontext) { g_exit_flag = 1; // ✅ sig_atomic_t是唯一保证原子读写的类型 } int main() { struct sigaction sa; sa.sa_flags = SA_SIGINFO | SA_RESTART; // 显式启用SA_RESTART sa.sa_sigaction = sigterm_handler; sigemptyset(&sa.sa_mask); sigaddset(&sa.sa_mask, SIGUSR1); // 阻塞SIGUSR1,避免嵌套 if (sigaction(SIGTERM, &sa, nullptr) == -1) { perror("sigaction"); return 1; } while (!g_exit_flag) { // 主循环工作 pause(); // 等待信号,比while(1) sleep(1)省电 } cleanup_resources(); }这里的关键升级:
SA_SIGINFO允许获取siginfo_t,拿到发送信号的PID、UID、附加数据(sigqueue()传入);SA_RESTART显式控制系统调用重启行为,消除平台差异;sigemptyset()+sigaddset()精确管理信号掩码,避免handler执行时被同信号再次打断(递归风险);sig_atomic_t替代bool,这是C++标准唯一保证信号上下文安全的整数类型(std::atomic不在此列!)。
但sigaction()仍有个致命局限:它无法解决信号和主逻辑的竞态问题。比如你想在SIGUSR1handler里记录时间戳,然后主循环定期检查并上报。g_exit_flag可以,但时间戳呢?struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts)——clock_gettime()在async-signal-safe列表里,但ts是局部变量,handler里写完,主循环怎么安全读?如果用全局struct timespec g_last_signal_time,主循环读时可能读到半写入状态(64位时间戳在32位系统上需两次写入)。解决方案只能是:handler里只写sig_atomic_t标志位,主循环用clock_gettime()自己取时间——把计算移出危险区。
2.3 第三重:signalfd() + epoll()——现代C++的终极解法,彻底告别异步信号
Linux 2.6.27引入的signalfd(),让信号变成可epoll等待的文件描述符,彻底绕过传统handler的异步困境:
#include <sys/signalfd.h> #include <sys/epoll.h> #include <linux/types.h> #include <csignal> #include <vector> int create_signalfd(const std::vector<int>& signals) { sigset_t mask; sigemptyset(&mask); for (int sig : signals) { sigaddset(&mask, sig); } // 阻塞信号,让它们排队到signalfd if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) { perror("sigprocmask"); return -1; } int sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK); if (sfd == -1) { perror("signalfd"); return -1; } return sfd; } int main() { std::vector<int> signals = {SIGTERM, SIGINT, SIGUSR1}; int sfd = create_signalfd(signals); int epfd = epoll_create1(0); struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = sfd; epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev); while (true) { struct epoll_event events[10]; int nfds = epoll_wait(epfd, events, 10, -1); for (int i = 0; i < nfds; ++i) { if (events[i].data.fd == sfd) { struct signalfd_siginfo si; ssize_t s = read(sfd, &si, sizeof(si)); if (s == sizeof(si)) { switch (si.ssi_signo) { case SIGTERM: std::cout << "Graceful shutdown requested\n"; goto cleanup; case SIGUSR1: std::cout << "USR1 from PID " << si.ssi_pid << "\n"; break; } } } } } cleanup: close(sfd); close(epfd); }这套方案的优势是降维打击:
- 信号处理完全同步化:
read()在epoll_wait()之后发生,主逻辑和信号处理共享同一执行流,无需sig_atomic_t,普通int、std::string、std::shared_ptr全部可用; - 精准控制时机:
epoll_wait()可设超时,主循环能做其他事(如检查网络连接、更新UI),信号不再“随时打断”; - 天然支持多路复用:
epoll可同时监听socket、timerfd、signalfd,一个事件循环统管所有IO和信号; - 规避所有async-signal-safe限制:
std::cout、new、std::mutex随便用,因为此时已脱离信号上下文。
代价是:仅Linux可用(macOS需用kqueue+EVFILT_SIGNAL模拟,但语义不同)。不过对于服务器/嵌入式场景,Linux占比超95%,这个限制实际可接受。我参与的某车载信息娱乐系统,就用signalfd+epoll统一处理CAN总线中断、GPS信号、用户按键(映射为SIGUSR1)——所有事件在一个epoll_wait()里分发,代码清晰度提升3倍,调试难度直线下降。
3. 核心细节深挖:从信号掩码到实时信号,那些手册里没写的实战参数
信号处理的魔鬼藏在细节里。下面这些参数和机制,决定你的代码是稳定运行还是随机崩溃:
3.1 信号掩码(Signal Mask):进程的“信号防火墙”
每个线程有独立的信号掩码(pthread_sigmask),进程有继承的掩码(sigprocmask)。掩码不是开关,而是阻塞队列:被阻塞的信号不会被投递,但会排队等待——直到你解除阻塞。这带来两个关键应用:
场景1:临界区保护
假设主循环在修改一个全局链表,此时若SIGUSR1handler也尝试修改同一链表,必然崩溃。传统做法是handler里加锁——但pthread_mutex_lock()非async-signal-safe!正确做法是:
// 主循环临界区前 sigset_t oldmask; sigset_t blockmask; sigemptyset(&blockmask); sigaddset(&blockmask, SIGUSR1); pthread_sigmask(SIG_BLOCK, &blockmask, &oldmask); // 阻塞SIGUSR1 // 修改全局链表... modify_global_list(); pthread_sigmask(SIG_SETMASK, &oldmask, nullptr); // 恢复原掩码这样,SIGUSR1在临界区内到达时会被挂起,等临界区结束后才投递。注意:pthread_sigmask是线程级的,sigprocmask是进程级的(影响所有线程),多线程程序务必用前者。
场景2:信号优先级调度
Linux支持实时信号(SIGRTMIN到SIGRTMAX,通常34-64),它们按数值升序排队——SIGRTMIN+1永远在SIGRTMIN之后处理。利用这点可实现优先级队列:
// 发送高优先级信号(数值小) sigqueue(getpid(), SIGRTMIN, {.sival_int = 1}); // 优先级1 // 发送低优先级信号(数值大) sigqueue(getpid(), SIGRTMIN+5, {.sival_int = 2}); // 优先级2当多个实时信号排队时,内核保证先投递SIGRTMIN,再SIGRTMIN+1……这比用SIGUSR1/SIGUSR2手动管理队列可靠得多。某实时音视频处理项目就用SIGRTMIN触发关键帧编码,SIGRTMIN+1触发B帧编码,确保关键帧永远优先。
3.2 siginfo_t结构体:信号的“身份证”,别只读signo
sigaction的SA_SIGINFO标志开启后,handler能拿到siginfo_t*,里面藏着关键元数据:
| 字段 | 类型 | 说明 | 实战价值 |
|---|---|---|---|
ssi_signo | int | 信号编号 | 基础判断 |
ssi_code | int | 产生原因 | SI_USER(kill),SI_KERNEL(内核),SI_QUEUE(sigqueue) |
ssi_pid | pid_t | 发送进程PID | 验证权限(如只响应父进程信号) |
ssi_uid | uid_t | 发送者UID | 安全审计(拒绝非root进程的SIGUSR1) |
ssi_value | union sigval | 用户数据 | sigqueue()传入的int或void* |
最易被忽视的是ssi_code。比如SIGCHLD,ssi_code=CLD_EXITED表示子进程正常退出,CLD_KILLED表示被信号杀死——这对资源清理策略完全不同。某监控程序曾因忽略ssi_code,把被SIGKILL杀死的子进程当成正常退出,导致僵尸进程堆积。
ssi_value是跨进程通信的核心。sigqueue()可传int或指针:
// 发送端 union sigval value; value.sival_int = 123; sigqueue(pid, SIGUSR1, value); // 接收端handler void handler(int sig, siginfo_t* info, void*) { if (info->si_code == SI_QUEUE) { int data = info->si_value.sival_int; // 安全获取 process_data(data); } }注意:sival_ptr传指针时,接收端必须确保该内存地址在信号到达时仍有效(通常用mmap分配的共享内存,或发送前malloc+sigqueue+free,但要防信号在free后到达——所以推荐int或固定大小结构体)。
3.3 信号可靠性:为什么SIGUSR1有时“收不到”
信号不可靠是经典误区。根本原因是:标准信号(1-31)不排队,实时信号(34+)才排队。发送两次SIGUSR1,如果第一次未处理完第二次就到,内核只保留一个,第二个丢失。验证代码:
// 发送端 for (int i = 0; i < 5; ++i) { kill(getpid(), SIGUSR1); usleep(1000); // 1ms间隔 }接收端signal()handler里计数,结果常是1或2,绝不是5。解决方案只有两个:
- 改用实时信号:
kill(getpid(), SIGRTMIN),发送5次必收5次; - 或在handler里用
sigpending()检查是否有未决信号,循环处理:
void handler(int sig) { sigset_t pending; while (1) { sigpending(&pending); if (sigismember(&pending, sig)) { handle_signal_once(); } else { break; } } }但sigpending()需配合sigprocmask阻塞信号才能安全使用,复杂度陡增。所以结论很明确:需要可靠通信,必须用实时信号。
4. 实操全流程:从零搭建一个带崩溃捕获的信号处理框架
现在我们整合所有知识,构建一个生产级信号处理框架。目标:
- 支持
SIGTERM/SIGINT优雅退出; SIGSEGV/SIGABRT自动生成堆栈快照;SIGUSR1触发日志级别切换;- 全部基于
signalfd+epoll,线程安全,C++17风格。
4.1 步骤1:信号注册与signalfd初始化
#include <sys/signalfd.h> #include <sys/epoll.h> #include <csignal> #include <vector> #include <memory> #include <stdexcept> class SignalManager { private: int m_sfd = -1; int m_epfd = -1; std::vector<int> m_signals; public: SignalManager(const std::vector<int>& signals) : m_signals(signals) { init_signalfd(); init_epoll(); } ~SignalManager() { if (m_sfd != -1) close(m_sfd); if (m_epfd != -1) close(m_epfd); } private: void init_signalfd() { sigset_t mask; sigemptyset(&mask); for (int sig : m_signals) { sigaddset(&mask, sig); } // 阻塞所有目标信号,让它们排队到signalfd if (sigprocmask(SIG_BLOCK, &mask, nullptr) == -1) { throw std::runtime_error("sigprocmask failed: " + std::string(strerror(errno))); } m_sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK); if (m_sfd == -1) { throw std::runtime_error("signalfd failed: " + std::string(strerror(errno))); } } void init_epoll() { m_epfd = epoll_create1(0); if (m_epfd == -1) { throw std::runtime_error("epoll_create1 failed: " + std::string(strerror(errno))); } struct epoll_event ev; ev.events = EPOLLIN; ev.data.fd = m_sfd; if (epoll_ctl(m_epfd, EPOLL_CTL_ADD, m_sfd, &ev) == -1) { throw std::runtime_error("epoll_ctl ADD failed: " + std::string(strerror(errno))); } } };关键点:
SFD_CLOEXEC确保fork()后子进程不继承signalfd;SFD_NONBLOCK避免read()阻塞,适配epoll;- 异常抛出而非
perror,符合C++习惯。
4.2 步骤2:信号事件分发器
#include <csignal> #include <functional> #include <unordered_map> #include <mutex> class SignalDispatcher { private: std::unordered_map<int, std::function<void(const siginfo_t&)>> m_handlers; mutable std::mutex m_mutex; public: void register_handler(int sig, std::function<void(const siginfo_t&)> handler) { std::lock_guard<std::mutex> lock(m_mutex); m_handlers[sig] = std::move(handler); } void dispatch(const siginfo_t& si) { std::lock_guard<std::mutex> lock(m_mutex); auto it = m_handlers.find(si.ssi_signo); if (it != m_handlers.end()) { it->second(si); } } }; // 全局单例(简化版,生产环境建议DI) static SignalDispatcher& get_dispatcher() { static SignalDispatcher dispatcher; return dispatcher; }4.3 步骤3:核心信号处理器实现
#include <execinfo.h> #include <cxxabi.h> #include <fstream> #include <chrono> #include <iomanip> class SignalHandler { public: static void setup() { // 注册SIGSEGV/SIGABRT崩溃处理器 get_dispatcher().register_handler(SIGSEGV, [](const siginfo_t& si) { handle_crash("SIGSEGV", si); }); get_dispatcher().register_handler(SIGABRT, [](const siginfo_t& si) { handle_crash("SIGABRT", si); }); // SIGTERM/SIGINT优雅退出 get_dispatcher().register_handler(SIGTERM, [](const siginfo_t& si) { std::cout << "Received SIGTERM, shutting down...\n"; graceful_shutdown(); }); get_dispatcher().register_handler(SIGINT, [](const siginfo_t& si) { std::cout << "Received SIGINT (Ctrl+C), shutting down...\n"; graceful_shutdown(); }); // SIGUSR1日志级别切换 get_dispatcher().register_handler(SIGUSR1, [](const siginfo_t& si) { toggle_log_level(); }); } private: static void handle_crash(const char* name, const siginfo_t& si) { // 生成崩溃快照 std::string filename = generate_crash_filename(); std::ofstream file(filename); if (!file.is_open()) { std::cerr << "Failed to open crash file: " << filename << "\n"; return; } file << "Crash Report: " << name << "\n"; file << "Time: " << get_timestamp() << "\n"; file << "PID: " << getpid() << ", UID: " << getuid() << "\n"; file << "Signal code: " << si.ssi_code << "\n"; // 打印堆栈(简化版,实际用libbacktrace或boost.stacktrace) void* buffer[100]; int nptrs = backtrace(buffer, 100); char** strings = backtrace_symbols(buffer, nptrs); if (strings) { file << "Stack trace:\n"; for (int i = 0; i < nptrs; ++i) { file << " " << strings[i] << "\n"; } free(strings); } file.close(); std::cerr << "Crash saved to " << filename << "\n"; // 触发core dump(如果系统允许) raise(SIGABRT); } static std::string generate_crash_filename() { auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; ss << "crash_" << std::put_time(std::localtime(&time_t), "%Y%m%d_%H%M%S") << ".log"; return ss.str(); } static std::string get_timestamp() { auto now = std::chrono::system_clock::now(); auto time_t = std::chrono::system_clock::to_time_t(now); std::stringstream ss; ss << std::put_time(std::localtime(&time_t), "%Y-%m-%d %H:%M:%S"); return ss.str(); } static void graceful_shutdown() { // 这里放你的资源清理逻辑 // 如:关闭数据库连接、flush日志、通知子进程 std::cout << "Performing cleanup...\n"; // ... 清理代码 std::cout << "Shutdown complete.\n"; _exit(0); // 用_exit避免atexit handlers二次调用 } static void toggle_log_level() { static int level = 1; level = (level % 3) + 1; std::cout << "Log level toggled to: " << level << "\n"; } };4.4 步骤4:主事件循环集成
#include <iostream> #include <unistd.h> int main() { try { // 初始化信号管理 SignalManager sm({SIGTERM, SIGINT, SIGSEGV, SIGABRT, SIGUSR1}); // 设置处理器 SignalHandler::setup(); // 主循环 std::cout << "Signal manager started. Send signals to PID " << getpid() << "\n"; while (true) { struct epoll_event events[10]; int nfds = epoll_wait(sm.get_epoll_fd(), events, 10, 1000); // 1s超时 if (nfds == -1) { if (errno == EINTR) continue; // 被信号中断,重试 perror("epoll_wait"); break; } for (int i = 0; i < nfds; ++i) { if (events[i].data.fd == sm.get_signalfd()) { struct signalfd_siginfo si; ssize_t s = read(sm.get_signalfd(), &si, sizeof(si)); if (s == sizeof(si)) { get_dispatcher().dispatch(si); } } } // 主循环其他任务(如检查网络、更新状态) // do_other_work(); } } catch (const std::exception& e) { std::cerr << "Fatal error: " << e.what() << "\n"; return 1; } return 0; }编译命令:
g++ -std=c++17 -O2 -g signal_demo.cpp -o signal_demo -lpthread测试方法:
# 启动程序,记下PID ./signal_demo # 发送SIGUSR1切换日志级别 kill -USR1 <PID> # 发送SIGTERM优雅退出 kill -TERM <PID> # 触发崩溃(需取消注释raise(SIGSEGV)或用gdb注入) gdb ./signal_demo -ex "attach <PID>" -ex "signal SIGSEGV" -ex "detach" -ex "quit"5. 常见问题排查与避坑指南:那些让我熬夜三天的信号bug
信号处理的坑,往往在看似无关的代码里。以下是我在多个项目中踩过的典型问题及排查方法:
5.1 问题速查表
| 现象 | 可能原因 | 排查命令 | 解决方案 |
|---|---|---|---|
程序收到SIGUSR1后无反应 | 信号被阻塞未解除 | cat /proc/<PID>/status | grep Sig | 检查SigBlk字段,用pthread_sigmask确认 |
SIGSEGVhandler里std::cout崩溃 | std::cout非async-signal-safe | strace -e trace=write,brk,mmap ./app | 改用write(2, ...)或迁移到signalfd |
多线程程序SIGTERM只杀主线程 | pthread_kill()未广播 | ps -T -p <PID>查看线程ID | 对每个线程调用pthread_kill(tid, SIGTERM) |
signalfdread()返回0 | 信号掩码未正确阻塞 | sigprocmask(SIG_SETMASK, nullptr, &old) | 确保sigprocmask在signalfd前调用 |
SIGCHLDhandler被调用但waitpid()返回-1 | errno=ECHILD(子进程已回收) | strace -e trace=waitpid ./app | 检查是否重复waitpid(),加WNOHANG标志 |
5.2 独家避坑技巧
技巧1:用strace定位信号丢失strace -e trace=signal,kill,rt_sigprocmask ./app可看到所有信号相关系统调用。特别关注rt_sigprocmask的SIG_BLOCK/SIG_UNBLOCK,确认信号是否被意外阻塞。
技巧2:gdb动态注入信号调试
gdb ./app (gdb) attach <PID> (gdb) signal SIGUSR1 # 向进程发送信号 (gdb) cont比kill更可控,可配合断点观察handler执行。
技巧3:/proc/<PID>/status信号状态解读
关键字段:
SigQ: 队列中未决信号数(实时信号)SigP: 进程可接收的信号位图SigBlk: 当前阻塞的信号位图SigIgn: 忽略的信号位图
若SigQ持续增长,说明signalfd未及时read();若SigBlk包含目标信号,说明sigprocmask未生效。
技巧4:SIGPIPE的静默死亡
管道写端关闭时,写操作触发SIGPIPE,默认终止进程。网络程序常见:客户端断开后,服务端继续write()触发SIGPIPE。解决方案:
signal(SIGPIPE, SIG_IGN)忽略它(最常用);- 或在
write()前检查send()返回值,errno==EPIPE时主动关闭连接。
技巧5:fork()后的信号继承陷阱fork()后子进程继承父进程的信号掩码和signalfd,但signalfd的文件描述符是独立的。常见错误:父进程fork()后,子进程也epoll_wait()监听同一signalfd,导致信号被子进程消费,父进程收不到。解决方案:fork()后子进程立即close(m_sfd),或用prctl(PR_SET_CHILD_SUBREAPER, 1)让子进程成为子reaper。
最后分享一个血泪教训:某金融交易系统用signal(SIGUSR1, handler)做配置热加载,handler里调用dlopen()加载新so。dlopen()非async-signal-safe,某次SIGUSR1恰好在malloc锁内到达,导致主线程和handler死锁。上线后随机延迟10秒才响应信号。最终方案:SIGUSR1handler只写sig_atomic_t flag=1,主循环检测flag后,在安全上下文调用dlopen()。信号handler的唯一使命,是设置一个标志,把所有复杂操作留给主逻辑——这句话值得刻在C++信号处理的墓碑上。