# 进程卡住了怎么办：用 ps 找到它，看懂 PID、父子关系和状态，再用 kill 正确结束

ps aux 和 ps -ef 默认显示的列差在哪，STAT 里的 S、D、Z 各是什么意思，端口为什么得用 ss 查，以及 kill 与 kill -9 到底差在哪

> 进程管理 · 命令行操作 · Linux 基础 · 约 9 分钟 · 10 月 07 日

## 本篇要点

1. `ps` 是内核进程表的一张快照，不是实时的；要最新情况就重新运行一次。
2. `ps aux` 默认显示 `STAT` 但不显示父进程，`ps -ef` 默认显示 `PPID` 但不显示 `STAT`；想一次都拿到用 `ps -eo pid,ppid,stat,cmd`。
3. `ps aux | grep nginx` 里多出的那行 `grep nginx` 是 `grep` 自己，因为它的命令行里含有关键词；用 `pgrep -a` 或 `grep '[n]ginx'` 可以避开。
4. 端口不是进程的属性，`ps` 看不到端口；从端口查进程要用 `sudo ss -ltnp 'sport = :8080'` 或 `lsof -nP -iTCP:8080 -sTCP:LISTEN`，看到别人的 PID 通常需要 root。
5. `STAT` 里 `S` 是可中断睡眠，正常服务长期就是 `S`；`D` 是不可中断睡眠，在等不返回的 I/O；`Z` 是僵尸，进程已死只等父进程回收。
6. 子进程退出后要由父进程读取退出状态才会从进程表消失，父进程不读就留下 `Z`；僵尸无法被 `kill`，父进程退出后由 `init`/`systemd` 销毁。
7. 杀父进程不会连带杀子进程，子进程会被 PID 1 收养并继续运行，所以主进程死了端口可能还被 worker 占着。
8. `kill` 的本质是投递信号：默认发 SIGTERM（15），程序可以接住并做清理；SIGKILL（9）不能被捕获、阻塞或忽略，由内核直接终止，代价是丢数据、留半截文件和残留锁文件。
9. 信号是进程在运行时才被送达的，所以处于 `D` 状态的进程收到的 SIGKILL 只是被排队压着，等 I/O 结束才会生效，此时反复 `kill -9` 没有意义。
10. PID 会被复用，`kill` 只认编号，所以查到 PID 后要尽快下手，不要拿隔了很久的旧编号去杀。
11. 可以照做的顺序是：先确认 PID 和 STAT，发 SIGTERM，等几秒用 `ps -p PID` 或 `kill -0 PID` 复查，仍在且状态正常再发 SIGKILL；之后若是 `Z` 就处理父进程，是 `D` 就查那次不返回的 I/O。

---

上一篇讲的是命令的输出被 `>`、`2>&1`、`|` 改去了哪里。这一篇往上游走一步：那些输出到底是谁产生的——跑在服务器上的那个进程。

启动服务时冒出 `Address already in use`，说明端口被别的进程占着；某个接口不响应，但机器并没有重启；或者你之前按了 `Ctrl+Z`、又或者 SSH 断线，某个任务还赖着不走。要收拾这些情况，先得能回答三个问题：**它是谁（PID）、它是被谁拉起来的（父子关系）、它现在是什么状态（STAT）**。这三个问题的答案不同，`kill` 该怎么用也完全不同。

## 进程就是“正在跑的程序”，PID 只保证此刻唯一

磁盘上的 `/usr/sbin/nginx` 只是一个文件，不占内存、不占端口。每次内核把它加载起来执行，就在进程表里登记一条记录，并发一个编号，这就是 **PID**（process ID）。同一份程序跑两次，是两个进程、两个 PID，互不相干。

PID 用完会回收再分配。所以 PID 只在“那个进程还活着”的这段时间里指得准：**拿一个隔了很久的旧 PID 去 kill，可能杀掉的是完全不相干的另一个进程**。`kill` 的手册里专门提到这种复用带来的风险 [2]。这也是为什么下面所有操作都要求“先确认、再下手”，而不是抄一个编号就走。

## `ps` 给的是此刻的一张快照

