☰
报错排查 03|磁盘满了却找不到大文件:lsof +L1 与 /proc 实战
2026/10/2 23:10:45 网站建设 项目流程

这大概是每个运维都撞过的一幕:

$ df -h / Filesystem Size Used Avail Use% Mounted on /dev/sda2 98G 98G 0 100% / ​ $ sudo du -xsh / 2>/dev/null 43G /

df 说 98G 用满了,du 把整台机器翻完只找到 43G。

剩下的 55G 去哪了?

我当时的做法很蠢:du换了六七种参数跑了一遍又一遍,甚至开始怀疑磁盘坏了。直到有人提醒我一句:「你删过什么大文件吗?」

那一刻我才反应过来——删掉了,但空间没回来。因为文件虽然从目录里消失了,可还有一个进程死死攥着它不放。

〇、先花两分钟:为什么「删了文件」空间还在

先建立这个认知,后面全通。

Linux 里,删除一个文件其实是两步,而且这两步是分开的:

  1. 从目录里摘掉这个文件名(rm干的事);

  2. 释放它占用的数据块(当没有任何进程还打开着它时,才发生)。

换个比喻:你把书从图书馆的书架上撤下来了,但还有个人正捧着这本书在读——那本书还在馆里,还占着地方。管理员要等这个人放下书,才能真的把它收走。

这时候ls、du都看不见它了(目录里已经没有这个名字),但df是按文件系统实际占用统计的,它照样算这一份。

所以就会出现:

df 数字 > du 数字,差值就是「被进程攥着的已删除文件」

这也是一个特别经典的运维事故:日志把磁盘打满 → 你rm掉日志文件 → 磁盘还是满的。因为写日志的进程(比如 nginx、java 应用)句柄一直开着,文件根本没释放。

先把基线说清楚:上面那个「98G 满、du 只有 43G」的场面,是我为了讲清楚构造出来的典型现场。真实情况是——我机器上lsof +L1有输出,但一条都不占磁盘。这一点最容易被误判,先看清楚:

$ sudo lsof +L1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME systemd 1 root 76u REG 0,1 0 0 11264 /memfd:systemd-udevd (deleted) pipewire 4174 rainbow 34u REG 0,1 2312 0 7190 /memfd:pipewire-memfd:flags=0x0000000f,type=2,size=2312 (deleted) gnome-she 4614 rainbow 32u REG 0,1 36346 0 7199 /memfd:mutter-anonymous-file-wayland-keymap (deleted) gnome-she 4614 rainbow 78u REG 0,1 67108864 0 27 /memfd:pulseaudio (deleted)

⚠️第一件必须记牢的事:lsof +L1有输出,不等于磁盘有问题。

看NAME列——上面每一条都以/memfd:开头,那是内存文件(进程在内存里造出来的匿名文件,DEVICE列也不是真实块设备,显示成0,1这类)。它们rm掉不释放磁盘,因为压根不在磁盘上。第三条那个 64 MiB 的pulseaudio看着挺唬人,占的是内存。

真正要找的长这样:DEVICE是真实块设备(比如8,2、253,1)、NAME是真实路径、末尾带(deleted)。下一节就是。

不装lsof也有办法查,同一条命令的另一种写法:

