在Ubuntu服务器运维中,端口被占用和磁盘空间释放不了是两个最常见的"坑"。lsof(List Open Files)是一个强大的命令行工具,它能列出当前系统打开的所有文件描述符,包括网络连接、普通文件、甚至已经被删除但仍被进程持有的文件。当你遇到"Address already in use"报错、或者明明删除了日志文件但df -h显示磁盘没释放的时候,lsof就是你的第一把手术刀。下面我会从实际场景出发,把这两个核心用法彻底讲透。
一、lsof是什么,为什么运维必须掌握它
lsof的本质是遍历/proc文件系统中每个进程的文件描述符表,然后把结果格式化输出。在Linux中,一切皆文件——网络socket是文件,管道是文件,设备也是文件。所以lsof能看到的东西远比你想象的多。它默认不是Ubuntu预装的,需要手动安装:
sudo apt update sudo apt install lsof
安装完成后直接输入lsof不加任何参数,会输出当前系统所有打开的文件,信息量巨大,一般不建议直接裸跑。真正有用的是带参数的精准查询。
二、用lsof排查端口占用的完整流程
场景:你要启动一个Nginx服务,报错"bind() to 0.0.0.0:80 failed (98: Address already in use)"。这时候你需要知道到底是谁占了80端口。
第一步,用lsof定位端口:
sudo lsof -i :80
输出类似这样:
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME nginx 1234 root 6u IPv4 12345 0t0 TCP *:http (LISTEN) nginx 1235 www-data 6u IPv4 12345 0t0 TCP *:http (LISTEN)
这里COMMAND是进程名,PID是进程ID,FD是文件描述符编号,TYPE是连接类型(IPv4/IPv6),NAME显示监听状态。看到PID之后,你就知道是哪个进程在搞事情了。
第二步,如果你只想看TCP连接,可以加-i tcp:
sudo lsof -i tcp:80
第三步,如果你想看某个进程打开了哪些端口,用-p指定PID:
sudo lsof -p 1234 -i
第四步,看UDP端口占用:
sudo lsof -i udp:53
第五步,找到占用进程后,有两种处理方式。如果是正常业务进程不该杀,那就改你新服务的端口;如果是僵尸进程或者测试遗留,直接kill:
sudo kill -9 1234
这里有个实战技巧:有时候你会发现端口被占用但lsof查不到具体进程,这种情况大概率是内核层面的TIME_WAIT状态连接堆积。这时候用ss命令配合查看更直观:
ss -tlnp | grep :80
但lsof的优势在于它能同时看到进程名、用户、文件描述符等多维度信息,排查链路更完整。
三、用lsof排查已删除文件句柄——磁盘空间不释放的元凶
这是运维中最容易被忽视的问题。你执行了rm -rf /var/log/app.log,文件确实删了,但df -h一看,磁盘空间一点没少。原因是:有进程还持有这个文件的文件描述符,Linux内核不会真正释放这块磁盘空间,直到所有引用该文件的进程都关闭它。
第一步,用lsof找出所有已删除但仍被打开的文件:
sudo lsof | grep deleted
输出会显示类似:
java 5678 appuser 4w REG 8,1 1073741824 12345 /var/log/app.log (deleted)
这里4w表示以写方式打开的第4个文件描述符,1073741824字节就是大约1GB的空间被"锁"住了。PID是5678,进程名是java。
第二步,更精准的查找——只看某个目录下被删除的文件:
sudo lsof +D /var/log | grep deleted
+D参数指定目录递归查找,非常适合日志目录排查。
第三步,如果你想看某个特定进程打开了哪些已删除文件:
sudo lsof -p 5678 | grep deleted
第四步,找到元凶进程后怎么处理?有三种方案:
方案一:重启该进程。最安全,进程重启后会重新打开新的日志文件,旧的文件描述符自然释放。这是生产环境推荐做法。
方案二:如果不能重启,可以尝试向进程发送信号让它重新打开日志文件。比如很多守护进程支持HUP信号:
sudo kill -HUP 5678
方案三:如果是你自己写的程序,代码里要确保日志轮转时正确关闭旧文件描述符。用logrotate配合copytruncate或者程序内部的日志重新打开机制。
这里有个硬核知识点:你甚至可以直接通过/proc文件系统手动释放文件描述符,但这属于危险操作,仅限调试环境:
ls -la /proc/5678/fd/4
这会显示该文件描述符指向的文件信息,确认无误后才能操作。生产环境绝对不要直接操作/proc下的fd。
四、lsof的高级过滤技巧和组合用法
lsof支持非常丰富的参数组合,掌握这些能让排查效率翻倍。
按用户过滤:只看某个用户打开的文件
sudo lsof -u www-data
按进程名过滤:
sudo lsof -c nginx
多条件组合:查看www-data用户的nginx进程打开的IPv4连接
sudo lsof -u www-data -c nginx -i 4
查看某个文件被哪些进程打开(反查):
sudo lsof /var/log/syslog
只看网络连接,不看普通文件:
sudo lsof -i
查看指定PID的所有文件描述符(包括普通文件、socket、管道等):
sudo lsof -p 1234
以JSON格式输出(适合脚本处理):
sudo lsof -Fn -p 1234
五、实战案例:一次完整的排查过程
假设你是一个Ubuntu运维,接到告警说/data分区使用率95%,但你刚清理了一批旧文件。排查步骤:
1. 先用df确认哪个分区满了:
df -h /data
2. 用du看哪个目录占用大:
sudo du -sh /data/* | sort -rh | head -10
3. 发现/data/logs目录很大,但文件已经删了,这时候上lsof:
sudo lsof +D /data/logs | grep deleted
4. 发现是一个Python脚本(PID 8888)持有了3GB的已删除日志文件。确认是测试环境的遗留进程。
5. 评估后kill该进程:
sudo kill -9 8888
6. 再次df -h确认空间已释放。
整个过程五分钟搞定,如果不会lsof,你可能要重启服务器或者花几个小时找原因。
六、lsof使用的注意事项和常见误区
第一,lsof默认需要root权限才能看到所有进程的文件描述符,普通用户只能看到自己的。所以排查时务必加sudo。
第二,lsof在高并发服务器上运行会比较慢,因为它要遍历所有进程。如果系统有上万个进程,建议先用pidof或ps缩小范围再查。
第三,不要把lsof的输出直接管道给grep就完事了。grep deleted只能看到"已删除"标记的文件,但有些文件虽然没标记deleted,可能是因为进程以追加模式打开且文件被truncate了,这种情况需要结合lsof的SIZE列和文件系统状态综合判断。
第四,lsof看到的是瞬时状态。如果你想持续监控端口变化,应该用watch配合:
watch -n 2 'sudo lsof -i :3306'
第五,在容器环境中(Docker/LXC),lsof看到的是宿主机视角还是容器视角取决于你在哪里执行。在容器内执行只能看到容器内进程,排查宿主机端口占用需要在宿主机上执行。
七、总结:lsof是Ubuntu运维的瑞士军刀
端口占用和已删除文件句柄是Ubuntu服务器上最高频的两类问题,lsof一个命令就能同时覆盖。它的核心价值不在于命令本身有多复杂,而在于它提供了一个统一的视角——把文件、网络、进程三者关联起来。掌握了lsof的-i、-p、-u、-c、+D、grep deleted这些组合,你在排查问题时就能从"猜测"变成"精准定位"。建议每个Ubuntu运维都把lsof的常用参数记在手边,遇到问题先lsof,再决定下一步动作,这是最高效的排查思路。
