第22讲 文件系统崩溃一致性II

第22讲 文件系统崩溃一致性II
agsd从日志到闪存:日志结构文件系统与F2FS如何组织持续写入
引言
传统带日志文件系统通常采用如下更新流程:
1 | 先把修改写入日志 |
这种设计能够有效保证崩溃一致性,但也存在明显代价:同一份修改可能先写入日志,再写入正式位置,产生额外写入。
于是,日志结构文件系统提出了一个更彻底的问题:
既然日志中已经保存了最新数据,为什么还要维护另一套固定位置的文件系统结构?
日志结构文件系统,即Log-structured File System,简称LFS,选择直接把日志作为文件系统的主要存储形式。文件数据、inode和其他元数据不再频繁覆盖原位置,而是不断追加到新的位置。
其基本更新方式是:
1 | 旧版本保留在原位置 |
这种设计能够把许多离散的小写入合并成大块顺序写,尤其适合随机写代价较高的机械硬盘。
然而,异地追加也带来新的问题:
- inode不断移动后,系统怎样找到它?
- 旧版本留下的无效空间怎样回收?
- 清理时怎样判断哪些数据仍然有效?
- 崩溃后怎样快速恢复最新状态?
- 面对闪存的擦除限制、有限寿命和内部并行性,传统LFS是否仍然合适?
F2FS,即Flash-Friendly File System,正是在日志结构思想基础上,针对闪存设备进一步优化的文件系统。
一、带日志文件系统与日志结构文件系统
“带日志文件系统”和“日志结构文件系统”虽然名称相似,但日志在二者中的地位完全不同。
| 对比点 | 带日志文件系统 | 日志结构文件系统 |
|---|---|---|
| 英文 | Journaling File System | Log-structured File System |
| 日志作用 | 临时记录待执行的修改 | 文件系统主要存储形式 |
| 是否维护正式位置 | 是 | 数据主要直接存在日志中 |
| 更新方式 | 日志提交后覆盖原位置 | 向日志末尾追加新版本 |
| 主要恢复机制 | 重放已提交日志 | 检查点与前滚恢复 |
| 空间回收 | 循环复用日志区域 | 清理无效块和段 |
带日志文件系统的结构可以理解为:
1 | 日志区域:暂时保存修改 |
而LFS的结构则是:
1 | 持续增长的日志 |
因此,LFS不是简单地把日志区域做得更大,而是改变了整个文件系统的组织原则。
1. LFS为什么采用追加写
LFS的提出建立在两个重要观察上。
读取经常被缓存吸收
操作系统会使用页缓存保存近期访问过的文件内容。许多读取请求可以直接从内存完成,不必真正访问磁盘。
因此,文件系统性能瓶颈常常集中在必须持久化的写入上。
机械硬盘顺序写远快于随机写
传统文件系统更新不同对象时,可能需要写入:
- inode区域
- 位图区域
- 目录块
- 文件数据块
- 间接索引块
这些位置分散在磁盘不同区域,会引发频繁寻道。
LFS将内存中积累的修改整理后,连续写入日志末尾:
flowchart LR
A[多个离散文件修改] --> B[在内存中聚合]
B --> C[形成连续日志块]
C --> D[顺序写入存储设备]
这样可以将大量小型随机写转换成较大的顺序写。
2. 异地更新
LFS最基本的规则是:
不在原位置覆盖旧版本,而是在新位置写入更新后的版本。
假设一个文件的inode位于磁盘位置A。
文件被修改后,LFS不会在A处覆盖inode,而是:
- 在日志末尾写入新的文件数据。
- 在日志末尾写入新的inode。
- 更新映射,使系统找到新inode。
- 旧inode因为不再被当前映射引用而失效。
这与普通的原地更新不同:
1 | 原地更新: |
异地更新避免了随机覆盖,却使文件对象的位置不断变化。
这就引出了LFS的第一个核心结构:inode map。
❗ 本节核心结论: 带日志文件系统把日志当作临时恢复记录;LFS则直接用持续追加的日志保存文件和元数据,从而把随机更新转化为顺序写。
二、inode map:如何找到不断移动的inode
在传统inode文件系统中,inode通常位于固定的inode表中。只要知道inode号,就可以按照固定公式计算其位置。
例如:
1 | inode号 |
但在LFS中,inode每次更新都会被写到新位置。inode号保持不变,磁盘位置却不断变化。
因此,系统需要维护:
1 | inode号 → 最新inode位置 |
这张映射表就是inode map,也称imap。
1. inode map的工作方式
假设当前映射为:
1 | inode 1 → 位置A |
inode 2被修改后,新的inode被追加到位置K。系统只需把映射更新为:
1 | inode 2 → 位置K |
旧位置D上的inode仍然存在,但已经不再是当前版本。
完整查找流程变为:
flowchart LR
A[inode号] --> B[inode map]
B --> C[最新inode位置]
C --> D[inode]
D --> E[文件数据块]
inode map增加了一层间接映射,使inode可以自由移动而不改变其编号。
2. inode map本身也会移动
inode map同样属于文件系统元数据,也会被修改。
如果inode位置变化,inode map中对应的表项就必须更新;而inode map的新版本通常也以日志形式写入新位置。
这会产生新的问题:
inode map的位置又由谁记录?
LFS通常通过检查点区域记录inode map各部分的位置。检查点位于固定、可直接找到的位置,是文件系统启动恢复的入口。
因此,查找链条进一步变为:
1 | 固定检查点 |
这是一种多层间接定位结构。
3. 创建文件时的完整更新
假设在根目录下创建文件/file3,其内容为hello。
从用户角度看,这只是创建并写入一个文件。但LFS需要追加多个对象。
写入文件数据和inode
系统先追加:
- 保存
hello的数据块 - 指向该数据块的新文件inode
此时文件自身的数据结构已经存在,但目录中还无法通过名字找到它。
更新父目录
根目录需要新增目录项:
1 | file3 → inode 6 |
LFS不能在原位置修改根目录数据块,因此需要追加:
- 新的根目录数据块
- 指向新目录数据块的新根目录inode
更新映射
之后还需要更新:
- inode 6的分配状态
- inode 6的新位置
- 根目录inode的新位置
- inode map的新版本
完整过程可以表示为:
flowchart TD
A[写入file3数据块] --> B[写入file3 inode]
B --> C[写入新根目录数据块]
C --> D[写入新根目录inode]
D --> E[更新inode map]
E --> F[旧版本失效]
一次简单的文件创建,最终可能触发多个数据和元数据对象的新版本写入。
4. 无效不等于已删除
新版本生效以后,旧inode和旧数据通常仍然保留在磁盘中。
它们只是:
1 | 不再被最新inode map或inode指针引用 |
因此旧块处于“无效”状态,但尚未被物理清除。
需要区分:
| 状态 | 含义 |
|---|---|
| 有效 | 当前文件系统状态仍引用该块 |
| 无效 | 已被新版本替代,不再被引用 |
| 空闲或干净 | 可以直接重新用于写入 |
| 已擦除 | 对闪存而言,物理擦除已经完成 |
无效块只有在后续空间清理中被回收后,所在区域才可以重新使用。
❗ 本节核心结论: inode map记录inode号到最新inode位置的映射,使inode能够在日志中不断移动;旧版本不会立即消失,而是先变成等待回收的无效数据。
三、为什么LFS必须进行空间回收
LFS不断向新位置追加数据,但设备容量有限。
随着文件反复修改,磁盘会同时存在:
- 当前有效版本
- 被新版本替代的旧inode
- 被覆盖的旧文件数据
- 旧目录块
- 旧inode map块
这些旧内容虽然已经无效,却散布在日志的各个位置。
最终可能出现:
1 | 设备表面上已经写满 |
因此,LFS必须识别无效内容,并重新组织剩余有效数据,从而释放可连续写入的空间。
四、Segment:顺序写与空间回收的折中单位
1. 为什么不按单个块回收
一种简单方法是把每个空闲块加入空闲链表。之后需要空间时,从链表中取任意块使用。
这样虽然能充分利用零散空间,但新写入会散落到各处,重新变成随机写,破坏LFS的主要优势。
1 | 空闲块链表 |
2. 为什么不能每次整理整个磁盘
另一个极端是把所有有效数据完整复制到连续区域,然后释放其他空间。
这样可以获得非常整齐的布局,但每次整理都要扫描和复制大量数据,成本过高。
3. 段的折中
LFS把存储空间划分为固定大小的段,即Segment。
段通常远大于单个文件系统块,例如:
- 512KB
- 1MB
- 2MB
系统以段为单位进行:
- 顺序写入
- 有效性统计
- 数据搬迁
- 空间回收
这种设计避免了单块管理过于零散,也避免了全盘整理代价过高。
4. 段内写入规则
一个可写段内部通常按照顺序追加。
1 | 块0 → 块1 → 块2 → 块3 → … |
当前段写满后,系统从干净段列表中选择另一个段继续写入。
所谓干净段,是指:
整个段已经可以重新作为新的日志区域使用。
段中只要还存在不能丢弃的有效数据,就不能直接作为干净段覆盖。
5. 段使用表
LFS使用段使用表记录每个段的整体状态,例如:
- 段中有效字节或有效块数量
- 最近修改时间
- 是否已经清理
- 是否可直接回收
例如:
| 段 | 有效块数量 | 最近修改时间 |
|---|---|---|
| Segment 1 | 0 | 较早 |
| Segment 2 | 8 | 较新 |
| Segment 3 | 2 | 较早 |
如果某段的有效块数量为0,说明段内所有内容都已失效,可以直接转变为干净段。
如果仍有少量有效块,则必须先搬走这些块,才能释放整段。
段使用表只能说明:
1 | 一个段中还有多少有效内容 |
但不能说明:
1 | 具体哪个块有效,以及它属于哪个文件 |
后一个问题由段概要解决。
❗ 本节核心结论: Segment是LFS顺序写与空间回收的基本单位,在零散块管理和全盘整理之间取得折中。
五、段概要:如何判断块是否仍然有效
每个段需要记录段内各块的身份,这类信息称为Segment Summary,即段概要。
某个块的概要可能记录:
1 | 块类型:文件数据块 |
含义是:
这个物理块曾经保存inode 8所代表文件的第2个逻辑数据块。
1. 有效性的判断标准
仅凭段概要不能直接判断块是否仍然有效,因为段概要只能说明这个块“曾经属于谁”。
系统还需要检查当前文件系统状态是否仍然引用它。
判断过程为:
- 从段概要得知该块属于inode 8的逻辑块2。
- 通过inode map找到inode 8的最新版本。
- 查看最新inode中逻辑块2的当前地址。
- 比较当前地址是否仍然是该物理块。
flowchart TD
A[读取段中某块] --> B[段概要:inode 8,逻辑块2]
B --> C[通过inode map找到最新inode 8]
C --> D{最新指针是否仍指向此块}
D -- 是 --> E[块仍然有效]
D -- 否 --> F[块已经失效]
因此,块是否有效取决于:
当前文件系统的最新指针是否仍然认领它。
数据仍然存在于磁盘上,并不代表它仍属于当前文件系统状态。
2. inode块如何判断有效性
如果段中的对象本身是inode,也可以利用inode map判断。
例如段概要表明某块保存inode 8,那么系统检查:
1 | inode map[8]是否仍然指向该块 |
如果是,该inode块仍然有效;如果inode map已经指向新位置,旧inode块就失效。
3. 段概要与段使用表
| 对比点 | 段概要 | 段使用表 |
|---|---|---|
| 粒度 | 段内单个块 | 整个段 |
| 记录内容 | 块属于哪个inode及逻辑位置 | 有效块数量、时间等 |
| 主要用途 | 识别块身份、判断有效性 | 管理段、选择清理对象 |
| 查询方向 | 物理块到文件对象 | 段到整体使用状态 |
inode map则提供相反方向的定位:
1 | inode号 → inode位置 |
所以可以将三者区分为:
- inode map:正向找到inode。
- 段概要:从磁盘块反查其身份。
- 段使用表:统计整个段的有效程度。
六、段清理
段清理的目标是:
把若干包含大量无效数据的旧段,整理成少量保存有效数据的新段,并释放旧段。
1. 选择待清理段
系统通常优先选择:
- 无效块比例高的段
- 长时间未清理的段
- 清理收益较高的段
如果一个段包含大量仍然有效的数据,清理时需要搬迁很多块,收益较低。
如果一个段几乎全部无效,只需搬迁少量内容即可释放整个段,收益较高。
2. 读取段与概要
系统读取待清理段及其段概要,识别每个块曾经保存的对象。
3. 判断有效块
对每个块,系统通过最新inode、inode map和其他映射判断它是否仍然被引用。
4. 搬迁有效数据
仍然有效的数据不能丢弃。
系统将这些块重新整理,并顺序写入新的干净段。
例如:
1 | 旧段A:有效1、无效、无效、有效2 |
清理后:
1 | 新段C:有效1、有效2、有效3 |
5. 更新映射
有效块迁移到新位置后,需要更新:
- inode中的数据块位置
- inode map
- 段使用信息
- 其他相关元数据
这又会产生新的日志写入。
6. 释放旧段
所有有效数据迁出后,旧段可以标记为干净段,加入可写段列表。
flowchart TD
A[选择低利用率旧段] --> B[读取段与段概要]
B --> C[识别有效块]
C --> D[把有效块写入新段]
D --> E[更新映射]
E --> F[旧段转为干净段]
7. 清理带来的写放大
段清理会产生额外I/O:
- 读取旧段
- 复制仍有效的数据
- 更新映射
- 写入新的段概要和管理信息
因此,LFS的前台更新虽然主要是顺序追加,但后台清理可能重新写入大量未被用户修改的数据。
这说明:
LFS避免了传统日志的“日志加正式位置”双写,却不能避免整个生命周期中的额外写入。
如果清理策略选择不当,写放大仍然可能很严重。
❗ 本节核心结论: 段清理通过搬迁仍有效的数据来释放整个旧段;它恢复了连续空间,但会产生额外读写和写放大。
七、Checkpoint:崩溃恢复的可信起点
如果系统每次挂载时都从日志开头扫描到末尾,理论上可以重建最新状态。
但日志可能非常长,其中还包含大量失效旧版本。随着文件系统增大,全量扫描会使挂载时间不可接受。
因此,LFS会定期创建Checkpoint,即检查点。
1. 检查点保存什么
检查点记录某一时刻的关键文件系统状态,例如:
- inode map各部分的位置
- 段使用表
- 当前日志尾位置
- 当前时间或版本号
- 其他恢复所需元数据
有了这些信息,文件系统可以直接恢复到检查点时刻,而不必从日志最早位置开始扫描。
2. 检查点的恢复流程
1 | 读取检查点 |
检查点并不保存整个文件系统的数据副本,而是保存足以找到当前主要结构的入口信息。
3. 为什么需要两个检查点区域
如果只有一个检查点区域,系统更新检查点时可能在中途崩溃。
例如:
1 | 旧检查点已经被部分覆盖 |
此时唯一恢复入口可能损坏。
因此,系统通常准备两个检查点区域,并交替写入:
1 | Checkpoint A:上一次完整状态 |
新检查点完整持久化以前,旧检查点仍然保留。
下一次再更新时,可以反向写入另一个区域。
这种设计遵循崩溃一致性的基本原则:
在新版本确认完整之前,不破坏最后一个有效旧版本。
八、Roll-forward:恢复检查点之后的更新
检查点是周期性生成的,并不会在每次文件写入后立即更新。
因此,系统可能在以下时刻崩溃:
1 | 检查点完成 |
如果恢复时只加载检查点,检查点之后已经完整写入的修改就会全部丢失。
LFS因此采用Roll-forward,即前滚恢复。
1. 前滚恢复流程
- 选择最新完整检查点。
- 恢复检查点记录的inode map和段状态。
- 从检查点对应的日志尾开始继续扫描。
- 根据段概要识别检查点之后写入的新inode和数据。
- 验证这些结构是否构成完整更新。
- 将完整更新加入恢复后的文件系统状态。
- 丢弃未被有效对象引用的孤立块。
flowchart TD
A[读取最新检查点] --> B[恢复检查点状态]
B --> C[扫描检查点后的日志]
C --> D[识别完整新inode和数据]
D --> E[前滚到更新后的状态]
D --> F[丢弃不完整或孤立数据]
2. Checkpoint与Roll-forward的关系
| 机制 | 作用 |
|---|---|
| Checkpoint | 提供可信、完整的恢复起点 |
| Roll-forward | 尽量找回检查点之后的完整更新 |
检查点减少扫描范围,前滚减少数据丢失。
二者结合后,系统既不需要扫描整个磁盘,也不必简单回退到较早状态。
❗ 本节核心结论: 双检查点保存最近完整状态,Roll-forward再扫描检查点之后的日志,恢复已经完整写入但尚未进入新检查点的更新。
九、顺序追加为什么仍不能自动保证目录一致性
LFS把内容按顺序追加,并不意味着一次复杂操作的所有部分一定同时完成。
例如创建新文件可能涉及:
- 写入新文件数据
- 写入新文件inode
- 修改父目录
- 更新inode链接数
- 更新inode map
系统可能在任意两步之间崩溃。
假设:
1 | 新文件inode已经写入 |
此时inode可能认为自己已被目录引用,但实际上没有任何目录项能够访问它。
段概要只能说明:
1 | 某个块属于哪个inode |
它无法表达:
- create操作是否完整完成
- link是否完成
- rename的两个目录修改是否都完成
- unlink是否已经完成
- inode链接数与目录项数量是否一致
因此,LFS还需要专门记录目录操作。
十、目录修改日志
目录修改日志用于记录目录相关操作的语义信息,例如:
- create
- link
- unlink
- rename
- 目录项位置
- 目录项内容
- inode链接数变化
该日志需要在真正修改目录前持久化,使恢复程序能够判断操作意图。
例如重命名可能包含:
1 | 在新目录中增加新名字 |
如果系统在中途崩溃,目录修改日志可以帮助恢复程序决定:
- 应该完成剩余步骤
- 应该撤销不完整步骤
- 应如何修正inode链接计数
这形成一个看似特殊的结构:
1 | LFS本身已经采用日志结构 |
原因在于:
物理上的顺序写入只能证明某些块已写入,不能自动证明这些块共同表达了一个完整、合法的文件系统操作。
崩溃一致性不仅是块级完整性问题,也是高层语义关系问题。
❗ 本节核心结论: 顺序追加只能保证数据被依次写出,不能自动保证inode、目录项和链接计数的语义一致,因此仍需要目录修改日志。
十一、NAND Flash的基本特性
LFS最初主要针对机械硬盘设计。将类似思想用于SSD时,必须理解NAND Flash的物理限制。
闪存内部通常具有如下层次:
1 | Package |
SSD控制器还通过多个Channel并行访问多个Die和Plane。
1. 读取、写入与擦除单位不同
NAND Flash通常以Page为读写或编程单位,以更大的Erase Block为擦除单位。
例如:
1 | NAND Page:8KB或16KB |
具体大小随设备而变化,但关键关系是:
1 | 擦除单位远大于写入单位 |
2. 闪存不能任意原地覆盖
已经写入的闪存页不能像内存一样随意改写。
如果某个页需要更新,典型做法是:
- 把新数据写入另一个空闲页。
- 更新地址映射。
- 将旧页标记为无效。
- 之后通过垃圾回收统一擦除旧块。
如果擦除块中仍存在有效页,垃圾回收还要:
- 读取有效页。
- 将其复制到其他位置。
- 擦除整个块。
- 重新使用该块。
这一过程会产生写放大。
3. 擦写寿命
闪存块能够承受的Program/Erase Cycle有限。
如果少数物理块被反复擦除,它们会比其他块更快损坏。
因此SSD需要磨损均衡:
将写入和擦除尽量分散到不同物理块,使设备整体寿命更加均衡。
4. SSD具有内部并行性
SSD没有机械磁头,但它不是一个单线程存储设备。
控制器可以通过多个:
- Channel
- Die
- Plane
并行执行操作。
因此,SSD性能不仅取决于是否顺序写,还取决于写入是否能分布到多个内部单元。
如果文件系统只维持一个单一串行写入点,可能无法充分利用设备内部并行性。
5. Cell类型
按照每个Cell保存的比特数量,闪存可分为:
| 类型 | 每Cell比特数 | 一般特征 |
|---|---|---|
| SLC | 1 | 速度快、耐久性高、成本高 |
| MLC | 通常2 | 容量与性能折中 |
| TLC | 3 | 密度更高、广泛使用 |
| QLC | 4 | 容量密度高、写入与寿命压力更大 |
每个Cell保存的状态越多,通常意味着:
- 容量密度提高
- 每GB成本下降
- 编程更复杂
- 误差容忍更困难
- 写入性能和耐久性面临更大压力
十二、FTL:SSD内部的地址转换层
操作系统把SSD视为普通块设备,并向其发送逻辑块地址。
但SSD内部真正操作的是闪存页和擦除块。
FTL,即Flash Translation Layer,负责完成:
1 | 主机逻辑块地址 |
1. 覆盖写如何被FTL处理
当操作系统要求覆盖某个逻辑块时,SSD可能执行:
- 找到一个新的空闲物理页。
- 将新数据写入该页。
- 更新逻辑块到物理页的映射。
- 将旧物理页标记为无效。
- 在之后的垃圾回收中擦除旧页所在块。
这与LFS的异地更新非常相似。
2. FTL的职责
FTL通常负责:
- 逻辑地址到物理地址映射
- 异地更新
- 垃圾回收
- 磨损均衡
- 坏块管理
- 数据迁移
- 内部并行调度
因此,即使上层文件系统看起来在执行覆盖写,SSD内部也可能把它转化为异地写入。
3. 有FTL为什么还需要闪存友好文件系统
FTL不能完全消除上层文件系统低效写入的代价。
如果文件系统产生大量:
- 小型随机写
- 冷热数据混合
- 反复元数据更新
- 短生命周期数据
- 无规律覆盖
FTL就需要更频繁地:
- 更新映射
- 搬迁有效页
- 执行垃圾回收
- 擦除闪存块
这会增加写放大并缩短设备寿命。
因此,文件系统仍然需要尽量产生更适合闪存的写入模式。
❗ 本节核心结论: FTL把逻辑块映射到物理闪存页,并在设备内部执行异地更新和垃圾回收;但上层文件系统的访问模式仍会影响SSD性能和寿命。
十三、传统LFS在闪存上的两个问题
LFS的异地更新与闪存天然契合,但传统LFS并不能直接发挥现代SSD的全部能力。
1. 递归更新
假设只修改文件的一个数据块。
由于数据块写到了新位置,索引块中的地址必须更新;索引块写到新位置后,inode中的指针也要更新;inode移动后,inode map又要更新。
flowchart TD
A[修改一个数据块] --> B[数据块位置改变]
B --> C[更新索引node]
C --> D[索引node位置改变]
D --> E[更新inode]
E --> F[inode位置改变]
F --> G[更新inode map]
这种位置变化沿索引层级向上传播,称为递归更新。
用户只修改少量数据,却可能触发多层元数据重写。
在写入寿命有限的闪存上,这类写放大尤其不利。
2. 单一日志限制并行性
传统LFS通常强调一条连续日志,在一个日志尾部不断追加。
对于机械硬盘,这可以最大限度减少寻道。
但SSD具有多个并行通道。如果所有更新都集中在一个写入点,可能形成串行瓶颈。
因此,更适合SSD的文件系统需要:
- 多个并行日志
- 多个当前写入段
- 根据数据类型分流
- 根据更新频率分流
这正是F2FS的重要改进方向。
十四、F2FS:面向闪存的日志结构文件系统
F2FS全称为Flash-Friendly File System。
它继承了LFS的核心思想:
- 异地更新
- 顺序追加
- 段式空间管理
- 垃圾回收
- 检查点恢复
同时针对闪存加入了:
- NAT减少递归更新
- 多日志提高并行性
- 冷热数据分离减少垃圾回收开销
- 面向闪存的段、区和管理结构
十五、NAT:用稳定Node ID切断递归更新
NAT是Node Address Table。
它维护:
1 | Node ID → 当前逻辑块地址 |
这里的Node不仅包括inode,还包括:
- 直接node
- 间接node
- 其他索引node
1. 传统指针结构
传统结构中,父节点直接保存子节点位置:
1 | 父节点 → 子节点当前地址 |
子节点一旦移动,父节点就必须修改。
父节点修改后也会移动,从而继续影响更高层节点。
2. 引入NAT后的结构
F2FS让父节点保存稳定的Node ID,而不是容易变化的位置:
1 | 父节点 |
flowchart LR
A[父Node] --> B[子Node ID]
B --> C[NAT]
C --> D[子Node当前位置]
当子node被写入新位置时,只需更新:
1 | NAT[Node ID] = 新地址 |
父node中的Node ID保持不变,不必逐层向上修改。
3. NAT减少了什么
NAT不能让元数据更新完全消失。
它做的是:
把分散在多层父节点中的地址更新,集中到一张Node地址映射表中。
因此,它能够显著减少因node移动导致的递归传播。
4. NAT自身仍需持久化
NAT也是文件系统元数据,需要:
- 缓存
- 更新
- 写入
- 崩溃恢复
因此F2FS还要通过检查点和NAT相关管理机制保证其一致性。
增加一层间接映射换来了较少的树形递归更新,但也增加了映射表管理成本。
十六、NAT与FTL不是同一映射
NAT和FTL都执行地址转换,但位于不同层次。
| 对比点 | NAT | FTL |
|---|---|---|
| 所属层次 | F2FS文件系统 | SSD控制器或闪存管理层 |
| 输入 | Node ID | 主机逻辑块地址 |
| 输出 | 文件系统逻辑块地址 | 闪存物理页 |
| 主要作用 | 找到node当前逻辑位置 | 管理物理闪存布局 |
| 维护者 | 操作系统中的F2FS | SSD固件或闪存软件 |
一次node访问可能经过:
1 | Node ID |
二者共同使用间接映射思想,但解决的问题不同。
NAT解决的是:
1 | 文件系统中的node移动 |
FTL解决的是:
1 | 逻辑块在物理闪存中的移动 |
❗ 本节核心结论: NAT通过稳定Node ID定位可移动node,避免地址变化沿索引树递归传播;FTL则在更低层把逻辑块映射到物理闪存页。
十七、多日志与冷热数据分离
F2FS并不只维护一个当前日志段,而是设置多个当前写入段,将不同类型和生命周期的数据分别写入。
常见分类包括:
- 热数据
- 温数据
- 冷数据
- 热node
- 温node
- 冷node
1. 为什么要区分冷热
热数据频繁更新,冷数据长期不变。
如果把二者混在同一段中:
1 | 一个段: |
一段时间后:
- A和C可能很快失效。
- B和D仍然有效。
为了回收A和C留下的空间,垃圾回收必须反复搬迁B和D。
这会增加写放大。
2. 生命周期相似的数据放在一起
如果将热数据集中存放:
1 | 热段: |
这些内容更可能在相近时间失效,使整个段更容易被完整回收。
冷数据则集中在冷段中,长期保持有效,减少不必要搬迁。
因此,冷热分离的目标是:
让同一个段中的数据具有尽可能相似的生命周期。
3. 多日志与SSD并行性
多个写入段还意味着多个逻辑写入流。
底层FTL和SSD控制器可以把这些写入分散到:
- 不同Channel
- 不同Die
- 不同Plane
从而提高并行度。
不过,文件系统无法完全直接控制SSD内部物理布局,最终效果还依赖FTL策略。
多日志提供的是更有利的并行机会,而不是绝对物理位置保证。
十八、F2FS的空间层次
F2FS从小到大组织为:
1 | Block |
1. Block
F2FS Block通常约为4KB,是文件系统逻辑读写单位。
2. Segment
一个Segment由多个Block组成,典型大小约为2MB。
Segment是顺序写入和状态统计的重要单位。
3. Section
Section由多个Segment组成,常作为垃圾回收的较大单位。
4. Zone
Zone由多个Section组成,可以帮助文件系统进行更大范围的空间组织。
5. 不同层次的“块”
需要特别区分:
| 名称 | 所属层次 | 含义 |
|---|---|---|
| F2FS Block | 文件系统 | 逻辑读写块,通常约4KB |
| F2FS Segment | 文件系统 | 多个Block组成的顺序写区域 |
| NAND Page | 闪存硬件 | 闪存编程和读取单位 |
| NAND Erase Block | 闪存硬件 | 闪存擦除单位 |
F2FS Segment不必等于NAND Erase Block。
文件系统看到的是逻辑块设备,底层物理映射由FTL控制。因此二者只能在设计上尽量协调,不能简单一一对应。
十九、F2FS的主要磁盘结构
F2FS通常包含以下重要区域:
- CP:Checkpoint
- SIT:Segment Information Table
- NAT:Node Address Table
- SSA:Segment Summary Area
- Main Area:主要数据区域
1. CP
Checkpoint保存恢复所需的关键状态,例如:
- 当前有效版本
- NAT和SIT相关信息
- 当前段位置
- 文件系统状态
它是崩溃恢复的重要起点。
2. SIT
SIT记录Segment状态,例如:
- 有效块数量
- 有效块位图
- 段类型
- 修改时间
它帮助系统选择垃圾回收对象并管理空间。
3. NAT
NAT记录:
1 | Node ID → 当前逻辑块地址 |
用于找到inode和各种索引node。
4. SSA
SSA保存Segment Summary,即段概要信息。
它说明Main Area中每个块属于:
- 哪个node
- 哪个文件逻辑位置
- 哪种数据类型
主要用于垃圾回收和恢复。
5. Main Area
Main Area保存:
- 文件数据
- inode
- 直接node
- 间接node
不同类型和温度的数据可以写入不同当前Segment。
| 结构 | 主要作用 |
|---|---|
| CP | 提供崩溃恢复入口 |
| SIT | 管理段状态和有效块 |
| NAT | 定位node当前位置 |
| SSA | 记录段中块的身份 |
| Main Area | 保存文件数据和node |
❗ 本节核心结论: F2FS通过CP、SIT、NAT和SSA分别管理恢复入口、段状态、node位置和块身份,Main Area则负责多日志追加文件数据与node。
二十、F2FS更新一个node的过程
假设某个node需要修改。
传统结构可能需要:
1 | 修改node |
F2FS使用NAT后:
1 | 修改node |
完整流程为:
flowchart TD
A[修改node内容] --> B[在新位置写入node]
B --> C[更新NAT映射]
C --> D[旧node变为无效]
D --> E[父node无需因地址变化而更新]
这样可以减少因地址改变产生的元数据级联更新。
二十一、F2FS的垃圾回收
F2FS同样需要回收包含无效块的Segment。
垃圾回收大致包括:
- 根据SIT选择回收目标。
- 通过SSA识别段中各块身份。
- 通过NAT、inode和其他结构判断块是否仍然有效。
- 把有效块迁移到新的Segment。
- 更新NAT或文件数据映射。
- 释放旧Section或Segment。
这与LFS段清理思想一致,但F2FS还要考虑:
- 热冷分类
- 前台与后台垃圾回收
- 闪存写放大
- 空间不足时的紧急回收
- 与FTL垃圾回收的叠加
1. 双重垃圾回收
使用SSD时,可能同时存在两层垃圾回收:
1 | F2FS垃圾回收 |
文件系统认为某些逻辑块已经释放,并不意味着底层物理页立即被擦除。
FTL还需要根据自己的映射和物理块状态进行回收。
如果两层策略配合不好,可能产生额外的数据搬迁。
二十二、LFS为什么不是“只写一次”
LFS避免了带日志文件系统中的典型双写:
1 | 先写日志 |
但LFS中的有效数据仍然可能因为以下原因多次写入:
- 文件自身再次修改
- inode更新
- 递归元数据更新
- 段清理
- 垃圾回收
- 有效块搬迁
- 检查点更新
因此,“追加写”不等于“每份数据生命周期中只写一次”。
更准确地说:
LFS把前台更新组织成异地追加,但后台空间回收仍然可能重复搬迁有效数据。
二十三、关键概念对比
1. inode map与NAT
| 对比点 | inode map | NAT |
|---|---|---|
| 典型系统 | Sprite LFS | F2FS |
| 映射对象 | inode号 | 广义Node ID |
| 定位目标 | inode当前位置 | inode及索引node当前位置 |
| 主要目的 | 找到移动后的inode | 减少node移动导致的递归更新 |
| 覆盖范围 | 主要是inode | inode、直接node、间接node |
NAT并不是inode map的简单改名。
它扩展了映射对象,并通过稳定Node ID切断多级node之间的位置依赖。
2. inode map与段概要
| 对比点 | inode map | 段概要 |
|---|---|---|
| 查询方向 | inode号到inode位置 | 物理块到所属对象 |
| 主要场景 | 正常文件访问 | 垃圾回收和恢复 |
| 回答问题 | inode现在在哪里 | 这个块曾经属于谁 |
3. Checkpoint与增量日志
| 对比点 | Checkpoint | 检查点之后的日志 |
|---|---|---|
| 内容 | 某一时刻的关键完整状态 | 之后产生的增量更新 |
| 作用 | 快速恢复起点 | Roll-forward |
| 写入频率 | 周期性 | 持续产生 |
| 恢复方式 | 直接加载 | 顺序扫描验证 |
4. LFS清理与FTL垃圾回收
| 对比点 | LFS或F2FS清理 | FTL垃圾回收 |
|---|---|---|
| 所属层次 | 文件系统 | SSD控制器 |
| 管理对象 | 逻辑块和Segment | 物理页和擦除块 |
| 有效性依据 | 文件系统指针和元数据 | 逻辑到物理映射 |
| 主要目标 | 回收文件系统逻辑空间 | 回收可擦除物理空间 |
二十四、容易混淆的几个问题
1. 追加写不自动保证崩溃一致性
即使所有内容按顺序追加,系统仍可能在以下状态崩溃:
1 | 数据块已写 |
1 | inode已写 |
1 | 文件inode已写 |
1 | 检查点只写了一半 |
因此仍需要:
- 双检查点
- Roll-forward
- 目录修改日志
- 明确的持久化协议
2. 无效块并不等于已经擦除
LFS中旧块只是失去引用。
在磁盘上,它可能仍然保留原始字节;在闪存上,它可能仍占据物理页。
真正重新利用空间需要:
- 文件系统段清理
- 可能的FTL垃圾回收
- 闪存块擦除
3. 顺序写仍然可能产生写放大
顺序写减少了随机写代价,但清理会搬迁有效数据。
所以评价LFS不能只看前台写入,还要看:
1 | 用户实际写入量 |
之间的比例。
4. Segment不是NAND Erase Block
Segment是文件系统逻辑单位。
Erase Block是闪存物理单位。
由于FTL存在,文件系统通常不知道一个逻辑Segment最终对应哪些物理擦除块。
5. FTL不能替代文件系统优化
FTL可以处理异地更新,但无法无代价地消除上层产生的大量小写、冷热混合和频繁覆盖。
低效文件系统写入最终仍会表现为:
- 更多映射更新
- 更多物理搬迁
- 更多垃圾回收
- 更高写放大
- 更短设备寿命
二十五、统一理解:用映射换取可移动性
LFS和F2FS中反复出现同一种设计思想:
对象的位置可能改变,但对象的逻辑身份保持稳定;通过映射表把稳定身份转换为当前位置。
在Sprite LFS中:
1 | inode号 |
在F2FS中:
1 | Node ID |
在SSD内部:
1 | 逻辑块地址 |
这些映射表使底层对象能够自由迁移:
- inode可以在日志中移动
- node可以在Segment之间移动
- 逻辑块可以在闪存页之间移动
代价是系统必须维护更多映射元数据,并保证映射本身的崩溃一致性。
flowchart TD
A[稳定逻辑身份] --> B[映射层]
B --> C[当前可变位置]
C --> D[允许异地更新与迁移]
D --> E[需要维护映射一致性]
结论
日志结构文件系统将日志从临时恢复区域提升为文件系统的主要存储形式。
它不再频繁覆盖固定位置,而是:
- 把文件数据和元数据持续追加到日志末尾。
- 使用inode map定位不断移动的inode。
- 以Segment为单位保持顺序写。
- 使用段概要判断块的身份和有效性。
- 通过段清理搬迁有效数据并回收空间。
- 通过双Checkpoint提供可信恢复起点。
- 通过Roll-forward恢复检查点之后的完整修改。
- 使用目录修改日志维护高层目录语义一致性。
面对闪存设备,传统LFS仍存在递归更新和单日志并行度不足的问题。
F2FS通过:
- NAT维护稳定Node ID到当前位置的映射。
- 多日志将不同类型的数据分流。
- 冷热数据分离减少垃圾回收时的有效数据搬迁。
- CP、SIT、NAT和SSA协同管理恢复、段状态、node位置与块身份。
最终形成更适合SSD特性的日志结构设计。
❗ LFS的核心不是简单地“把数据顺序写下去”,而是通过追加写、映射、清理和检查点,把不断变化的文件系统组织成一条可恢复、可回收的日志。
❗ F2FS的关键改进,则是在保持异地更新优势的同时,减少递归元数据更新,分离不同生命周期的数据,并尽可能利用闪存设备的内部并行性。




