2023 年,一个"磁盘明明满了,可那些空间却凭空消失了"的诡异事故,把我对"删除一个文件"这件事的理解,从头到尾翻新了一遍。那天半夜,监控告警:一台日志服务器,根分区 / 用满了。我 SSH 上去,df -h 一看,/ 这一行赫然写着 Use% 100%。我心想,简单,找出谁这么能吃,删掉它就行。我顺手 du -sh /*,把根目录下每个子目录的体积挨个量了一遍,然后我把这些数字加起来——13G。可这块盘,是 50G 的。我又量了一遍,还是 13G。我整个人卡在那儿了:df 说,这盘用掉了 49G,只剩 1G;du 说,我把所有文件夹翻了个底朝天,加起来只有 13G。中间那 36G,去哪了?它不在任何一个文件夹里,我 ls 不到它,我 du 不到它,可 df 又咬定它实实在在地被占着。一块盘上,怎么会有 36G 的空间,【既被占用着,又不属于任何一个文件】?我盯着这个对不上的数字,第一次意识到:我以为 du 和 df 量的是同一个东西——"这盘用了多少"。可它俩报出来的数字,差了快 3 倍。它们,根本不是在量同一样东西。这件事逼着我把 df 与 du 的区别、文件删除的真正含义、"被删除但仍被打开"的幽灵文件、lsof 这一整套彻底理清了。本文复盘这次实战。
问题背景
环境:CentOS 7,一台日志服务器,根分区 50G
事故现象:
- df -h 显示 / 分区 Use% 100%,几乎满了
- ★ 可 du 把所有目录加起来,只有 13G —— 对不上!
- 36G 的空间,不属于任何一个看得见的文件
现场排查:
# 1. df 看分区使用率 —— 确实满了
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 49G 1.0G 98% / # ★ 用了 49G
# 2. ★ du 把根下所有目录量一遍 —— 加起来才 13G
$ du -sh /* 2>/dev/null
1.2G /usr
8.6G /var
2.1G /opt
...
# ★ 全部加起来 ≈ 13G。df 说 49G。差了 36G!
# 3. ★ 重点查 /var/log(日志服务器,嫌疑最大)
$ du -sh /var/log
2.3G /var/log # ★ 才 2.3G
$ ls -lh /var/log/app/
-rw-r--r-- 1 root root 180M ... app.log # ★ 当前日志才 180M
# 4. ★ 关键一招:lsof 找"被删除但还开着"的文件
$ lsof | grep deleted
java 4021 root 5w REG 253,1 37580963840 /var/log/app/app.log.1 (deleted)
# ★★ 看这行!一个【已被删除(deleted)】的文件,
# app.log.1,体积 37580963840 字节 ≈ 35G,
# 还被 PID 4021 那个 java 进程【开着】!
# 5. 看这个进程
$ ps -p 4021 -o pid,cmd
PID CMD
4021 java -jar app.jar # ★ 业务主进程
根因(后来想清楚的):
1. ★ 有人之前嫌日志大,rm 删掉了 app.log.1。
2. ★ 但那个 java 进程,当时正【开着】这个文件
写日志。rm 只是把"文件名"从目录里去掉了,
★ 文件本身的数据【没有被释放】—— 因为还有
一个进程攥着它。
3. ★ 于是 app.log.1 成了"幽灵":没有名字、ls
看不到、du 也算不到(du 走的是目录树),
但它的 35G 数据,还实实在在地占着磁盘。
4. ★ 这就是 df 和 du 对不上的原因:df 问的是
文件系统"还剩多少块可用",它【算上了】这个
幽灵文件占的块;du 走的是目录树,目录里
早没这个文件名了,它【算不到】。
5. ★ 真正释放这 35G,只能让那个 java 进程关掉
它手里的文件描述符 —— 进程退出,或重启。
不是空间凭空消失,是它被一个"删了名字、却还
开着"的幽灵文件占着,du 走目录树永远看不见它。
修复 1:df 与 du——它俩量的根本不是同一个东西
# === ★ 先把这俩的根本区别讲透 ===
# === ★ du:沿着"目录树",把文件一个个加起来 ===
# du(disk usage)的工作方式:从你给的目录出发,
# ★ 递归走遍这个目录下的每一个文件和子目录,
# 把它们各自占的磁盘量,一个一个【累加】起来。
$ du -sh /var/log
2.3G /var/log
# ★ 它回答的是:"从这个目录往下,所有【能通过
# 目录名找到】的文件,加起来有多大。"
# ★ 关键词:它走的是【目录树】。一个文件,如果
# 在目录树里【没有名字】—— du 就【永远走不到
# 它】,自然也算不到。
# === ★ df:直接问"文件系统"还剩多少 ===
# df(disk free)的工作方式完全不同。它【不】去
# 走目录树,它直接去问【文件系统本身】:
# ★ "你一共有多少个数据块?用掉了多少?还剩
# 多少?"
$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 50G 49G 1.0G 98% /
# ★ 文件系统内部有个"账本"(超级块等元数据),
# 实时记着"哪些块被占用、哪些空闲"。df 看的
# 就是这个账本。
# ★ 关键:只要一个数据块被【某个文件的数据】占着,
# df 就把它算进 Used —— ★【不管这个文件在不在
# 目录树里、有没有名字】。
# === ★ 所以,这俩在一种情况下会对不上 ===
# 一个文件,如果它:
# - 数据块还实实在在地占着 -> df 算它(Used)
# - 但在目录树里已经没有名字了 -> du 算不到它
# ★ 这样的文件,就是 df 和 du 数字对不上的根源。
# 它【占着空间(df 看得见)】,却【没有名字
# (du 看不见)】。本文那 36G,就是这么来的。
# === ★ 还有个小差异:du 默认按"块"算,可能偏大 ===
# 文件系统是按【块】(常见 4KB)分配空间的。一个
# 只有 1 字节的文件,也要占整整一个块。
# ★ du 算的是"占了多少块"(实际磁盘占用);
# ls -l 看的是"文件内容多少字节"(逻辑大小)。
# ★ 小文件特别多时,du 会明显比 ls 的总和大。
$ du --apparent-size -sh /some/dir # 让 du 按
# "内容字节数"算,而非块 —— 便于和 ls 对比。
# === ★ 一句话记住这两个工具 ===
# du = "从这个目录往下走,看得见的文件有多大"
# —— 站在【目录树】的视角。
# df = "这个文件系统,总共用掉了多少块"
# —— 站在【文件系统账本】的视角。
# ★ 正常情况下两者接近;一旦出现"占着块、却没
# 名字"的文件,df 就会比 du 大出一截。
# === 认知 ===
# ★ du 沿目录树累加"有名字的文件",df 直接问
# 文件系统"用了多少块"。一个文件只要数据块
# 还占着,df 就算它 —— 哪怕它在目录树里已经
# 没名字。所以"df 满了、du 找不到",几乎总是
# 有"占着块却没名字"的文件。
修复 2:rm 一个文件,到底删掉了什么
# === ★ 本文最反直觉的一点:rm 删的不是"文件" ===
# === ★ 一个文件,其实是"两样东西" ===
# 在 Linux 里,你以为的"一个文件",实际由两部分
# 组成:
# 1. ★ 文件的【数据本体】+ 元信息(大小、权限、
# 数据块在哪)—— 这一坨,叫【inode】。文件
# 真正的"实体",是 inode。
# 2. ★ 一个【目录项】:在某个目录里,记着
# "app.log.1 这个名字 -> 指向 12345 号 inode"。
# ★ 这只是一个【名字到 inode 的指针】。
# ★ 即"名字"和"文件实体(inode)",是分开的两样
# 东西。名字,只是挂在 inode 上的一个【标签】。
# === ★ rm 干的事,其实叫 unlink ===
# 你 rm app.log.1 时,系统调用的是 unlink。
# ★ unlink 做的,只是:把"app.log.1 这个名字"
# 这个【目录项】删掉 —— 也就是【撕掉那个标签】。
# ★ 它【不】直接去抹掉 inode 和数据块。
$ stat app.log.1
... Links: 1 # ★ 注意这个 "Links"
# ★ 每个 inode 有个【链接计数(link count)】,
# 记着"有几个名字指向我"。
# ★ unlink 做的是:把链接计数【减 1】。
# === ★ inode 和数据,什么时候才真正被释放 ===
# 文件系统释放一个 inode + 它的数据块,要同时
# 满足【两个】条件:
# ① ★ 链接计数 == 0(没有任何名字指向它了);
# ② ★ 没有任何进程,还【打开】着它。
# ★ 这两个条件【都】满足,空间才真正还给系统。
# === ★ 于是,关键的一幕来了 ===
# 本文的场景:
# - app.log.1 只有一个名字,rm 之后,链接计数
# -> 0。条件 ① 满足了。
# - ★ 但是!那个 java 进程,此刻正【开着】这个
# 文件在写日志 —— 条件 ② 【不】满足。
# ★ 结果:rm 成功了(名字没了),但 inode 和那
# 35G 数据,【一点没释放】—— 因为还有进程攥着它。
# ★ 文件进入一种"半死不活"的状态:
# - 没有名字了 -> ls / du 都找不到它;
# - 数据还在、还在涨 -> df 照样算它占着空间;
# - 那个 java 进程,还能通过它手里的"文件描述符"
# 继续读写它。
# ★ 这,就是"被删除但仍被打开"的【幽灵文件】。
# === ★ 为什么进程还能写一个"已删除"的文件 ===
# 进程打开一个文件后,拿到的是一个【文件描述符
# (fd)】。这个 fd,直接指向【inode】,★ 不经过
# "名字"。
# ★ 所以名字被 unlink 撕掉了,进程手里那个 fd
# 还稳稳指着 inode —— 它照读照写,毫无影响。
# 它甚至【不知道】这个文件已经被 rm 了。
# === 认知 ===
# ★ rm 的本质是 unlink —— 它删的是"名字"(目录项),
# 不是"文件实体"(inode)。inode 和数据真正被
# 释放,要同时满足"没有名字指向它"且"没有进程
# 开着它"。rm 一个进程正开着的文件,就会留下
# 一个"没名字、但还占着空间"的幽灵文件。
修复 3:lsof——揪出那些"删了名字却还开着"的幽灵文件
# === ★ df/du 对不上时,靠 lsof 把幽灵文件揪出来 ===
# === ★ lsof 是什么 ===
# lsof(list open files),列出"当前系统里,所有
# 被打开的文件",以及【是哪个进程】打开的。
# ★ 在 Linux 里,"文件"是个广义概念 —— 普通文件、
# 目录、socket、管道,都算。这里我们只关心
# 普通文件。
# === ★ 核心一招:lsof | grep deleted ===
# lsof 在列出一个打开的文件时,如果这个文件【已经
# 被删除(名字没了)、但进程还开着】,它会在
# 行尾明确标上 ★ (deleted)。
$ lsof | grep deleted
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
java 4021 root 5w REG 253,1 37580963840 ... /var/log/app/app.log.1 (deleted)
# ★ 逐列读这一行:
# - COMMAND/PID :java 进程,PID 4021 —— 谁攥着它。
# - FD :5w —— 它用 5 号文件描述符,以
# 【写(w)】方式开着。
# - SIZE/OFF :37580963840 字节 ≈ 35G —— ★ 这个
# 幽灵文件有多大,一目了然。
# - NAME :/var/log/app/app.log.1 (deleted)
# ★ (deleted) —— 铁证:这文件名字已经没了,
# 但进程还开着它。
# ★ 这一行,就把本文 36G 的去向,彻底锁定了。
# === ★ 更精准的写法:lsof +L1 ===
# +L1 = 只列出"链接计数 < 1"的文件 —— 也就是
# "已经没有任何名字指向它"的文件。
$ lsof +L1
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
java 4021 root 5w REG 253,1 3.5e10 0 ... /var/log/app/app.log.1
# ★ NLINK 那一列 = 0,意思就是"零个名字" —— 标准
# 的幽灵文件。+L1 比 grep deleted 更准、更专业。
# === ★ 只查某个分区上的幽灵文件 ===
$ lsof +L1 / | sort -k7 -n
# ★ 给 lsof 指定一个挂载点,只看这个文件系统上
# 的;再按 SIZE 列排序,最大的幽灵文件浮到最上面。
# === ★ 算一笔账:幽灵文件总共占了多少 ===
$ lsof +L1 2>/dev/null | awk '{s+=$7} END{print s/1024/1024/1024" GB"}'
35.0 GB
# ★ 把所有幽灵文件的 SIZE 累加 —— 如果这个数,正好
# 约等于"df 减 du 的差额",那基本可以盖章定论:
# 空间就是被这些幽灵文件吃掉的。
# === ★ 为什么排查"磁盘满"必须想到 lsof ===
# 排查"磁盘满",大多数人只会 du 一通找大文件。
# ★ 但 du 永远看不见幽灵文件 —— 它走目录树,而
# 幽灵文件没有名字、不在目录树里。
# ★ 所以一旦 df 和 du 对不上,du 就【已经失效】了,
# 必须换 lsof —— lsof 是从"进程打开了什么"这个
# 角度看,它能看见没有名字的文件。
# === 认知 ===
# ★ df 和 du 对不上,第一反应就是 lsof +L1
# (或 lsof | grep deleted)—— 它能列出"已删名字、
# 仍被进程打开"的幽灵文件,告诉你是哪个进程、
# 用哪个 fd、占了多大。du 看不见的空间,lsof
# 能看见。
修复 4:怎么把幽灵文件占的空间真正收回来
# === ★ 找到幽灵文件后,怎么把那几十 G 拿回来 ===
# === ★ 错误做法:再去 rm 一次(根本没有名字可删)===
# ★ 别想着"再删一次"。幽灵文件【已经没有名字了】,
# 你 rm 不到它 —— ls 都看不见,rm 什么?
# 问题的关键从来不是"名字",是那个【还开着它的
# 进程】。
# === ★ 正道:让那个进程,松开它手里的文件描述符 ===
# 空间释放的条件之一是"没有进程开着它"。所以核心
# 动作,就是让 PID 4021 那个 java 进程,关掉它
# 开着幽灵文件的那个 fd。有两种方式:
# === ★ 方式 1:重启(或优雅地通知)那个进程 ===
# 进程一退出,它打开的所有 fd 都被内核关闭 ——
# 幽灵文件的最后一个持有者没了,空间【立刻】释放。
$ systemctl restart app # ★ 重启那个服务
$ df -h / # 重启后立刻看
/dev/vda1 50G 14G 36G 28% / # ★ 36G 回来了!
# ★ 这是最干净、最彻底的方式。但要重启业务进程,
# 得挑个能接受的时间窗口。
# ★ 很多服务支持"重新打开日志文件"的优雅操作
# (如给 nginx 发 USR1 信号、或 logrotate 的
# copytruncate / postrotate)—— 不用整个重启,
# 只让它换一个新日志文件,也能放掉旧 fd。
# === ★ 方式 2:不重启进程,直接把幽灵文件"清空" ===
# 如果这个进程【绝对不能重启】,还有一招:通过
# /proc,直接把那个文件描述符指向的内容【截断
# 为 0】。
# ★ 每个进程的 fd,在 /proc/PID/fd/ 下都有体现:
$ ls -l /proc/4021/fd/ | grep deleted
lrwx... 5 -> /var/log/app/app.log.1 (deleted) # ★ 就是 5 号 fd
# ★ 把这个 fd 指向的内容清空(truncate 成 0 字节):
$ : > /proc/4021/fd/5
# 或者
$ truncate -s 0 /proc/4021/fd/5
$ df -h / # ★ 空间立刻释放
# ★ 原理:/proc/4021/fd/5 就是那个 inode 的一个
# "入口"。往它写空、或截断它,等于把幽灵文件
# 的数据清掉,数据块就还给系统了。
# ★ 注意:这适合"幽灵文件是日志"这种、内容丢了
# 也无所谓的场景。进程会继续往这个 fd 写(从
# offset 接着写),不影响它运行。
# === ★ 方式 1 和方式 2 怎么选 ===
# - ★ 能接受重启 / 进程支持优雅 reopen -> 选方式 1,
# 最干净。
# - ★ 进程绝不能动、且幽灵文件是可丢弃的日志
# -> 用方式 2(truncate /proc/PID/fd/N)应急。
# - ★ 如果幽灵文件是【重要数据】不是日志 —— 别
# truncate!可以先 cp /proc/PID/fd/N /救回来的文件
# 把数据抢救出来,再处理。
# === ★ 治本:从源头别再制造幽灵文件 ===
# ★ 幽灵文件的根源是"rm 了一个进程正开着的文件"。
# 日志场景最常犯。正确做法:
# - 用 logrotate 管理日志轮转,别手动 rm 活动
# 日志;
# - logrotate 配置里,对"不会重开日志"的程序用
# copytruncate;
# - 真要手动清空一个活动日志,用 ★ : > app.log
# (清空内容、保留 inode 和名字),★ 不要 rm。
# === 认知 ===
# ★ 幽灵文件占的空间,rm 收不回(它没名字)——
# 要么重启 / 优雅 reopen 那个进程让它松开 fd,
# 要么 truncate /proc/PID/fd/N 把内容清空。治本
# 是别再 rm 进程正开着的文件:清空活动日志用
# ": > 文件",不要 rm。
修复 5:另一种 df/du 对不上——挂载点被"遮住"了
# === ★ df/du 对不上,还有第二种常见原因 ===
# === ★ 场景:du 比 df 还【大】(和本文相反)===
# 本文是 df > du(幽灵文件)。还有一种情况是
# ★ du > df,或者 du 把空间算重了 —— 起因是
# 【挂载点遮蔽(mount point shadowing)】。
# === ★ 什么是挂载点遮蔽 ===
# 假设 /data 这个目录,本来在根分区上,里面已经
# 存了 20G 文件。
$ du -sh /data
20G /data
# ★ 现在,你往 /data 上,挂载了一块【新的硬盘】:
$ mount /dev/vdb1 /data
# ★ 挂载之后:你再 ls /data,看到的是【新硬盘】
# 的内容(可能是空的)。
# ★ 而原来那 20G 文件,还在【根分区】上,只是被
# 新挂上来的硬盘【盖住了】—— 你通过 /data 这个
# 路径,再也看不到它们了。它们成了"被遮蔽"的
# 文件。
# === ★ 遮蔽会造成什么混乱 ===
# - 根分区的 df:那 20G 还在根分区上占着 -> df 算它。
# - 但你 du /data,量到的是【新硬盘】的内容,
# ★ 量不到被盖住的那 20G。
# - 于是根分区"莫名其妙被占着 20G",du 满世界
# 找不到 —— 因为它被一个挂载点死死盖在下面。
# === ★ 怎么查"有没有被遮蔽的文件" ===
# 关键技巧:在【不经过挂载】的情况下,去看那个
# 目录的"本来面目"。
# 方法:把根设备,单独再挂到一个空目录上:
$ mkdir /mnt/root_raw
$ mount --bind / /mnt/root_raw # ★ bind 挂载根
$ du -sh /mnt/root_raw/data # ★ 看 /data 这个
# 位置,在根分区上"原本"有多少东西
20G /mnt/root_raw/data # ★ 被遮蔽的 20G 现形!
$ umount /mnt/root_raw
# ★ bind 挂载会"穿透"上层的挂载,让你看见挂载点
# 底下被盖住的原始内容。
# === ★ 怎么处理被遮蔽的文件 ===
# 1. 先 umount 那个盖在上面的挂载:
$ umount /data
# 2. ★ umount 之后,被遮蔽的 20G 文件就重新露出来了:
$ du -sh /data
20G /data # ★ 它们回来了
# 3. 决定:要么把这 20G 清掉 / 移走,要么把它们
# 拷进新硬盘 —— 然后再 mount 回去。
# ★ 教训:往一个目录挂硬盘【之前】,先确认这个
# 目录是【空的】。往非空目录挂载,就会把里面
# 原有的文件全遮蔽掉,平白吃掉底层分区的空间。
# === ★ 排查 df/du 不一致,先分清是哪一种 ===
# - ★ df > du(df 显示用得多)-> 大概率是【幽灵
# 文件】,用 lsof +L1 查(本文的主线)。
# - ★ 某个挂载点下 du 量不到、但底层分区被占
# -> 大概率是【挂载遮蔽】,用 mount --bind 查。
# 两种成因,两套查法,别混。
# === 认知 ===
# ★ df/du 对不上还有一种是"挂载点遮蔽":往一个
# 非空目录上挂了新设备,原有文件被盖住,du
# 看不到、底层分区却仍被占。用 mount --bind 把
# 设备单独挂出来,就能看见被遮蔽的文件。挂载
# 目录前务必确认它是空的。
修复 6:磁盘空间排查纪律
# === 这次事故暴露的认知盲区,定几条纪律 ===
# === 1. ★ df 问文件系统"用了多少块",du 沿目录树累加"有名字的文件" ===
# === 2. ★ df 和 du 对不上,不是工具坏了,是有"占着块却没名字"的文件 ===
# === 3. ★ rm 的本质是 unlink —— 删的是名字,不是文件实体(inode)===
# === 4. ★ 空间释放要同时满足:没有名字指向它 且 没有进程开着它 ===
# === 5. ★ rm 一个进程正开着的文件,会留下"没名字、还占空间"的幽灵文件 ===
# === 6. ★ df > du 时,第一招 lsof +L1(或 lsof | grep deleted)揪幽灵文件 ===
$ lsof +L1
# === 7. ★ 幽灵文件 rm 收不回,要重启/优雅 reopen 进程,或 truncate /proc/PID/fd/N ===
# === 8. ★ 清空活动日志用 ": > 文件",绝不要 rm 一个进程正在写的日志 ===
# === 9. 往目录挂载设备前,先确认目录是空的,否则会遮蔽原有文件 ===
# === 10. 排查"磁盘满但找不到大文件"的步骤链 ===
$ df -h # ① 确认哪个分区满了
$ du -sh /* 2>/dev/null # ② 目录树能找到多少
$ lsof +L1 2>/dev/null # ③ df 比 du 大 -> 查幽灵文件
$ ls -l /proc/PID/fd/ | grep deleted # ④ 定位到具体 fd
$ : > /proc/PID/fd/N # ⑤ 应急释放(或重启进程)
# 按这个顺序,"磁盘满了却找不到空间"基本能定位、能根治。
命令速查
需求 命令
=============================================================
看各分区使用率 df -h
看某目录占多大 du -sh 目录
看根下每个子目录体积 du -sh /* 2>/dev/null
du 按内容字节算(对比 ls) du --apparent-size -sh 目录
揪幽灵文件(已删仍打开) lsof +L1
另一种揪法 lsof | grep deleted
看某进程打开的 fd ls -l /proc/PID/fd/
应急清空一个幽灵文件 : > /proc/PID/fd/N
清空活动日志(不删名字) : > app.log
查挂载点遮蔽 mount --bind / /mnt/x 再 du
看挂载情况 mount | column -t
口诀:df 问文件系统 du 走目录树,对不上多半是删了名字还被进程开着的幽灵文件
lsof +L1 揪出来,重启进程或 truncate proc fd 收回空间,清日志用 :> 别 rm
避坑清单
- df 直接问文件系统用了多少数据块,du 沿着目录树把有名字的文件一个个累加,两者视角不同
- df 和 du 数字对不上不是工具出 bug,是磁盘上存在占着数据块却在目录树里没有名字的文件
- rm 的本质是 unlink,它删掉的只是文件的名字也就是目录项,并不是文件实体 inode 本身
- 一个文件的 inode 和数据真正被释放,要同时满足没有名字指向它且没有任何进程开着它
- rm 掉一个进程正在打开的文件,会留下一个没有名字但数据块还占着空间的幽灵文件
- df 显示满了 du 却找不到空间,第一招就是 lsof +L1 揪出已删除但仍被进程打开的文件
- 幽灵文件没有名字 rm 收不回它,要重启或优雅 reopen 那个进程,或 truncate /proc/PID/fd/N
- 要清空一个进程正在写的活动日志,用冒号重定向 : 大于 文件,绝对不要直接 rm 它
- 往一个目录挂载新设备前务必确认目录是空的,否则原有文件会被遮蔽平白占着底层分区
- 排查磁盘满先 df 定位分区再 du 找大文件,du 找不到就换 lsof,这是固定的步骤链
总结
这次"磁盘满了,空间却凭空消失"的事故,纠正了我一个关于"删除"的、深植于本能的错觉。在我过去的脑子里,"删除一个文件",是一个干净利落、当场结清的动作。我按下 rm,就像把一张纸揉成一团扔进碎纸机——这张纸,连同它占的那点地方,【当场、彻底、再无痕迹】地没了。"删除"在我心里,等于"它不存在了",这两件事是同一个瞬间发生的,中间没有任何缝隙。可这次事故,从那个缝隙里,塞进来了整整 36G。现场逼着我承认:我按下的那个 rm,根本没有"销毁"任何东西。它做的,只是撕掉了一张【标签】。那个文件的实体——那 35G 数据——它一个字节都没少,还稳稳地躺在磁盘上。我删掉的,只是它的【名字】。一个东西失去了名字,和一个东西不再存在,我一直以为是一回事,可它们隔着十万八千里。失去名字,只是【我】再也喊不到它了;而它存不存在,取决于【是不是还有别人,正攥着它】。那个 java 进程,就是那个"别人"。它在我 rm 之前,早就攥住了这个文件——它攥的不是"名字",是文件的【实体本身】。所以我撕掉名字这个动作,对它毫无影响,它甚至根本不知道这文件已经"被删了",它还在心安理得地、源源不断地往里写。那 36G 就是这么涨起来的:一个我以为"已经删掉"的文件,在我看不见的地方,被另一个我没留意的进程,继续喂养着、继续膨胀着。复盘到根上我才看清,我混淆了"删除"这个动作的两个层面:一个是【解除我和它的关系】(撕掉名字),这个是即时的、单方面的;另一个是【这个东西真正从世界上消失】(回收实体),这个不由我单方面说了算——它要等到【最后一个】还attached在它身上的人,也松了手。我以为我一个人就能"删掉"一样东西,可"消失"这件事,从来是所有相关方【共同】的结果。这次最大的收获,是我对"我以为我处理掉了的事情"多了一层警觉。我撤回了一条消息,不代表看过的人脑子里就没了;我注销了一个账号,不代表它关联的数据在每个系统里都跟着消失了;我从一个列表里移除了一个名字,不代表那个名字背后的实体,就停止了运转和占用。"我这边看不见了",和"它真的结束了",是两件需要分开确认的事——前者只是我单方面松了手,后者要确认【所有】攥着它的手都松开了。所以下一次,当我"删掉""关闭""撤销"某样东西,而它本该带来的空间、资源、影响却迟迟没有消退时,我不会再对着我那份"已删除"的清单干瞪眼、怀疑世界出了 bug。我会换一个问题问自己:这个东西,除了我,【还有谁,正握着它不放】?因为真正让一样东西赖着不走的,从来不是它的名字——名字我一撕就掉;而是那一只只我没有看见的、仍然紧紧攥着它实体的手。要让它真正消失,我得先找到那些手,而不是反复去撕那张早就掉了的标签。
—— 别看了 · 2026