计算机 · 2026 年 11 月 11 日

文件删了空间没释放?inode 与文件描述符的真相

rm 之后 df 还是满的,多半是进程还握着文件描述符。从 inode 引用计数讲到 df -i,把「删了没释放」的几种情况一次说清。

rm -f app.log 删掉一个 40G 的日志,df -h 一看使用率还是 96%。du -sh /var/log 却只有 2G。这不是文件系统算错了,是文件还在,只是名字没了。

另一台机器报 No space left on device,df -h 显示还有 30% 空闲,df -i 的 IUse% 却是 100%。空间够、inode 不够,一样写不进新文件。两种「磁盘满」,处置方式完全不同。

还有第三种:Kubernetes 节点上 ephemeral-storage 报警,节点 df 看着正常,实际是被某个容器的可写层吃掉了。这属于 kubelet 的本地存储记账,df 只统计挂载点,看不到这一层。

Linux 里文件名只是目录项到 inode 的映射。rm 调用 unlink,把目录项去掉,inode 的链接计数减一。只有当链接计数为 0、且没有任何进程持有打开的文件描述符时,内核才释放数据块。

所以进程还开着这个文件时,空间不会回来。lsof +L1 专门找链接数小于 1 的已打开文件:

bash
lsof +L1
lsof | grep -i deleted
# 不装 lsof 时,直接扫 procfs
ls -l /proc/*/fd 2>/dev/null | grep deleted

输出里的 DEL 或 (deleted) 标记就是证据。lsof +L1 比 lsof | grep deleted 快,因为它按链接数过滤,不用遍历所有条目再匹配字符串。

释放空间的两个动作#

确认了 PID 和 fd,有两条路:

  1. 重启进程。最稳,代价是服务中断。
  2. 原地截断。truncate -s 0 /proc/<pid>/fd/<fd>,或者 shell 里 : > /proc/<pid>/fd/<fd>。

第二条有前提:这个 fd 必须以可写方式打开,进程有权限,截断的是同一个 inode。另外 fd 的文件偏移不会重置。如果进程不是以 O_APPEND 打开,下一次写会落在旧偏移处,文件变成稀疏文件,du 看着小、ls -l 看着大。日志框架多用 append,一般没事,自己写的 writer 要留意。

为什么日志轮转后空间还是没回来#

这是线上最常见的一幕:logrotate 配了 create 模式,把 app.log 改名成 app.log.1,再建一个新的空文件。nginx 之类持有旧 fd 的进程继续往 app.log.1 里写,新文件永远是空的。

copytruncate 模式看起来能解决,因为它复制完直接截断原文件,但代价是复制和截断之间写入的日志会丢。要在「可能丢日志」和「空间不释放」之间选,我选后者:把轮转配置改成 create,并在轮转后给进程发信号(nginx 用 USR1 重开文件),日志和空间都能保住。

空间被谁吃了:按目录下钻#

确认不是已删除文件之后,才轮到找大目录。我习惯按这个顺序:

bash
du -x --max-depth=1 /var | sort -h | tail -10
find / -xdev -type f -size +1G -printf '%s %p\n' | sort -n | tail -10
stat -f /var

-x 保证不跨文件系统,sort -h 按人类可读单位排序。find 的 -size +1G 只匹配大文件,比全量 du 快得多,适合在几百万文件的机器上快速定位。ncdu 交互式浏览更直观,但遍历开销一样,大目录上要等。

stat -f 给出块大小、总块数和可用块数。当 df 和 du 死活对不上时,先确认两条命令看的是不是同一个挂载点——路径上多一层 bind mount,结论就会完全不同。

inode 也会被用光#

df -i 看 inode 使用率。ext4 在 mkfs 时按 -i 参数固定(默认每 16KB 一个 inode),之后只能靠扩容按比例增加,不能凭空变多。XFS 动态分配,相对不容易撞上。

inode 耗尽的典型场景:海量小文件、session 文件、邮件队列、容器层里成千上万的碎文件。表现和磁盘满一模一样,但 df -h 正常,只有 df -i 能看出来。

这个方法主要对 ext4 这类预分配 inode 的文件系统有意义。btrfs 和 xfs 的 inode 是动态分配的,df -i 的数字会一直涨,拿它做告警阈值会误报。

df 和 du 对不上,还有这几种原因#

现象 原因 验证方式
df 满、du 小 进程持有已删除文件 lsof +L1
df 满、du 也满 真有大文件 du -x --max-depth=1 /
df 有空间但写入报 ENOSPC inode 耗尽 df -i
root 能写、普通用户不能 ext4 保留块 tune2fs -l 看 Reserved block count
df 满但找不到大文件 挂载点遮住了原目录 findmnt、du -x
删了目录仍报忙 有进程 cwd 在里面 ls -l /proc/*/cwd

