第三章 Flash文件系统——在不可靠的世界上建立可靠
3.1 思想实验——100张纸如何管理
你手里有一百张纸,每张只能写一次
想象一个场景:你是一个办公室文员。你有一本空白的笔记本——不对,准确地说,你有一百张散装的A4纸。你可以往任何一张纸上写字。你有一支钢笔。你的工作很简单:记录来访客人的姓名、时间和事由。一行一条,写完归档。
但问题是——你们老板很喜欢改主意。上午10点他登记了“张三,拜访采购部“,11点他派人来说“不对,是拜访财务部“。你要改。
在普通的笔记本上,你拿起橡皮擦掉“采购部“,写上“财务部“。一秒完成。
但在你的办公桌上,每一张纸只能写一次。纸上的墨水是特制的——它干了以后就永久蚀刻在纸面上。你不能在原位置涂改。你不能用橡皮擦。
你想改?唯一的办法是:拿一张空白纸,把旧纸上要保留的内容全部抄过来,在对应位置写上修改。旧纸上的墨迹还在——但你不用它了。
你的办公桌上,逐渐堆满了写满旧内容的废纸。
这就是Flash文件系统面对的物理现实。
一百张纸的物理约束
这场“纸上Flash“的物理规则可以严格定义为:
纸上Flash 物理约束
======================
规则一:单次写入(Write-Once)
一张新纸,你可以写任意多个字,直到写满。
但墨迹干透后,你不能再在上面加字——墨会糊掉。
规则二:整张清空才能重用(Block-Erase)
要让一张写满的纸变回空白,
你必须用特殊药水把整张纸浸一遍——
药水会一次性溶解掉全部墨迹。
纸变回空白,可以重新使用。
规则三:不能只清一角(最小擦除单位)
你不能用药水只抹掉右下角那一行。
必须整张纸一起浸。
即使你只想改三个字。
规则四:一百张纸是总数上限
你只有一百张纸。
废了就没了。
你要在有限的空间里,假装有无限的自由。
规则五:每次清空都磨损纸张(P/E Cycle)
每次用药水浸泡,纸张纤维都会受损变薄。
浸一次薄一点。浸一万次纸就开始透光。
浸到十万次——纸就穿孔了,彻底废了。
这五个规则,就是NOR Flash文件系统设计的全部物理边界条件。
100张纸 = 100个物理块。每张纸 = 一个4KB的NOR Flash扇区。清空整张纸 = 擦除操作。单次写入 = Flash编程只能1→0。修改 = Copy-on-Write。
我们来玩这个游戏。看看你能活多久。
游戏第一关:天真的你
最简单的办法是什么?
你给每张纸编号:1号到100号。第一行数据写在1号纸上。第二行写在2号纸上。依此类推。
天真方案:顺序写入
=======================
1号纸: [张三, 10:00, 采购部]
2号纸: [李四, 10:15, 技术部]
3号纸: [王五, 10:30, 人事部]
...
“张三要改成财务部?没问题。“你想:把1号纸上的“采购部“改成“财务部“就行。
但你做不到——墨迹已经蚀进纸里了。你不能在原位置涂改。
那怎么办?你把1号纸上其他要保留的内容抄到一张空白纸上——比如45号纸——然后把张三那条也抄过来,把“采购部“改成“财务部“。
45号纸: [张三, 10:00, 财务部] ← 新记录
1号纸: [张三, 10:00, 采购部] ← 旧记录,废弃不用
你现在有一个问题:谁来告诉你哪张纸上的记录是“真的“?你翻到45号纸,看到张三的记录是“财务部“。但如果改天你又翻到1号纸,上面张三写的是“采购部“。哪条是有效的?
你需要在纸上标记一个“版本号“——或者说“序列号“。
方案修订二:增加序列号
=======================
45号纸: [SEQ=002] [张三, 10:00, 财务部] ← 有效
1号纸: [SEQ=001] [张三, 10:00, 采购部] ← 无效(旧版本)
规则:同一记录,选SEQ号大的作为有效版本。
你在纸上加了序列号。这很好。但新问题来了:你要查找张三的时候,你得从第1张纸翻到第100张纸——因为张三的记录可能出现在任何一张空闲的纸上。
你需要一个“目录“——一张专门的纸,记录每个人在哪张纸上。
方案修订三:增加目录纸
=======================
目录纸(0号纸):
[张三 → 45号纸, SEQ=002]
[李四 → 2号纸, SEQ=001]
[王五 → 3号纸, SEQ=001]
45号纸: [张三, 10:00, 财务部]
2号纸: [李四, 10:15, 技术部]
3号纸: [王五, 10:30, 人事部]
现在你可以快速找到每个人的记录了。但问题是——目录纸本身也要修改。 每次张三的记录换了一张纸,你就要更新目录纸上的映射。
而目录纸——它也是一张纸。它也只能写一次。你每改一次目录,就要把新目录写在一张新的空白纸上,旧目录纸变成废纸。
你绝望地发现,你在用一个写一次的东西,去管理另一个写一次的东西。很快,目录成了你这个系统里消耗纸张最快的东西——因为它每次数据修改都要更新,每次更新都要占用一张新纸。
这就是FAT表的问题。
游戏第二关:学聪明的你
你开始反思:既然每张纸只能写一次,那我能不能在一张纸上写多条记录——不是一纸一条,而是一纸多条,按时间顺序追加?
追加方案(Append-Only)
=======================
45号纸:
[记录1: 张三, 10:00, 采购部] ← 第一次写
[记录2: 张三, 10:05, 财务部] ← 第二次写(修正!)
[记录3: 李四, 10:15, 技术部] ← 新增
[ 空白区域 → ← 还能继续写
这个方案的美妙之处在于:你不需要另拿一张新纸来修改张三。 你只需要在45号纸的末尾追加一条新记录“张三→财务部“。读的时候,从45号纸的最后一条记录往前读,碰到第一条“张三“的记录就是最新的。
但问题是——45号纸总有写满的一天。写满了怎么办?
45号纸(已满):
[████████████████████████████████]
[████████████████████████████████]
[████████████████████████████████]
[████████████████████████████████]
全满了,没有空白可以写新的追加记录
你需要垃圾回收(Garbage Collection):
垃圾回收流程
======================
1. 挑选一张"垃圾最多的纸"——比如45号纸
45号纸上有:
- 张三的2条记录(第一条已过时,是"垃圾")
- 李四的1条记录(有效)
- 共3条记录,其中1条垃圾,2条有效
2. 拿一张新的空白纸——比如89号纸
3. 把45号纸上有效的2条记录抄到89号纸上
89号纸:
[张三, 10:05, 财务部] ← 只保留最新版本
[李四, 10:15, 技术部]
4. 用药水清空45号纸(整张浸药水→墨迹全部溶解)
45号纸变回空白,重新进入可用纸池
5. 89号纸现在有大量空白区域,可以继续追加新记录
你完成了垃圾回收算法的设计。恭喜。
但问题还没完。垃圾回收本身也有成本:你为了回收一张纸(45号),清空了一张纸,写了一张新纸(89号),并且把有效的2条记录又写了一遍——这加剧了纸张的消耗。
每一条有效记录,可能在它的生命周期中被“搬家“很多次——从A纸搬到B纸,再从B纸搬到C纸……每一次搬家都是一次额外的写入。这个额外的写入量和原始写入量之间的比例,就是写放大(Write Amplification)。
游戏第三关:100张纸的生死簿
停下来算一笔完整的账。
假设你每天接待100位客人。每条记录的原始信息量是50个字节。每张纸的容量是4000个字节。理论上,你100张纸的总容量是400,000字节——够用4000天,将近11年。
但这是理论值。在现实中:
100张纸的真实寿命计算
=======================
原始写入:100条/天 × 50字节 = 5,000字节/天
但还有:
✗ 修改操作:约20%的记录会被修改
每次修改 = 重新写50字节(追加模式)或 另拿新纸重写(CoW模式)
追加模式下写放大 ≈ 1.2
✗ 垃圾回收:当纸写满后触发GC
每次GC = 搬移有效记录 + 清空垃圾纸(药水浸泡→变空白)
在纸平均利用率50%时触发GC:
每次回收释放2000字节 = 50%(垃圾)
同时写入2000字节 = 50%(有效数据搬家)
GC写放大 ≈ 1.0
✗ 目录管理:目录纸自身的修改
每条新记录 = 更新一次目录映射
每条目录映射 ≈ 10字节
每天100条 = 1000字节的目录修改
综合写放大 ≈ 1.2(追加修改) + 1.0(GC) + 0.2(目录)
≈ 2.4
真实日写入量 ≈ 5,000 × 2.4 = 12,000 字节/天
理论日写入量 = 5,000 字节/天
写放大 = 2.4 倍
总纸张寿命:400,000 ÷ 12,000 ≈ 33 天
而不是理论上的 400,000 ÷ 5,000 = 80 天
写放大会吃掉你一半以上的纸张寿命。
这还没算磨损。100张纸,你最常用的是存放目录的那张纸——它被频繁更新。当你把它清空了1000次之后,纸张纤维已经薄得像葱皮。旁边的纸也因为清空次数不同而厚度不均,整叠纸开始变形。
磨损不均衡示意图
======================
无磨损均衡:
目录纸(0号): ████████████████████ 清空1000次 ← 快穿孔了
1号纸: ██ 清空120次
2号纸: ██ 清空150次
...
99号纸: █ 清空1次 ← 几乎全新
问题:目录纸先穿孔变坏块,整个系统瘫痪
有磨损均衡:
每张纸的清空次数均匀分布在 250~280 次
所有纸同步老化,系统整体寿命最大化
到这里,你已经亲手触摸到了Flash文件系统设计的三个核心问题:
- 空间管理(Allocation):如何找到一张空白的纸?
- 垃圾回收(Garbage Collection):如何高效地把写满废记录的纸变回空白纸?
- 磨损均衡(Wear Leveling):如何让所有纸老得一样快?
从纸到Flash:映射关系
这场思想实验的每一个要素,在Flash文件系统里都有精确的对应:
纸 → Flash 映射表
======================
100张纸 = 100个物理块(4KB each)
每张纸只能写一次 = 编程只能1→0
清空整张纸(药水浸泡) = 擦除(Block Erase)
每次清空磨损纸张 = P/E Cycle消耗
纸上写序列号 = 单调递增的sequence number
目录纸 = Superblock / File Table
追加模式 = Log-structured metadata
垃圾回收 = GC (Garbage Collection)
空白纸利用率 = 写放大系数
纸张清空次数不均衡 = 磨损不均衡
目录纸被频繁清空 = 热数据(Hot Data)问题
几乎从不被清空的纸 = 冷数据(Cold Data)问题
现在你可以把“纸“这个词全部替换成“块(block)“,把“清空“替换成“擦除(erase)”,把“抄写“替换成“编程(program)“——你得到的就是一个简化版的Flash文件系统设计文档。
三个核心操作的形式化定义
这场思想实验中最核心的三个操作可以形式化如下:
操作一:分配(Allocate)
allocate(n) → 返回n张空白纸的编号
算法(简单版):
1. 遍历第0号到第99号纸
2. 找到第一张状态为"空白"的纸
3. 标记为"已使用"
4. 返回编号
算法(磨损均衡版):
1. 遍历第0号到第99号纸
2. 找到所有状态为"空白"的纸
3. 从中选择"清空次数最少"的那一张
4. 标记为"已使用"
5. 返回编号
6. 该纸的清空次数 +1
操作二:写入/追加(Write/Append)
write(paper_id, offset, data, length):
1. 检查纸[paper_id]是否有足够的空白空间
2. 在纸的[offset]位置开始写入data
3. 更新纸内偏移指针
append(paper_id, data, length):
1. 检查纸[paper_id]的当前写入偏移 + length ≤ 纸容量
2. 在当前偏移位置写入data
3. 当前偏移 += length
操作三:垃圾回收(Garbage Collect)
gc_compact(victim_paper_id):
1. 选择一张垃圾最多的纸(victim)
2. 将该纸上所有"有效"的记录复制到一张新的空白纸
3. 用药水清空 victim 纸(整张浸泡→墨迹溶解→变空白)
4. victim 纸状态改为"空白",重新进入可用纸池
5. 更新目录纸上的映射关系
这三个操作,构成了任意一个Flash文件系统的骨架。无论你是看LittleFS、SPIFFS、YAFFS还是KnotFS——它们的区别只是这三个操作的具体实现策略不同。
你其实已经设计好了一个文件系统
回顾一下,你在玩“100张纸“这个思想实验的过程中,不知不觉做了哪些设计决策:
你的设计决策清单
=======================
1. 你选择了"序列号方案"来区分新旧版本
→ 对应块序列号单调递增
2. 你选择了"目录纸"来快速查找数据位置
→ 对应嵌入在SB中的文件表(nodes[8]),双副本由SB双槽位保证
3. 你选择了"追加模式"来避免频繁清空纸张
→ 对应 SB Log 的日志追加
4. 你选择了"垃圾回收"把写满废记录的纸变回空白纸
→ 对应垃圾回收 (GC)
5. 你开始考虑"磨损均衡"来让所有纸均匀老化
→ 对应磨损计数 和 自由块选择
6. 你意识到了"目录纸"本身也是热数据,需要特殊处理
→ 对应元数据的双副本 + 日志
你还没有开始写一行C代码,但你已经完成了Flash文件系统的核心架构设计。
这就是思想实验的力量:把复杂的技术问题抽象成一个足够简单但足够精确的物理模型,让你用人脑就能遍历所有的设计空间。
从办公桌到ECU
你抬头看看你的办公桌。一百张纸散乱地堆在面前。有的纸面粗糙发黄(被药水泡过太多次),有的笔迹清晰(新写的),有的墨迹斑斑(写满了追加记录)。你的手因为频繁浸泡纸张而沾满了药水。
你要管理的不是一百张纸。你要管理的是车规级ECU里的512KB NOR Flash——128个4KB的块,要求0ppm的现场故障率,在-40°C到+125°C的温度范围内可靠工作15年。
但核心问题没有变。一百张纸还是128个块。清空纸张还是擦除块。药水腐蚀还是P/E磨损。版本号还是序列号。目录纸还是superblock。
你从办公桌站起来,走到车间的测试台前。示波器的探针夹在Flash芯片的CS引脚上。你看到每一次擦除操作的SPI命令序列:06, D8, 00 00 XX XX。你看到FPGA逻辑分析仪上每一个bit的电平跳变。你知道,这背后是浮栅隧道氧化层里几千个电子在量子隧穿。
而你,只是在一张纸上多写了一个名字。
这就是Flash文件系统的魅力:它把量子物理的世界,翻译成了人类可以理解的“纸和药水“隐喻。而这本书的目的,就是带你走完这场翻译的全程——从纸上的墨迹,到硅里的电子。
下集预告
“100张纸“的游戏玩明白了。但你有没有想过:谁告诉你Flash是一叠可以读、可以写的纸的?谁来保证“读第45号纸“这个操作真的能把你想要的数据还给你?
这背后有一个巨大的“谎言“——块设备抽象。它把一块脾气暴躁的硅片,伪装成了一个温顺的“读块/写块“设备。下一节,我们揭开这层谎言,看看一个异步I/O驱动是如何用状态机和缓冲区,把Flash的物理缺陷藏起来的。
悬念留给:NOR Flash的读延迟是70纳秒,擦除延迟是200毫秒——这两个数字之间差了285万倍。一个“统一的块设备接口“,如何包住这个鸿沟?