信号处理函数中调用malloc会导致死锁,因为malloc线程安全但不可重入;主线程在持锁执行malloc时若被信号中断,信号处理函数再次调用malloc会尝试获取同一非递归锁,从而阻塞在__lll_lock_wait_private。

信号处理函数里调用malloc为什么会死锁
因为malloc不是可重入的,哪怕它线程安全。当主线程正在malloc内部持锁分配内存时,信号中断进来,信号处理函数再调用malloc,就会尝试对同一个锁(比如主arena的mutex)再次加锁——而glibc默认用的是非递归互斥锁,不是PTHREAD_MUTEX_RECURSIVE。结果就是卡在__lll_lock_wait_private,所有相关线程停住。
这不是概率问题,是确定性风险:只要信号恰好在malloc临界区内到达,且信号处理函数触发了malloc(比如通过syslog、std::string构造、日志宏里的asprintf等间接调用),就必然死锁。
哪些日志/调试代码会偷偷触发malloc
很多你以为“只是打个日志”的操作,底层都在悄悄调用malloc:
-
syslog(LOG_ERR, "msg %d", x)—— 格式化字符串时可能动态分配缓冲区 std::cerr —— <code>std::ostream内部可能触发new或malloc-
std::string s = "SIGUSR1"—— 小字符串优化(SSO)虽常避免分配,但超出阈值(如15字节)就必然malloc - 任何使用
vasprintf、asprintf、fmt::format(未预分配buffer)的日志封装
安全替代方案:异步信号安全的写法
POSIX明确定义了async-signal-safe函数列表,信号处理函数里只允许调用这些函数。与内存分配相关的只有write和极少数固定大小的栈操作。
立即学习“C++免费学习笔记(深入)”;
- 用
write(STDERR_FILENO, "SIGTERM\n", 10)代替所有格式化输出——不分配堆内存,不依赖stdio缓冲 - 预先分配好固定长度的字符数组(如
char sigbuf[64]),用snprintf写入(注意:snprintf本身是async-signal-safe,但目标buffer必须在栈上或全局data段) - 把信号转为线程间通信:用
sigwait配合pthread_sigmask屏蔽信号,由专用线程统一处理,此时可放心用malloc、std::cout等 - 若必须记录信号上下文(如
si_code),先用volatile sig_atomic_t标志位通知主逻辑,延迟到安全上下文中处理
验证是否真被malloc卡住:gdb里看什么
进程僵死时,用gdb -p PID进来看线程状态,重点关注:
- 所有线程是否都停在
__lll_lock_wait_private?如果是,基本锁定是arena锁争用 - 哪个线程的调用栈顶部是
signal_handler,且下一层是malloc或syslog?那就是罪魁祸首 - 用
info proc mappings确认是否在访问[heap]区域;用thread apply all bt扫一遍,找共用的锁持有者
真正棘手的不是发现它,而是它往往只在高负载+特定信号时机复现。别依赖“没出过就等于安全”,只要信号处理函数里有任意一条路径通向malloc,这个坑就一直开着。


















