上一篇讲的是命令的输出被 >、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:LISTENss 的 -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 换成你实际查到的编号):
ps -eo pid,ppid,stat,cmd | grep -i 关键词,或者pgrep -af 关键词,确认目标,顺便看STAT和PPID;- 如果是查端口:
sudo ss -ltnp 'sport = :8080'; - 确认无误后发礼貌信号:
kill $PID(即 SIGTERM); - 等几秒再看一次:
ps -p $PID -o pid,stat,cmd。-p是指定 PID,比在几百行输出里翻要快。想确认它是否还存在,也可以用kill -0 $PID——-0不发任何信号,只做错误检查 2; - 还在、且状态是
S或R,再上kill -9 $PID; - 如果
-9之后它依然在:Z就去处理它的父进程,D就去查那次不返回的 I/O——这两条都不该再加kill了。
最后提醒一句:kill 靠 PID 认人,而 PID 会复用。所以在“查到 PID”和“发出信号”之间别插很长的等待,更不要隔几小时再拿旧编号去杀。