`ps` 不是守护进程，也不实时刷新。你运行它，它读一遍内核的进程表，打印出来，就退出了。要看现在的情况，就再运行一次。

最常用的两种写法长得几乎一样，来历不同：`ps aux`（BSD 风格，没有横杠）和 `ps -ef`（UNIX 风格，带横杠）。差别不在风格，而在**默认显示哪些列**：`ps aux` 给的是 `USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND`，有 `STAT` 却没有父进程；`ps -ef` 给的是 `UID PID PPID C STIME TTY TIME CMD`，有 **PPID** 却没有 `STAT` [1]。

本篇最需要的就是 PPID 和 STAT，所以更省事的办法是直接指定列，一次都拿到：

```
ps -eo pid,ppid,stat,cmd
```

`-e` 是全部进程，`-o` 后面跟你想看的列名。（顺带提一句，`ps aux` 不要写成 `ps -aux`，按 POSIX 的理解那是在指定一个叫 `x` 的用户，手册说这个兼容行为不可靠 [1]。）

## 从一堆进程里挑出目标：`grep` 会把自己也挑出来

进程多的机器上，`ps` 的输出有几百行。上一篇讲过 `grep` 筛的是行，于是自然会写：

```
ps aux | grep nginx
```

结果里会多出一行 `grep nginx`。那不是 bug——`grep` 自己也是一个正在跑的进程，它的命令行里就含着 `nginx` 这几个字符，所以它筛到了自己。

避开它有两种写法。一是用 `ps` 自带的筛选：`ps -ef | grep '[n]ginx'`。二是用专门做这件事的命令：

```
pgrep -a nginx      # 按进程名匹配，直接给 PID；-a 顺带打出命令行
pgrep -af deploy    # -f 改成匹配整条命令行
```

`pgrep` 只去进程表里找，它自己不参与匹配，所以不会再出现多一行的问题。

## 端口不是进程的属性，得换个方向查

这里有个新手最容易卡住的地方：**`ps` 看不到端口**。进程表里记的是进程，而“哪个端口被占”是内核 socket 层的信息，两边是两套数据。你从 `ps` 出发能看到一个叫 `java` 的进程，但看不出它听的是 8080 还是 8081。

所以要反过来，**从端口查进程**：

```
sudo ss -ltnp 'sport = :8080'
sudo lsof -nP -iTCP:8080 -sTCP:LISTEN
```

`ss` 的 `-l` 只看监听状态的 socket，`-t` 限 TCP，`-n` 不让它把端口号翻译成服务名，`-p` 是让它把占用该 socket 的进程名和 PID 一起打出来 [5]。注意 `-p` 对别的用户的进程通常需要 root：普通用户看自己起的进程没问题，看别人的那一列会是空的，这不是命令坏了，是权限。（`ss` 和 socket 信息本身值得单独讲一次，这里只要记住“查端口用 `ss`/`lsof`，不归 `ps` 管”。）

## `STAT` 那一列：不是所有“不动”都叫卡住

拿到 PID 之后，先别急着 `kill`，看 `STAT` [1]：

- **`R`** — 正在 CPU 上跑，或者排在队列里等着跑。
- **`S`** — 可中断睡眠，在等一个事件（网络、锁、时间）。**绝大多数进程绝大多数时间都躺在这个状态**，数据库、Web 服务都是。看到 `S` 不等于它坏了。
- **`D`** — 不可中断睡眠，通常是在等磁盘或网络 I/O。
- **`T`** — 被暂停了，比如你按过 `Ctrl+Z`。
- **`Z`** — defunct，也就是“僵尸”，进程已经死了，只是还没被回收。

`STAT` 后面还可能跟一两个小写字母：`s` 表示它是会话首进程（很多服务主进程都带 `s`），`l` 表示它有多线程，`+` 表示它在前台进程组里。

`D` 和 `Z` 尤其重要，它们是“`kill -9` 也打不动”的常见原因，下面会用到。

## 父子关系：谁生的，谁负责收尸

每个进程除了自己的 PID，还记着一个 **PPID**，就是生它的那个进程。你在终端里敲的每条命令，父进程都是那个 shell。

