☰
lsof实战:从端口占用到文件句柄泄漏的排查指南
2026/10/27 19:51:17 网站建设 项目流程

排查端口冲突的时候,我第一把抓起来的工具基本都是 lsof。这条全称 list open files 的命令,直译过来是“列出打开的文件”,听着像是个查文件的工具,实际上它覆盖了端口占用、网络连接、文件句柄泄漏、磁盘空间不释放、进程工作目录定位这一大票日常运维场景。Linux 的设计哲学是“一切皆文件”,套接字是文件、管道是文件、设备是文件、内存映射也是文件,所以“谁打开了什么文件”这个问题一旦能用一条命令回答,排查效率会立刻上一个台阶。这篇内容面向的是已经能登录服务器、会用 ps 和 grep,但对 lsof 的输出还停留在“看得懂一半”的同学,也适合那些被“磁盘满了 du 却查不出来”折磨过的老手回头补一补原理。整篇我会从输出结构讲起,一路讲到参数组合的坑、性能代价,以及我自己踩过的几次典型事故,尽量让你看完就能直接抄命令去用。

1. 一个命令背后的问题现场:为什么端口被占了总能想到它

1.1 从“文件即一切”的设计哲学说起

很多人第一次接触 lsof,是在服务启动失败、日志里冒出一行地址已被占用的报错时。这个时候你知道有个进程占着 8080,但用 ps 看一圈,进程名全是业务模块,看不出谁监听在哪个端口。lsof 之所以能解决这个问题,是因为它问的不是“进程在干什么”,而是“进程打开了哪些文件”。在内核视角里,一个监听套接字就是一个文件描述符,和普通文件、管道、设备节点放在同一张打开文件表里。lsof 的做法就是把这张表按进程维度展开,然后逐个打印出来。这就意味着它天然具备一种双向查询能力:既可以从进程反查文件,也可以从文件反查进程。前者对应“这个进程到底占了哪些资源”,后者对应“这个资源到底被谁占着”。这两个方向恰好覆盖了绝大部分资源类故障的排查路径。

理解这一点之后,很多看似奇怪的输出就顺了。比如你会在结果里看到 cwd 这一行,它代表的不是进程打开了某个文件,而是进程的当前工作目录,本质上也是一个目录文件句柄。再比如你会在网络连接的行里看到 NODE 列写着 TCP,那是因为套接字没有磁盘 inode,内核只能给出协议相关的标识。lsof 输出的不是“端口表”,而是“打开文件表”,这个认知差决定了你能不能读懂它的全部列。

1.2 什么时候不该用它:能力边界与替代路径

lsof 不是万能的,用错场景反而浪费时间。第一个明显的边界是性能。它的数据来源主要是 /proc 下的进程信息和内核内存结构,全量跑一次要遍历系统上所有进程的所有文件描述符。在一台开了几十万句柄的机器上,lsof 跑一次要好几秒,内存占用也可能冲到几百兆。如果只是想看某个端口有没有人监听,用 ss 或 netstat 这类直接读内核网络表的工具,速度会快一个数量级。第二个边界是权限。普通用户跑 lsof,默认只能看到自己名下的进程,别人的进程会被过滤掉,于是你得到一份“空结果”,误以为端口没被占。第三个边界是某些特殊文件系统。挂在网络文件系统或者用户态文件系统上的路径,lsof 去 stat 的时候可能卡住甚至报警告,这时候要么加参数抑制,要么干脆换个思路。

所以我的习惯是:定位到具体进程之后,才请 lsof 出场查它打开了什么;单纯的端口监听检查,先用轻量工具扫一遍。分清楚“查连接”和“查句柄”是两件事,能省下不少时间。

2. 把输出切成九列:lsof 的信息结构和判读逻辑

2.1 默认九列逐列拆解

不加任何参数的 lsof 输出很长,但结构是固定的九列。我把它拆成一张表,方便对着看:

