rm 删了文件 df 空间却没释放:一次硬链接与 inode 引用计数的复盘

服务器根分区快满了,我 rm 掉一个确认没用的 5GB 大文件 bigdata.bin,df 一刷新磁盘 Used 纹丝不动一个字节都没释放,ls 确认文件确实没了,du 也显示 /data 目录少了 5GB,可整个磁盘的 df 就是不动,文件既被删了又没被释放。排查梳理:删之前 ls -l 看那个文件第二列的链接数发现是 2 不是 1,stat 显示 Links 2,find / -inum 用 inode 号找出指向同一文件的所有名字发现 /backup 下还有一个;一个文件在磁盘上不是一个东西是三层,文件名是挂在目录里指向 inode 的标签、inode 是文件的元信息加数据块索引、数据块才是真正存内容占那 5GB 的地方,文件名和 inode 是多对一同一个 inode 可挂好几个文件名每个名字叫一个硬链接;inode 里有个引用计数 link count 记着现在有几个文件名指向我就是 ls -l 第二列,rm 这个命令它不直接删数据它的真名是 unlink 解除一个链接它做两件事把文件名从目录里抹掉再把 inode 引用计数减 1 自始至终没碰过数据块;之前有人对这文件做过 ln 创建了硬链接引用计数变 2,我 rm 一个名字计数从 2 减到 1 还不是 0 内核绝不释放数据块,只有最后一个名字也删掉计数归 0 内核才回收 inode 和数据块还给磁盘;硬链接是同一 inode 的另一个名字不复制数据 inode 号相同地位完全平等改一个另一个同步变删一个另一个无损,两个硬限制不能跨文件系统会报 Invalid cross-device link 不能给目录做;软链接 ln -s 是完全不同的东西是一个独立的小文件有自己 inode 内容是目标的路径字符串像个路标 ls -l 首字符是 l,目标被删改名移走后软链接会悬空 dangling 自己还在却指向虚无,软链接能跨文件系统能指向目录这两点硬链接都做不到;cp 默认会跟随软链接复制的是目标内容要原样复制链接得用 cp -P,分次 du 含硬链接的目录会重复计数。正确做法是删大文件前先 ls -l 看链接数大于 1 就 find -inum 查清所有名字,以及一套链接与删除排查纪律。

2021 年,一次"我明明把一个 5GB 的大文件 rm 删掉了,df 一看磁盘空间【一个字节都没释放】"的事故,把我对"文件"和"删除"这两件事的理解,从头到尾翻新了一遍。那台服务器磁盘报警,根分区快满了。我盯上了 /data 下一个 5GB 的旧数据文件 bigdata.bin,确认它没用了,rm 干净利落地删掉。我心想:5G 到手,警报该解除了。可我 df -h 一刷新——根分区的 Used,纹丝不动,还是那个快满的数字。我以为 df 缓存了,等了一会儿再看,还是没变。我又怀疑是不是没删干净,ls /data/bigdata.bin——No such file or directory,文件确确实实没了。我甚至 du -sh /data 看了下,/data 这个目录的大小,确实【少了 5GB】。可整个磁盘的 df,就是不动。我彻底懵了:文件,我删了;du 也证实 /data 小了 5G;可这 5G 的空间,既没回到 df 的"可用"里,也不在 /data 里了。它【既被删了,又没被释放】——它去哪了?一个文件被 rm 之后,它占的空间,难道不是【立刻】就还给磁盘了吗?如果不是——那 rm 这个动作,到底【删掉的是什么】?这件事逼着我把文件名、inode、数据块这三层结构、硬链接与软链接的天壤之别、inode 的引用计数,还有 rm 真正的含义,彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,磁盘紧张,要删大文件腾空间
事故现象:
- rm 删掉了一个 5GB 的 /data/bigdata.bin
- ★ df -h 一看,磁盘 Used 一点没变,空间没释放
- ls 确认文件确实没了,du 也显示 /data 少了 5G

现场排查:
# 1. ★ 确认文件真的删了
$ ls -l /data/bigdata.bin
ls: cannot access '/data/bigdata.bin': No such file or directory

# 2. ★ df 看磁盘 —— 空间没还回来
$ df -h /
Filesystem  Size  Used Avail Use% Mounted on
/dev/vda1   100G   95G  3.5G  97% /            # ★ 删了 5G,Used 还是 95G