父子关系决定了两件很实际的事。

**第一，父进程负责回收子进程。** 子进程退出后并不会立刻从进程表里消失，它要把“我是怎么死的”这个退出状态交给父进程读取。父进程读完之后，这条记录才被清掉。如果父进程一直不读，子进程就留在表里，状态是 `Z`——这就是僵尸进程。**僵尸已经死了，内存也早就释放了，它只是在等一个还没来读的父进程**，占的只有一条 PID 记录。所以对 `Z` 状态的 PID 执行 `kill -9` 没有任何效果，你会收到“进程不存在”这类错误（对应内核错误码 `ESRCH`）；手册在这一条后面专门补了一句：不存在的进程，也可能是已经终止但还没被 `wait` 的僵尸 [4]。要清掉它，正确做法是处理它的父进程——父进程一退出，`init`／`systemd` 会接手并销毁这个僵尸 [1]。

**第二，杀父进程不会连带杀子进程。** 父进程死了，子进程不会跟着死，只会被 PID 1（`systemd`）收养，继续跑。所以“杀死那个主进程，服务就停了”这句直觉在有的软件上不成立：主进程只是个壳，真正监听端口的是它拉起来的 worker。你把壳杀了，worker 还在，端口还在占，下次启动照样 `Address already in use`。这种情况的稳妥做法是先用 `ps -eo pid,ppid,stat,cmd` 数清楚到底有几个相关进程、它们什么关系，再分别处理。

## `kill` 这个命令名有误导性：它其实是“发信号”

`kill` 并不直接去掐断什么，它只是往一个进程投递一个**信号**（signal）。信号有编号也有名字，`kill -l` 可以列出全部 [2]。真正决定后果的是送到了哪一个：

- `kill PID`，不带信号，默认送 **SIGTERM（15）**；
- `kill -9 PID` 等价于 `kill -KILL PID`，送 **SIGKILL（9）**；
- 你在终端按 `Ctrl+C`，是 shell 给前台进程发了 **SIGINT（2）**；按 `Ctrl+Z` 发的是暂停信号，进程就变成 `T`。

**SIGTERM 是可以被程序接住的。** 程序收到它可以先关掉监听、写完日志、把数据库连接还回去，然后自己退出。这就是手册推荐先发 TERM 的原因 [2]。**SIGKILL 接不住**，`signal(7)` 里的原话是：SIGKILL 和 SIGSTOP 不能被捕获、不能被阻塞、不能被忽略 [3]。它由内核直接执行，进程没有机会做任何清理。

所以在真的需要硬杀的时候，代价是具体的：正在写的文件可能只剩半截，还没落盘的数据直接丢，锁文件、PID 文件留在那里，下次启动可能起不来。**`kill -9` 是“先 15、等一会儿、还不行再 9”里的最后一步，不是第一步。**

## `kill -9` 也打不动的两种情况

**`Z`（僵尸）**：上面说过了，已经死了，无信号可收。

**`D`（不可中断睡眠）**：信号是进程在运行时才被送达的。`D` 状态的进程正卡在内核里等 I/O——典型是磁盘或网络设备不响应。这段时间里它不回到可运行状态，你发的 SIGKILL 并没有丢，只是被排队压着，等那次 I/O 结束、进程重新被调度，信号才会被处理 [6]。表现出来就是“我 `kill -9` 了，进程还在”。这种情况下再连发几次 `kill -9` 没有意义，该做的是查那次 I/O 为什么不返回，`dmesg` 里常能看到内核的报错线索。

还有一种情况是权限。普通用户只能给属于自己的进程发信号，否则会得到 `EPERM`；另外 PID 1 只接受它自己装了处理函数的那些信号，这是为了防止系统被意外关掉 [4]。

## 可以照做的一遍流程

知道了机制，流程就很短了（`$PID` 换成你实际查到的编号）：