$ sudo find /proc/*/fd -lname '*deleted*' -printf '%p -> %l\n' 2>/dev/null | head -3 /proc/1/fd/76 -> /memfd:systemd-udevd (deleted) /proc/4174/fd/34 -> /memfd:pipewire-memfd:flags=0x0000000f,type=2,size=2312 (deleted) /proc/4614/fd/78 -> /memfd:pulseaudio (deleted)

怎么读:/proc/<PID>/fd/<编号>= 「哪个进程的第几个文件描述符」,箭头后面是它指向的文件。两个命令一一对得上——/proc/1/fd/76就是lsof里systemd那条76u。

(想亲眼看看这个现象是怎么发生的,我在文末写了一段三分钟就能跑完的复现脚本,只动/tmp,跑完自己清理。)

一、打法一:lsof +L1(最快,先跑这个)

一台真出了事的机器上,输出长这样(下面是按经典事故构造的示意,方便你看清每一列):

$ sudo lsof +L1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME nginx 1042 root 2w REG 8,2 12884901888 0 524301 /var/log/nginx/access.log (deleted) mysqld 2311 mysql 12u REG 8,2 6442450944 0 524318 /var/log/mysql/slow.log (deleted)

怎么读——这张表只要看四列:

列看什么
COMMAND/PID谁攥着这个文件——要释放就得动这个进程
SIZE/OFF它占了多少空间(上面两条加起来约 18G,正好补上了缺口)
NLINK0是关键字:链接数为 0 = 这个文件已经没有任何目录引用了
NAME末尾有(deleted)字样 = 已删除但未释放

先把SIZE/OFF加起来,看看是不是正好等于你缺的那部分空间——对上了,案子就破了。

二、打法二:直接看/proc(不装lsof也能查)

lsof需要额外装(Ubuntu 上sudo apt install lsof)。但lsof能看的东西,/proc里本来就有。

每个进程在/proc/进程号/fd/下,把自己的每个打开文件列成一个符号链接,链接指向哪里一目了然:

$ sudo ls -l /proc/1042/fd | head -5 lrwx------ 1 root root 64 Sep 27 03:00 0 -> /dev/null lrwx------ 1 root root 64 Sep 27 03:00 1 -> /var/log/nginx/access.log (deleted) lrwx------ 1 root root 64 Sep 27 03:00 2 -> /var/log/nginx/error.log

怎么读:

  • 左边1、2这些数字是文件描述符编号(相当于「这个进程手上有几号牌」);

  • 箭头后面是它指向的真实文件;

  • 箭头后面带(deleted)的,就是我们要找的。

上面那份是构造的现场。这套读法你随时能在自己机器上验证——看当前终端自己的 fd:

$ ls -l /proc/self/fd total 0 lrwx------ 1 rainbow rainbow 64 Sep 28 10:02 0 -> /dev/pts/1 l-wx------ 1 rainbow rainbow 64 Sep 28 10:02 1 -> pipe:[58522] l-wx------ 1 rainbow rainbow 64 Sep 28 10:02 2 -> pipe:[50927] lr-x------ 1 rainbow rainbow 64 Sep 28 10:02 3 -> /proc/6788/fd

这是我终端里的真实输出。怎么读:

编号指向是什么
0/dev/pts/1标准输入——你的终端
1/2pipe:[...]标准输出 / 标准错误(在管道里就长这样)
3/proc/6788/fdls自己正在读的那个目录

看清楚了吗——它就是一张「编号 → 文件」的对照表,没有别的玄机。把0/1/2之外的某个编号后面看成xxx (deleted),就是你要抓的那个洞。

批量扫全系统(不用装任何东西):

sudo find /proc/*/fd -lname '*deleted*' -printf '%p -> %l\n' 2>/dev/null

怎么读:输出一行就是「某个进程的某个 fd 指着某个已删除的文件」。这条命令是这类问题里最通用的探针,任何 Linux 都能跑。

想看得更细,连它占多少空间一起看:

sudo ls -l /proc/1042/fd/1 # 用 stat 看这个 fd 指向的 inode 大小 sudo stat -L /proc/1042/fd/1 | grep -E "Size|Blocks"

三、打法三:只想知道"哪个服务在漏",按进程聚合

如果进程很多,直接按占用量排序,一眼看出元凶:

sudo lsof +L1 -S 2>/dev/null | awk 'NR>1 {print $1, $2, $7, $9}' | sort -k3 -rn | head

怎么读:sort -k3 -rn是「按第三列(大小)从大到小排」,排在最上面的就是最大的那个洞。

四、怎么释放:三条路,按代价从低到高

找到之后,问题变成「怎么把空间要回来」。

路线一:重启服务(最稳,优先选)

sudo systemctl restart nginx

进程一重启,旧的句柄全部关掉,空间立刻释放。

代价是有服务中断(哪怕只有一两秒)。所以:

  • 能重启就重启,这是最干净的做法;

  • 心里有数:重启会让连接断开,负载均衡后面的其他节点能顶住的话,影响就是可接受的。

路线二:清空文件内容而不删文件(更平滑,但要改配置)

如果这个文件是日志,而且服务配置支持重新打开日志文件(比如 nginx 收到USR1信号会重开日志),那可以:

sudo truncate -s 0 /proc/1042/fd/1

怎么读:truncate -s 0是「把这个文件截断成 0 字节」。这里操作的不是文件路径(路径早没了),而是通过/proc里的句柄直接操作那个文件对象——文件还开着,但里面的内容被清空了,空间当场还回来,进程完全无感知。

⚠️两个必须知道的限制:

  1. 这招只对「持续追加写」的文件安全(日志就是典型)。如果进程用固定偏移读写(比如数据库文件),截断会让它以为数据还在,接下来读到一片空洞——那是数据损坏;

  2. 你清的是「现在这个句柄」指向的文件。如果进程之后又按原路径创建了新文件(很多服务会),旧句柄还会留着,得配合USR1之类的重开日志信号。

所以:不确定文件性质时,老老实实重启服务。

路线三:查清根因,从源头掐断

释放完空间,还有一件事必须做:问清楚它为什么会涨到撑满。

  • 日志没轮转?→ 配logrotate(这是最常见的原因,很多服务默认的轮转策略形同虚设);

  • 服务把 stdout 重定向到了一个从不切割的文件?→ 改成交给 journald,或者配 journald 的容量上限(/etc/systemd/journald.conf里的SystemMaxUse);

  • 应用在写一个自己不清理的临时文件?→ 这是应用 bug,要反馈。

不查根因,下周同一时间你会再来一次。这不是危言耸听,是这类故障的标准复现周期。

五、三个坑

