﻿# lsof —— 查看进程打开的文件 / 套接字 / 句柄

## 这个工具是做什么的

`lsof` = list open files。Linux 下"一切皆文件"，所以它能列出进程打开的普通文件、目录、设备、管道、以及**网络连接**。核心用途：排查 fd 泄漏、"文件/挂载点被谁占用"、进程连了哪些地址、磁盘空间被已删文件吃掉。

## 可以回答什么问题

| 问题 | 怎么用 |
|------|--------|
| 进程打开了多少个 fd？是不是要耗尽了？ | `lsof -p <PID> \| wc -l` 对比 `ulimit -n` |
| 某端口被哪个进程占用？ | `lsof -i :8080` |
| 磁盘满了但 `du` 找不到大文件？ | `lsof +L1` 找已删除但仍被占用的文件 |
| 挂载点卸载失败，谁还在用？ | `lsof /mnt/data` 找占用进程 |
| 某进程连了哪些网络地址？ | `lsof -i -a -p <PID>` |
| fd 泄漏是文件还是 socket？ | `lsof -p <PID> \| awk '{print $5}' \| sort \| uniq -c` 按 TYPE 归类 |

## 数据来源

- **来源文件**：逐个进程读 `/proc/<pid>/fd/`（打开的 fd 符号链接）、`/proc/<pid>/maps`（内存映射的文件/库）、`/proc/net/*`（网络套接字），再汇总。
- **采集方式**：遍历 `/proc` 下所有（或指定）进程，对每个进程展开其 fd 与映射——是**广度遍历**而非单次查询。
- **由此决定的特性**：① 进程和 fd 很多时较**慢**（遍历量大）；② 看别的进程的 fd 需要相应**权限**（普通用户看不全，常需 root）；③ 网络查询不如专用的 `ss` 快，但优势是能同时给出"是哪个进程的哪个 fd"。

```bash
yum install lsof / apt install lsof
```

## 一、按进程看

```bash
lsof -p <PID>              # 该进程打开的所有 fd
lsof -p <PID> | wc -l      # 数 fd 数量（排查 fd 泄漏，和 ulimit -n 上限对比）
lsof -c cpu_demo           # 按命令名前缀匹配（不用先查 PID）
lsof -c cpu_demo -a -d 0-99   # -a 与条件：只看 0~99 号 fd
```

输出：

```bash
COMMAND   PID  USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
cpu_demo 8921 derek  cwd    DIR  259,2     4096 131074 /home/derek/perf
cpu_demo 8921 derek  txt    REG  259,2   108176 262145 /home/derek/perf/cpu_demo
cpu_demo 8921 derek    3w   REG  259,2    52400 262200 /tmp/test_kernel_io.tmp
cpu_demo 8921 derek    5u  IPv4  54321      0t0    TCP  10.0.0.2:44512->10.0.0.9:6379 (ESTABLISHED)
```

| 列 | 含义 |
|----|------|
| `FD` | 描述符：`cwd` 当前目录、`rtd` 根目录、`txt` 程序本身、`mem` 内存映射(库)、`0/1/2` 标准流；数字后缀 `r`只读 `w`只写 `u`读写（如 `3w`、`5u`）|
| `TYPE` | `REG` 普通文件、`DIR` 目录、`CHR`/`BLK` 字符/块设备、`FIFO` 管道、`unix` Unix域套接字、`IPv4`/`IPv6` 网络 |
| `DEVICE` | 设备号 |
| `SIZE/OFF` | 文件大小或当前偏移 |
| `NODE` | inode 号（网络行显示协议）|
| `NAME` | 文件路径 / 网络地址（`本地->对端 (状态)`）|

## 二、按文件/目录看：谁在占用

```bash
lsof /tmp/test_kernel_io.tmp     # cpu_demo 场景2 写的临时文件被哪些进程打开
lsof /path/to/file               # 某文件被谁占用
lsof +D /var/log                 # 递归整个目录下被打开的文件（遍历，较慢）
lsof +d /var/log                 # 只看该目录一层（不递归）
```

## 三、排查"设备/挂载点忙，无法卸载"

```bash
umount /mnt/data                 # 报 "target is busy"
lsof /mnt/data                   # 找出还占着该挂载点的进程
fuser -m /mnt/data               # 另一种查法，-k 可直接杀（谨慎）
```

## 四、网络连接

```bash
lsof -i                          # 所有网络连接
lsof -i :8080                    # 占用 8080 端口的进程（端口冲突排查）
lsof -i tcp:8080                 # 限定 TCP 的 8080
lsof -i TCP -s TCP:LISTEN        # 所有监听中的 TCP
lsof -i -a -p <PID>              # 该进程的网络连接（-a 表条件相与）
lsof -i @10.0.0.9                # 与某 IP 的所有连接
lsof -i -s TCP:ESTABLISHED       # 已建立的 TCP
```

> 端口/连接排查现在多用更快的 `ss`；`lsof -i` 的优势是**同时给出进程和 fd**，适合"这个连接是哪个进程哪个 fd"的定位。