列含义判读要点
COMMAND进程名默认截断到 9 个字符,容易被误认,配合 -c 或 +c 调整宽度
PID进程号最关键的定位锚点,配合 -p 做二次过滤
USER进程属主排查权限问题时先看这一列,注意可能是纯数字 UID
FD文件描述符语义最丰富的一列,后面单独讲
TYPE文件类型REG/DIR/IPv4/IPv6/unix/PIPE 等,决定后续链接方向
DEVICE设备号主设备号,次设备号;网络连接一般是协议相关值
SIZE/OFF大小或偏移普通文件是字节数,网络连接常显示为 0t0
NODE索引节点普通文件是 inode 号,网络连接显示协议名
NAME名称绝对路径、挂载点、连接四元组、管道名都在这

单看某一列信息量有限,真正有用的是列之间的联动。举个常见的输出:

COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME appsvc 18432 app cwd DIR 253,0 4096 2 /opt/app appsvc 18432 app txt REG 253,0 23451784 131073 /opt/app/bin/appsvc appsvc 18432 app 7u IPv4 882134 0t0 TCP *:8080 (LISTEN) appsvc 18432 app 12w REG 253,0 1048576 262145 /var/log/app/access.log

从 FD 是 7u、TYPE 是 IPv4、NAME 带 (LISTEN) 这三条同时成立,才能确定这是一个监听套接字。如果只看 NAME 里的星号和端口,遇到 UDP 或者已建立连接的场景就会判断错。同理,最后一行 FD 是 12w,w 表示以写方式打开,NAME 指向日志路径,这就说明该日志文件正被持续写入,如果不小心把它 rm 掉,空间不会释放。

2.2 FD 列的隐藏语义与 TYPE/NODE 的联动

FD 列是整个输出里信息密度最高的地方,值得单独展开。纯数字后面可以跟三种模式标记:r 表示只读、w 表示只写、u 表示读写。没有标记说明进程没有指定打开模式,仅持有引用。除此之外还有几个固定单词,含义如下:

  • cwd:进程当前工作目录。排查“进程跑在哪个目录下”时直接看这行,比重启服务去猜路径靠谱得多。
  • rtd:根目录,通常是 chroot 场景下的根。
  • txt:程序代码段,也就是可执行文件本身。删掉可执行文件但进程还在跑,这一行会显示带删除标记的路径。
  • mem:内存映射文件,常见于共享库。
  • mmap:内存映射设备。
  • DEL:文件已被删除,但仍然被打开。这个标记是磁盘空间排查的核心线索。

TYPE 和 NODE 的配合也有讲究。TYPE 为 REG 时,NODE 是真实的 inode 号,用 stat 命令能对上;TYPE 为 IPv4 或 IPv6 时,NODE 显示的是 TCP 或 UDP 协议名,不是 inode;TYPE 为 unix 时,NODE 又变回 inode,因为 unix 域套接字在内核里有对应的节点对象;TYPE 为 PIPE 时,NODE 往往显示一个不易对应的编号。这几个对应关系看起来琐碎,但在写过滤脚本的时候是硬约束。你按 inode 去匹配网络连接,永远匹配不上;反过来按协议名去过滤 unix 套接字,也会一无所获。

提示:判断一个文件是否“真的被删除”,不要只看 NAME 里有没有路径,要看 FD 列是否存在 DEL 标记,或者用 +L1 参数专门筛链接数为 0 的文件。有些版本在 NAME 后面追加删除提示,有些版本只在 FD 列标记,行为不完全一致。

3. 六大高频场景的命令拼装

3.1 端口占用与连接状态定位

最经典的用法是查某个端口被谁监听。直接写lsof -i :8080看起来没问题,但它有个隐蔽行为:这个条件同时匹配本地端口和远端端口。也就是说,如果有一批客户端连到本机的 8080,那些已建立的连接也会出现在结果里,把真正的监听进程淹没掉。更精确的写法是叠加 TCP 状态过滤:

