服务器卡了,第一反应往往是 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 由内核直接终止进程,不给任何清理机会,进程无法捕获也无法忽略。两者的差异:
| 信号 | 编号 | 进程可否捕获 | 典型用途 |
|---|---|---|---|
| SIGTERM | 15 | 可以 | 日常停止服务,默认选择 |
| SIGKILL | 9 | 不可以 | 进程假死、TERM 无效后的最后手段 |
| SIGHUP | 1 | 可以 | 守护进程重读配置(Nginx、sshd) |
| SIGINT | 2 | 可以 | 等价于终端里按 Ctrl-C |
| SIGUSR1 | 10 | 可以 | 应用自定义,如 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 在这里帮不上忙。