写一个文件其实是三步:标记块、写数据、改 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 秒实验
dumpe2fs -h /dev/sda1 | grep -i state,看是不是 clean。ls /lost+found,看里面有没有东西(有就说明曾经修过)。- 想一想:你的数据库文件如果落在 ext4 上,断电后最坏会怎样?
常见误区
- 【断电只会丢最后那点数据】。更常见的是【元数据不一致】——文件还在,但大小、块号、引用计数互相矛盾。
- 【fsck 能把一切修好】。它只能把盘修成【自洽】,不能保证【你的数据还是原来那份】。不一致的 inode 会被直接删掉。
- 【SSD 不会因为断电出这种问题】。SSD 有自己的映射表与缓存,掉电同样可能只写了一半,只是表现不同。
自测三问(点开对答案)
1. 为什么说【坏掉的是账,不是数据】? —— **因为三步操作里,数据块那一步是独立完成的**;**出错的是位图与 inode 这两本账**。**账不对,数据就没法被正确读到。** 2. 【漏】与【错】哪个更严重? —— **错更严重**:**漏只是浪费空间,伤不到别人;****错会让一个文件读到别人的块**,可能连锁污染。 3. 为什么 fsck 的耗时正比于文件数而不是容量? —— **因为它要遍历所有 inode 与目录项**,**这是【个数】的工作量,不是【字节数】的工作量。**- ☐ 7.5 日志
相关笔记
- 数据与元数据 — 两本账分别存在哪
- 日志 — 把错变成漏的第一种办法
- 写时复制 — 第二种办法
- 🐧 Linux是怎样工作的 · 索引