lsof -nP -i :8080 -sTCP:LISTEN

-n 禁止反向解析主机名,-P 禁止把端口号翻译成服务名,这两个参数能显著减少卡顿和误读。如果你需要看完整连接分布,把状态换成 ESTABLISHED 或 CLOSE_WAIT 即可。还有一个容易忽略的点:不指定协议时,IPv4 和 IPv6 会一起列出来,输出里会出现两个监听行,别以为是重复占用。

如果只是想拿到 PID 交给别的命令,用 -t 参数只输出 PID 列表,配合管道非常顺手:

kill -TERM $(lsof -t -i :8080 -sTCP:LISTEN)

不过这种写法我一般会先lsof -t ...看一眼结果再执行,避免误杀。生产环境上更稳妥的做法是先确认进程属主和启动参数,再决定是否发送信号。

3.2 磁盘明明满了却 du 不出来

这是 lsof 最值钱的一个场景。现象是 df 显示磁盘使用率接近 100%,但用 du 逐层统计目录,加起来远远对不上。原因几乎总是同一个:某个大文件被 rm 或者被日志轮转删除,但进程还持有打开的文件描述符,内核不会真正释放数据块,空间就一直被占着。定位方法是筛出链接数为 0 的文件:

lsof +L1

+L1 的意思是“链接数小于 1”,也就是已经被删但仍在打开状态的文件。输出里 NAME 列会带路径和删除标记,SIZE/OFF 列就是它能释放的空间大小。看到结果之后有两条路可走。

第一条是让进程重新打开文件,比如给应用发一个信号触发日志重开,或者直接重启。这种方式最安全,但会中断服务。第二条是从 /proc 里把文件内容捞出来再处理:

cp /proc/18432/fd/12 /tmp/recovered.log

PID 和 fd 号从 lsof 输出里对应取。这里要注意,如果进程还在往这个 fd 里写,复制出来的内容只是某一时刻的快照。另外复制之前先确认目标分区有足够空间,别把问题从一块盘挪到另一块盘。还有一种比较激进的处理方式是对该 fd 执行截断:

: > /proc/18432/fd/12

效果是文件被清空但句柄保留,空间立刻释放,进程继续往同一个 fd 写也不会报错。这个操作适合日志文件,但用在数据库或消息队列的数据文件上很危险,可能造成数据文件内部结构不一致。我的经验是,对日志类文件可以截断,对数据类文件一律走重启或重新打开的路子。

3.3 进程、用户、目录的双向查询

反查进程打开的文件,最常用的是lsof -p PID。输出会包含这个进程的 cwd、可执行文件、共享库、配置文件和所有网络连接,信息量很大。实际排查时我更倾向于先缩小 FD 范围,比如只看文件描述符 0 到 9:

lsof -p 18432 -a -d 0-9

这里的 -a 是必须加的,原因在第 4 节会详细说。反过来,从目录反查进程用lsof +D /opt/app,注意这是递归遍历,目录一大就非常慢,能改成+d只查一级就别用+D。从用户维度查用lsof -u app,想排除某个用户就写lsof -u ^root。

还有一个被低估的用法是按命令名过滤:lsof -c appsvc会匹配所有以 appsvc 开头的进程名,前缀匹配的语义和 grep 完全不同,不需要写通配符。默认命令名只保留 9 个字符,超过就被截断,这时候用+c 20把宽度调大,否则你会因为名字被截断而匹配不到目标进程。

3.4 句柄泄漏与文件描述符上限排查

应用报“打开文件过多”的时候,需要同时确认两件事:当前进程开了多少个句柄,以及系统上限是多少。当前数量可以直接数 /proc:

ls /proc/18432/fd | wc -l