# 3. ★★ 关键:删之前,我看一下这个文件的"链接数"
#    (其实是删之前就该看的)
$ ls -l /data/bigdata.bin            # (假设还没删时)
-rw-r--r-- 2 root root 5368709120 ... bigdata.bin
#          ^ ★★ 这个数字是 2,不是 1!

# 4. ★ 这个 "2" 是什么:它表示这个文件有"2 个名字"
$ stat /data/bigdata.bin
#   Size: 5368709120   ...
#   Inode: 1572864   Links: 2          # ★★ Links: 2

# 5. ★ 用 inode 号,找出指向同一个文件的所有名字
$ find / -inum 1572864 2>/dev/null
/data/bigdata.bin
/backup/bigdata.bin                    # ★★ 还有一个!在 /backup 下

根因(后来想清楚的):
1. ★ 一个文件,在磁盘上不是"一个东西",是【三层】:
   - 文件名:挂在某个目录里的一个"标签";
   - inode:文件的元信息 + 数据块索引;
   - 数据块:真正存内容的地方(这才是占的 5GB)。
2. ★ "文件名"和"inode"是【多对一】的:同一个
   inode,可以挂【好几个文件名】。每个名字,叫一个
   "硬链接"。
3. ★ inode 里有个【引用计数(link count)】,记着
   "现在有几个文件名指向我"。ls -l 第二列就是它。
4. ★ 之前有人对这个文件做过 ln /data/bigdata.bin
   /backup/bigdata.bin —— 这创建了一个【硬链接】,
   于是这个 inode 的引用计数变成了 2。
5. ★★ rm 这个命令,它【不直接删数据】。它做的是:
   把那个"文件名"从目录里抹掉,然后把 inode 的
   引用计数【减 1】。
6. ★ 我 rm /data/bigdata.bin -> 引用计数从 2 减到
   1 -> ★ 计数还不是 0 -> 内核【绝不释放】数据块。
   因为 /backup/bigdata.bin 这个名字,还指着它。
真相:rm 删的是"名字",不是"文件内容"。只有当
最后一个名字也没了、引用计数归 0,空间才真正释放。

修复 1:删了文件空间没释放——问题出在"链接数"

# === ★ 先解开本文最直接的困惑:rm 了为什么不释放 ===

# === ★ 颠覆认知:rm 不是"删除文件" ===
# ★ 我们都以为 rm bigfile = "把这个文件删掉、把它
#   占的空间还给磁盘"。★ 这个理解,是错的。
# ★ rm 这个命令,它的真实动作,准确地说是
#   ★ "unlink" —— 解除一个【链接】。它做两件事:
#  - ① 把这个【文件名】,从它所在的目录里抹掉;
#  - ② 把这个文件名指向的 inode,引用计数【减 1】。
# ★ ★ 注意:rm 自始至终,【没有碰过数据块】。

# === ★ 那数据块(那 5GB)什么时候才释放 ===
# ★ 内核的规则只有一条:★ 当一个 inode 的引用计数
#   【减到 0】时 —— 也就是,指向它的【最后一个
#   文件名】也被删掉时 —— 内核才会把这个 inode
#   连同它的【数据块】,真正回收、还给磁盘。
# ★ 只要引用计数还 > 0,哪怕只剩 1,数据块就【一定
#   还在】,空间就【一定不释放】。

# === ★ 看一个文件的"引用计数"在哪 ===
$ ls -l /data/somefile
# -rw-r--r-- 1 root root 1024 ... somefile
#            ^ ★ 第二列那个数字,就是引用计数(硬链接数)
$ stat /data/somefile
#  Inode: 1572880   Links: 1            # ★ Links 就是它
# ★ 普通文件,这个数通常是 1(只有一个名字)。
#   ★ 如果它 > 1,说明这个文件【有多个名字(硬链接)】。

# === ★ 本文事故,完整对上了 ===
# ★ bigdata.bin 的引用计数是 2 —— 它有两个名字:
#   /data/bigdata.bin 和 /backup/bigdata.bin。
# ★ 我 rm /data/bigdata.bin -> 计数 2 减到 1。
# ★ 计数没到 0(/backup/那个名字还在)-> 内核【不
#   释放】那 5GB。这就是 df 纹丝不动的原因。

