运维不加班
唯有热爱,方能成就非凡,若无痴迷,岂能窥得天机

追踪打开文件的瑞士军刀 - lsof 运维实操手册

你是否有过这些疑惑?

1、已经删了文件,但是df查看时,发现磁盘空间还被占着?
2、明明没跑几个应用,文件描述符使用率报警了?
3、吊炸天的重定向到底是什么原理?
4、被删掉的但是还没被释放的文件,有没有可能还能恢复?
5、都说linux的世界”一切皆文件“,那到底是什么意思?
6、清理了入侵系统的挖矿木马,不到10分钟,它换了个名字又跑起来了?
7、为什么一个查看打开文件的命令,居然可以用来排查各种各样的故障?

1. lsof 是什么,FD又是什么?

lsof = list open files,能列出系统中所有打开的文件。
FD = File Descriptor,文件描述符。
上面这些疑惑其实都跟linux的核心设计理念有关,即 ”一切皆文件“。
从用户视角来看,linux系统里发生的所有与I/O设备和通信资源相关的一切,都是在操作文件,
它把块设备、进程、网络连接、管道等等统统抽象为文件,然后提供统一的文件接口来操作他们。
所以 lsof 不仅能看普通文件,还能看网络连接、设备、管道等。
因此学会lsof在排查问题时,就又多了一件趁手的神兵利器!

# opsnot.com - 常见的文件类型:
REG    # 普通文件
DIR    # 目录
CHR    # 字符设备
BLK    # 块设备
FIFO   # 命名管道
unix   # UNIX 域套接字
IPv4   # IPv4 网络连接
IPv6   # IPv6 网络连接
PIPE   # 匿名管道   
DEL    # 已删除文件  

如果把linux世界看作一个图书馆,那lsof就是档案查阅员,注意:它只有查阅权限,无管理权限。

# by opsnot.com
图书馆系统 (Linux 内核)
    │
    ├── 图书分类法 (VFS 虚拟文件系统) - 统一所有"书籍"的借阅规则
    │
    ├── 各种书籍类型:
    │   ├── 普通图书 (REG 文件)
    │   ├── 期刊杂志 (DIR 目录) 
    │   ├── 电子设备 (CHR/BLK 设备文件)
    │   ├── 电话线路 (IPv4/IPv6 网络连接)
    │   ├── 内部传话 (PIPE/FIFO 管道)
    │   └── 会议记录 (unix 套接字)
    │
    ├── 借书证 (FD 文件描述符) - 每个进程的"借阅权限"
    │
    └── 档案查阅员 (lsof) - 只能查看借阅记录,不能管理书籍

FD就是借书证,它记录了每个程序的借阅权限,随时供lsof查阅

# opsnot - 重要的 FD 类型:
cwd    # 当前工作目录
txt    # 程序代码(二进制文件或共享库)
rtd    # 根目录
mem    # 内存映射文件
DEL    # 已删除但仍在使用的文件

附:lsof工作流程

// 简化的 lsof 工作流程
1. 遍历 /proc/<pid>/fd/ 目录
2. 读取每个文件描述符的符号链接
3. 通过 VFS 接口获取文件信息:
   - 文件类型(TYPE)
   - 设备号(DEVICE) 
   - 文件大小(SIZE)
   - inode 号(NODE)
   - 文件路径(NAME)

2. 最常用的 5 个场景

2.1 明明删了文件,磁盘空间却没有释放

# 精准查找被删除但进程还占用的文件
# +L1 表示查找链接数小于1的文件(即已删除)
lsof +L1

# 或者用传统方式(opsnot.com)
lsof -nP | grep deleted

**输出示例**:

COMMAND   PID     USER   FD   TYPE DEVICE SIZE/OFF NLINK    NODE NAME
nginx    12345  opsnot    7w   REG    8,1 2147483648    0  123456 /var/log/nginx/opsnot-access.log (deleted)

原理:文件虽然 rm 了,但进程的文件描述符还开着,磁盘空间不会释放。

解决方案:

# 方法1:重启(简单粗暴)
systemctl restart nginx

# 方法2:截断文件(加班哥推荐:不停服)
# 先确认 PID 和 FD
> /proc/12345/fd/7

2.2 端口被占用

# 查看 8080 端口被谁占用
lsof -nP -i :8080

# 输出
COMMAND   PID     USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java     8964  opsnot   42u  IPv6  98765      0t0  TCP *:8080 (LISTEN)

# 查看端口范围
lsof -nP -i :8000-9000

# 加班哥提醒:比 netstat -nltp 更快,输出更直观。记得加 -nP 参数,否则会很慢。

