在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,再决定下一步动作,这是最高效的排查思路。