# === ★ 怎么真正释放这 5GB ===
$ find / -inum 1572864 2>/dev/null      # ★ 先找出所有指向它的名字
# /backup/bigdata.bin
$ rm /backup/bigdata.bin                # ★ 把最后一个名字也删掉
$ df -h /                                # ★ 这下计数归 0,5GB 才回来
# ★ 删之前【务必】先 find -inum 确认还有哪些名字、
#   那些名字是不是也确实没用了 —— 别误删了还在用的。

# === 认知 ===
# ★ rm 不是"删除文件",它的真实动作是 unlink:把一个
#   【文件名】从目录里抹掉,并把对应 inode 的引用计数
#   减 1 —— 它【从不直接碰数据块】。数据块(磁盘空间)
#   只有在引用计数【减到 0】、即最后一个文件名也被删
#   时才释放。ls -l 第二列 / stat 的 Links 就是引用
#   计数,> 1 说明这文件有多个名字(硬链接)。

修复 2:一个文件其实是三层——文件名、inode、数据块

# === ★ 把"文件"这个东西,拆成三层看清楚 ===

# === ★ 第一层:数据块(data block)===
# ★ 文件的【内容】,实实在在存在磁盘的数据块里。
#   一个 5GB 的文件,就占了 5GB 的数据块。
# ★ ★ 这一层,才是"占磁盘空间"的那一层。

# === ★ 第二层:inode ===
# ★ inode 是文件的"档案卡":记着文件的权限、属主、
#   大小、三个时间戳,以及【内容散落在哪些数据块】
#   的索引。
# ★ ★ 注意:inode 里【没有文件名】。inode 只有一个
#   编号(inode number),没有名字。
$ ls -i /etc/hosts
# 1234567 /etc/hosts                    # ★ 前面那个数字是 inode 号

# === ★ 第三层:文件名(目录条目)===
# ★ 文件名不在文件里,也不在 inode 里。文件名,是
#   记在【目录】里的。
# ★ 一个目录,本质就是一张表,每一行是
#   ★ "一个文件名 -> 一个 inode 号" 的对应。
# ★ 所以"文件名"这一层,真正的身份是:★ 一个指向
#   inode 的【指针 / 引用】。

# === ★ 三层的关系,串起来 ===
#   文件名(目录里的条目)
#        |  指向
#        v
#      inode(档案卡,含数据块索引)
#        |  索引到
#        v
#   数据块(真正的内容,占空间的就是它)

# === ★ 关键洞察:文件名和 inode 是"多对一" ===
# ★ 一个 inode,可以被【多个文件名】同时指向。
# ★ 这些名字,地位【完全平等】—— 没有谁是"正本"、
#   谁是"副本"。它们都只是"指向同一个 inode 的
#   指针"而已。每一个这样的名字,就叫一个【硬链接】。
# ★ inode 里那个"引用计数",数的就是:★ 现在有
#   几个文件名,正指向我。

# === ★ 于是 rm / "删除" 这件事,本质清楚了 ===
# ★ rm 一个名字 = 从目录表里划掉一行 + inode 计数减 1。
# ★ 数据块(内容)是挂在 inode 下的。inode 只要还被
#   【至少一个名字】指着(计数 > 0),它和它的数据块
#   就都【必须活着】。
# ★ 最后一个名字也没了 -> 计数 0 -> inode 没人要了
#   -> 内核回收 inode + 数据块。空间这才释放。

# === 认知 ===
# ★ 一个文件是【三层】:数据块(存内容,占空间的是
#   它)、inode(档案卡,记元信息和数据块索引,只有
#   编号没有名字)、文件名(记在目录里,本质是"名字
#   -> inode 号"的指针)。文件名和 inode 是【多对一】:
#   一个 inode 可被多个平等的名字指向,每个名字就是
#   一个硬链接。rm 删的是名字层,数据块只在 inode
#   引用计数归 0 时才回收。

修复 3:硬链接——同一个 inode 的多个名字

# === ★ 把硬链接(ln,不加 -s)彻底讲清楚 ===

# === ★ 创建一个硬链接:ln(不加 -s)===
$ ln /data/bigdata.bin /backup/bigdata.bin
# ★ 这一行做的事:在 /backup 目录里,新增一个名字
#   "bigdata.bin",让它指向【和 /data/bigdata.bin
#   完全同一个 inode】。
# ★ 它【没有复制】任何数据。两个名字,共用一份数据块。

