🐧 第七章 文件系统
📄 一句话概括

写一个文件其实是三步:标记块、写数据、改 inode。断电落在第三步中间,inode 说【我有 24 字节占 1 块】,而数据要 2 块——这时读文件会读到别人的块。

📝 本节和前面几节的关系

7.2 讲了数据与元数据是两样东西,这一节讲它们不同步的后果:数据都还在盘上,坏掉的是【哪块是谁的】这本账。

一、写一个文件其实是三步

① 在块位图上把那几个块标记为【已占用】。这是防止别人来抢。 ② 把文件内容写进那几个块。 ③ 改 inode:记下大小、记下块号。 这三步不是原子的。磁盘一次只能原子地写一个扇区,而这三步横跨位图区、数据区、inode 区三个地方。 所以断电(或者内核崩、或者拔掉电源)可能落在任何两步之间。 这就是【文件系统不一致】的全部起因:不是数据坏了,是【账做了一半】。

二、三种断电点,三种残局

断在①之后:块被标占,但没有任何 inode 引用它。结果是【空间泄漏】——盘上少了一块,谁也说不清被谁占了。 断在②之后:数据已经躺在块里,inode 还是不知道。和上一种一样是泄漏,只是白写了一次数据。 断在③中间:最坏。inode 的大小与块号只写了一半,两者互相矛盾——比如大小说 24 字节、块号只指了 1 块,而实际数据要 2 块。 这时读这个文件会读到【不属于自己的块】,甚至可能读到别人的文件内容。这才是真正的【文件系统损坏】。 三种残局里,用户的数据其实都还好端端躺在盘上。坏的是账。

三、漏与错是两回事

前两种叫【漏】(leak):少了一块,但没伤到别人。fsck 一扫,把没人引用的块标回空闲就行,修起来又快又安全。 第三种叫【错】(inconsistency):两个地方互相矛盾。fsck 没法知道哪个是对的——是按 inode 的大小算,还是按块号算?它只能选一个【比较安全】的处理:把这个 inode 删掉,把块放进 lost+found。 这个区别决定了所有后续设计的思路:日志与写时复制的目标都是【把错变成漏】。宁可丢一个文件,也不要让两个文件互相污染。 记住这一句,后面的 7.5、7.6 就都好理解了。

四、为什么修起来那么贵

因为 fsck 要【把整个元数据读一遍】。每个 inode 都要查、每个目录项都要对、每张位图都要核。 所以它的耗时正比于【文件数】,不是【容量】。一块 1 TB 的盘,存了 100 万个小文件,fsck 可能跑十几分钟;只存几个大文件,几秒钟就完了。 这也是为什么【现代文件系统都想让你别跑 fsck】:日志让它在挂载时几秒钟就能回放完,写时复制让它根本不需要扫。 一块盘如果每次开机都要 fsck,说明有更深的问题:可能是电源、可能是 SATA 线、可能是盘快坏了。

五、动手 Demo:文件系统不一致——写一个文件,中间断电

这一节用一个 64 块的迷你磁盘模型,在三个位置模拟断电,看每个位置上【位图、数据块、inode】三者的状态是否自洽。

nblk = 64

def crash(done):
    bitmap = [0] * nblk      # 块位图
    disk = [""] * nblk       # 数据块
    inode = {"size": 0, "blocks": []}

    bitmap[10] = bitmap[11] = 1        # ① 标占
    if done >= 2:
        disk[10], disk[11] = "前半", "后半"   # ② 写数据
    if done >= 3:
        inode["size"] = 24             # ③ 改 inode(只写一半)
        inode["blocks"] = [10]
    return sum(bitmap), sum(1 for x in disk if x), len(inode["blocks"])

for name, done in (("断在①之后", 1), ("断在②之后", 2),
                   ("断在③一半", 3)):
    print(name, crash(done))

实跑输出:

--------------------------------------------------------------------
文件系统不一致——写一个文件,中间断电
--------------------------------------------------------------------
① 写一个文件,文件系统其实做了三步

  **① 在块位图上把那几个块标记为【已占用】。**
  **② 把文件内容写进那几个块。**
  **③ 改 inode:记下大小、记下块号。**

  **三步不是原子的**——**断电可能落在任何一步之间。**

② 三种断电点,三种残局

  断在哪        盘上的状态                   fsck 怎么修        用户看到
  -----------------------------------------------------------------------------------
  ①之后         块被标占,但没人引用         标记为空闲         空间回来,文件没了
  ②之后         数据在块里,inode 不知道     标记为空闲         同上,白写一次
  ③写了一半     inode 指向垃圾或大小错       删掉这个 inode     文件损坏

  **前两种是【漏】——空间泄漏,伤不到别的文件,fsck 一扫就清干净。**
  **第三种是【错】——inode 里的块号与大小互相矛盾,**
  **严重时会指到别人的块上。**

③ 用代码把三种残局跑一遍

  断电点        占用块     有数据块     引用块     inode 大小     结论
  -------------------------------------------------------------------------
  断在①之后     2 块       0 块         0 块       0 字节         泄漏
  断在②之后     2 块       2 块         0 块       0 字节         泄漏
  断在③一半     2 块       2 块         1 块       24 字节        不一致

  **①②两行都是【泄漏】**:**块被标占,inode 里一个都没有**,**空间回不来。**
  **第二行与第一行的差别只在【数据到底写没写进去】**——
  **但既然 inode 不知道,写进去也等于白写。**

  **断电落在③中间是最坏的。**
  **inode 说【我有 24 字节,占 1 块】,而实际上数据要 2 块。**
  **这时读文件会读到【不属于自己的块】——这就是文件系统损坏。**

  **注意:三种残局里,【用户的数据】其实都还好端端躺在盘上。**
  **坏掉的是【元数据】——也就是【哪块是谁的】这本账。**
  **所以文件系统一致性,说的从来不是数据,是这本账。**

①②两行都是【泄漏】:位图占了 2 块,inode 里一个都没有,空间回不来。 第二行与第一行的差别只在【数据到底写没写进去】——但既然 inode 不知道,写进去也等于白写。 第三行才是要命的:inode 说 24 字节占 1 块,而数据实际跨了 2 块。这种状态读出来的内容是不可信的。 注意这个模型是【故意做成只写一半】的——真实磁盘上 inode 是被一个扇区一个扇区写的,所以【写了一半】是常态,不是极端情况。

📋 Linux 上观察:dumpe2fs -h /dev/sda1 看Filesystem state 是不是 clean;不是 clean 就说明上次没过干净卸载。

30 秒实验

  1. dumpe2fs -h /dev/sda1 | grep -i state,看是不是 clean。
  2. ls /lost+found,看里面有没有东西(有就说明曾经修过)。
  3. 想一想:你的数据库文件如果落在 ext4 上,断电后最坏会怎样?

常见误区

  1. 【断电只会丢最后那点数据】。更常见的是【元数据不一致】——文件还在,但大小、块号、引用计数互相矛盾。
  2. 【fsck 能把一切修好】。它只能把盘修成【自洽】,不能保证【你的数据还是原来那份】。不一致的 inode 会被直接删掉。
  3. 【SSD 不会因为断电出这种问题】。SSD 有自己的映射表与缓存,掉电同样可能只写了一半,只是表现不同。
自测三问(点开对答案) 1. 为什么说【坏掉的是账,不是数据】? —— **因为三步操作里,数据块那一步是独立完成的**;**出错的是位图与 inode 这两本账**。**账不对,数据就没法被正确读到。** 2. 【漏】与【错】哪个更严重? —— **错更严重**:**漏只是浪费空间,伤不到别人;****错会让一个文件读到别人的块**,可能连锁污染。 3. 为什么 fsck 的耗时正比于文件数而不是容量? —— **因为它要遍历所有 inode 与目录项**,**这是【个数】的工作量,不是【字节数】的工作量。**

相关笔记