ps 与 kill 的信号语义:进程管理别再盲敲 -9

ps 与 kill 的信号语义:进程管理别再盲敲 -9

Linuxpskill进程管理Ubuntu
📶 入门 🧩 Ubuntu · procps

服务器卡了,第一反应往往是 ps aux 找进程,然后 kill -9 了事。这套动作能解决问题,但 kill -9 跳过了进程的清理逻辑:数据库可能没刷盘,Nginx 可能没保存状态。进程管理三件套 ps、top、kill 各自有明确的语义,值得花十分钟弄懂。

本文围绕一个张力问题展开:同样一个 PID,为什么有时候 kill 一下就退,有时候 -9 才有效,有时候根本杀不掉? 答案全在信号语义和进程状态里。

ps aux 到底在选哪些进程

ps 默认只显示当前用户、且挂在当前终端上的进程。这就是为什么你在 SSH 里直接敲 ps,看到的往往只有自己刚启动的命令,后台服务一个都看不见。

要看全系统进程,用这两条之一:

ps aux    # BSD 风格:所有用户的所有进程,带 STAT 列
ps -ef    # Unix 风格:全量列表,带 PPID 父进程号

两条命令的选择范围相同,列结构不同。aux 的 STAT 列包含进程状态码(后面详说),排查时信息量更大;-ef 能看到父进程关系。日常用 ps aux,追查进程归属时换 ps -ef。

还有一类常用选择器,按条件过滤而不是全量列出:

ps -u www-data            # 某个用户的所有进程
ps -C nginx               # 按命令名筛选
ps -p 1234,5678           # 按 PID 筛选
ps --ppid 1               # 列出 PID 1 的所有子进程

找进程:top、grep、pgrep 各管一段

ps 是快照,top 是实时流。要观察 CPU 或内存的动态变化,用 top 按 P(CPU 排序)或 M(内存排序);要看单次排名,ps aux --sort=-%mem | head 比 top 更顺手。

配合 grep 筛选是常见写法,但要注意 grep 自己也会出现在结果里:

ps aux | grep nginx
# 输出里通常混着 grep nginx 这一行,需要人工排除

pgrep 没有这个问题,而且直接返回 PID,方便接管道:

pgrep -a nginx            # -a 同时输出命令行
kill $(pgrep -f worker.py)  # 按完整命令行匹配再批量发信号

pgrep -f 匹配完整命令行而不是进程名,慎用——匹配串太宽会把不相干的进程卷进来。

STAT 列怎么读:D 状态和僵尸进程才是重点

ps aux 的 STAT 列是信息密度最高的部分。主要状态码:

状态含义你该做什么
R运行中或就绪正常
S可中断睡眠,等事件正常,绝大多数进程长驻于此
D不可中断睡眠,通常是 I/O检查磁盘或 NFS 挂载,kill 无效
T被作业控制信号暂停fg 恢复或发 SIGCONT
Z僵尸:已终止,父进程未回收找父进程,见下文
I空闲内核线程正常,别碰

重点说两个容易误判的。

D 状态:进程卡在不可中断的系统调用里,常见于磁盘故障或 NFS 服务端失联。此时 kill -9 也无效,因为内核根本不把信号递给正在等待硬件返回的进程。看到 D 状态堆积,先查 dmesg 里的 I/O 错误,别反复重试 kill。

Z 状态(僵尸):进程已经退出,只剩一条进程表项等父进程 wait() 回收。僵尸本身不占内存,kill 它毫无意义——它已经死了。正确动作是找到父进程:

ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
ps -p <父PID> -o pid,comm        # 看僵尸的父进程是谁

父进程要么修掉它不 wait 的 bug,要么重启。父进程退出后,僵尸会被移交 init(PID 1)自动收割。

kill 默认发 TERM:先礼后兵

kill 这个名字有误导性,它做的事是发信号,默认信号是 SIGTERM(15)。TERM 的语义是”请你退出”,进程可以捕获它、执行清理逻辑(关闭连接、刷写缓冲)然后自行结束。数据库进程收到 TERM 会优雅关闭,这正是我们想要的行为。

只有进程无视 TERM 时才升级到 SIGKILL(9)。KILL 由内核直接终止进程,不给任何清理机会,进程无法捕获也无法忽略。两者的差异:

信号编号进程可否捕获典型用途
SIGTERM15可以日常停止服务,默认选择
SIGKILL9不可以进程假死、TERM 无效后的最后手段
SIGHUP1可以守护进程重读配置(Nginx、sshd)
SIGINT2可以等价于终端里按 Ctrl-C
SIGUSR110可以应用自定义,如 Nginx 重开日志文件

Nginx 是信号语义的教科书案例:nginx -s reload 发的就是 SIGHUP,master 进程重读配置并平滑替换 worker,已有连接不中断。这类”重载”逻辑全靠应用自己捕获信号实现——这就是为什么 KILL 发出去之后没有优雅退出可言。

标准流程是逐级升级,每级之间留观察窗口:

kill 1234            # 发 TERM
sleep 10; ps -p 1234 # 还在?再等或升级
kill -9 1234         # 最后手段

跳过 TERM 直接 -9,省下的是几秒钟,赌上的是数据完整性。

kill -9 之前还有两件事没做

第一个被忽略的选项是 SIGQUIT(3)。TERM 是”退出”,QUIT 是”退出并转储 core dump”。进程卡死时,QUIT 留下的 core 文件能回答”它当时卡在哪”,比直接 -9 少损失一份现场:

kill -QUIT 1234      # 终止 + core dump(需 core ulimit 配合)

第二个是进程组信号。后台脚本起了一堆子进程时,只 kill 父进程会留下孤儿。给负数 PID 发信号可以覆盖整组:

kill -TERM -- -1234  # 注意负号,发给 PGID 1234 的整组

KILL 还有一个副作用值得知道:它不给进程回收子进程的机会,被杀进程的子进程全部变成孤儿,转交 init。杀掉一个批处理任务的父进程,往往意味着下游任务散落一地。

实战排查:五步定位一个进程

按顺序过一遍,大多数进程问题在第三步就有答案:

# 1. 找到目标进程及其状态
ps aux | grep -v grep | grep myapp
# 2. 看它卡在什么状态
ps -o pid,stat,wchan:20,comm -p $(pgrep -d, myapp)
# 3. D 状态 → 查 I/O;S/R 状态 → 正常发信号
kill -TERM $(pgrep myapp)
# 4. 观察退出,无效再升级
kill -KILL $(pgrep myapp)

wchan 字段显示进程阻塞在哪个内核函数上,配合 stat 基本能判断进程是在等 I/O、等锁还是正常调度。

常见问题

Q:kill 一个 PID 之后进程还在,为什么? 三种可能:进程处于 D 状态,信号没被递送;进程捕获了 TERM 并在做清理,等一会儿;进程被忽略了该信号(常见于 nohup 启动的进程忽略 HUP)。先看 STAT 再决定升级。

Q:僵尸进程怎么清掉? kill 僵尸本身无效,它已经终止了。找到父进程(ps -o ppid= -p <僵尸PID>),修复其不调用 wait 的逻辑,或重启父进程。父进程退出后僵尸由 init 收割。

Q:kill -9 会不会丢数据? 会。KILL 跳过全部清理逻辑:未 fsync 的写入停留在页缓存、临时文件不删除、锁文件残留。对数据库和消息队列尤其危险,务必先 TERM。

Q:ps aux 里的 %CPU 为什么加起来远超 100%? %CPU 是进程整个生命周期内的平均占用(CPU 时间除以存活时间),且多线程进程可超过 100%。它反映历史均值,不是瞬时值——看瞬时负载用 top。

Q:怎么确认进程收不到信号是因为 D 状态? ps -o stat,wchan -p <PID>,STAT 为 D 且 wchan 指向 I/O 相关内核函数(如 nfs_wait 或块设备等待队列),基本可以确认。此时优先排查存储链路,信号帮不上忙。


进程管理的正确顺序是:先用 ps 的 STAT 列判断进程处于什么状态,再根据状态选择信号,TERM 失败才升级 KILL。kill -9 是最后手段,用它开场,等于每次都放弃让进程善后。下次遇到杀不掉的进程,先看 STAT 列:D 状态指向存储链路故障,kill 在这里帮不上忙。