这个方式比跑 lsof 快得多,适合高频采样。想看具体是哪些文件在堆积,再用 lsof 按 FD 数字过滤。常见的泄漏形态有两种:一种是某个目录下的临时文件不断被打开却从不关闭,表现为大量 REG 类型条目指向同一目录;另一种是套接字泄漏,表现为大量 IPv4 条目停留在某个状态。区分这两类只需要看一眼 TYPE 列的分布。

上限方面,进程级限制看 /proc/PID/limits 里的 open files 一行,系统级总量看内核参数。很多人只调了进程级 ulimit,忽略了系统级上限,结果用户数一多还是撑不住。我的建议是把监控做在前面:定期采集句柄数量并画曲线,比出事之后再翻 lsof 有效得多。

3.5 网络连接堆积:从 CLOSE_WAIT 找元凶

连接数异常时,lsof 能直接告诉你哪些进程在堆积连接。查 CLOSE_WAIT 状态:

lsof -nP -i TCP -sTCP:CLOSE_WAIT

如果这个状态数量持续上涨,基本可以判定是应用侧没有正确关闭连接,问题在代码而不在网络。查已建立连接并按进程聚合:

lsof -nP -i TCP -sTCP:ESTABLISHED | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn

这条命令能快速看出是哪个进程占了绝大多数连接。如果发现某个进程的连接数远高于其他实例,再看它的远端地址分布,就能判断是流量倾斜还是连接没释放。需要提醒的是,连接数很大的时候 lsof 输出会非常长,建议加| head或者按 PID 二次过滤,别直接刷屏把终端卡死。

3.6 定期采样与脚本落地的写法

lsof 自带重复模式,用-r指定间隔秒数就能持续输出:

lsof -r 5 -p 18432 -a -d 0-20

这个模式适合短时间观察,长时间跑会一直占用终端。真正要落地到巡检脚本里,我更倾向于用 -F 输出可解析格式,避免解析对齐的文本列。比如只取 PID、命令名和文件名:

lsof -F pcn -p 18432

输出会变成每行以字段标识开头的紧凑格式,p 开头是 PID,c 开头是命令名,n 开头是文件名。这种格式不怕列宽变化,也不怕文件名里带空格——这一点很关键,默认输出用空格对齐,路径里有空格时按列切分会直接切错。写脚本时我踩过这个坑,后来统一改成两种方案:要么用 -F,要么在 awk 里用$9之后的内容整体拼接。

注意:不同发行版打包的 lsof 版本差异不小,某些参数在新版本被移除,某些参数在老版本不存在。脚本里用到不常见参数时,先在目标机器上跑一次lsof -v确认版本,再决定写法。

4. 参数体系与组合陷阱:OR 还是 AND 是个大问题

4.1 参数分类速查表

lsof 的参数数量很多,硬背没意义,按用途分组记更实用:

分类常用参数说明
进程维度-p、-c、-u、-g按 PID、命令名前缀、用户、进程组过滤
文件维度-d、+d、+D、+L1按 fd 编号、目录层级、链接数过滤
网络维度-i、-U、-n、-P按协议/端口/主机、unix 套接字过滤,禁用解析
输出控制-t、-F、-w、+c只出 PID、字段化输出、抑制警告、调整命令名宽度
性能与安全-b、-r、-X避免阻塞调用、循环输出、跳过部分协议文件

表格里 -X 需要单独说明:它的作用是跳过 TCP 和 UDP 文件,在某些版本上能明显加快速度,但较新的版本已经把它移除了。如果你的脚本依赖这个参数,迁移机器时很可能直接报错。

4.2 -a 的坑与性能参数

-a是我认为最容易被忽略的参数,没有之一。lsof 的多个选择条件默认是“或”的关系,不是“与”。也就是说,lsof -p 18432 -i的含义是“PID 为 18432 的进程”或者“所有网络连接”,而不是“18432 这个进程的网络连接”。第一次遇到这个行为的人几乎都会困惑:明明只查一个进程,为什么输出里出现了系统上所有监听端口。解决办法就是显式加 -a:

lsof -a -p 18432 -i

加上之后才是交集语义。同理,lsof -p 18432 -d 0-9也要加 -a 才是“这个进程的 0 到 9 号 fd”。这个设计谈不上优雅,但既然是既定行为,记住“多条件必加 -a”就能避开九成的误判。

性能方面,-n 和 -P 是最值得加的两个参数。默认情况下 lsof 会对网络地址做反向解析,DNS 慢的时候一条命令能卡十几秒;端口号也会被翻译成服务名,8080 变成 http-alt 之类,看着反而不直观。这两个参数关掉解析之后,输出更干净,速度也更快。此外 -b 可以用来避免那些可能造成阻塞的内核调用,在挂载了异常文件系统的机器上比较有用,代价是会损失一部分信息。

4.3 为什么它在生产机上很“重”

有必要说明 lsof 的资源代价,避免有人在高负载机器上随手全量跑。它的工作方式是遍历系统中的进程和文件描述符,构建完整的内存数据结构之后再输出。句柄数量巨大的机器上,这个过程占用的内存可能达到几百兆,CPU 也会有一个明显的尖峰。如果机器本身已经内存紧张,全量 lsof 有可能成为压垮它的最后一根稻草。

规避办法有几个。能用 PID 限定的就限定,lsof -p相比全量能省下大量扫描开销。能用 -c 限定命令名的也加上。实在需要看全局视图,挑业务低峰执行,并且用-w抑制掉那些文件系统访问失败的警告,避免输出被噪声淹没。我个人的习惯是:全量 lsof 只在明确需要“找出谁占着这个文件”时才用,日常巡检一律走 /proc 直接读,成本低得多。

5. 报错与异常输出的排查实录

5.1 权限不足与输出缺失

普通用户跑 lsof 最常见的结果是“查不到”。端口确实被占着,但lsof -i :8080返回空。原因不是命令写错了,而是权限不够,看不到其他用户的进程信息。验证方法很简单,在前面加提权执行,如果结果立刻出现,就说明是权限问题。

另一种输出缺失更隐蔽:部分进程出现在结果里,但网络连接一行都没显示。这通常也和权限有关,某些内核配置下普通用户读取他人进程的套接字信息会被过滤。所以排查网络类问题时,我基本都用提权身份执行,避免被“看起来没占用”误导。顺带一提,输出里 USER 列显示成数字而不是用户名,是因为该 UID 在本地没有对应的用户记录,不影响判断,只是看着别扭。

5.2 警告刷屏与结果为空

跑 lsof 时被一堆警告刷屏,是很多人第一次用就放弃的原因。典型形态是提示某个挂载点无法 stat,比如用户态文件系统或者已经失联的网络挂载。这些警告不影响主结果,用 -w 直接抑制即可。如果警告伴随命令长时间卡住,说明它在尝试访问一个无响应的挂载点,这时候加 -b 跳过相关调用,或者干脆把那个挂载点卸载掉再排查。

结果为空还有一类原因是条件写错了。最典型的是忘了加 -a,导致条件变成并集之后被其他条件覆盖;其次是命令名前缀不匹配,进程名被截断到 9 个字符,你按完整名字去查自然查不到;还有就是端口号写成了服务名,而 -P 又没加,双向解析对不上。遇到空结果,我的排查顺序是:先去掉所有条件跑一次看有没有输出,再逐个加条件定位是哪一步把它过滤没了。

5.3 常见问题速查表

现象可能原因处理方式
查不到端口占用权限不足,看不到他人进程提权执行后重试
输出里出现全系统监听多条件未加 -a,变成并集显式补 -a
命令卡住十几秒反向解析慢或挂载点无响应加 -n -P,必要时加 -b
警告满屏特殊文件系统 stat 失败加 -w 抑制
按进程名查不到命令名被截断到 9 字符用 +c 调整宽度或前缀匹配
磁盘满但 du 对不上已删除文件仍被打开用 +L1 筛选
路径含空格解析错列默认输出按空格对齐改用 -F 字段化输出
连接数统计偏差输出被截断或权限过滤加 -n -P 并核对权限