坑 1:rm之后df没变化,就以为删错了文件,接着删别的。

删对了。空间没释放不是因为你删错了,是因为有进程开着它。这时候正确的动作是去查谁开着它(打法一),而不是继续删——继续删只会把现场破坏掉,甚至删掉不该删的东西。

坑 2:日志文件用rm而不是truncate。

这是最标准的错误示范。日志文件的正确清理方式是清空内容(truncate -s 0或: > 文件),然后让服务重新打开:

sudo truncate -s 0 /var/log/nginx/access.log

原因:rm会把文件名摘掉,服务还攥着旧句柄继续往「那个已经不存在的文件」里写,空间一直不释放;而清空内容不改变 inode,服务继续写同一个文件,一切正常。

坑 3:lsof的数字和/proc/PID/fd对不上。

lsof -p PID | wc -l会比ls /proc/PID/fd | wc -l多——因为lsof会把工作目录、root 目录、可执行文件、内存映射的库也算进去,而这些不占文件描述符。要判断「这个进程还剩多少 fd 额度」,用/proc/PID/fd的数字才准(这在下一篇讲Too many open files时会用到)。

附:三分钟亲手复现一次

光看文字不够直观。下面这段脚本你可以直接贴进终端跑——它只在/tmp下操作,结束时自己清理,不碰系统任何其他东西:

#!/bin/bash # 演示「删除后空间不释放」,全程只在 /tmp 下 D=/tmp/demo-deleted mkdir -p "$D" echo "① 删除前的 /tmp:"; df -h /tmp | tail -1 # 造一个 100M 的文件(/tmp 小的机器把 100 改成 50) dd if=/dev/zero of="$D/big.log" bs=1M count=100 status=none echo "② 造好 100M 文件后:"; df -h /tmp | tail -1 # 起一个后台进程打开它(扮演"一直不放手"的服务) tail -f "$D/big.log" > /dev/null & HOLDER=$! sleep 1 # 教科书式的误操作:日志用 rm 删 rm "$D/big.log" echo "③ rm 之后(文件名没了,空间没回来):"; df -h /tmp | tail -1 echo " 谁在攥着它:" ls -l /proc/$HOLDER/fd 2>/dev/null | grep deleted # 释放:把攥着它的进程干掉 kill $HOLDER 2>/dev/null sleep 1 echo "④ 进程结束后:"; df -h /tmp | tail -1 rm -rf "$D"; echo "清理完成"

我在自己机器上跑了一遍,原样贴出来(我这台的/tmp是 tmpfs,也就是内存盘——照样能演示,这个机制跟文件系统类型无关):

① 删除前的 /tmp: tmpfs 3.7G 8.0K 3.7G 1% /tmp ② 造好 100M 文件后: tmpfs 3.7G 101M 3.6G 3% /tmp ③ rm 之后(文件没了,空间没回来): tmpfs 3.7G 101M 3.6G 3% /tmp 谁在攥着它: lr-x------ 1 rainbow rainbow 64 Sep 28 15:18 6 -> /tmp/demo-deleted/big.log (deleted) ④ 进程结束后: tmpfs 3.7G 8.0K 3.7G 1% /tmp

三个细节值得多看一眼:

  • ③ 和 ② 一模一样(都是 101M)——rm完毫无变化,这就是事故现场;

  • 铁证那行开头的lr-x------,第一个字符l说明它是个软链接(/proc里的 fd 都是),紧跟的r-x说明这个句柄是只读打开的(tail -f只读),6 ->是文件描述符编号;

  • ④ 一kill,101M 当场掉回 8.0K——不是「慢慢回收」,是立刻释放。

这就是全部原理。生产环境上,把你的nginx、java、mysql代进那个「HOLDER」的位置,就是你现在遇到的事故。

速查表(文末收藏版)

谁在占用已删除文件 → sudo lsof +L1 (先看 NAME:/memfd: 开头的是内存文件,不占磁盘) (不装 lsof 的版本) → sudo find /proc/*/fd -lname '*deleted*' -printf '%p -> %l\n' 2>/dev/null 按大小排序找元凶 → sudo lsof +L1 | awk 'NR>1{print $1,$2,$7,$9}' | sort -k3 -rn | head 看某个进程开了哪些文件 → sudo ls -l /proc/PID/fd 判断 df 与 du 的差值 → df -h / + sudo du -xsh / (差值 = 被攥住的已删除文件) 释放方式一(最稳) → sudo systemctl restart 服务名 释放方式二(平滑) → sudo truncate -s 0 /proc/PID/fd/N ← 仅限追加写的日志文件 清空日志的正确姿势 → sudo truncate -s 0 /var/log/xxx.log (不要用 rm) 切日志(nginx) → sudo kill -USR1 $(cat /run/nginx.pid) 根治:配日志轮转 → /etc/logrotate.d/ 下加配置 根治:限制日志总量 → /etc/systemd/journald.conf 里 SystemMaxUse=500M

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询