1. `ps -eo pid,ppid,stat,cmd | grep -i 关键词`，或者 `pgrep -af 关键词`，确认目标，顺便看 `STAT` 和 `PPID`；
2. 如果是查端口：`sudo ss -ltnp 'sport = :8080'`；
3. 确认无误后发礼貌信号：`kill $PID`（即 SIGTERM）；
4. 等几秒再看一次：`ps -p $PID -o pid,stat,cmd`。`-p` 是指定 PID，比在几百行输出里翻要快。想确认它是否还存在，也可以用 `kill -0 $PID`——`-0` 不发任何信号，只做错误检查 [2]；
5. 还在、且状态是 `S` 或 `R`，再上 `kill -9 $PID`；
6. 如果 `-9` 之后它依然在：`Z` 就去处理它的父进程，`D` 就去查那次不返回的 I/O——这两条都不该再加 `kill` 了。

最后提醒一句：`kill` 靠 PID 认人，而 PID 会复用。所以在“查到 PID”和“发出信号”之间别插很长的等待，更不要隔几小时再拿旧编号去杀。

```mermaid
flowchart LR
  A["服务没停 / 端口被占"] --> B["ps -eo pid,ppid,stat,cmd"]
  A --> C["查端口: ss -ltnp 'sport = :8080'"]
  B --> D["确认 PID 与 STAT"]
  C --> D
  D --> E["kill PID 送 SIGTERM 15"]
  E --> F{"还在吗?"}
  F -->|"没了"| G["结束"]
  F -->|"还在, S 或 R"| H["kill -9 PID 送 SIGKILL 9"]
  H --> I{"还在吗?"}
  I -->|"没了"| G
  I -->|"Z 僵尸"| J["处理父进程 PPID"]
  I -->|"D 不可中断睡眠"| K["查不返回的 I/O"]
```

## 术语表

- 进程与 PID：程序被加载执行后在内核进程表里登记的一条记录，PID 是它这一趟运行的编号，同一个程序跑两次就是两个进程；PID 用完会回收再分配。
- PPID：每个进程记着生它的那个进程的编号，父进程负责读取子进程的退出状态并让这条记录消失。
- 僵尸进程（`Z`）：子进程已经退出，但父进程还没读它的退出状态，于是留在进程表里等回收；它已经死了，所以 `kill` 对它无效。
- 不可中断睡眠（`D`）：进程卡在内核里等一个磁盘或网络 I/O 返回，这段时间不响应任何信号，包括 SIGKILL，所以看起来“杀不掉”。
- 信号：`kill` 实际投递的东西，带编号也有名字（`kill -l` 可列出）。SIGTERM（15）可以被程序接住并做清理，SIGKILL（9）接不住、由内核直接执行。
- `ss` / `lsof`：从“哪个端口被占”反向查出 PID 的工具；端口属于内核的 socket 信息，不在进程表里，所以 `ps` 查不到。
- `pgrep`：直接去进程表按进程名（`-f` 时按整条命令行）匹配并输出 PID，不会像 `ps | grep` 那样把自己也筛出来。

## 来源

1. [Ubuntu manpage: ps — 进程状态码、ps aux 与 ps -ef 的列差异、僵尸进程](https://manpages.ubuntu.com/manpages/noble/man1/ps.1.html)
2. [kill(1) — Linux manual page：默认发送 TERM、优先 TERM 再用 KILL、信号 0 只做错误检查、PID 复用风险](https://www.man7.org/linux/man-pages/man1/kill.1.html)
3. [signal(7) — Linux manual page：SIGKILL 与 SIGSTOP 不能被捕获、阻塞或忽略](https://man7.org/linux/man-pages/man7/signal.7.html)
4. [kill(2) — Linux manual page：EPERM/ESRCH 的含义，以及向 PID 1 发信号受限制](https://man7.org/linux/man-pages/man2/kill.2.html)
5. [ss(8) — Linux man page：-l、-t、-n、-p 各选项的作用](https://linux.die.net/man/8/ss)
6. [Why can't we kill uninterruptible D state process? — 解释 D 状态为何收不到包括 SIGKILL 在内的信号](https://unix.stackexchange.com/questions/364100/why-cant-we-kill-uninterruptible-d-state)

---

原文：https://pangzhengboyin.com/articles/ps-kill-find-and-stop-stuck-process-c9b012a0

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