# === ★ 验证:两个名字,inode 号完全一样 ===
$ ls -li /data/bigdata.bin /backup/bigdata.bin
# 1572864 -rw-r--r-- 2 root root 5368709120 ... /data/bigdata.bin
# 1572864 -rw-r--r-- 2 root root 5368709120 ... /backup/bigdata.bin
#  ^^^^^^^ ★ inode 号一样      ^ ★ 引用计数都是 2
# ★ inode 号相同 = 它们【就是同一个文件】,只是
#   有两个名字。引用计数 2 = 有两个名字指着它。

# === ★ 硬链接的几个关键性质 ===
# ★ 性质 1:★ 多个硬链接,地位完全平等。没有"原件"
#   和"链接"之分。/data/那个 和 /backup/那个,谁是
#   "本尊"?★ 都是,也都不是 —— 它们是平级的两个名字。
# ★ 性质 2:改其中一个,另一个【同步变】。因为它们
#   是同一份数据。echo x >> /data/bigdata.bin,
#   /backup/bigdata.bin 的内容也跟着变。
# ★ 性质 3:删其中一个,另一个【完好无损】。计数
#   减 1 而已,数据还在。

# === ★ 硬链接的两个硬限制(★ 很重要)===
# ★ 限制 1:★ 硬链接【不能跨文件系统 / 跨分区】。
#   inode 号只在【一个文件系统内部】唯一。/data 和
#   /backup 若在不同分区,硬链接做不了:
$ ln /data/x.bin /mnt/otherdisk/x.bin
# ln: failed to create hard link: Invalid cross-device link
# ★★ "Invalid cross-device link" —— 见到这个报错,
#   就是你想跨分区做硬链接了,这是物理上不可能的。
# ★ 限制 2:★ 普通用户【不能给目录做硬链接】。
#   (只允许给文件做。否则会破坏目录树结构。)

# === ★ 找出一个文件的所有硬链接 ===
$ ls -l /data/bigdata.bin            # 第二列 > 1,就说明有多个
$ find / -inum 1572864 2>/dev/null   # 按 inode 号找出全部名字
$ find / -samefile /data/bigdata.bin 2>/dev/null   # 或用 -samefile
# ★ ★ 删一个引用计数 > 1 的文件前,务必先这样查一遍
#   —— 否则你以为删了,空间根本不会释放。

# === 认知 ===
# ★ 硬链接(ln 不加 -s)= 给同一个 inode 再起一个
#   名字,不复制数据。两个名字 inode 号相同、地位
#   完全平等(没有原件副本之分)、改一个另一个同步
#   变、删一个另一个无损。两个硬限制:★ 不能跨文件
#   系统(报 Invalid cross-device link)、不能给目录
#   做。删引用计数 > 1 的文件前,必须 find -inum 或
#   -samefile 查清所有名字。

修复 4:软链接——一个存着路径字符串的小文件

# === ★ 把软链接(符号链接,ln -s)讲清楚 ===

# === ★ 创建一个软链接:ln -s ===
$ ln -s /data/bigdata.bin /home/me/data.bin
# ★ -s = symbolic(符号)。这创建的是【软链接】,
#   和硬链接是【完全不同】的东西。

# === ★ 软链接的本质:它是一个【独立的小文件】 ===
# ★ ★ 软链接,自己是一个【独立的文件,有自己的
#   inode】。它的"内容",就是一个【字符串】——
#   它指向的那个目标的【路径】。
$ ls -li /home/me/data.bin
# 2000001 lrwxrwxrwx 1 me me 17 ... data.bin -> /data/bigdata.bin
# ^^^^^^^ ★ 它有自己的 inode 号(和目标不同!)
#         ^ ★★ 第一个字符是 l,表示这是个软链接(link)
#                                 ^^^^^^^^^^^^^ ★ 它存的就是这串路径
# ★ 看那个 Size:17 —— 这个软链接文件本身才 17 字节,
#   因为它只存了 "/data/bigdata.bin" 这 17 个字符。
#   ★ 它【不占】目标文件的空间。

# === ★ 软链接是怎么工作的:路径替换 ===
# ★ 你访问 /home/me/data.bin,内核发现它是个软链接,
#   就读出它内容里那串路径,然后【转而去访问那串
#   路径】。这叫"跟随(follow)软链接"。
# ★ 所以软链接像一个"路标":它本身不是目的地,它
#   上面写着"目的地在那边 ->",内核照着走过去。

