Linux服务器如何查看进程并结束异常进程?

CPU 飙高、内存吃满、服务卡死不动,排查到最后总要走一步:找到那个进程,然后结束它。这一步看起来简单,实际上是最容易产生次生事故的地方——kill -9 下去,数据库可能来不及刷盘,订单可能停在半路,锁文件没清理导致下次起不来。

这篇文章讲清楚怎么看进程(ps、pstree、pgrep、top 和 /proc// 目录),怎么正确地结束进程(kill、killall、pkill 与不同信号的区别),以及三类特殊进程该怎么处理:僵尸进程、孤儿进程和 D 状态进程。核心观点只有一句:kill -9 是最后手段,不是第一选择。

先读懂进程状态

看进程之前,必须先能看懂状态列,否则你连"这个进程是不是卡死了"都判断不了。

ps -eo pid,ppid,user,stat,%cpu,%mem,etime,comm

STAT 列由若干字符组成,第一位是主状态:

字符 含义 说明
R Running / Runnable 正在运行或在运行队列等待
S Interruptible Sleep 可中断睡眠,等待事件,最常见
D Uninterruptible Sleep 不可中断睡眠,通常在等磁盘 I/O
T Stopped 被作业控制信号暂停,如 SIGSTOP
t Tracing stop 被调试器跟踪而暂停
X Dead 已死亡,理论上看不到
Z Zombie 僵尸进程,已退出但父进程未回收
I Idle 空闲的内核线程

后面的附加字符同样重要:

字符 含义
< 高优先级
N 低优先级(nice)
L 有页面被锁在内存中
s 会话首进程(session leader)
l 多线程进程
+ 前台进程组的进程

常见的组合如 Ss(睡眠状态的会话首进程)、Ssl(多线程的会话首进程)、R+(前台运行)、D(等 I/O)。

判断要点: 看到大量 R 说明 CPU 有压力;看到 D 要警惕,它通常意味着进程在等磁盘或网络文件系统,且无法被信号打断,包括 kill -9;看到 Z 是僵尸,需要关注它的数量是否增长;T 状态的进程是被暂停的,可能有人误按了 Ctrl+Z 或被调试器挂起。

ps:查看进程的基础命令

ps 有两种参数风格,BSD 风格不带短横线,System V 风格带短横线,两者输出列不同:

ps aux                     # BSD 风格,最常用
ps -ef                     # System V 风格,显示 PPID
ps -eLf                    # 显示线程(LWP)
ps aux --sort=-%cpu | head -n 15
ps aux --sort=-%mem | head -n 15

ps aux 各列含义:

列 含义
USER 进程所有者
PID 进程号
%CPU 自启动以来的 CPU 平均占用,非瞬时值
%MEM 物理内存占用百分比
VSZ 虚拟内存大小(KB),含映射但未使用部分
RSS 常驻物理内存(KB),这才是真实占用
TTY 所属终端,? 表示无终端
STAT 状态,见上节
START 启动时间
TIME 累计 CPU 时间
COMMAND 命令,加 -ww 可避免截断

自定义输出列更实用:

ps -eo pid,ppid,user,stat,%cpu,%mem,rss,etime,args --sort=-%cpu | head -n 20
ps -eo pid,ppid,user,stat,etime,args --sort=start_time | tail -n 20   # 最近启动的
ps -eo pid,ppid,stat,args --forest | head -n 40                       # 树状

etime 是进程已经运行的时间,格式为 [[DD-]hh:]mm:ss,用它一眼就能看出进程是刚起来的还是跑了很久。--sort=-%cpu 里的负号表示降序。

关于 %CPU 的重要提醒: ps 显示的是进程生命周期内的平均值,一个跑了三天的进程即使刚才占满 CPU,%CPU 也可能只有个位数。要看瞬时占用请用 top 或者 ps -eo 配合两次采样。

按名字或用户过滤:

ps -u nginx                      # 某用户的进程
ps -C nginx                      # 按命令名精确匹配
ps aux | grep -v grep | grep nginx

用 grep 过滤时记得排除 grep 自身,否则总会多出一行干扰结果。

pgrep 与 pstree:快速定位与关系梳理

pgrep 直接返回 PID,写脚本时比 ps | grep 干净得多:

pgrep nginx                       # 只输出 PID
pgrep -a nginx                    # 同时输出命令行
pgrep -u nginx java               # 某用户的某进程
pgrep -f 'python3 worker.py'      # 匹配完整命令行
pgrep -P 1234                     # 某进程的子进程
pgrep -o nginx / -n nginx         # 最老 / 最新的匹配进程

pstree 看父子关系,判断是谁拉起来的进程:

pstree
pstree -p                         # 显示 PID
pstree -p 1234                    # 只看某个进程的子树
pstree -a -p | head -n 50         # 显示命令行参数
pstree -h                         # 高亮当前进程

判断一个进程是不是 systemd 服务,pstree -p 看它的祖先链里有没有 PID 1,或者直接:

systemctl status PID              # systemd 能反查进程所属服务
ps -o pid,ppid,args -p PID
cat /proc/PID/cgroup              # 容器或服务的 cgroup 路径会暴露归属

systemctl status 后面直接跟 PID 是个很实用的技巧,能立刻知道这个进程属于哪个 unit,从而用 systemctl stop 优雅停止,而不是去 kill。

top 与 htop:实时观察

top
top -p 1234
top -H -p 1234                    # 线程视图
top -b -n 1 | head -n 20          # 批处理模式,适合脚本
top -u nginx                      # 只看某用户

交互键:P CPU 排序、M 内存排序、N PID 排序、T 累计时间排序、1 展开各逻辑 CPU、c 显示完整命令、H 线程视图、k 直接发信号(会提示输入 PID 和信号)、r 调整 nice 值、q 退出。

用 k 发信号时,默认信号是 15(TERM),这比手工 kill -9 安全得多。htop 的 F9 会列出所有信号供选择,更适合新手。

# 安装 htop
# RHEL、Rocky、CentOS
sudo dnf install -y epel-release && sudo dnf install -y htop
# Debian、Ubuntu
sudo apt update && sudo apt install -y htop

/proc/PID/:进程信息的宝库

/proc 是内核暴露的虚拟文件系统,每个进程一个目录,里面包含大量诊断信息。定位到 PID 之后,先查这些再决定怎么处理。

PID=1234

# 完整命令行(用 tr 把 NUL 换成空格)
sudo tr '\0' ' ' < /proc/$PID/cmdline; echo

# 可执行文件的真实路径
sudo readlink -f /proc/$PID/exe

# 工作目录
sudo readlink -f /proc/$PID/cwd

# 环境变量
sudo tr '\0' '\n' < /proc/$PID/environ | head -n 30

# 打开的文件(包括日志、socket、管道)
sudo ls -l /proc/$PID/fd | head -n 40
sudo ls /proc/$PID/fd | wc -l

# 内存映射与占用
sudo cat /proc/$PID/status | grep -E 'Name|State|Threads|VmRSS|VmSize'
sudo cat /proc/$PID/status

# 线程数
ls /proc/$PID/task | wc -l

# 资源限制
cat /proc/$PID/limits

# cgroup 归属(判断是否属于某个服务或容器)
cat /proc/$PID/cgroup

# I/O 统计
sudo cat /proc/$PID/io

# 进程启动以来的 CPU 时钟数
cat /proc/$PID/stat

cmdline、exe、cwd 三项组合起来能回答"这个进程到底是什么、从哪里启动的",对识别未知进程尤其重要。如果 exe 指向 /tmp 或某个奇怪路径,就要警惕是不是异常程序。

fd 目录里的句柄数量如果接近 limits 中的 open files 上限,说明连接或文件泄漏,进程很快会无法接受新请求。这是判断"服务突然不接受连接"的一个关键点。

信号:TERM 与 KILL 的本质区别

kill 命令的名字有误导性,它的本质是发送信号,进程收到信号后按自己的方式处理(或者忽略)。

信号 编号 默认行为 是否能被捕获 典型用途
SIGHUP 1 终止 能 通知守护进程重读配置
SIGINT 2 终止 能 Ctrl+C 中断
SIGQUIT 3 终止并 core dump 能 Java 线程转储常用
SIGTERM 15 终止 能 优雅停止,首选
SIGUSR1 10 终止 能 Nginx 重开日志文件
SIGUSR2 12 终止 能 Nginx 平滑升级
SIGKILL 9 立即终止 不能 最后手段
SIGSTOP 19 暂停 不能 暂停进程,可用 CONT 恢复
SIGCONT 18 继续 能 恢复被暂停的进程

为什么不能一上来就 kill -9? 因为 SIGKILL 由内核直接处理,进程完全收不到,也就没有机会执行清理:写缓冲区的数据可能丢失、打开的文件不会正常关闭、锁文件不会删除、子进程可能变成孤儿、数据库连接不会优雅断开(可能在服务端留下僵尸会话)、临时文件留在磁盘上。对数据库和消息队列这类有状态服务,kill -9 可能导致数据文件损坏或需要长时间恢复。

正确的停止流程是逐级升级:

# 第一优先:通过服务管理器停止,它会按 unit 文件定义的逻辑优雅处理
sudo systemctl stop nginx

# 第二:服务管理器不可用,或进程不属于任何服务时,先发 TERM
sudo kill -TERM 1234

# 第三:等待一段时间(比如 10 到 30 秒),观察是否已退出
sleep 15; ps -o pid,stat,etime -p 1234

# 第四:仍然存活才考虑 KILL,且必须确认无数据风险
sudo kill -KILL 1234

# 最后确认
ps -p 1234 > /dev/null && echo "仍存在" || echo "已退出"

如果进程是 systemd 服务,还可以在 unit 里设置 TimeoutStopSec 来控制 systemd 等待多久后强制杀掉,这比手工 kill -9 规范得多。

发送信号前的三项确认: 这个 PID 确实是要停的进程(核对 cmdline 和 exe);停掉它业务能否承接(有没有其他副本、要不要先摘流量);有没有未完成任务(数据库写入、队列消费、文件传输)。

killall 与 pkill:按名字批量操作

killall nginx                     # 按进程名发送 TERM
killall -TERM nginx
killall -u www-data php-fpm       # 限定用户

pkill nginx                       # 默认 TERM
pkill -f 'python3 worker.py'      # 匹配完整命令行
pkill -u deploy java              # 限定用户
pkill -TERM -P 1234               # 结束某进程的子进程
pgrep -a -f 'worker.py'           # 先确认会命中谁

风险提示: pkill -f 按完整命令行匹配,正则写得太宽松会误杀无关进程,比如 pkill -f python 可能把监控系统一起杀掉。批量操作前必须用 pgrep -a -f 先列出将命中的进程,确认清单后再执行。killall 在部分系统上按进程名精确匹配(限制在 15 个字符内),长名字可能匹配不到,这是它和 pkill -f 的重要区别。

killall 还有一个 -i 交互模式,会逐个询问是否终止,批量操作时建议加上:

killall -i -u www-data php-fpm

僵尸进程、孤儿进程与 D 状态进程

这三类是最容易被误解的"异常进程"。

僵尸进程(Z)。 进程已经退出,占用的资源基本释放,但内核保留了它的进程表项,等待父进程调用 wait() 读取退出状态。僵尸进程不占用内存和 CPU,只占一个 PID。

少量僵尸(几个)无需处理。数量持续增长,说明父进程有缺陷,没有正确回收子进程。

ps -eo pid,ppid,stat,comm | awk '$3 ~ /Z/'
ps -eo stat,ppid,pid,comm | grep -w Z

僵尸进程无法被 kill -9 杀死,因为它已经死了。处理方式有两种:

一是通知父进程回收,给它发 SIGCHLD:

sudo kill -CHLD 父进程PID

二是父进程确实有 bug 且不打算修复,那就结束父进程,僵尸会被 PID 1(systemd)收养并自动回收。结束父进程会影响它管理的所有子进程,必须先评估影响。

孤儿进程。 父进程先退出,子进程被 PID 1 收养。这不是错误,是正常机制。守护进程故意 fork 后让父进程退出,就是利用这个机制脱离终端。所以孤儿进程不需要处理,用 pstree 看到某个进程的父进程是 1,不代表异常。

D 状态进程(不可中断睡眠)。 这类进程在等待内核态操作返回,通常是磁盘 I/O、NFS 或者其他阻塞的系统调用。kill -9 对它无效,信号会被挂起,直到 I/O 完成。

ps -eo pid,ppid,stat,wchan:30,comm | awk '$3 ~ /D/'
cat /proc/PID/stack 2>/dev/null      # 需要内核支持
sudo cat /proc/PID/wchan; echo       # 显示等待的内核函数名

处理 D 状态进程的正确方向不是杀,而是解决它等待的资源:NFS 服务端是否挂了(用 mount、nfsstat 确认,必要时 umount -f);磁盘是否故障(dmesg 看 I/O error);存储是否拥塞(iostat -x 看 await)。资源恢复后进程会自己醒来。强行重启是最后的办法,且重启前要确认数据一致性。

顺带一提,wchan 列能显示进程正在等待哪个内核函数,是判断 D 状态成因的直接线索。配合 iostat 和 dmesg 基本能定位到具体设备或挂载点。

异常进程的标准处置流程

把前面的内容串成一个可复用的流程:

# 第一步:找到目标进程
ps -eo pid,ppid,user,stat,%cpu,%mem,etime,args --sort=-%cpu | head -n 20

# 第二步:确认身份,不要急着杀
PID=1234
sudo tr '\0' ' ' < /proc/$PID/cmdline; echo
sudo readlink -f /proc/$PID/exe
sudo readlink -f /proc/$PID/cwd
ps -o pid,ppid,stat,etime -p $PID

# 第三步:查归属,能走服务管理器就走管理器
systemctl status $PID --no-pager
sudo journalctl -u 服务名 --since '-30 min' --no-pager

# 第四步:优雅停止
sudo systemctl stop 服务名
# 或
sudo kill -TERM $PID

# 第五步:等待并观察
sleep 15
ps -o pid,stat -p $PID

# 第六步:仅在前一步无效且确认无数据风险时强制
sudo kill -KILL $PID

不熟悉的服务先查再动。 用 systemctl list-units --type=service 看有哪些服务,用 dpkg -S 或 rpm -qf 查命令属于哪个软件包。确认是恶意程序时,不要只删文件,要先断网、保留证据、查清入侵途径,否则删了还会再来。

常见问题

kill -9 之后进程还在,为什么?

三种可能:进程处于 D 状态等 I/O,kill -9 无法立即生效;进程是僵尸,已经死了只是没被回收;你 kill 的是父进程而实际工作的是子进程。用 ps -o pid,ppid,stat -p PID 确认状态,用 pstree -p PID 看子进程。

僵尸进程需要清理吗?

少量不用,它只占一个 PID。持续增长才说明父进程有问题。僵尸无法被杀死,正确做法是让父进程回收(kill -CHLD 父进程)或在评估影响后重启父进程。

kill 和 systemctl stop 有什么区别?

systemctl stop 会按 unit 文件执行完整的停止流程,包括发送停止命令、等待超时、清理 cgroup、处理依赖关系,并留下完整日志。kill 只是发一个信号。对 systemd 管理的服务,一律优先用 systemctl stop。

怎么防止误杀进程?

三个习惯:操作前用 /proc/PID/cmdline 和 exe 双重确认身份;批量操作前用 pgrep -a -f 先列出命中清单;生产环境的操作先在测试环境验证,并确认有回滚方案。

进程杀掉了,端口还被占着怎么办?

可能是 socket 处于 TIME-WAIT 状态,这是正常的 TCP 行为,通常几十秒内自动消失。用 ss -tan state time-wait | wc -l 确认。也可能是子进程还活着,用 pstree -p 查一下。

总结

  • 先看懂 STAT 列:R 是运行、S 是睡眠、D 是等 I/O 且杀不掉、Z 是僵尸、T 是被暂停。
  • 定位进程用 ps -eo 自定义列加 pgrep,看父子关系用 pstree,看归属用 systemctl status PID。
  • /proc// 里的 cmdline、exe、cwd、fd、status 能确认进程身份和资源占用,动手前必查。
  • 停止顺序固定为 systemctl stop → kill -TERM → 等待观察 → kill -KILL,kill -9 只在确认无数据风险且前序手段无效时使用。
  • 僵尸进程杀不死,要处理父进程;D 状态进程杀不动,要解决它等待的 I/O 资源;两者都不是靠 kill -9 能解决的。