除了表格里的问题,还有一个不算报错但很容易误判的现象:同一个进程在输出里出现多行,看起来像重复。实际上每一行对应一个打开的文件描述符,同一个可执行文件可能同时以 txt 和 mem 两种方式出现,这是正常的,不要当成重复项去重。

6. 我踩过的坑与落地建议

6.1 三个场景的复盘

第一个场景是某次磁盘告警。监控显示数据盘使用率 98%,但把目录逐层统计下来只有 40% 左右。当时第一反应是有人偷偷往隐藏目录写东西,翻了一圈没有。后来用lsof +L1一跑,发现一个服务进程持有一个已经删除的日志文件,大小接近 300G,正是缺口所在。处理方式是通知业务侧重新打开日志句柄,空间立刻回落。这件事之后我把 +L1 加进了磁盘告警的排查清单,只要出现 df 和 du 对不上的情况,第一步就跑它。

第二个场景是句柄泄漏。某个服务运行几天之后开始报打开文件过多,重启就好,但根因一直没找到。用 /proc 采样句柄数量,发现曲线是阶梯式上升而不是平稳增长,说明是某个周期性任务在泄漏。再用 lsof 按 FD 过滤看堆积的条目,发现全部指向同一个临时目录下的小文件,最后定位到一个定时任务打开了文件却没有关闭。这个案例说明,句柄数量本身的绝对值意义有限,曲线的形状才是有用信息。

第三个场景是端口误判。某次排查端口冲突,lsof -i :9000输出了几十行,看着像是有多个进程在抢。仔细看才发现,其中大部分是已建立的客户端连接,真正处于监听状态的只有一行。问题就出在没有加状态过滤。从那以后我养成习惯,查监听一律带上-sTCP:LISTEN,避免自己被自己的输出误导。

6.2 一套可以直接用的巡检组合

把上面这些东西串起来,我给日常巡检准备了一组命令,需要的时候直接改 PID 和端口就能用:

# 1. 确认端口监听归属 lsof -nP -i :8080 -sTCP:LISTEN # 2. 统计某进程各类文件类型分布 lsof -nP -p 18432 | awk 'NR>1 {print $5}' | sort | uniq -c | sort -rn # 3. 找已删除但仍占空间的文件 lsof -nP +L1 | awk '$7 ~ /^[0-9]+$/ {print $7, $9}' | sort -rn | head -20 # 4. 连接状态分布 lsof -nP -i TCP | awk '/\(/{print $NF}' | sort | uniq -c | sort -rn

第 3 条里的 $7 是 SIZE/OFF 列,$9 是 NAME 列,前提是路径不含空格。如果环境里路径可能带空格,老老实实换成 -F 输出。第 4 条用括号内容做状态聚合,比按固定列位置切更稳,因为连接四元组的长度会变。

注意:把这些命令放进定时任务之前,先评估执行频率和机器规模。全量 lsof 在大型机上不适合高频跑,建议按需触发或者限定到具体 PID,避免巡检本身成为负担。

我个人在实际操作中的体会是,lsof 的价值不在参数多,而在于它提供的那个“从资源反查进程”的视角。大部分排障工具都是顺着进程往下找,只有遇到“这个东西是谁占着”的问题时,才会发现反向查询有多重要。把这个视角用熟之后,很多以前要靠重启解决的模糊问题,都能在一次命令输出里定位到具体进程和具体 fd。最后再分享一个小习惯:每次用完 lsof 定位到目标,顺手把 PID、端口、路径记在排查记录里,下次同类问题再来的时候,比对一下就能判断是偶发还是累积性故障,比重新从头查一遍省事得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询