## 五、常用过滤与组合

```bash
lsof -u derek                    # 某用户打开的文件
lsof -u ^root                    # 排除 root（^ 取反）
lsof -p <PID> -a -i              # 交集：该进程的网络连接（多个条件默认是"或"，-a 变"与"）
lsof +L1                         # 链接数(link count)为 0 的文件 = 已删除但仍被进程占用
lsof | grep deleted              # 同上，另一种找法
lsof -nP                         # -n 不反解主机名、-P 不转端口名 → 更快、地址更清晰
```

## 六、关键指标速查

| 指标 | 缩写/英文 | 命令 | 正常值 | 异常信号 |
|------|----------|------|--------|---------|
| fd 总数 | FD Count | `lsof -p <PID> \| wc -l` | 远小于 ulimit | 接近 ulimit → 即将 EMFILE 崩溃 |
| fd 上限 | FD Limit / nofile | `ulimit -n` | 视业务（默认 1024） | 太小 → 高并发服务必然不够 |
| socket 数 | Socket Count | `lsof -p <PID> \| grep -c sock` | 视业务 | 持续增长 → socket fd 泄漏 |
| 已删除文件占用 | Deleted Files | `lsof +L1` | 0 | 有记录 → 空间未释放（文件删了但进程还开着） |
| 端口占用 | Port Occupancy | `lsof -i :8080` | 一个进程监听 | 多个进程 → 端口冲突或子进程继承 |
| 按类型归类 | FD by Type | `lsof -p <PID> \| awk '{print $5}' \| sort \| uniq -c` | 视业务 | REG 越来越多 → 文件泄漏；IPv4 越来越多 → socket 泄漏 |

## 七、常见排查方法与分析

### 7.1 fd 泄漏 → EMFILE

```bash
# 程序报 Too many open files
lsof -p <PID> | wc -l        # 看当前 fd 数
ulimit -n                     # 看上限
# 按类型归类看是哪种泄漏
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn
  → IPv4/v6 多 → socket 泄漏
    → ss -tanp | grep <PID>  → 看是否有大量 CLOSE-WAIT
  → REG 多 → 文件打开忘了关
    → lsof -p <PID> | grep REG | awk '{print $NF}' | sort | uniq -c
      找出被重复打开最多的文件路径
```

### 7.2 磁盘满了但 du 找不到大文件

```bash
df -h → 分区 Usage 接近 100%
du -sh /* 2>/dev/null | sort -rh → 找不到对应大文件
  → lsof +L1 → 显示 link count 为 0 的文件
    → lsof | grep deleted | sort -k7 -rn | head
      → 找出占空间最大的已删除但未释放的文件
        → 重启持句柄的进程或 echo > /proc/<PID>/fd/<N> 清空
```

### 7.3 挂载点卸载失败

```bash
umount /mnt/data → "target is busy"
  → lsof /mnt/data → 谁还在占用
    → 关闭对应进程或 cd 到其他目录
```

### 7.4 端口被占用

```bash
# 服务启动报 EADDRINUSE
lsof -i :8080 → 找出占用端口的进程
  → kill PID 或改端口
# 或
ss -tlnp | grep :8080 → 更快（推荐先用 ss）
```

### 7.5 fd 使用趋势监控

```bash
# 用脚本持续监控 fd 是否泄漏
while true; do
    echo "$(date) $(lsof -p <PID> | wc -l)"
    sleep 60
done
# 如果 fd 数单调增长且不回落 → 泄漏确认
```

## 八、交叉引用

- **连接状态**：[ss](/tools/network/ss.md)（比 lsof -i 更快，但 lsof 能关联进程 fd）
- **进程资源**：[procfs](/tools/proc/procfs.md) §二（`/proc/<pid>/fd/` 是 lsof 的数据源）
- **系统调用追踪**：[strace](/tools/code/strace.md)（open/socket 调用与 close 不配对 → 泄漏根源）
- **资源限制**：[kernel-tuning](/tools/proc/kernel-tuning.md) §四（ulimit/nofile 配置）

## 九、两个高频实战

**① fd 泄漏**：进程报 `Too many open files`（EMFILE）。

```bash
lsof -p <PID> | wc -l            # 当前 fd 数
ulimit -n                        # 该进程 fd 上限
lsof -p <PID> | awk '{print $5}' | sort | uniq -c | sort -rn   # 按 TYPE 归类，看是 socket 还是文件泄漏
```

socket 多 → 常配合 `ss state close-wait` 确认是连接没关。

**② 磁盘满了但 `du` 找不到大文件**：

```bash
df -h                            # 显示某分区满了
du -sh /* 2>/dev/null            # 却找不到对应大文件
lsof +L1                         # 元凶：文件被删了但进程还开着句柄，空间不释放
lsof | grep deleted | sort -k7 -rn | head   # 找出占空间最大的已删文件
```

→ 重启（或让进程关闭）持有句柄的进程，空间立即释放。

