为什么 kill 不一定杀得死
有一条命令叫 kill,但它做的事只是「发一个信号」。收到信号的进程完全可以不理。所以「我 kill 过了,它还在」并不是命令坏了。
把几种情况摆在一起看。先是一个普通进程收到 SIGTERM:
发送 SIGTERM -> 进程结束,退出码 143
143 不是随便一个数,它是 128 加上信号编号。SIGTERM 是 15,128 加 15 就是 143。
可如果进程声明自己忽略这个信号,同一个 SIGTERM 就什么也做不成:
trap "" TERM # 声明忽略 SIGTERM
发送 SIGTERM -> 进程仍在运行
改用 SIGKILL -> 进程结束,退出码 137
SIGKILL 是 9,128 加 9 就是 137。两个数字对应两件完全不同的事:143 是进程收到请求之后自己结束,137 是被强制结束。想知道一个进程是怎么死的,看退出码就够了。这个 128+N 的约定写在 Bash 手册里,原话是「当命令因编号为 N 的致命信号而终止时,Bash 用 128+N 作为退出状态」。
顺带一提,信号的名字和编号是一回事:kill -TERM、kill -15、kill -s TERM 三种写法我试过,效果相同,都是退出码 143。
能忽略,是权利而不是毛病
进程能忽略 SIGTERM,这是设计的一部分。它把「请你退出」和「你必须退出」拆成了两件事。
第一件是请求。进程收到它之后可以保存数据、删掉临时文件、关闭连接,然后自己退出。我让一个进程在收到 TERM 时先做清理再退出,它的退出码是 0,和正常跑完没有区别,从外面看是一次体面的收场。
第二件只有在第一件不管用的时候才该动用。signal(7) 的原文很短:「SIGKILL 和 SIGSTOP 这两个信号不能被捕获、阻塞或忽略。」我试着让一个进程忽略 SIGKILL,它照样被杀掉了,连「我拒绝」都说不出口。
两种结束方式,差别看得见
让同一个进程在收到 TERM 时写一句「清理完成」到文件再退出,然后分别用两种信号结束它:
用 SIGTERM 结束 -> 退出码 0,文件里写着「清理完成」
用 SIGKILL 结束 -> 退出码 137,文件根本没有被创建
差别不在进程,在信号。前一种它来得及说完最后一句话,后一种连收尾都没写出来。这就是「先请求」换到的东西:不是慢了几毫秒,而是那些本该落到磁盘上的事,有没有机会发生。
同一套机制,也能只是按个暂停
信号不只是用来结束进程的。SIGSTOP 和 SIGCONT 是一对,前者让进程停下,后者让它接着跑。我观察了一个进程的状态字段:
初始 状态 S
SIGSTOP 状态 T
SIGCONT 状态 S
它从头到尾没有被结束过,只是被按了暂停又松开。注意 SIGSTOP 和 SIGKILL 一样无法被忽略,区别在于它的动作是暂停,不是终止。所以 kill 这个名字比它做的事窄得多:它只是把某个信号递给某个进程,收到之后做什么,由进程和信号共同决定 —— 可以退出,可以忽略,可以先收尾,也可以只是停一下。
这是一个顺序问题
kill 的默认行为是经过考虑的:不带参数时它发的是 SIGTERM,先请求,而不是先动手。常见的做法是「不行就上 -9」,更稳的顺序是反过来 —— 先发默认的那个,等一等,确认它真的没反应,再升级。多等的这几秒换到的是一次干净的收尾。
检查进程还在不在,也不必真的动手。信号 0 是个特殊的编号,kill -0 不发送任何信号,只做存在性与权限检查:进程还在就返回成功,没了就返回失败。上面每个例子我都是用 kill -0 确认状态的,它把「我以为它已经死了」变成「我知道它还在」。
参考:signal(7)、Bash 手册 · 退出状态。文中的退出码、状态与文件内容都是我实测的输出。
—— M-D