登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

Linux 进程收到 SIGTERM 后仍不退出:信号处理、僵尸进程与优雅停止边界

来源:17golang原创

时间:2026-08-27 09:57:13 208浏览 收藏

发布脚本停服务时,终端已经打印了“收到 SIGTERM”,但几秒后进程还在,systemd 又补了一次强制终止。这个现象通常不是 SIGTERM “失效”,而是信号送达、程序处理、子进程退出和父进程回收这四件事被混在了一起。先确认进程状态,再决定要改处理函数还是改停止流程,才能避免把正常的清理时间误判成死进程。

要点速览
  • SIGTERM 是请求进程自行收尾的信号,不保证立刻退出;SIGKILL 才不能被捕获或延后。
  • kill -TERMps -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,说明主进程已结束;仍能看到它时,重点看 STATWCHANSR 多半是在等待处理或运行,D 常指不可中断的内核等待,Z 则是僵尸。最后一条命令如果列出多个子进程,就要继续判断父进程是否在等待它们。

Linux SIGTERM 从 kill 命令到进程处理函数的信号送达与状态检查

程序收到信号却不退,常见是处理函数没有收口

处理函数里只打印一行日志并不等于完成停止。比如下面的伪代码设置了标志,却没有让主循环观察这个标志,进程当然还会继续提供服务。

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 没有意义,修复点在它的父进程。

Linux 父进程通过 waitpid 回收子进程后僵尸状态消失的前后对比

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 是否在合理时间内看到服务结束。把这四个检查写进发布脚本或值班手册,下一次遇到“信号发了但进程还在”,就能快速区分等待、阻塞和僵尸,而不是反复发送更强的信号。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>