# === ★ 软链接最大的特点:它会"悬空" ===
# ★ 因为软链接存的只是【一串路径文字】,它和目标
#   之间【没有 inode 级别的绑定】。
# ★ 一旦目标文件被删除 / 改名 / 移走:
$ rm /data/bigdata.bin               # 把目标删了
$ cat /home/me/data.bin
# cat: /home/me/data.bin: No such file or directory
$ ls -l /home/me/data.bin
# lrwxrwxrwx ... data.bin -> /data/bigdata.bin   # ★ 链接还在
# ★ ★ 软链接文件【自己还在】,但它指向的路径已经
#   空了 —— 这叫【悬空链接(dangling link)】。
#   一个指向虚无的路标。

# === ★ 软链接能做硬链接做不到的两件事 ===
# ★ ① 软链接【可以跨文件系统】—— 它只存一串路径
#   文字,路径写跨分区的地址完全没问题。
# ★ ② 软链接【可以指向目录】—— 部署里 current ->
#   releases/v5 这种,用的就是软链接指目录。

# === ★ 看软链接到底指向哪、最终指向谁 ===
$ readlink /home/me/data.bin          # 看它直接指向的路径
$ readlink -f /home/me/data.bin       # ★ 一路跟到底,看最终的真实文件
$ ls -lL /home/me/data.bin            # -L:看它【目标】的信息,而非链接自身

# === 认知 ===
# ★ 软链接(ln -s)和硬链接是完全不同的东西:它是
#   一个【独立的小文件,有自己的 inode】,内容就是
#   目标的【路径字符串】(像个路标)。ls -l 第一个
#   字符是 l。访问它时内核读出路径再转去访问目标。
#   ★ 目标一旦被删/改名/移走,软链接就【悬空】——
#   自己还在,却指向虚无。软链接能跨文件系统、能指向
#   目录,这两点硬链接都做不到。

修复 5:硬链接 vs 软链接——完整对照与各自的坑

# === ★ 把两者并排放,一次看明白 ===

# === ★ 核心区别一张表 ===
# 维度              硬链接(ln)            软链接(ln -s)
# ----------------------------------------------------------
# 本质            同一 inode 的另一个名字   独立的文件,存目标路径
# 有没有自己 inode  没有(共用目标的)        ★ 有(自己一个)
# inode 号         和目标【相同】            和目标【不同】
# ls -l 首字符      普通(-)                ★ l
# 占空间           不额外占                  占一点点(存路径串)
# 删了目标后       ★ 不受影响(数据还在)    ★ 悬空,失效
# 跨文件系统       ★ 不行                   ★ 可以
# 链接目录         ★ 不行                   ★ 可以
# 引用计数         会让目标计数 +1           ★ 不影响目标计数

# === ★ 坑 1:rm 一个硬链接 > 1 的文件,空间不释放 ===
# ★ 本文的事故。删之前 ls -l 看第二列,> 1 就要警惕。

# === ★ 坑 2:误以为软链接"占了双份空间" ===
# ★ 有人看到 du 把软链接和目标都算了,以为浪费。其实
#   软链接自己只占几十字节。倒是【硬链接】要小心 du:
$ du -sh /data /backup
# ★ 如果 /data/big 和 /backup/big 是硬链接,du 分别
#   算两个目录时,可能【把那 5GB 算两次】(取决于
#   是否在同一次 du 调用里)。同一次 du 多个目录,du
#   会去重;分两次单独 du,就会各算一遍 -> 看着像
#   占了 10G,实则 5G。

# === ★ 坑 3:cp 一个软链接,你以为复制了链接 ===
$ cp /home/me/data.bin /tmp/d.bin
# ★ ★ cp 默认会【跟随软链接】—— 它复制的是【目标
#   文件的内容】,/tmp/d.bin 是一个真实的大文件,
#   不再是链接!想原样复制链接本身,要 cp -P 或 cp -d。
$ cp -P /home/me/data.bin /tmp/d.bin   # -P:不跟随,链接照搬

# === ★ 坑 4:tar / rsync 对链接的处理,要确认 ===
# ★ tar 默认会把软链接当链接打包;rsync 要不要跟随
#   软链接,有 -l / -L 等一堆选项。批量同步前务必
#   确认,否则链接到了对端可能变成实文件、或悬空。

