Linux 进程收到 SIGTERM 后仍不退出:信号处理、僵尸进程与优雅停止边界
来源:17golang原创
时间:2026-08-27 09:57:13 208浏览 收藏
发布脚本停服务时,终端已经打印了“收到 SIGTERM”,但几秒后进程还在,systemd 又补了一次强制终止。这个现象通常不是 SIGTERM “失效”,而是信号送达、程序处理、子进程退出和父进程回收这四件事被混在了一起。先确认进程状态,再决定要改处理函数还是改停止流程,才能避免把正常的清理时间误判成死进程。
- SIGTERM 是请求进程自行收尾的信号,不保证立刻退出;SIGKILL 才不能被捕获或延后。
- 用
kill -TERM、ps -o pid,ppid,stat,wchan,cmd和日志分别确认送达、状态与阻塞点。 - 父进程退出前必须回收子进程,否则子进程可能暂时成为僵尸;僵尸不是“还在运行”,而是等待父进程读取退出状态。
- systemd 的
TimeoutStopSec只应给真实清理留时间,不能代替程序处理 SIGTERM。
SIGTERM 到底要求进程做什么
Linux 的 SIGTERM(信号编号 15)表达的是“请停止”。内核把信号放入目标进程的待处理队列,进程获得调度后才会进入默认动作或自定义处理函数。默认动作是终止进程;如果程序注册了处理函数,就可能先关闭监听套接字、刷新文件、通知子进程,然后再退出。
所以看到 kill -TERM 4182 返回成功,只能说明发送动作被接受,不能证明 PID 4182 已经退出。权限不足、PID 已变化、程序处于不可中断睡眠(D 状态)时,后续表现都不同。
先用三条命令确认卡在哪一层
假设服务主进程是 4182,先不要连续补发信号。下面三条命令分别回答“还在不在、是什么状态、父子关系怎样”。
kill -TERM 4182 ps -p 4182 -o pid,ppid,stat,wchan:24,etime,cmd ps --ppid 4182 -o pid,ppid,stat,etime,cmd
ps 查不到 PID,说明主进程已结束;仍能看到它时,重点看 STAT 与 WCHAN。S 或 R 多半是在等待处理或运行,D 常指不可中断的内核等待,Z 则是僵尸。最后一条命令如果列出多个子进程,就要继续判断父进程是否在等待它们。

程序收到信号却不退,常见是处理函数没有收口
处理函数里只打印一行日志并不等于完成停止。比如下面的伪代码设置了标志,却没有让主循环观察这个标志,进程当然还会继续提供服务。
static volatile sig_atomic_t stopping = 0;
static void on_term(int sig) {
(void)sig;
stopping = 1;
}
int main(void) {
signal(SIGTERM, on_term);
while (!stopping) {
serve_one_request();
}
close_listener();
return 0;
}
真实程序还可能卡在阻塞 I/O、锁等待或第三方库的关闭流程。日志只证明处理函数被调用,不证明 serve_one_request 已经返回。排查时把“收到信号”和“主循环结束”“监听套接字关闭”“子进程回收”分别打点,四个时间点才构成完整证据。
父进程退出前为什么必须回收子进程
子进程退出时,内核仍要保留 PID、退出码和资源使用量,等父进程调用 waitpid 或相关接口读取。父进程如果一直不读,子进程就会显示为 Z。它不再消耗 CPU,也不会继续执行,但 PID 表项仍被占用。
ps -eo pid,ppid,stat,cmd | awk '$3 ~ /^Z/ {print}'
cat /proc/4182/task/4182/children
strace -p 4182 -e trace=wait4,waitid -tt
如果子进程先退出而父进程仍活着,父进程应处理 SIGCHLD 并循环调用 waitpid(-1, ..., WNOHANG),直到没有可回收的子进程。不要把僵尸直接当成“进程杀不掉”:对僵尸发送 SIGKILL 没有意义,修复点在它的父进程。

systemd 的停止流程与 SIGKILL 边界
由 systemd 管理时,systemctl stop demo.service 通常先向服务的控制组发送 SIGTERM,并等待 TimeoutStopSec。超时后才会按服务配置升级为 SIGKILL。可以用下面的命令核对实际配置和最近状态:
systemctl show demo.service -p KillMode -p TimeoutStopUSec -p ControlGroup systemctl status demo.service --no-pager journalctl -u demo.service -b --no-pager -n 80
KillMode=control-group 会把同一服务控制组里的进程一起纳入停止范围,适合主进程会创建工作进程的服务。无论采用哪种模式,程序都应该在 SIGTERM 中停止接收新任务、等待有限时间、回收子进程并退出;把 TimeoutStopSec 调得很大,只会延迟故障暴露。
一张表区分“慢退出”“阻塞”和“僵尸”
| 观察结果 | 含义 | 下一步 |
|---|---|---|
| 主 PID 消失,子进程也消失 | 停止已完成 | 核对端口和服务状态 |
| 主 PID 为 S/R,日志有 SIGTERM | 处理函数或清理流程仍在运行 | 查关闭阶段耗时与锁/I/O |
| 主 PID 为 D | 卡在不可中断内核等待 | 查磁盘、网络文件系统或驱动 |
| 子进程为 Z | 子进程已退,父进程未读取退出码 | 修复 waitpid/SIGCHLD 回收 |
容易误判的三个停止细节
不要用 kill -9 代替修复
SIGKILL 会立即终止目标,来不及刷新队列、删除临时文件或回收子进程。它适合明确失控且无法收尾的最后一步,不适合作为日常停止脚本的第一选择。
不要只看父进程的退出码
父进程返回 0 只说明它自己的收尾路径结束,仍需检查控制组、监听端口和子进程列表。服务重启很快时,旧 PID 消失也可能只是新实例已经换了 PID。
不要在处理函数里做长时间工作
信号处理函数应尽量设置状态、写入自管道或唤醒事件循环,把真正的清理放回主流程。否则处理函数自身阻塞,会让“已经收到信号”的日志掩盖真正的停机瓶颈。
相关问题
SIGTERM 和 SIGINT 有什么区别?
SIGTERM 通常由服务管理器或脚本发送,表示请求程序停止;SIGINT 常由终端的 Ctrl+C 产生。程序可以分别处理它们,也可以共用一套有界的收尾流程。
僵尸进程会不会继续占用内存?
僵尸已经结束,不再占用运行所需的用户态内存和 CPU,主要保留少量进程表信息。真正的风险是父进程长期不回收,导致 PID 表项逐渐耗尽。
什么时候应该调整 TimeoutStopSec?
先用日志和状态命令量出正常清理耗时,再给它留出有限余量。若耗时持续增长,应先处理锁、I/O 或子进程回收问题,而不是无限放大超时。
收尾检查
一次可靠的 Linux 优雅停止,至少要能回答四个问题:SIGTERM 是否送达,主循环是否停止,子进程是否退出并被回收,systemd 是否在合理时间内看到服务结束。把这四个检查写进发布脚本或值班手册,下一次遇到“信号发了但进程还在”,就能快速区分等待、阻塞和僵尸,而不是反复发送更强的信号。
-
426 收藏
-
387 收藏
-
242 收藏
-
227 收藏
-
338 收藏
-
293 收藏
-
126 收藏
-
482 收藏
-
189 收藏
-
301 收藏
-
489 收藏
-
304 收藏
-
484 收藏
-
405 收藏
-
252 收藏
-
462 收藏
-
134 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习