2.3 进程打开了哪些文件

# 查看 nginx 进程的所有文件
lsof -c nginx

# 或者用 PID
lsof -p 1234

# 只看网络连接(注意 -a 参数)
lsof -c nginx -a -i

加班哥重要提醒: -a 表示 AND 条件,不加就是 OR

# 错误写法:会输出 nginx 的所有文件 + 系统所有网络连接(这输出绝对不是你想要的)
lsof -c nginx -i

# 正确:只输出 nginx 的网络连接
lsof -c nginx -a -i

2.4 文件被哪个进程占用

# 查看某个文件被谁打开
lsof /var/log/mysql/error.log

# 卸载 U 盘时提示 device is busy
lsof /mnt/usb

# 只查一层目录(快)
lsof +d /var/log

# 递归查目录(慢,慎用!!!)
lsof +D /var/log

2.5 监控网络连接

# 查看所有 TCP 连接
lsof -nP -i TCP

# IPv4 TCP 连接
lsof -nP -i4TCP

# IPv6 UDP 连接
lsof -nP -i6UDP

# 查看特定 IP 的连接
lsof -nP -i @192.168.1.100

# 查看 ESTABLISHED 状态的连接
lsof -nP -i TCP -s TCP:ESTABLISHED

# 持续监控(opsnot 运维脚本)
watch -n 1 'lsof -nP -i TCP -s TCP:ESTABLISHED | wc -l'

3. 进阶用法

3.1 组合条件查询

# 查看 opsnot 用户打开的网络连接
lsof -u opsnot -a -i

# 查看除了 root 外的所有用户
lsof -u ^root

# 查看端口 3306 且状态是 LISTEN
lsof -nP -i :3306 -a -s TCP:LISTEN

再次提醒: 多个条件必须加 -a,否则是 OR 关系

3.2 输出格式化

# 只显示 PID
lsof -t -i :80

# 批量 kill(慎用!)
# 先用 SIGTERM,给进程清理资源的机会
kill $(lsof -t -i :8080)

# 等几秒后如果进程还在,再用 SIGKILL
sleep 3
kill -9 $(lsof -t -i :8080)

# 自定义输出字段(适合脚本解析)
lsof -F pcn -i :80

加班哥提醒: kill -9 会强制杀死进程,不给它任何清理机会(关闭文件、断开连接等),可能导致数据丢失。生产环境慎用!

3.3 性能优化

# lsof 默认会做 DNS 反解析和端口转服务名,很慢
# 生产环境必加这两个参数
lsof -nP -i

# -n:不做主机名解析(避免 DNS 查询)
# -P:不做端口名解析(避免读 /etc/services)

4. 实战脚本

4.1 监控进程文件描述符泄露

#!/bin/bash
# opsnot.com - 文件描述符监控脚本

THRESHOLD=1000
PID=$1

if [ -z "$PID" ]; then
    echo "Usage: $0 <pid>"
    exit 1
fi

while true; do
    FD_COUNT=$(lsof -p $PID 2>/dev/null | wc -l)
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] FD Count: $FD_COUNT"

    if [ $FD_COUNT -gt $THRESHOLD ]; then
        echo "WARNING: FD count exceeds $THRESHOLD"
        # 发送告警(接入你的告警系统)
    fi

    sleep 5
done

4.2 找出连接数最多的进程

#!/bin/bash
# 统计每个进程的网络连接数 - opsnot

lsof -nP -i TCP -s TCP:ESTABLISHED | \
awk 'NR>1 {print $1, $2}' | \
sort | uniq -c | \
sort -rn | head -10

**输出**:

    127 tomcat 8964
     45 nginx  1234
     23 redis  5678

4.3 诊断 CLOSE_WAIT 连接

#!/bin/bash
# 诊断 CLOSE_WAIT 连接 - opsnot.com
# 加班哥提醒:CLOSE_WAIT 通常是应用没有正确关闭 socket
# 直接干掉进程可能导致数据丢失,这个脚本只做诊断

echo "=== CLOSE_WAIT 连接统计 ==="
lsof -nP -i TCP -s TCP:CLOSE_WAIT | \
awk 'NR>1 {count[$1]++} END {
    for (cmd in count) 
        print cmd, count[cmd]
}' | sort -k2 -rn

echo -e "\n=== 详细列表(PID + 命令 + 远程地址)==="
lsof -nP -i TCP -s TCP:CLOSE_WAIT | \
awk 'NR>1 {print $2, $1, $9}' | sort -n

echo -e "\n建议:检查应用代码是否正确关闭了 socket"