# === ★ 坑 5:删目录时,小心里面的软链接 ===
# ★ rm -rf 一个【软链接指向的目录】,行为和带不带
#   末尾斜杠有关,容易误删到目标目录的真实内容。
#   操作软链接指向的目录前,先 readlink -f 看清楚
#   它到底指到哪。

# === ★ 该用哪个:经验法则 ===
# ★ 要"同一份数据多个入口、删任意一个其他不受影响"
#   -> 硬链接(但受同文件系统限制)。
# ★ 要"跨分区、要链目录、要一个能随时改指向的路标"
#   (如部署的 current -> release)-> 软链接。
# ★ ★ 日常绝大多数场景,用软链接。它直观、能跨盘、
#   能链目录,且 ls 一眼能看出它是个链接。

# === 认知 ===
# ★ 硬链接是同 inode 的另一个名字(inode 号相同、删
#   目标不受影响、不能跨文件系统、不能链目录);软
#   链接是存路径的独立小文件(inode 号不同、ls 首字符
#   l、目标删了就悬空、能跨盘能链目录)。常见坑:删
#   硬链接数 > 1 的文件不释放空间、分次 du 硬链接会
#   重复计数、cp 默认跟随软链接复制的是目标内容(要
#   cp -P 才照搬链接)。日常多数场景用软链接。

修复 6:链接与删除排查纪律

# === 这次事故暴露的认知盲区,定几条纪律 ===

# === 1. ★ rm 删的是"文件名",不是数据;空间只在引用计数归 0 才释放 ===

# === 2. ★ 删大文件前,先 ls -l 看第二列引用计数,> 1 说明有别的硬链接 ===
$ ls -l 大文件        # 第二列就是硬链接数

# === 3. ★ 引用计数 > 1,先 find -inum 或 -samefile 找出所有名字 ===
$ find / -samefile /path/to/file 2>/dev/null

# === 4. ★ 硬链接和目标 inode 号相同;软链接 ls -l 首字符是 l ===
$ ls -li 文件         # 看 inode 号 和 首字符

# === 5. ★ 硬链接不能跨文件系统(报 Invalid cross-device link)不能链目录 ===

# === 6. ★ 软链接只存一串路径,目标删了它就悬空,自己还在却指向虚无 ===

# === 7. 看软链接指向哪、最终指向谁,用 readlink 和 readlink -f ===
$ readlink -f 软链接

# === 8. ★ cp 默认跟随软链接复制的是目标内容,要原样复制链接用 cp -P ===

# === 9. 分次 du 多个含硬链接的目录会重复计数,以 df 的真实占用为准 ===

# === 10. 排查"删了文件空间没释放"的步骤链 ===
$ df -h                              # ① 确认空间确实没释放
$ ls -l 被删文件 2>/dev/null          # ② 删之前看引用计数(>1?)
$ find / -samefile 文件 2>/dev/null   # ③ 找出还有哪些硬链接名字
$ lsof | grep deleted                # ④ 或:文件被进程句柄占着(另一种)
# 引用计数没到 0,或有进程占着句柄 -> 空间都不会释放。

命令速查

需求                        命令
=============================================================
看文件引用计数(硬链接数)  ls -l(第二列)/ stat 看 Links
看文件 inode 号             ls -i 文件
创建硬链接                  ln 源文件 新名字
创建软链接                  ln -s 目标 链接名
找出某文件的所有硬链接      find / -samefile 文件 2>/dev/null
按 inode 号找文件           find / -inum 编号 2>/dev/null
看软链接指向哪              readlink 软链接
看软链接最终的真实文件      readlink -f 软链接
原样复制软链接(不跟随)    cp -P 软链接 目标
看软链接目标的信息          ls -lL 软链接
查已删除但被句柄占着的文件  lsof | grep deleted

口诀:rm 删的是名字不是数据 引用计数归 0 空间才释放
      硬链接同 inode 不能跨盘 软链接是存路径的小文件目标没了就悬空

