
F2FS 这个文件系统我接触了大概三年多从刚开始只是拿它在开发板上跑个根文件系统到后来因为项目里要存大量小文件、还要扛意外断电不得不把它从 ext4 换过来才算真正开始读它的源码和磁盘布局。说实话F2FS 的上手门槛最核心的地方就在“布局”这两个字上——它跟 ext4 那种传统块文件系统的思路差别太大了你要是还拿老眼光去看它很多参数、很多行为你根本解释不通。这一篇就是我自己梳理 F2FS 磁盘布局的学习笔记争取把“一张空盘被 mkfs.f2fs 之后到底变成了什么”这件事讲透。这篇内容适合两类人一类是想把 F2FS 用明白的人比如嵌入式开发、存储工程师你想知道为什么默认参数这样设置、为什么 GC 有周期、为什么掉电之后恢复很快另一类是纯粹想读内核源码、想搞懂日志结构文件系统设计思路的人。我不打算把源码贴一大堆重点是讲布局设计逻辑和实际操作中怎么理解和验证。1. 先看整体F2FS 把一块 Flash 分区切成了什么F2FS 全称 Flash-Friendly File System是三星专门为 NAND Flash 设计的日志结构文件系统最早在 2012 年前后进入 Linux 内核。它的核心思想很简单因为 Flash 不能原地覆盖写必须先擦除再写而且擦除以块Block为单位、写入以页Page为单位所以文件系统干脆不要维护一个“原地更新的文件结构”而是把所有新数据都顺序追加写到空闲区域把旧数据视为垃圾留给后台清理。这个思路在文件系统设计里叫 LFSLog-structured File SystemF2FS 把 1992 年那篇 LFS 论文里的很多想法变成了工业级实现。整块分区在 F2FS 里的逻辑结构可以切成六大块超级块Superblock、检查点Checkpoint、段信息表Segment Information Table简称 SIT、节点地址表Node Address Table简称 NAT、段汇总区Segment Summary Area简称 SSA以及最后真正存文件数据的主区域Main Area。| Boot | Superblock | Checkpoint | SIT | NAT | SSA | Main Area | | Sector | | | | | | |你不用紧张这张图看着区域多但逻辑上其实就三件事一是启动和元数据总入口超级块检查点二是两种地址翻译表SIT 管“段内哪些块有效”NAT 管“node id 对应的块在哪”三是真正装数据的地方SSA 和主区域。F2FS 里有两个很基础的单位概念必须一开始就建立起来。第一个是块block大小固定为 4KB这是文件系统读写的最小单位跟磁盘扇区、Flash 页的大小对齐。第二个是段segment默认由 512 个连续的块组成所以默认大小是 2MB。段是 F2FS 里日志管理、GC、SIT 记录的基本单位。再往上还有区section和段区zone分别默认由 1 个段和 1 个区组成但这些都是逻辑分组用于多设备、GC 策略和 Flash 几个块的对齐理解布局时先记住 block 和 segment 就够了。F2FS 对“对齐”这件事执着到什么程度呢mkfs.f2fs 的时候有个-a参数默认值是 1表示自动把分区起始位置对齐到 4K 边界。SSD 本身内部页大小通常是 4K 或者 8KFlash 的页大小也是 4K 的倍数所以所有元数据区、主区域全部按 4K 对齐能显著减少读写时的跨页开销。这一点在我后面讲 mkfs 参数时还会单独展开因为很多人没注意这个对齐参数结果在某些存储设备上 IO 性能能差百分之二三十。从 Linux 内核里把 F2FS 挂载起来之后你用dumpe2fs那套工具是看不了它的得用 f2fs 自己的一套工具链最常用的是dump.f2fs。它能把你盘上的布局直接打出来后面我会带大家实操看一遍。2. 六个核心区域的职责拆解与设计动机2.1 超级块一切元数据的入口任何一个文件系统都得有超级块F2FS 的超级块干的事情比较纯粹它记录了整个分区的魔数、版本、块大小、段大小、段总数、各元数据区域的起始地址、当前检查点编号以及文件系统特性标志feature flags。它放在分区最开头占据从第 0 号块开始的固定大小区域。F2FS 的超级块设置了两份备份一个在分区块偏移 0 的位置另一个在偏移 512也就是 2MB 偏移处具体取决于块大小的位置。跟 ext4 把超级块散布在多个块组不同F2FS 就放在最前面因为对整个分区做管理时不需要像 ext4 那样考虑“块组本地化”的问题。两份备份是为了保险万一第一份因为坏块损坏了内核还能从第二份恢复。超级块里有个字段叫segment_count_main它决定主区域有多少个段这个值直接跟整个文件系统容量挂钩。你可以用一条命令看具体数值比如一个 16GB 的分区默认参数下会有大约 8000 个段但要注意这个数字会被 SIT、NAT 等元数据区域所占用的空间占掉一部分实际可用空间会略小于物理空间。2.2 检查点掉电安全的锚点检查点Checkpoint这个概念如果你第一次接触可以先把它理解成一个“状态快照”。F2FS 是一个日志结构文件系统新数据不断往后写那问题来了我怎么知道哪些数据是新的、哪些元数据是有效的答案是看检查点里记录了什么东西。检查点区域位于超级块之后、SIT 和 NAT 之前。它保存的东西非常关键当前活跃的 SIT 和 NAT 区域的起始地址、当前段分配状态、孤儿 inode 列表、以及若干统计信息。F2FS 采用双检查点机制也就是有两个检查点槽位交替使用。每次执行fsync、fdatasync或者系统触发检查点操作时文件系统会把当前状态写入其中一个槽位更新编号等下次再写另一个槽位。这样即使写一半掉电内核挂载时也能找到编号最新且校验完整的那个检查点来恢复状态。为什么用双检查点而不是单检查点这就跟 Flash 的写特性相关了。如果只有一个检查点写检查点的过程掉电了那整个元数据状态可能就断裂了有两个槽位的话最坏情况也就是退回上一次检查点状态最多丢一个事务窗口内的未同步数据不会破坏文件系统一致性。这是 F2FS 在掉电安全上的第一道保险也是它比很多玩具型日志文件系统扎实的地方。2.3 SIT 与 NAT两张映射表在 F2FS 里SITSegment Information Table管的是“每个段里块的死活”NATNode Address Table管的是“node 结构体的位置索引”。这两张表是整个文件系统地址翻译的基石值得分开讲。先看 SIT。F2FS 是日志结构的一段地址空间里前面写的数据可能已经被覆盖更新变成旧版本了这些旧版本块就是“死块”invalid。SIT 本质上就是一个数组数组下标是段号数组元素记录了这个段里哪些块有效、哪些块无效还有一个有效块计数。这个有效块计数对 GC 算法极其重要GC 就靠它决定先捡哪些垃圾段扫。再看 NAT。F2FS 中所有的文件元数据inode、间接块索引、目录项块都通过 node 结构体来管理每个 node 都有一个唯一的 node ID。NAT 就是一张从 node ID 到块地址的映射表。你在 F2FS 上创建一个文件这个文件的 inode 也是一个 node它的 node ID 会通过 NAT 找到实际所在的块号然后才能读出文件内容。为什么 LFS 型文件系统需要这么两张表因为物理数据被当成日志一直顺序写文件数据的物理位置是不断漂移的逻辑结构必须通过映射关系来“钉住”。ext4 是 inode 直接存块号更新一个块的指针就直接改 inode 所在块F2FS 是 inode 存的是 NIDnode ID真正的块地址在 NAT 表里所以更新数据块时只需把新块地址写进 NAT 表项inode 本身不需要动。这个设计省去了大量更新原 inode 的写放大。不过代价也很明显——NAT 本身很大。一个 16GB 的分区如果按每 4KB 块算块总数约 400 万node 数量可能达到几十万到上百万每个 NAT 表项占 8 字节那光 NAT 就可能占 8MB 以上。SIT 同理每个段需要一个数组项16GB 分区约 8000 个段每项 64 字节的话也就几百 KB相对还好。所以说 F2FS 的天生成本就是要花一部分磁盘空间维护这两张表这是它跟 ext4 对比时容量利用率上吃亏的地方但买来的是 GC 高效和顺序写友好。2.4 SSA连接日志与 GC 的关键SSASegment Summary Area这个区域在一般文件系统里没有对应物很容易被忽略但恰恰是它把“数据写了不知道是什么”这个问题解决了。F2FS 的主区域被分割成大量段新数据总是按顺序往“当前段”里塞。问题来了比如 GC 要清理一段垃圾段它得知道这个段里那些有效块到底属于哪些文件、在文件中的偏移是多少这样才能把有效数据搬走再更新映射。这个“块到文件逻辑位置的逆向信息”就存在 SSA 里。SSA 的每一项对应一个段逆向记录了段中每个块属于哪个文件 inode/node、在文件里的逻辑块号是几。可以这么理解SIT 是“段视角”的有效性统计SSA 是“块视角”的逻辑归属信息。一个管死活一个管身份。GC 处理某个段时先查 SIT 知道哪些块有效再查 SSA 知道这些有效块是谁的数据然后就可以把它们搬到新的日志位置并更新对应的 NAT 和 inode 指针。SSA 还有一个用途是支持 SSRSegment Slient Reuse其实就是“段内原地复用”。F2FS 在分配新块时如果发现当前段的尾部还有一些无效块并且正好能写进去它可以直接改写这些无效块而不必非得往后追加。这个技巧能减少磁盘末端的老化集中度提高磁盘空间利用率和写入局部性。SSA 提供的信息正好支持这种“精准填空”的写方式。2.5 主区域与多段日志主区域就是真正存放数据的地方它被分成很多个段。F2FS 最漂亮的设计之一就是把主区域按“日志类型”划分成多个可同时顺序写的子区域每个子区域就叫一个“日志”log。它一共有 6 类日志热节点hot node、温节点warm node、冷节点cold node、热数据hot data、温数据warm data、冷数据cold data。这 6 类日志在主区域里并不是严格按照物理位置切死的而是通过“当前活跃段指针”动态分布。mkfs 之后主区域一开始是整齐的段阵列但是随着写操作推进每个类型的日志各自占一个当前段各写各的。运行久了主区域里各个段的“身份”就不再固定了一段一段轮流被不同的日志类型占用。这正好体现了日志结构的动态性。为什么要分 6 个日志因为不同数据的生命周期不一样。目录项、inode 映射这类元数据更新非常频繁属于“热”大文件数据块写完了就基本不动属于“温”或“冷”。把热数据和冷数据分开写可以避免互相干扰——GC 清理时冷数据段几乎没有更新可以很快整段回收热数据段则频繁产生无效块回收频率高。如果全都混在一起后台 GC 就会疲于奔命写的放大率会很难看。F2FS 还支持多设备布局。它允许通过-d参数把不同的段区指定到不同的块设备上比如一个 SSD 一个 HDD 的组合把热数据放 SSD、冷数据放 HDD。这个功能在 mkfs 时通过 zone 划分来体现本质上还是把段区作为基本分配单位只是不同设备承担了不同段区。实际项目里用这个特性的人不多但理解它对理解布局的“段”概念有帮助——段在 F2FS 里就是一块能被任意摆布的逻辑积木。2.6 目录与文件数据在布局中的落点很多人学布局只盯着元数据区域忽视了文件数据的实际走向。在 F2FS 中你创建一个文件需要分配一个 inode node走 hot node 日志写入目录项块走 hot data 日志写入文件内容走 warm data 或 cold data 日志。这就意味着一次简单的 createwrite 操作可能会让 3 个不同的日志段同时产生新数据。这也解释了为什么 F2FS 写小文件时你会看到它消耗的段数量相对比较多——不是它空间利用率低而是它为了保持日志并行性会同时开多个活跃段。文件数据落位时F2FS 默认按照文件大小和温度进行区分小于某个阈值的文件数据会尽量放在 hot data 区域大文件数据放到 cold data 区域。这是从 write-hint 和历史上类似 LFS 的经验得来的。SSD 用户如果想知道自己文件到底落在哪个区域可以用f2fs_io或者dump.f2fs看块地址再反查 SIT能看到段的类型标记。3. 结合 mkfs 参数与布局计算的实操视角3.1 常用 mkfs.f2fs 参数影响布局的关键项光看区域概念还不行得落实到命令参数上不然你很难解释“为什么我 mkfs 之后容量少了这么多”这种问题。mkfs.f2fs常用参数里直接影响布局的有这几个-s每个区section包含的段数默认 1。调大成 4 或 8 后GC 以区为单位处理多个段可以减少元数据碎片但牺牲部分灵活性。-z每个段区zone包含的区数默认 1主要为了适配某些设备的多通道特性。-t预留空间比例overprovision ratio默认是 7.5%可以通过 0~95 之间调整。这个值直接影响主区域里有多少段不会暴露给用户相当于给 GC 留出“周转空间”。-a起始地址对齐方式默认 1 表示自动对齐到 4K。-O开启特性比如-O extra_attr、-O inode_checksum这些特性会修改 inode 结构的大小间接影响节点区域的布局。-d多设备支持指定额外设备。最让人迷惑的是-t预留空间。默认 7.5% 意味着 100GB 盘中大约 7.5GB 不会出现在“可用空间”里而是作为 GC 的缓冲。这对于 Flash 设备是必要的因为 GC 需要有地方腾挪有效块如果你把它设成 0可用空间看多了但 GC 压力会骤增极端情况下写放大可以到 3~5 倍性能一下就垮了。3.2 一次基于 100GB 分区的布局盘点我用一个具体例子把布局算一遍方便大家对着自己的盘验证。假设你有一个 100GB 的分区块大小 4KB段大小 2MB512 块/段默认 7.5% 预留。100GB 换算成字节约 107,374,182,400 B取整后可以认为分区总共约 51,200 个块组每个块组 2MB。如果元数据区域占掉约 1GB剩下约 49,000 个段可以进入主区域。预留 7.5% 后用户可用段约 45,300 个换算可用空间约 88.5GB。这就解释了为什么 100GB 的 SSD 挂载后你看到的容量往往只有 88~90GB——不是厂商虚标而是文件系统层面就预留了。SIT 和 NAT 的大小也可以粗算。SIT 每个段对应一项假设每项 64 字节实际内核中 sit_entry 约 64 字节——但要知道 SIT 区域本身也是由 4KB 块组成的每个 4KB 块能放约 64 个 SIT 项。如果一个盘有 50,000 个段SIT 就需要约 782 个块也就是约 3MB。NAT 要看你文件系统的 node 数量保守估计一个 4KB 块能容纳约 512 个 NAT 项8 字节/项如果预计有 400 万个 node就需要约 7,812 个块也就是 31MB 左右。当然这些数字在不同内核版本、不同 feature 下会有差异但量级可以帮你判断一个盘“被吃掉的容量”是否正常。3.3 用 dump.f2fs 验证布局纸上算完还得实际验证。dump.f2fs这个工具在 f2fs-tools 包里安装后可以直接查看磁盘布局。基本用法dump.f2fs -i 分区设备 -o 输出文件它会输出超级块信息、各区段地址范围、SIT 和 NAT 的起始块号、当前检查点编号等。我在一个 32GB 的 SD 卡上跑过输出里能看到主区域起始段号大约是 128NAT 区域比 SIT 大不少。最直观的验证方法是先创建一个小文件记录它的大小然后重新挂载看可用空间变化再用dump.f2fs找到文件 data block 的地址反查它落在主区域的哪个段段类型是 warm data。这种“从布局到文件实际落点”的验证过程特别能加深理解。4. 布局如何影响文件读写与 GC真正看懂设计逻辑4.1 读文件地址翻译的链路看布局不只是为了考试它直接决定了你在使用中看到的各种现象。先说读。当你open一个文件并read时F2FS 的路径大致是从 inode 里拿到 node ID去 NAT 查到这个 inode node 实际所在的块地址读出 inode node然后根据文件内偏移找到对应的数据块索引如果这个数据块索引是间接块还得通过 NAT 把间接 node 块地址也查出来拿到数据块地址后直接从主区域读 4KB 数据。这一条链路上你会发现每次读文件可能涉及两次 NAT 查询和一次数据块读。NAT 本身是存在磁盘上的如果 NAT 项不在内存缓存里那么读一个文件数据块其实要先做一次元数据读。这就是为什么 F2FS 对随机读场景下如果没有足够内存做 NAT 缓存表现会不如 ext4。反过来它的顺序读非常优秀因为顺序读时所有块地址都可以通过日志目标的连续性推测出来预读逻辑可以提前把大量块调度出来。4.2 写文件日志追加与 SSR写文件时F2FS 优先在当前活跃日志段里顺序写如果当前段写满了就新分配一个段继续。由于 F2FS 会为不同温度的数据维护多个活跃段所以你在vmstat里看到的写请求实际上是交织地落在不同段上。对 Flash 来说这种多活跃段的写入模式能更好地利用设备的多通道并行能力。F2FS 还有 SSR 机制它发现当前活跃段尾部有一些无效块时可以把新写入直接填进这些无效块而不是继续往后追加。这种行为打破了“LFS 一定只追加”的刻板印象照顾了那些不支持“先擦后写”以外场景的设备也减少了 GC 的压力。但注意SSR 只是局部复用不是随便乱写前提是 SSA 能准确知道这个段里哪些块是死的这又要依赖我们前面说的 SIT 和 SSA 的准确维护。4.3 GC 与掉电恢复的直接后果布局设计的最终受益者就是 GC 和掉电恢复。GC 流程是后台线程或者写压力触发时F2FS 调用 GC 选择 victim 段选择依据主要是 SIT 里的有效块数——有效块越少越优先因为搬移代价低。选定 victim 段后用 SSA 拿到每个有效块的文件归属和逻辑块号把数据搬到新的活跃日志段然后更新 NAT 和 inode 索引最后把旧段标记为空闲。整个流程里SIT 提供“值不值得搬”的判断依据SSA 提供“往哪搬、怎么改指针”的依据。没有这两张表LFS 的 GC 根本跑不起来。这也就是为什么前面我说这两张表是 F2FS 的基石。掉电恢复路径则是挂载时读取两个检查点槽位选择编号较新且 CRC 校验通过的那个然后重建内存中的 NAT 和 SIT 缓存。F2FS 还支持 roll-forward recovery前滚恢复它利用 SSA 中记录的块归属信息把最近几个数据块的写操作回放到 inode 指针里从而恢复崩溃前 fsync 过的数据。这种恢复机制比 ext4 的日志回放要简单高效主要的准确性保证就来自检查点和 SSA 的联合记录。我实际测过在一个边缘设备上反复断电 100 次F2FS 每次都能在 1 秒内完成挂载并恢复到崩溃前的数据fsck.f2fs几乎没被触发过。相比之下ext4 偶尔会因为日志回放时间过长导致启动变慢。这就是布局设计带来的直接体验差异。5. 排障与经验总结几个关键点和避坑建议5.1 常见困惑与排查思路很多人第一次用 F2FS 会被几个现象搞懵我列一下常见问题基本都是布局直接导致的。第一挂载后容量比标称小不少。这是预留空间 元数据区共同导致的不是 bug。你可以调低-t到 3~5% 来扩大可用空间但要评估 GC 行为是否还能接受。第二为什么用了很久之后感觉更卡了因为 GC 开始频繁工作。初始阶段盘上全是空段分配很快用久了之后产生大量碎片段SIT 里有效块太低的段增多GC 需要不停搬运。解决办法是保证有足够的预留空间或者定期执行fstrim给设备下发 discard回收匿名块。第三为什么小文件创建速度很快但大量删除时系统会卡删除操作会触发 NAT 和 SIT 的大量更新如果一段区域里全是死块删除时会产生很多元数据写。这是 LFS 的固有特点热数据分离做得越细这个问题越轻。第四为什么dump.f2fs输出的段号跟我文件的物理偏移对不上因为段号是主区域内的逻辑编号而物理偏移还要加上元数据区域的起点两者查表换算即可。5.2 实操心得与注意事项最后分享几个我自己踩过的坑和总结出来的经验。第一mkfs 之前一定要确认设备的 4K 对齐。很多开发板的 eMMC 分区不是从 4K 边界开始的如果没有对齐F2FS 跑起来会有额外的读改写放大性能损失肉眼可见。可以先用parted或sgdisk把分区起始扇区设成 8 的倍数即 4K 对齐再跑 mkfs.f2fs。第二不要在生产环境把-t设成 0。我测试过在无预留空间情况下跑满盘GC 的写放大能到 4 以上同时 fsync 响应时间飙升。如果你确实需要大容量建议至少留 3%并配合定期 trim。第三F2FS 的 fsync 性能非常好但有代价fsync 会触发检查点而检查点会把当前活跃段的 SIT/NAT 更新落盘。如果你在代码里每写一小块数据就 fsync 一次会导致检查点频繁被触发元数据写放大严重。合理做法是攒一批数据再 fsync比如 4~8MB 一批。第四多设备布局功能虽然强大但要确认内核版本支持并且你的块设备故障处理策略要跟上。一旦某个设备掉线整个 F2FS 是不可访问的因为一张 NAT 表横跨所有设备。在我实际的项目中F2FS 布局给我最大的启发是文件系统不是一个静态的“文件管理表格”而是一套不断演化的日志系统。理解了“数据永远在流动”这个核心很多设计决策就都顺理成章了。希望这篇布局笔记能帮后来的人少走弯路。