深夜两点半,您在服务器上部署新版本服务,启动命令一敲,控制台却冷冷地吐出一行:Address already in use。这是Linux运维人最熟悉也最讨厌的瞬间——端口被占了。可端口只是个数字,它背后站着的不是一个端口,而是一个进程、一个socket、一个可能正在干活的程序。这时候该找谁?ps、ss、lsof,这三条命令就是Linux进程与端口世界里公认的“三剑客”,也是运维人的火眼金睛。这篇总结既写给刚入行、一查到端口占用就慌着kill -9的新手,也写给排查进程异常、端口不通时经常漏掉关键步骤的进阶伙伴。看完您能直接上手一套从进程查端口、从端口查进程、判定能不能杀、验证通不通的方法论,关键时刻能救命。
1. 为什么是这三把刀:ps、ss、lsof的分工逻辑
1.1 一个进程和一个端口,是同一件事的两张脸
要理解这三条命令为什么能组成“三剑客”,得先想明白一个底层事实:进程和端口不是两个独立的东西,而是同一个网络程序的两张脸。
一个程序跑起来,在内核里成为一个进程,有唯一的PID,占用CPU、内存,这是它的“本体”;如果这个程序需要对外提供网络服务,它就得申请一个socket,然后把这个socket绑定到一个IP和端口上,调用listen开始监听。这时候,端口就成了这个进程在网络上的一扇门。别人能访问你的服务,不是因为端口自己会干活,而是因为有一个进程站在端口后面,不断处理进来的连接。
所以,排查“端口被占”这件事,表面是查端口,本质是查进程;反过来,排查“进程怎么卡住了",有时也得先看它占着哪些端口、开了哪些文件。ps管进程视角,ss管端口和连接视角,lsof管两者之间的关联视角——三把刀各管一摊,合起来才是一条完整的排查链。
1.2 三剑客的选用原则与定位对比
先说结论,再详细展开。以下是我日常排查时的分工习惯:
| 工具 | 所属工具集 | 负责视角 | 典型问题 | 最常用姿势 |
|---|---|---|---|---|
ps | procps | 进程本身 | 进程在不在、什么状态、占多少资源、谁的孩子 | ps -ef、ps aux、ps -eo pid,ppid,stat,cmd |
ss | iproute2 | 端口与连接 | 端口谁在监听、连接什么状态、队列有没有堆积 | ss -tlnp、ss -tunap |
lsof | lsof套件 | 进程与端口/文件的关联 | 端口背后是哪个进程、进程开了哪些socket | lsof -i:8080、lsof -p PID |
可能有人会问,为什么没有netstat?不是它不行,而是它在多数现代发行版里已经被ss替代。netstat来自net-tools,很多命令已经停止维护;ss来自iproute2包,它读取的是内核inet_diag接口,直接问内核拿数据,速度比netstat这种解析/proc/net的方式快得多,在连接数上万时差距尤其明显。当然,netstat -antp在很多老系统上依然是救命的,我不反对大家继续用,但新装系统优先练ss。
1.3 什么时候需要三剑客合体
很多人每个命令都背得滚瓜烂熟,可真遇到事故却不知道该先用谁。我的经验是:不要按命令背,要按场景记忆。
- 服务起不来、报“端口被占用”:先用
ss -tlnp看端口所属进程,再用ps -fp PID确认这个进程是什么、能不能动。 - 服务起来了但连不上:先
ss -tlnp确认服务确实在监听,再确认监听地址对不对(是0.0.0.0还是127.0.0.1),再测通不通。 - 某进程把CPU打满、或者怎么都杀不死:先用
ps看状态和父进程,再考虑是不是进入了D状态或是不是僵尸进程。 - 怀疑进程有异常外联或者后门:用
ss -tunap查所有TCP连接,再用lsof -p PID看这个进程开了哪些不该开的socket。
这三个命令加起来,覆盖了“进程活着吗→端口开着吗→这俩到底谁连着谁”的全部排查链路。下面我从第一剑正式开始,每一剑都会带上输出样例和我的读法,保证看完就能对着终端敲。
2. 第一剑:ps——把进程看穿
2.1 核心字段读法:PID、PPID、STAT、TIME、COMMAND
ps可能是Linux里被人用最多、也误解最多的命令。很多人只会ps -ef然后调grep,其实关键是读懂输出字段的意义。
最常用的两条是ps -ef和ps aux。ps -ef格式偏BSD风格,ps aux格式偏SysV风格,两者显示的信息略有差异,但对日常排查来说,我更推荐一条自定义命令:
ps -eo pid,ppid,user,stat,lstart,time,cmd逐个讲一下重点字段:
PID:进程ID,查端口、杀进程、看日志都靠它。PPID:父进程ID,这是最容易被忽略但最值钱的字段。进程被谁拉起来的?PPID一查便知。如果是1号进程(systemd)收养的,说明它已经是孤儿进程;如果是某个Shell拉起来的,说明它还在被会话持有。STAT:进程当前状态。这个字段我下面单独讲,几乎一半进程故障都写在它上面。lstart:进程启动时间。判断一个进程是“刚被拉起来的老配置”还是“真正历史遗留”,就看它跟业务发版时间对不对得上。TIME:进程累计占用的CPU时间,注意这不是运行时长,而是所有线程消耗CPU时间的总和。它和%CPU结合能判断进程是不是“一直在干活”。CMD:完整命令行,注意可能包含参数。同一个程序启动方式不同,CMD差异巨大。
另一个实用姿势是通过pgrep和pidof快速找进程ID:
pgrep -af java pidof nginxpidof只能按程序名匹配,pgrep更灵活,-a还能同时打印完整命令行。这两个命令在脚本里比ps | grep干净得多,不会出现grep自己匹配自己的尴尬。
2.2 STAT状态机:运行、睡眠、不可中断、僵尸
ps输出的STAT字段看着像乱码,其实是很有信息量的编码。Linux进程的主要状态对应关系如下:
| STAT字符 | 含义 | 常见场景 | 处理方式 |
|---|---|---|---|
| R | 运行中或可运行 | 正常执行或抢不到CPU疯狂轮转 | 看%CPU判断是正常还是死循环 |
| S | 可中断睡眠 | 等待I/O或等待事件,最常见 | 正常状态 |
| D | 不可中断睡眠 | 等内核I/O,通常卡在磁盘或NFS | 非常棘手,普通kill无效,需排查底层I/O |
| Z | 僵尸进程 | 子进程已退出但父进程没回收 | 检查父进程,让父进程wait或直接处理父进程 |
| T | 停止 | 被Ctrl+Z或SIGSTOP暂停 | kill -CONT恢复 |
| I | 空闲 | 内核线程的idle状态 | 正常 |
第二列常出现<(高优先级)、N(低优先级)、l(多线程)、s(会话领导者)等修饰符。
运维里最让人头疼的两个状态是D和Z。
D状态进程在等不可中断的内核I/O,典型触发点是磁盘卡死、NFS挂载超时、或者某个驱动函数卡住。这种进程你kill -9都没用,因为它在内核态根本没机会处理信号。我遇到过一次MySQL卡在D状态,排查到最后是底层存储的LUN整个不可用。经验是:看到D不要急着杀进程,先去看iostat、dmesg,确认是不是存储层的锅。
Z状态则是子进程退出后,父进程没有调用wait回收它。如果只是一两个僵尸,通常不用管;但僵尸进程如果积累到几百个,说明父进程逻辑有bug或父进程本身就是个僵尸。处理僵尸的根本思路不是kill僵尸本身(信号对它无效),而是处理它的父进程——让父进程正常退出,然后僵尸被systemd收养并回收。
2.3 实战:一个java进程到底在干什么
特意把Java单拎出来,因为后台服务里Java占了极大比例,而Java进程的排查又有自己的独特路数。
当你看到这样一个输出时:
$ ps -eo pid,ppid,user,stat,lstart,time,cmd | grep java 12345 1 app java -Xms2g -Xmx2g -jar app.jar --server.port=8080几个判断立刻成立:PPID是1,说明它是孤儿进程被systemd收养;lstart如果是三个月前,而业务版本是三天前发版的,那它大概率是没被正确重启的旧进程;TIME如果已经累计了上千分钟,这个进程几乎一直在烧CPU。
Java进程的多线程特性在ps里也有体现,STAT第二列会出现l标记。要深入看线程级情况,单靠ps不够,还得配合top -Hp PID或jstack PID。但在那之前,ps已经告诉你“这个Java进程还活着、一直在跑、被谁拉起来”——这已经完成了事故定级的第一环。
顺带说一个冷门但面试和排查都可能遇到的知识点:Linux的comm字段(进程名)长度限制是15个字符(TASK_COMM_LEN),超过会被截断。你如果给进程起了一个特别长的名字,用ps -C匹配会得到截断后的名称。解决办法是用prctl设置进程名,或者直接把名字控制在15字符内。这不是什么高级操作,但知道它能避免你在脚本匹配进程时踩坑。
3. 第二剑:ss/netstat——把端口看穿
3.1 端口是怎么被“占”的:从bind到listen
先补一个常被误解的基础:一个进程要监听端口,内核发生了什么。
程序创建一个socket,调用bind把socket绑定到某个地址和端口。如果这个端口已经被别的socket独占绑定,bind就会失败,内核返回EADDRINUSE,翻译给用户就是“Address already in use”。等bind成功后再调用listen,端口才进入监听状态。
这里有个很重要的细节:端口冲突在bind阶段就发生了,所以报错往往在服务启动瞬间出现。而SO_REUSEADDR这个socket选项允许新启动的进程绑定到一个处于TIME_WAIT状态的端口——这是服务重启频繁时能不能快速恢复的关键。很多框架默认开了这个选项,所以重启没事;但你如果手写socket服务又没设置它,就会遇到“进程明明杀了、端口还报被占”的诡异情况。
再说一个几乎所有面试都会考的点:监听地址。端口和IP是绑在一起的,有人问“0.0.0.0:80被占,是所有地址的80端口都没法用了吗”?答案不是。0.0.0.0表示监听所有IPv4地址,它占的是“所有本地地址的80端口”这个组合;如果另一个进程想单独监听127.0.0.1:80,它会因为0.0.0.0的覆盖范围而冲突。反过来,如果某个进程只监听了127.0.0.1:80,那它在公网网卡上完全没占任何端口,外部请求当然也连不上。
顺带一提,小于1024的端口叫特权端口,非root用户进程默认没有权限绑定。这也是“1024端口分界线”这个说法背后的真实逻辑:内核要求bind特权端口时进程必须有CAP_NET_BIND_SERVICE能力,普通用户要么用root启动,要么给程序加capability。
3.2 ss和netstat怎么选,输出怎么看
先说结论:推荐主用ss,但两台机器都装了netstat就当备用。下面是几个高频命令:
# 看所有TCP监听端口和对应进程,必须加-p才能看PID ss -tlnp # 看所有TCP/UDP连接,带上进程 ss -tunap # 只看指定端口的监听和连接 ss -tlnp | grep 8080 # 等价的netstat写法 netstat -antp | grep 8080ss -tlnp的输出样例:
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=12345,fd=29))这一段信息量极大:
State列:当前socket状态,LISTEN表示在监听,ESTAB表示已建立连接,TIME-WAIT表示连接正在关闭等待。Recv-Q/Send-Q:接收和发送队列。对监听socket来说,Recv-Q表示已完成的握手连接等待accept的数量。如果这个数字一直很大,说明应用层处理不过来,连接在堆积。Local Address:Port:监听地址。注意区分0.0.0.0和127.0.0.1,含义完全不同。Process列:关键信息。内核已经给出了占用这个端口的进程PID和文件描述符编号。没有root权限时这里是空的。
ss比netstat直观的另一个点:它不用解析所有/proc/net文件就给出数据,命令执行速度快一截。在高并发服务器上,netstat -antp可能卡顿,ss基本瞬间返回。
3.3 几种必须认识的TCP状态:LISTEN、ESTABLISHED、TIME_WAIT、CLOSE_WAIT
运维排查端口问题,绕不开TCP状态。我列一张最实用的表,按认识优先级排序:
| 状态 | 含义 | 出现在哪一侧 | 排查提示 |
|---|---|---|---|
| LISTEN | 正在监听,等待连接 | 服务端 | 服务正常对外提供服务 |
| ESTABLISHED | 连接已建立,数据可传输 | 两端 | 正常,但量大时注意fd和带宽 |
| SYN_SENT | 主动发起连接,等对方SYN+ACK | 客户端 | 对方可能不存在或防火墙拦截 |
| SYN_RECV | 收到SYN,等最后一次握手确认 | 服务端 | 可能是半连接洪泛,看Recv-Q |
| TIME_WAIT | 主动关闭方等待2MSL,确保旧包消失 | 主动关闭方 | 大量出现是正常现象,别慌 |
| CLOSE_WAIT | 对方已关闭,本方还没关自己的socket | 被动关闭方 | 大量出现=应用bug,连接泄漏 |
| FIN_WAIT_2 / LAST_ACK | 关闭过程中的过渡态 | 对应端 | 少量正常,堆积说明异常 |
我最想强调两个状态。
第一是TIME_WAIT。高并发短连接场景下,主动关闭连接的一方会产生大量TIME_WAIT,这本身就是TCP可靠关闭的机制,不是故障。默认情况下它约63秒后自动消失。如果一看到几万个TIME_WAIT就急着调内核参数,先冷静:你的连接模型本身改一改(比如用长连接复用),比动net.ipv4.tcp_fin_timeout更健康。
第二是CLOSE_WAIT。这个状态堆积起来几乎都是应用的问题:对端已经发起了关闭,但你的程序没有调用close(),socket挂在半关闭状态。常见于业务代码里读取完响应后忘了关闭连接。大量CLOSE_WAIT意味着文件描述符会被耗尽,最终报“Too many open files”。排查它,先从ss -tanp | grep CLOSE_WAIT找到是哪些进程,再看它们的逻辑有没有正确关闭连接。
4. 第三剑:lsof——打通进程与端口的最后一米
4.1 从端口反查进程:lsof和fuser
ps看进程、ss看端口,但真要把“端口号”变成“PID+进程名+文件描述符”,最顺手的是lsof。
查8080端口被谁占用:
lsof -i:8080输出:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME java 12345 app 29u IPv6 123456 0t0 TCP *:8080 (LISTEN)这里面能直接看到进程名、PID、文件描述符号(29)、socket类型和监听地址。比ss多给了一个信息:FD编号,这对接下来lsof -p深挖很重要。
fuser是另一个更“暴力”一点的反查工具:
fuser 8080/tcp fuser -v 8080/tcp fuser -k 8080/tcpfuser -v会打印占用的进程,-k会直接发SIGKILL。我的建议是:日常排查可以看fuser -v,但fuser -k要非常谨慎地使用。它默认直接SIGKILL,如果8080端口背后是一个正在跑业务的进程,你没有确认就把它杀了,后果自己体会。真要用,可以先用fuser -v确认PID,再用ps看这个进程是什么,最后再决定怎么处理。
4.2 从进程反查端口:lsof -p PID
反过来,已知一个PID,想查它到底监听了哪些端口、和哪些IP建立了连接:
lsof -p 12345 | grep TCP输出里能看到这个进程所有的TCP socket:监听中的显示LISTEN,已连接的显示对端IP和端口。这在排查“这个进程是不是在偷偷外联”“配置里写的端口到底生效没有”的时候极其有用。
还有一个更狠的用法,查看某个进程打开的所有文件描述符,而不仅仅是socket:
lsof -p 12345你会看到它打开了哪些普通文件、哪些日志文件、哪些socket、哪些管道。Linux“一切皆文件”的理念在这里体现得淋漓尽致。如果一个进程的文件描述符数量异常增多,lsof -p能直接告诉你它到底在死死攥着什么东西不放。
4.3 为什么lsof本质上是文件系统的侦探
lsof能同时查出进程和端口,秘密在于/proc文件系统。Linux内核把每个进程的信息都暴露在/proc/PID/下面,其中/proc/PID/fd/目录里,每个打开的文件描述符对应一个符号链接。执行ls -l /proc/12345/fd,你能看到这个进程所有打开的fd指向哪里——普通文件会指向路径,socket会指向socket:[inode号]这样的描述。
lsof就是把这些信息整理翻译成人类可读的格式。所以,即便你在一台没装lsof的机器上,也能用原始方式粗略侦察一个进程的socket:
ls -l /proc/12345/fd | grep socket这个思路在排查容器、或者系统被精简过的情况时很管用,也是理解lsof原理的一条捷径。
4.4 常见坑:权限与不可见性
用lsof和ss查别的进程时,如果当前用户权限不够,Process列会为空,lsof也只会显示你自己的进程信息。解决办法很简单:用root,或者用sudo。线上环境出于安全考虑通常不会给所有运维同学root权限,但排查网络问题时,sudo lsof -i:8080几乎绕不过去。
另一个坑在容器环境:在容器内执行lsof只能看到容器内进程,宿主机的端口映射在容器里看不见。你看到容器内8080端口空闲,但宿主机上映射的8080却被另一个容器占了——这时候得在宿主机上查,别在容器里傻找。
5. 实战:一次端口被占引发的完整排查链路
理论讲了这么多,必须来一场完整的实战演练。场景很常见:部署一个新服务,启动时报Address already in use,端口8080。下面是我会走的完整排查链路,每一步都讲为什么。
5.1 第一步:先看谁占了端口,不要盲目重启
第一反应不是把服务再起一次,而是先看端口的状态:
ss -tlnp | grep 8080输出:
LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=23456,fd=21))好了,占用者是PID 23456的一个Java进程。但这还不够,不能因为这个PID存在就直接kill -9。
5.2 第二步:用ps确认这个进程能不能动
ps -fp 23456输出:
UID PID PPID C STIME TTY TIME CMD app 23456 1 0 3月12 ? 00:10:22 java -jar old-app.jar --server.port=8080看到PPID=1,且STIME是3月12日,说明这是个已经运行很久的Java进程,很可能是上次发版时没有停干净的旧版本服务。这时候有两种处理思路:
- 如果它就是旧版服务,且确认要部署新版顶替它:用服务管理工具正常停止,不要直接
kill -9。 - 如果它根本不是自己负责的服务:先查一下它是哪个应用、哪个团队在用的,避免误杀。
这一步是整个排查里最容易省掉、也最容易出事的环节。我见过太多人查完ss就kill -9 23456,结果那是隔壁团队的数据库管控进程。判断进程能不能动,ps -fp给出的PPID、启动时间、完整命令行就是三个最直接的依据。
5.3 第三步:按顺序停进程,而不是上来就kill
如果确认要清理这个进程,正确的顺序是:
- 先找有没有系统d服务管理:
systemctl status 23456或systemctl list-units | grep old-app,能走systemctl就走systemctl stop。 - 没有服务管理,就发
SIGTERM让进程自己收尾:kill 23456。 - 过几秒检查是否退出:
ps -fp 23456。 - 还没退出,再用
SIGKILL:kill -9 23456。
为什么这个顺序很重要?SIGTERM允许进程做清理工作——关闭数据库连接、写结束日志、释放锁;SIGKILL则直接告诉内核“就地正法”,进程无法做任何善后。对Java进程来说,被kill -9还可能影响某些有状态数据的持久化,甚至日志文件处于写一半的状态。
5.4 第四步:进程杀完端口还报“被占”?考虑TIME_WAIT
这是最常见的诡异情况:用ss -tlnp | grep 8080已经查不到监听进程了,但服务启动依然报Address already in use。这时候要看的是另一个输出:
ss -tan | grep 8080如果看到大量TIME-WAIT状态的连接,比如TIME-WAIT 0 0 1.2.3.4:8080 1.2.3.4:56789,原因就清楚了:虽然监听进程已退出,但系统里仍有大量旧连接处于TCP的TIME_WAIT关闭状态,端口会在约2MSL时间内继续被占用。
这种情况通常不影响正常使用——因为绝大多数服务端框架默认设置了SO_REUSEADDR,允许新进程在TIME_WAIT状态下重新绑定端口。如果你服务的起停代码是手写socket且没开这个选项,就会卡住。解决办法两个:
- 在代码里加上
SO_REUSEADDR,这是根治方案; - 临时绕过的话,换个端口启动,或等待两分钟再启动。
值得单独说一句:看到TIME-WAIT不要恐慌地去改tcp_fin_timeout之类的内核参数。除非这是长期、大规模、影响业务的场景,否则绝大多数情况下它是正常关闭机制的副产品。
5.5 第五步:验证链路真的通了
进程清理完、新服务起来后,不要急着宣布胜利,做一套闭环验证:
# 确认新进程监听成功 ss -tlnp | grep 8080 # 本机自测 curl -v http://127.0.0.1:8080/health # 确认进程PID正确 ps -fp $(pgrep -f new-app.jar)这三步走完,才算真正排查结束。整个链路回顾起来就是:ss找到端口占用者 →ps判断进程身份与状态 → 决定怎么处理 → 杀完检查TIME_WAIT→ 重启后验证。三把剑在这里各自出场一次,缺一不可。
6. 端口通不通?telnet、nc、纯bash脚本三种姿势
6.1 telnet:最直观、但也最容易被环境限制
判断一个端口通不通,最经典的姿势是telnet:
telnet 192.168.1.50 8080连上了会显示Connected to 192.168.1.50;被拒绝则提示Connection refused;对方没响应则长时间卡在那。问题在于:很多系统默认不装telnet客户端,Windows 7/10也默认没启用这个功能。
Windows这边,在“启用或关闭Windows功能”里勾上“Telnet客户端”,重启后即可用。我见过不少同事在这一步被卡住,所以顺带提一句。但说实话,如果让我在日常运维里选,我更推荐nc。
6.2 nc:更现代的端口探测姿势
nc(netcat)几乎是我探测端口的第一选择了。最常用的探测姿势:
nc -zv 192.168.1.50 8080-z:只扫描不发送数据,专门测试连通性-v:显示详细信息- 还可以加
-w 3设置超时时间,避免在不可达IP上干等
输出Connection to 192.168.1.50 8080 port [tcp/*] succeeded!就是通了。nc还经常用来看某个端口返回的协议初始内容,比如:
echo "" | nc -w 2 192.168.1.50 3306能看到MySQL的版本握手信息,这在判断“端口通但协议不对”时非常有用。
6.3 纯bash的/dev/tcp探测:能少装一个依赖就少装一个
在某些最小化安装的服务器上,nc也没装,telnet更不可能有,但你又必须测端口。其实bash自己就内置了/dev/tcp这个虚拟设备:
timeout 3 bash -c 'cat < /dev/null > /dev/tcp/192.168.1.50/8080' && echo "port open" || echo "port closed/timeout"原理:bash会把/dev/tcp/host/port当作一个可读写的网络socket,cat < /dev/null往里传空数据并等待连接建立。能建立就说明端口通。用timeout 3限制最长等待3秒,避免卡死。
批量测多个端口,可以写个简单循环:
for p in 22 80 443 3306 8080; do timeout 2 bash -c "cat < /dev/null > /dev/tcp/192.168.1.50/$p" 2>/dev/null \ && echo "port $p: open" \ || echo "port $p: closed/filtered" done这段脚本在纯bash环境里就能跑,是出差到陌生环境时最轻量的探路工具。
6.4 端口不通时,别再重复测了,先分清是哪一类问题
端口不通有至少三种截然不同的原因,用同一个命令测N遍也没有意义。先做一个判断:
| 测试结果 | 可能原因 | 下一步操作 |
|---|---|---|
| 立即Connection refused | 目标主机可达,但端口上没有服务监听 | 上目标机查ss -tlnp,看服务是否真的起来了、是否监听在错误地址 |
| 长时间超时 | 目标主机不可达,或中间防火墙丢弃了包 | 用ping和traceroute判断网络路径;检查防火墙规则 |
| telnet能进但随后被断开,或打出乱码 | 端口有服务,但协议不匹配或服务异常 | 检查是不是端口对应了别的协议;看服务日志,可能是TLS握手失败等 |
最典型的一个误判案例:服务监听在127.0.0.1:8080,你拿另一台机器去测192.168.1.50:8080,结果永远Connection refused。这不是网络问题,是服务根本没监听对外网卡。先回到第二剑的ss -tlnp,看一眼监听地址是不是0.0.0.0,这类问题十秒就能定位。
7. 冷门但救命的细节:进程名、容器端口、资源极限和面试真题
7.1 进程名匹配的坑:截断、重名、pgrep的局限性
前面提到过Linux进程名15字符截断的问题。这里再补充一个实际教训:用ps -C匹配进程名时,-C匹配的是进程名(comm),不是完整命令行。而我们的Java进程往往是通过java -jar app.jar启动的,comm显示的是java,不是app.jar。所以ps -C app.jar什么都匹配不到很正常。
更可靠的是按完整命令行匹配:
pgrep -af "app.jar"如果重名进程很多,还有pidof和pgrep -o、pgrep -n可以取最老或最新的实例。这些看似小细节,在自动化脚本里就是致命的bug:本意是kill旧服务,结果pkill java把所有Java进程全杀了——这种事故我至少在事故报告里见过三次。
7.2 前台进程、孤儿进程与正确后台运行
热搜里“监控前台进程”背后有一个很实在的问题:服务在终端前台跑着,一关终端进程就没了,或者SSH断掉服务就被杀了。
这里的关键词是会话和终端信号。普通情况下,进程属于当前shell的会话,终端关闭时会收到挂断信号SIGHUP并退出。解决办法三个选项:
nohup ./server & ./server & disown setsid ./servernohup:忽略SIGHUP,但进程还是当前会话的成员;disown:把作业从shell的作业表里移除,让它不怕shell退出;setsid:直接让进程成为新会话的领导者,和当前终端彻底脱离。
现代Linux服务器上,最正经的做法是写systemd unit,用systemd托管,这样它开机自启、崩溃拉起都有系统保障。如果只是临时跑个东西,nohup或setsid足够,但千万别再养成“开个screen/ssh挂着”的习惯。
7.3 WSL、容器和端口映射:别在本机端口里找不存在的进程
随着WSL和容器环境越来越普及,“端口看着被占,但进程查不到”的情况也越来越多。原因通常是:你看到的端口映射并不在当前这个命名空间里。
WSL场景:WSL里启动的服务通常可以通过localhost从Windows侧访问,因为WSL2默认做了端口转发;但反过来,在WSL里查宿主机Windows的端口映射是看不到的。排查这种问题,思路是分清两侧的命名空间。
容器场景:宿主机上ss -tlnp显示0.0.0.0:8080被某个Docker-proxy进程占用,但ps里看不到业务进程本身,因为业务进程在容器内部命名空间。这时候要docker ps和docker exec进容器查,别拿着宿主机ps结果一脸困惑。
这些环境用“三剑客”时,边界感很重要——先确认你自己在哪一层,再判断该用哪一层的工具。
7.4 系统资源极限:句柄数、进程数、Too many open files
端口和进程排查到后期,经常撞上资源限制。最典型的是Too many open files。这里我想讲清楚一个最常见的误区:
ulimit -n限制的是“一个进程能打开的文件描述符数量”,不是系统全局的。所以服务报Too many open files时,先看这个进程的fd数:
ls /proc/PID/fd | wc -l如果这个数接近ulimit -n的软限,说明该调进程的fd限制,或者排查是不是有fd泄漏。我之前处理过一个网关进程,fd数一路涨到5万,查lsof -p PID发现全是CLOSE_WAIT的socket——不是系统配置不够,是代码里连接没关闭。
另外,进程数的限制也值得留意。如果大量线程或孤儿进程堆积,系统pid_max也可能成为瓶颈。这类问题初期症状很隐蔽,服务随机失败、端口连不上、进程起不来,最后用dmesg -T | tail一看,全是fork: Cannot allocate memory之类的字眼。排查这类问题,ss -s和ps -eLf | wc -l能快速估算系统整体连接数和线程数。
7.5 面试常考的这些概念,和现实排查怎么对应
热搜词里有“进程通信(IPC)”“进程等待wait”“线程与进程”,这些概念在面试里高频出现,但你要是只背概念不联系实际排查,面试官一问到场景就露馅。我通常这样理解:
- 进程等待(wait)对应僵尸进程的产生和回收,排查僵尸进程时就是现学现用。
- 进程通信(IPC)包括管道、信号量、共享内存、socket,看
lsof -p PID时那些FIFO、事件fd、socket就是这些IPC的具体形态。 - 进程调度算法对应
ps里的STAT状态、pri、ni优先级列,也对应D状态进程为什么拿到信号也无法退出。
说白了,考点从来不是靠背,是你在真实排查里遇到过、处理过,跟面试官聊的时候能自然讲出“当时我看到STAT=D的状态,就知道这不是应用能解决的,要去查底层I/O”。这种回答和背概念的回答,高下立判。
8. 我日常的“三剑客”使用清单与最后的小建议
文章到这里,核心内容已经讲完。最后不用做什么大总结,我只分享一些自己长期保留下来的使用习惯和踩出来的经验。
我习惯在每个新环境先确认命令可用,然后把这几个别名写进~/.bashrc:
alias port='ss -tlnp' alias prock='ps -eo pid,ppid,user,stat,lstart,time,cmd' alias whoisport='lsof -i' alias cport='lsof -Pn -i'lsof -Pn这个细节值得单独提一下:-P禁用端口号转服务名,-n禁用IP转主机名。不加这两个参数时,lsof可能因为反向DNS解析卡住,在DNS异常环境里让人抓狂。一加,秒出结果。
我最后想说的建议是:这些命令的威力不在单个命令背得多熟,而在遇到问题时的组合节奏。服务起不来先看监听、确认进程身份后再决定停不停、杀完要看时序状态、通不通要分层测。这套节奏只有在真实事故里练过才会有肌肉记忆,平时业务平稳时,找个测试环境故意模拟几次“端口被占”“进程假死”,把这套链路从头走一遍,比刷十篇教程都管用。
三剑客听着高大上,底层逻辑其实就是:先看清,再动手,最后验证。运维这份工作,火眼金睛靠的不是玄学,是每一步都有依据。