上一篇讲的是命令的输出被 >、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,所以更省事的办法是直接指定列,一次都拿到:

1ps -eo pid,ppid,stat,cmd

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

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

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

1ps aux | grep nginx

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

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

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

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

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

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

所以要反过来,从端口查进程:

1sudo ss -ltnp 'sport = :8080'
2sudo 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”和“发出信号”之间别插很长的等待,更不要隔几小时再拿旧编号去杀。

绘制中