避坑清单

  1. rm 删的是文件名不是数据,它把名字从目录抹掉并让 inode 引用计数减 1
  2. 磁盘空间只有在 inode 引用计数减到 0 即最后一个文件名也删掉时才真正释放
  3. 删大文件前先 ls -l 看第二列引用计数,大于 1 说明这文件还有别的硬链接名字
  4. 一个文件是三层:文件名是目录里的指针,inode 是档案卡,数据块才是占空间的内容
  5. 硬链接是同一个 inode 的多个平等名字,inode 号相同,删其中一个另一个完好无损
  6. 硬链接不能跨文件系统会报 Invalid cross-device link,也不能给目录创建硬链接
  7. 软链接是一个独立的小文件,内容是目标的路径字符串,ls -l 第一个字符是 l
  8. 软链接的目标被删除改名或移走后会悬空,链接文件还在却指向一个不存在的路径
  9. cp 默认会跟随软链接复制的是目标文件内容,要原样复制链接本身得用 cp -P
  10. 分多次 du 含硬链接的目录会把同一份数据重复计数,真实占用以 df 为准

总结

这次"删了文件空间却没释放"的事故,纠正了我一个关于"文件"的、藏得极深的错觉。在我过去的脑子里,一个文件,就是【一个东西】——一个不可分割的整体。它有一个名字,名字就是它;它有内容,内容也是它。名字、内容、它占的那块空间,在我心里是【焊死在一起】的一坨。基于这个"一坨"的认知,"删除"就顺理成章地是一个【原子动作】:我 rm 一下,这一坨——名字、内容、空间——就【一起】灰飞烟灭了。所以当 rm 之后,名字没了(ls 找不到了)、空间却还在,我整个人是崩溃的:我那个"一坨"的整体,怎么会【碎开】?怎么会一半消失、一半赖着?这在我的世界观里,是不可能发生的事。直到我把 inode 那一层挖出来,我才看清,我以为的那"一坨",其实从来就是【三层】拼起来的:最底下是数据块,是真正占地方的内容;中间是 inode,是那张管着内容的档案卡;最上面,是文件名——而文件名,根本不是"文件本身",它只是挂在某个目录里、【指向 inode 的一个指针】。我恍然大悟:原来一个文件可以有【好几个】这样的指针,它们平起平坐,都指着同一个 inode。而 rm,这个我以为"毁天灭地"的命令,它的真名其实叫 unlink——"解除一个链接"。它做的事,谦逊得超出我的想象:它只是【拔掉一根指针】,然后看一眼"还有没有别的指针指着这个 inode"。还有,它就什么都不做;一根都不剩了,它才通知内核回收。我那 5GB 没释放,不是 bug,是 rm 在尽职:它拔掉了 /data 那根指针,可 /backup 那根还指着——数据还"被人需要着",它当然不能动。复盘到最深我才明白,我犯的错,是把"名字"和"实体"这两样东西,当成了一样。我以为删掉名字,就等于销毁实体。可名字从来只是【对实体的一个引用】,实体真正的存亡,不取决于"某一个名字在不在",而取决于"【还有没有任何人】在引用它"。这件事一旦想通,我发现它的影子到处都是:一个对象的内存,不在你把某个变量置空时释放,而在【最后一个引用它的变量】都没了时,垃圾回收才动手——这就是引用计数,和 inode 是【同一个东西】。一个 Docker 镜像,你 rmi 它报"image is being used",因为还有容器引用着它。一个共享库,还有进程加载着它,你就删不干净。一份配置、一个密钥、一张数据库里的主表记录——但凡还有【别处】引用着它,它就不该、也不会,因为你在【这一处】划掉了它的名字,就真的消失。这次最大的收获,是我学会了把"删除"这个念头,从"销毁那个东西"改成"解除我和那个东西的这一处关联"。在我 rm 任何重要东西之前,我现在会多问一句:我划掉的,是它【唯一】的名字,还是【众多名字之一】?在我之外,还有没有别人,正握着指向它的另一根指针?rm 不叫"删除",它叫 unlink——这个我用了十几年才真正读懂的名字,把一个朴素的道理刻进了我心里:你能亲手了断的,从来只是【你和它的那一根链接】;那个东西本身,是死是活,要等所有牵着它的手,都一一松开。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

chmod 777 了还是 Permission denied:一次目录 x 权限与路径解析的复盘

2026-5-21 1:30:39

技术教程

Kubernetes Ingress 控制器 · 原理详解 完全指南:速查、踩坑与最佳实践

2026-5-19 0:53:01

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索