du -x 的 -x 很重要:不加它,du 会跨文件系统统计,把挂载进来的网络盘也算进去,结果自然对不上。

硬链接是另一种「删不干净」。文件有两个名字时,删掉一个只是链接计数从 2 变 1,数据还在。用 find / -xdev -inum <inode号> 能把剩下的名字找出来。

另一个上限:文件描述符#

inode 是文件系统的配额,fd 是进程的配额,两者都会以「资源不够」的形式报错。报 Too many open files 时按顺序看三个值:

bash
ulimit -n                      # 当前 shell 的软限,常见 1024
cat /proc/sys/fs/file-nr       # 已分配 / 未用 / 内核上限
cat /proc/<pid>/limits | grep 'open files'

systemd 服务要改 LimitNOFILE,注意写成 infinity 时实际会被内核截到 fs.nr_open(默认约 100 万),不是真的无限。调大之前先确认不是 fd 泄漏:ls /proc/<pid>/fd | wc -l 隔一段时间看两次,只涨不跌就是泄漏。

删掉的文件还能找回吗#

有进程开着的时候最简单:cp /proc/<pid>/fd/<n> /tmp/recover.log 直接把数据拷出来。所以发现误删的第一件事是别重启进程。

没进程持有就麻烦了。ext4 上可以试 debugfs -R 'lsdel' /dev/sdX,但延迟分配、discard 和 inode 复用会让成功率很低。这也是为什么备份比恢复工具靠谱——文件系统不承诺删除可恢复。

进程的根目录也会骗人#

容器里的问题经常漏看:在宿主上跑 lsof,看到的只是宿主的 mount namespace。容器内已删除但仍被持有的文件,要么不出现,要么路径指向一个宿主上不存在的位置,du 遍历自然也走不到。

对这类进程,nsenter -t <pid> -m lsof +L1 才能看到容器视角的文件表;ls -l /proc/<pid>/root/ 能看到它的根目录挂的是什么。这也是「节点磁盘满了,du / 却找不到大户」的常见解释。

被遮住的挂载点#

如果往一个非空目录上 mount 了别的文件系统,原目录里的文件并没有删,只是被遮住了。df 统计的是挂载后的文件系统,du 也看不到被遮住的内容,但空间确实被占着。卸载挂载点,或者 mount --bind 到别处就能看到。这类问题通常出现在手工运维过的机器上。

ext4 默认给 root 保留 5% 的空间,df 对普通用户显示的 available 会比 root 少。数据盘上这个比例纯属浪费,tune2fs -m 1 /dev/sdX 可以调到 1%,但系统盘不建议动。

结论#

  • rm 只删目录项,进程还开着文件时空间不释放;用 lsof +L1 定位,别急着重启。
  • 原地截断可行,但要确认 fd 可写和偏移量行为,否则会得到稀疏文件。
  • No space left on device 先看 df -i,inode 和容量是两套配额。
  • df 与 du 不一致时按表逐项排除,du 记得加 -x。
  • 长期方案是日志轮转配合正确的重开信号,而不是出事之后手工找文件。

完

#Linux#文件系统#inode#排障
回到顶部 ↑