为什么不直接 kill:

CLOSE_WAIT 是应用层问题,不是系统问题 直接杀进程可能导致正在处理的请求丢失 加班哥建议:应该先去排查代码,看是否有资源泄露

4.4 循环监控端口

# 每 2 秒监控一次 8080 端口
lsof -nP -r 2 -i :8080

# -r 参数:重复执行模式
# Ctrl+C 停止

5. 常见问题

5.1 lsof 很慢怎么办

# 1. 加 -nP 参数(必须)
lsof -nP -i

# 2. 缩小查询范围
lsof -c nginx -a -i  # 只查 nginx 的网络连接

# 3. 避免全量扫描
lsof +D /path  # 慢,会递归扫描所有子目录
lsof +d /path  # 快,只扫描一层

# 4. 用 -F 格式化后再处理(适合脚本)
lsof -Fn -i :80 | grep '^p' | cut -c2-  # 只获取 PID

# 5. 在负载高的系统上使用,-b 避免 lsof 自身阻塞
lsof -b -nP -i

5.2 权限问题

# sudo 普通用户只能看自己的进程,看不到其他用户的。生产环境排查问题时记得 `sudo`。
sudo lsof -nP -i :80

# 能看到所有进程

5.3 输出太多

# 只看关键信息
lsof -nP -i | awk '{print $1, $2, $8, $9}'

# 或者用 grep
lsof -nP -i | grep ESTABLISHED

# 统计而不是列出
lsof -nP -i TCP -s TCP:ESTABLISHED | wc -l

6. 核心参数速查表

# 基础过滤
-a      AND 条件(多个选项时必须)
-c      按命令名过滤
-p      按 PID 过滤
-u      按用户过滤(^user 排除某用户)
-i      网络连接

# 性能优化
-n      不做主机名解析
-P      不做端口名解析
-b      避免阻塞

# 输出控制
-t      只输出 PID
-F      字段格式化输出(适合脚本)
-R      显示父进程 PID

# 文件过滤
+d      非递归目录(快)
+D      递归目录(慢)
+L1     查找已删除的文件

# 网络过滤
-i4     只看 IPv4
-i6     只看 IPv6
-i TCP  只看 TCP
-i :80  只看 80 端口
-s TCP:ESTABLISHED  按状态过滤

# 循环执行
-r N    每 N 秒重复执行

-F 专用格式化参数

# 牛逼的 -F

lsof -F?     # 命令行输入这个,可以返回-F的帮助信息

a       访问权限,r = 只读;w = 只写;u = 读写
c       打开文件的进程命令名
d       设备标识符的字符形式
D       以 0x<十六进制> 格式显示的主/次设备号
f       打开文件的文件描述符编号
G       以 0x<十六进制> 格式显示的文件标志
i       文件在文件系统中的 inode 编号
k       文件的硬链接计数
K       任务标识符(TID)
l       锁状态,r/R = 读锁;w/W = 写锁;u = 读写锁
L       打开文件进程的登录用户名
m       在重复输出之间的分隔标记
M       任务的完整命令名称
n       文件名、注释或网络地址信息
o       以 0t<十进制> 或 0x<十六进制> 格式显示的文件偏移
p       打开文件进程的进程标识符(PID)
g       进程组标识符(PGID)
P       网络连接使用的协议名
r       以 0x<十六进制> 格式显示的原始设备号
R       父进程标识符
s       文件的大小(字节)
S       流模块和关联的设备名称
t       文件的类型(如普通文件、目录、套接字等)
T       TCP或TPI相关网络连接信息
u       打开文件进程的用户标识符(UID)
0       字段终止符,使用NUL字符(\0)而非换行符(NL)作为字段分隔符

7. 最后

lsof 是排查问题的利器,但也要注意:
- 生产环境务必加 -nP,否则很容易卡住,别坏人没找到,枪先卡壳了
- 多条件查询记得加 -a,否则是 OR 关系
- 不要频繁全量扫描,对系统有性能影响
- CLOSE_WAIT 不要无脑杀进程,先排查应用代码
- 慎用 kill -9,先用 SIGTERM,你要温柔,才会有魅力

相信看到这里,开篇的问题已经能自行解决了,恭喜!
更多强大排障利器:
Strace命令,Linux系统调用追踪神器!
运维火眼金睛之 - tcpdump抓包实操手册
Kubectl Describe,k8s故障排查利器!
Docker Inspect,值得族谱单开一页的命令

更多运维技术请点击左下方 阅读原文

本文由 opsnot.com 整理,转载请注明出处,点击下方卡片关注