遇到端口被占用的时候,我最常看到有人这么干:netstat -tunlp | grep 8080,翻dmesg,然后再挨个ps -ef去猜哪个进程才是罪魁祸首。这种连蒙带猜的排查方式,其实一行lsof就能结束战斗。
lsof全称是 list open files,直译过来就是“列出所有进程打开的所有资源”。它在Linux系统里的地位,有点像一把排查问题的瑞士军刀:端口被占了、文件删不掉、磁盘空间没释放、想知道某个进程到底在读写哪些文件、甚至想查某个用户的所有网络连接,它都能干。如果说ps是看“谁在跑”,netstat/ss是看“谁的网卡在用”,那lsof更像是把进程、文件、网络端口、用户这四张平时分散的表,按需要“join”起来一次性查清楚。这篇文章我打算从最基础的输出字段讲起,再按“查询维度”重新组织参数,然后用三个实际排查场景串一遍,最后聊聊它的原理和边界。不管是刚入门的Linux新手,还是想查漏补缺的老手,都能在这篇里找到有用的东西。
1. lsof的输出到底在说什么:先过读列这一关
很多教程上来就把参数糊你一脸,结果看完还是不知道怎么用。我的建议是先学会看输出,因为lsof几乎所有的查询结果都用同一套格式展示,把这套格式看懂了,后面用参数只是“按需过滤”的问题。
随便跑一条命令,输出大概长这样:
bash复制$ lsof -i:8080
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 12345 app 42u IPv4 52780 0t0 TCP *:8080 (LISTEN)
这里每一列的含义有必要交代清楚,不然你只看到一堆数字和缩写,根本不知道发生了什么。
| 列名 | 含义 | 排障时的主要用途 |
|---|---|---|
| COMMAND | 进程对应的命令名 | 快速判断是什么程序占用了资源 |
| PID | 进程ID | 后续kill、查看进程详细信息的关键 |
| TID | 线程ID,通常为空,多线程程序才显示 | 排查线程级别问题时有用,平时忽略 |
| USER | 运行该进程的用户 | 判断是系统服务还是业务进程 |
| FD | File Descriptor,文件描述符 | 最重要的一列,见下方详细拆解 |
| TYPE | 资源类型 | 判断是普通文件、目录、套接字还是设备 |
| DEVICE | 设备编号,格式为“主设备号,次设备号” | 一般用不到 |
| SIZE/OFF | 文件大小或偏移量 | 排查日志文件大小时偶尔有用 |
| NODE | inode节点号,网络套接字则是协议类型标识 | 理解文件系统时有用 |
| NAME | 完整的路径名、连接信息或协议状态 | 最直接可读的信息来源 |
首次接触lsof的人,卡壳的地方通常集中在FD和TYPE这两列,这两个也确实是最有“信息量”的字段。
FD列表示文件描述符以及打开模式,常见的值有这些:
cwd:进程的当前工作目录。进程启动时所在的目录,通常会持有这个目录的引用。txt:进程运行的程序文件本身,也就是二进制可执行文件。mem:内存映射文件,通常是动态链接库(.so)或者可执行文件被映射到内存中的部分。rwd:以“读-写”模式打开并带有“截断”标记,多用于日志文件。1u、2u:标准输出(stdout)和标准错误(stderr),u代表读写模式。DEL:文件已被删除,但还被进程占用,典型的表现就是磁盘空间不释放。
TYPE列则标识资源类型:
REG:普通文件。DIR:目录。CHR:字符设备,比如终端设备。BLK:块设备,比如磁盘。unix:Unix域套接字,常用于本机进程间通信。IPv4、IPv6:网络套接字。FIFO:命名管道,进程间通信的一种方式。
判读FD和TYPE组合的方式,举个例子你就明白了。假如输出里有一行是:
code复制nginx 3876 www-data 9u IPv4 0t12345 TCP 127.0.0.1:80->192.168.1.10:54321 (ESTABLISHED)
意思是:用户www-data运行的nginx进程(PID 3876)通过IPv4网络,从本机127.0.0.1的80端口,连到了192.168.1.10的54321端口,连接状态是ESTABLISHED。9u表示它的第9个文件描述符,以可读可写方式打开。
走到这一步,你已经能读懂lsof输出的大半内容了。但读输出只是开始,真正厉害的是懂得怎么把lsof的输出“按需裁剪”——这部分靠参数实现。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 按图索骥:把lsof参数按“查询维度”分组记忆
lsof的参数确实多,lsof --help能刷半屏,但如果按“查询维度”来分组,就能摆脱死记硬背。我通常把它分成四类来用。
2.1 查网络:-i系列参数
这是用得最频繁的一组。-i后面可以跟网络地址、端口、协议等多种格式,整体语法是:
code复制-i[46][protocol][@hostname|hostaddr][:service|port]
方括号都是可选项,实际使用可以灵活组合。几个经典用法:
bash复制# 查某个端口被谁占用了
lsof -i:8080
# 查某个端口上的TCP连接,并且只看监听状态
lsof -iTCP:8080 -sTCP:LISTEN
# 查某个端口的UDP连接
lsof -iUDP:53
# 查某一台主机相关的所有连接
lsof -i@192.168.1.10
# 查一个端口范围
lsof -iTCP:1-1024
这里有个特别容易误用的细节想提醒你:-i:8080默认会匹配TCP和UDP两种协议的8080端口,如果你只想查TCP,记得写成-iTCP:8080。另外-sTCP:LISTEN这种过滤项非常有用,能直接筛掉大量ESTABLISHED和TIME_WAIT连接,只留监听中的端口。
2.2 按进程、用户、命令反向查:-p、-u、-c
有时候你已经知道了进程ID,想看看这个进程到底打开了哪些文件,用-p。比如Nginx的worker进程PID是3876:
bash复制lsof -p 3876
按用户查也很常用,比如排查某个应用账号的所有活动:
bash复制lsof -u nginx
按命令名查,用的是-c,注意它是前缀匹配:
bash复制lsof -c ssh
这条会匹配所有命令名以ssh开头的进程,比如sshd、ssh-agent。
排除某些用户用^符号,比如“查看不是root用户打开的文件”:
bash复制lsof -u ^root
2.3 按文件、目录反查谁在用:+d、+D、直接给路径
这是lsof“反查”能力的核心,特别适合解决“文件删不掉”“目录卸载不了”这类问题。
bash复制# 查看某个目录下被打开的文件(不递归子目录)
lsof +d /var/log
# 递归查看某个目录及其子目录下被打开的文件
lsof +D /var/log
如果你已经知道是某一个具体文件,直接把路径给它就行:
bash复制lsof /var/log/nginx/access.log
输出会列出所有正在打开这个文件的进程。+D这个参数在卸载磁盘的时候非常有用,后面场景二会细说。
2.4 组合条件时的那个大坑:默认是OR,不是AND
这一点几乎每个用lsof的人都踩过,值得单独拎出来讲。
lsof默认情况下,你给多个条件,它执行的是或多个条件取并集的逻辑,而不是“同时满足”。举个例子:
bash复制lsof -u root -i
你以为这条命令查的是“root用户的网络连接”,但实际它的输出是:root用户打开的所有文件,加上系统里所有的网络连接。两者取的是并集,结果范围远比你想要的大。
想让它取交集,必须显式加-a参数:
bash复制lsof -a -u root -i
-a的意思是“前面所有条件同时成立”,加了它之后,上面那条才是真正意义上“root用户的网络连接”。
我在实际环境里见过有人因为漏了-a,在几百个连接里翻了一天才找到想看的那个进程。这不算什么难理解的原理,就是条件组合的默认逻辑容易让人想当然。建议以后写组合条件时,养成先问自己一句“我这几个条件是要同时满足,还是满足一个就行”的习惯。
2.5 其它高频小参数:-t、-n、-P
这几个参数不改变查询维度,但能改变输出形态和效率。
-t:只输出PID,不输出表头和其他列。非常适合脚本里配合kill使用。-n:不做主机名解析,直接用IP地址显示,解析慢的时候能快不少。-P:不做端口名解析,直接把80、443显示成数字,而不是http、https。
在高并发的机器上,不加-n -P可能会因为DNS反向解析超时导致命令等待很久,所以我在生产环境排查时经常直接写:
bash复制lsof -nP -i:8080
稍微总结一下常用参数,方便你一次性记住:
| 需求 | 参数 | 示例 |
|---|---|---|
| 查端口/网络连接 | -i |
lsof -iTCP:8080 |
| 只看监听状态 | -s |
lsof -iTCP:8080 -sTCP:LISTEN |
| 查指定进程 | -p |
lsof -p 12345 |
| 查指定用户 | -u |
lsof -u nginx |
| 排除指定用户 | -u ^root |
lsof -u ^root |
| 按命令名前缀查 | -c |
lsof -c ssh |
| 查目录下打开的文件 | +d / +D |
lsof +D /data |
| 只输出PID | -t |
lsof -t -i:8080 |
| 条件取交集 | -a |
lsof -a -u root -i |
| 不做解析加速 | -n -P |
lsof -nP -i:8080 |
3. 端口被占用、文件删不掉、磁盘分区“满而不实”:三个高频排查场景拆解
参数记熟了不落地等于没用。这里把运维里最常遇到的三个问题场景完整走一遍,每一个都是“发现问题→定位→解决”的链路。
3.1 场景一:服务启动报“端口被占用”
这个场景每个后端开发都应该遇到过。Java服务起不来,报错BindException: Address already in use,或者Nginx配置好之后reload失败,提示端口被占。
第一步,定位占用端口的进程:
bash复制lsof -i:8080
输出里如果有一行是java ... TCP *:8080 (LISTEN),那基本可以锁定。记下PID,然后确认一下这个进程是什么来路:
bash复制ps -ef | grep <PID>
如果你确定这个进程确实是可以重启的旧进程,那就先优雅结束,再强制结束:
bash复制kill <PID>
sleep 3
kill -9 <PID> # 如果还没退出再上
如果是在脚本里用,可以一行搞定:
bash复制kill -9 $(lsof -t -i:8080)
但这里必须给个特别提醒:lsof -t -i:8080的输出可能包含多个PID,可能是监听的进程,也可能是已经连到8080端口的客户端进程。 直接对结果全部kill -9风险极大,我建议先跑一遍不带-t的命令看清楚每行是什么状态,再决定杀谁。实在要脚本化,至少先用-sTCP:LISTEN过滤:
bash复制kill -9 $(lsof -t -sTCP:LISTEN -i:8080)
这样只会杀掉监听8080端口的进程,而不会误伤正在与8080通信的其他客户端连接。
3.2 场景二:umount失败,提示target is busy
运维在挂载盘迁移或者维护存储时,经常遇到umount卸载不了的情况:
bash复制umount /mnt/data
umount: /mnt/data: target is busy.
这个提示的潜台词是:有进程正在使用这个目录下的文件,系统出于数据安全不允许卸载。这时候你需要找出“谁在用”。
bash复制lsof +D /mnt/data
+D会递归扫描/mnt/data目录下的所有被打开文件,并列出对应的进程。假设输出里有一行:
code复制nginx 3876 www-data cwd DIR 253,2 4096 2 /mnt/data
说明Nginx进程的工作目录就在这个挂载点下。处理方式一般是:先停掉相关服务(或用systemctl stop nginx),等进程退出后再umount。有些场景下进程不能随便停,那就用kill通知进程释放句柄,或者临时把工作目录切走再重试。
如果你觉得lsof +D输出太多太杂,还可以用fuser命令做补充定位:
bash复制fuser -mv /mnt/data
-m的意思是查看挂载点,-v显示详细信息,它会直接列出占用该目录的进程PID和用户。两个命令配合使用,基本能覆盖大部分卸载失败场景。
3.3 场景三:删除日志文件后,磁盘空间没释放
这个场景最“诡异”。你明明用rm删掉了一个好几个G的日志文件,但df -h一看,磁盘使用率还是满的。新手遇到这个问题往往以为删错了文件,甚至重复删除。
真实原因通常不是你没删掉,而是有进程仍然持有这个被删除文件的文件描述符。在Linux里,“文件被删除”和“文件真正从磁盘消失”是两码事:只要还有进程打开着它,这个文件的inode就不会被回收,磁盘空间也不会真正释放。这就是标题里“打开的资源”四个字最值得玩味的地方。
排查方法非常简单:
bash复制lsof +L1
+L1表示列出link count(链接数)小于1的文件,而链接数为0基本就等于“已删除但仍被进程打开”。如果你的系统上有多个分区,可以先指定挂载点缩小范围:
bash复制lsof +L1 /var/log
输出里你会看到NAME列带有(deleted)标记,比如:
code复制nginx 3876 www-data 2u REG 253,0 4294967296 131072 /var/log/nginx/access.log (deleted)
这时候正常流程是:确认这个进程还有没有保留价值(比如是不是还活着提供服务),重启或kill它,空间就会释放。
bash复制kill <PID> # 或者 systemctl restart nginx
df -h # 再验证磁盘空间是否释放
需要留个心眼的点:在生产环境里,这个占用文件句柄的进程可能就是你的核心服务,直接kill会导致服务中断。更稳妥的做法是配合“日志切割”方案,先通知进程重新打开日志文件,再处理旧文件句柄。很多日志框架都支持发SIGUSR1信号重新打开日志文件,比如Nginx:
bash复制kill -USR1 <PID>
这可以让Nginx重新打开configure里的日志文件,然后你再确认旧的fd是否已经释放。
4. lsof背后读的是/proc:从原理理解命令的上限和边界
用熟了之后,我开始好奇lsof到底从哪里拿到这些信息。后来扒了一下它的实现,发现Linux版lsof的核心信息源就是/proc文件系统。理解这一点,对理解命令的边界非常有帮助。
4.1 /proc/PID/fd:手写一个最小版lsof
Linux系统把每个进程的资源情况都暴露在/proc/<PID>/目录下,其中fd子目录保存了这个进程所有打开的文件描述符。你可以进到这个目录里看看:
bash复制ls -l /proc/12345/fd
输出里会有一堆符号链接,比如:
code复制lrwx------ 1 app app 64 Feb 20 10:00 3 -> /var/log/nginx/access.log
lrwx------ 1 app app 64 Feb 20 10:00 4 -> socket:[12840321]
每个数字对应一个fd编号,箭头指向它指向的资源路径。如果指向的是socket:[xxx],说明这个fd是网络套接字,后面那串大数字是套接字在内核里的唯一标识。
所以lsof做的事情本质上就是把/proc/<PID>/fd目录下的所有符号链接扫一遍,再结合/proc/<PID>/maps(内存映射文件)、/proc/<PID>/cwd(当前工作目录)等信息,把它们汇总成一张表。你甚至可以写个简单的for循环,遍历所有进程的/proc目录,手动拼一个简化版lsof出来——虽然没那么好用,但能瞬间理解它的数据来源。
网络相关的信息则来自/proc/net/tcp、/proc/net/udp等文件。lsof的-i参数,本质上是把几十万条TCP连接记录按PID聚合,再和进程fd关联起来。
4.2 权限边界:为什么有时候lsof看到的信息不全
lsof可以显示的信息范围,取决于你执行它的用户身份。普通用户运行lsof,只能看到自己启动的进程,以及能访问到的进程信息;很多系统进程的详细信息需要root权限才能看到。
这意味着什么?你用自己的账号登录服务器,跑lsof -i:8080,如果输出的内容是空的,或者只有你自己的进程,先别急着下结论说“没有任何进程占用8080”,很可能是权限不够,看不到别人的进程。正确做法是用sudo跑一次确认。
4.3 快照式输出:lsof只能看某个瞬间
lsof输出的是命令执行瞬间的系统快照,它不是top那样持续刷新的工具。如果你需要持续观察fd数量的变化趋势(比如排查文件描述符泄漏),用循环配合sleep来采集,而不是直接指望lsof自带监控能力。
另外,在连接数非常高的服务器上,lsof -i会很慢,因为要遍历大量socket记录,还要做关联。这时候用-n -P禁用DNS和端口名解析,速度通常能提升几倍。
4.4 和其他命令的分工:netstat/ss、fuser、ps
lsof不是万能的,很多场景下其它命令更高效。我的习惯是:
- 只看端口监听情况,优先用
ss -lntp,它比netstat快,也比lsof -i:8080更快。 - 只看进程概况,用
ps。 - 想知道某个文件被哪些进程占用,用
lsof。 - 想解除某个挂载点的占用,
fuser -mv有时候输出更直接。
lsof的真正优势在于它的“多维反查能力”。你手里握着一个文件路径、一个端口、一个用户名、一个进程ID,想反查对应的其它维度,lsof几乎都能一步到位,这是任何单一命令都比不了的。
5. 进阶组合技巧与误操作避险:把lsof用成顺手工具
最后聊聊几个我日常经常用到的组合技巧,以及一些踩过的坑。
5.1 检查进程文件描述符是否泄漏
如果怀疑某个服务存在fd泄漏,可以用下面这段循环快速观察:
bash复制for i in $(seq 1 60); do
echo "$(date +%T) $(lsof -p 12345 | wc -l)"
sleep 1
done
正常情况下,进程fd数量应该保持平稳或有轻微波动。如果每秒都在增长且从未回落,那基本可以判定泄漏了。找到泄漏点后重启进程,再观察是否恢复正常。
配合+L1还能快速定位进程有没有“删了但没释放”的文件:
bash复制lsof -p <PID> | grep deleted
输出中如果出现deleted标记,说明这个进程持有已删除文件的句柄,要么重启释放,要么检查应用逻辑为什么没有关闭文件。
5.2 常用alias和函数
我把几个高频用法写成了shell函数,放在~/.bashrc或~/.zshrc里,效率提升明显:
bash复制# 查看某个端口谁在监听
listen() {
lsof -nP -iTCP:$1 -sTCP:LISTEN
}
# 查看某个端口所有连接
conn() {
lsof -nP -i:$1
}
# 杀掉监听某个端口的进程(先确认再杀)
killport() {
lsof -nP -iTCP:$1 -sTCP:LISTEN
}
有了它们,日常排查端口问题基本就是两条命令的事。
5.3 三个容易踩的坑
这几个坑我都在生产环境里踩过,写出来希望你绕开。
第一个是组合条件忘了-a,导致结果范围远大于预期。这个前文说过,解决方法是默认带-a,或者先用一个最小条件跑一遍看输出量再扩展。
第二个是-c的匹配规则是前缀匹配。lsof -c java匹配的是java开头的命令,但很多Java进程的命令名可能被设置成了java -Dxxx,未必能被匹配上。按用户查或者按PID查更可靠。
第三个是“看到结果直接kill”带来的误杀风险。lsof输出是多维表,同一个端口可能对应监听进程和已连接进程,直接对结果执行kill -9非常容易误伤。我的原则永远是:先看完整输出,确认进程身份和连接状态,再决定操作方式。要知道,lsof -i:3306的结果里可能有一大堆是“访问数据库的客户端连接”,而不是数据库服务本身,把这些客户端连接杀了,等于把线上业务直接切断。
5.4 关于习惯养成
我个人在使用lsof过程中最大的体会是:它改变了我的排查节奏。以前遇到“端口占用”“空间不释放”“卸载失败”这类问题,第一反应是上网搜报错信息,然后试各种命令碰运气。现在拿到问题,我会先在脑中过一遍“资源—进程”的关系,然后直接跑到lsof里填对应参数。这个习惯养成之后,很多问题定位从半小时缩短到一分钟以内。
lsof这个命令,说复杂也确实复杂,几十个参数够人学一阵;说简单也简单,核心思路就一条——“某个进程打开了什么,或者某个资源正被谁打开”。只要记住这条主线,参数只是按需组合的工具,遇到不会的场景,lsof --help或者man lsof翻一翻,很快就能找到答案。
