F2FS文件系统布局详解:SIT/NAT/SSA与Checkpoint机制
前段时间给一台闲置小主机换装F2FS格式化完成的那一瞬间我盯着分区看了很久。和ext4格式化完那种“大概知道inode表在哪个位置、位图挨着谁”的感觉完全不同F2FS直接在磁盘上画了一张不一样的地图Superblock、Checkpoint、SIT、NAT、SSA最后跟着一大片按段管理的主区域。F2FS文件系统布局不是传统意义上的“数据块索引表”而是一套围绕闪存写特性重新设计的物理组织方式。这篇笔记是F2FS学习笔记的第3篇前两篇我梳理了设计动机和mkfs表现这一篇把布局彻底摊开讲清楚每个区域存什么、为什么这样排、SIT/NAT/SSA三张账本怎么协作、六条活跃日志和Checkpoint如何影响性能与寿命最后用dump.f2fs验证一遍。适合正在啃F2FS源码、做嵌入式存储优化或者单纯想知道一块SSD格式化后内部到底长什么样的朋友。1. 为什么F2FS要“重画”磁盘地图传统布局在闪存上的水土不服1.1 ext4那套“原地更新”模型建立在机械盘假设上ext4的布局核心是位图加索引表超级块记录全局信息块组里有inode表、块位图、inode位图数据块散落在剩余空间里。文件一旦创建inode写在固定位置块位图标记哪些块可用数据和元数据的更新大多是“原地”发生的——把新内容覆盖到旧地址上。这在机械盘时代很合理因为磁头可以寻址到任意位置覆盖写入随机写和顺序写的成本差异虽然存在但没有到“改写会损坏介质”的程度。但是闪存设备完全不是这样。SSD、eMMC、UFS这类介质的存储单元在写入前必须先擦除而且擦除粒度远大于写入粒度。拿最常见的NAND来说读写通常按4KB页进行擦除却要按较大的块进行一个物理块往往包含几十上百个页。传统文件系统那种“频繁改位图、改inode表、覆盖小块数据”的行为在闪存上会被放大成大量的后台读改写和垃圾回收最终表现出来就是大家最熟悉的毛病用着用着掉速。1.2 闪存三大特性让固定位置元数据成了瓶颈第一个特性是写前擦除。机械盘扇区可以随时覆盖NAND不行写入前必须把所在的擦除块清空。一个文件改几个字节落在闪存里可能要牵连一个物理块的重写。第二个特性是擦除次数有限。SLC、MLC、TLC各有寿命上限频繁覆盖等于加速消耗写入寿命。第三个特性是读写、擦除粒度不对齐。文件系统一次改4KB元数据没问题但闪存为了腾出空间要整块擦除于是无效数据搬来搬去产生写放大。如果把ext4原封不动搬到闪存上最惨的就是元数据区域。文件创建、删除、重命名都要改inode表和位图这些小块随机写会反复触发底层FTL的搬移和擦除不但拖慢系统还让元数据所在物理块优先磨损。所以F2FS的布局设计出发点很简单把“频繁改固定位置”彻底放弃改成一整套“不断追加新内容、之后统一回收”的机制。1.3 日志结构化布局不修改只追加然后等待GC回收F2FS继承了Log-structured File SystemLFS的核心思想。新数据不写到旧位置而是不断追加到空闲段旧数据所在的空间变成无效区域后续由垃圾回收机制整理。这样做的好处是写入天然顺序化无论数据还是元数据都变成一段一段的顺序追加写GC可以按段回收避免了闪存擦除粒度和文件系统块粒度不匹配的问题。但LFS也带来了两个新问题第一数据搬来搬去之后怎么快速知道一个物理块属于哪个文件第二文件系统不断追加掉电时怎么恢复一致状态。F2FS布局里的SIT、NAT、SSA、Checkpoint本质上都是为这两个问题服务的。所以看F2FS布局时不要一个个区域孤立地看而是心里先有这条线顺序写、反向查、Checkpoint兜底、GC回收。搞清了这条线后面每个区域的作用就全都串起来了。2. 六区全景与三级粒度从盘头到主区的距离感2.1 从盘头到盘尾六个区域按什么顺序排mkfs.f2fs格式化完成后整个卷大致分成六块顺序是超级块、Checkpoint区、SIT区、NAT区、SSA区以及占绝大部分空间的主区域。这个排列不是随便排的它体现了一个基本思想把经常要读的元数据放在磁盘前端把真正存放文件内容的主区域放在后面。下表是六个区域的速查定位区域英文缩写存放内容核心职责超级块Superblock文件系统参数、UUID、各区域起始地址挂载入口决定其余区域位置检查点Checkpoint一致性状态、当前活跃段信息、SIT/NAT脏位图掉电恢复的锚点段信息表SIT每个段的使用状态和有效块位图段分配、GC决策依据节点地址表NAT节点号到节点块物理地址的映射定位inode和各级节点块段摘要区SSA每个段内数据块的摘要反向索引GC迁移时反查数据归属主区域Main Area文件数据块和节点块真正存用户数据的地方超级块有两份F2FS在第0块和第1块各存一份避免一块损坏就全军覆没。Checkpoint区放在超级块后面因为它决定了挂载时能否快速找到最新一致状态。SIT、NAT、SSA属于三类索引数据放在主区域之前这样读到主区域的数据块后再需要查索引时磁头/FTL的访问距离相对固定不至于从盘尾跑回盘头。2.2 主区域的度量衡Block、Segment、Section、Zone主区域是用户数据的归宿但它不是简单地“一块一块”管理F2FS在这里设计了四个层级。最底层是块Block默认4KB是读写的最小单位。块往上组合成段Segment默认一个段2MB正好512个块段是F2FS分配和GC管理的基本单元。段再往上组合成节Section默认一个节等于一个段节是GC实际清洗处理时的单位。节再往上组合成区Zone默认一个区等于一个节区的作用是对齐设备物理擦除块让底层闪存的擦除行为尽量落在文件系统的天然边界上。层级默认大小配置参数典型职责Block4KB固定读写最小单位Segment2MB512块mkfs-S可调分配单位、活跃日志切换单位Section1个Segment2MBmkfs-t可调GC清洗时的处理单位Zone1个Section2MBmkfs-z可调对齐设备擦除粒度这四级单位之间的关系可以做个简单换算。一块8GB的卷按4KB块算总共有约2097152个块每个段512块总段数就是4096个。如果保持一节一段、一区一节那整个主区域就是4096个段也就是4096个节。这样一个卷的元数据开销也能算SSA区每个段对应一个4KB摘要块4096个段就需要4096个块也就是16MB左右只占总容量的0.2%。SIT/NAT区域类似比例同样很低。这种“用少量磁盘空间换全局可管理性”的设计在F2FS整个布局里非常典型。2.3 主区域没有固定节点区和数据区只有两条动态推进线初学者容易把主区域想象成“左边节点区、右边数据区”的固定划分F2FS实际不是这样。默认的heap式分配策略是节点段从主区域起始位置开始向上增长数据段从主区域末尾开始向前扩展两条分配线相向而行。这样设计的原因有两点一是让节点块尽量靠近前面的NAT和SSA元数据区减少索引查询距离二是让初始的数据写入和节点写入分布在主区域两端避免互相干扰有利于顺序写。但段类型不是永久固定的。某个段今天可能被用作数据段等数据写完后被回收下次分配时可能变成节点段。真正决定段类型的是当前有哪些活跃日志在写以及SIT里记录的空闲段状态。这个动态边界和传统文件系统“inode区固定、数据区固定”的根本区别正是F2FS能在闪存上灵活调度的原因之一。3. SIT、NAT、SSA三张账本地址映射和垃圾回收的基石3.1 SIT给每个段记一本“入住登记表”SIT的全称是Segment Info Table直译就是“段信息表”。F2FS把主区域划分成数万个段之后必须有一个地方记录每个段的状态这个段是否空闲已用了多少个有效块哪些块有效哪些块已经废弃段最后一次写入是什么时候。这些信息全在SIT里。SIT的存储结构是“每个段对应一个SIT条目”。一个段512个块用位图记录每个块是否有效正好需要64字节512位再加上有效块计数、修改时间等信息形成一个固定大小的条目。SIT区域本身由mkfs按卷大小预先分配通常和NAT区域一起排布在主区域之前。SIT在运行中的作用有两个。第一个是分配空段F2FS需要新段时会从SIT中找到标为空闲的段作为下一个活跃日志的目标段。第二个是GC决策后台垃圾回收要找“回收代价低”的段主要看SIT里的有效块位图和有效块计数。有效块越少说明这个段里的废弃数据越多回收时只需要搬少量有效块就能释放整个段。SIT还记录段的修改时间让GC避开最近刚改过的“热段”防止刚刚写完的冷数据被反复搬运。可以说SIT是F2FS布局管理中最多人忽略、但最基础的一张表。3.2 NAT从节点号到物理块的“电话本”F2FS里所有的树状结构节点都以4KB块形式存在主区域里包括inode节点、直接节点、间接节点。这些节点本来只靠一个“节点号”nid互相引用。那么问题来了拿到一个nid之后怎么知道这个节点块物理存储在主区域的哪个位置答案就是NATNode Address Table。NAT可以理解成一本电话本键是节点号值是节点块在主区域的物理地址。F2FS用8字节保存一条NAT条目一个4KB的块可以放512条因此NAT区的容量能支撑大量节点映射。每当节点块因为GC或者其他原因被移动只需要更新对应NAT条目里的地址就行不用把引用它的所有父节点全部改一遍这大大降低了节点搬移的成本。很多人把NAT等同于ext4的inode表这个理解有偏差。NAT只记录节点块“在哪里”并不直接保存inode结构。inode节点本身是主区域里的普通节点块它的内容才是文件的元数据而它所在位置要去NAT里查。这个区别在分析F2FS寻址路径时特别重要。3.3 SSA从物理块反查文件归属的反向索引SIT解决了“段里哪些块有效”的问题NAT解决了“节点块去哪找”的问题但还有一个关键问题悬而未决GC要把某段中的一个有效数据块搬走时怎么知道这个块属于哪个文件的哪个逻辑位置如果不知道搬过去之后没法更新文件的索引数据就串了。SSASegment Summary Area就是为这个场景设计的。每个段在SSA区域里对应一个4KB的摘要块里面记录了这个段中每个块对应的文件inode节点号、逻辑偏移等信息。可以把它理解为反向索引给定物理块能反查出它主人的inode号和在文件内部的偏移。GC搬移某一个有效块之前先去SSA查它的“户口”更新源文件里的指针或者NAT里的映射再把数据写到新段。这样日志结构才能做到“数据随便搬家地址永远对得上”。3.4 把三张账本串起来一次读文件的寻址路径用一次简单读操作来串一下这三张表的作用。假设要读取文件第1000个逻辑块先从文件路径找到文件对应的inode节点号nid。查NAT得到inode节点块在主区域的物理地址把inode节点读进内存。根据逻辑块号判断这个块落在直接寻址、一级间接还是二级间接范围内。如果需要间接节点就通过节点号再去查NAT拿到间接节点块的物理地址并读取。最终在对应的节点块里拿到目标数据块在主区域的物理地址读取4KB数据返回。整个过程可以概括为三层文件逻辑块号映射到节点块内容节点块里的地址映射到数据块而每个节点块的物理位置都要靠NAT转换。SIT和SSA平时不在读路径上但它们维护着段级状态和反向归属是GC和分配时的真正主角。UFS上常见的随机读写卡顿很多时候就是这几张表在底层反复更新导致的这也是为什么F2FS的布局中每一张表都要设计成“可索引、可批量刷写”的形态。4. 六条活跃日志与Checkpoint布局里的寿命与性能博弈4.1 六条活跃日志冷热分行节点和数据分流F2FS维护了六条活跃日志curseg按“冷热程度×数据类型”交叉组合hot node、warm node、cold node以及hot data、warm data、cold data。六条日志各有一个当前活跃段写入时根据数据温度追加到对应段里。这样分类的第一个原因是把冷热数据隔离开。热数据频繁更新应该集中在少量段里这样GC可以只针对这些段频繁回收其他冷数据段长期不用动。冷数据如果和热数据混在一起热数据失效后回收时还得把大量冷数据搬走白白增加写放大。第二个原因是节点和数据分流。节点块的访问频率和生命周期与数据块差异很大分开写可以让每条日志的分配策略更贴近自己的使用规律。文件是冷还是热F2FS主要看创建时的扩展名提示和打开方式。mkfs.f2fs支持自定义扩展名列表常见做法是把日志、临时文件、媒体文件归为cold data普通用户文件归为warm data频繁改写的小文件归为hot data。实际部署中如果要压榨F2FS性能调整extlist和文件访问提示往往比盲目调GC参数更值得尝试。4.2 Checkpoint枢纽式的“状态快照”六条活跃日志不断移动SIT/NAT会不断记录新状态但如果每次都同步刷盘性能和寿命都承受不住。F2FS的做法是定期打一个Checkpoint把当前活跃日志段位置、SIT/NAT需要刷新的脏块信息、孤儿inode列表、版本号等关键状态打包写到Checkpoint区。Checkpoint区专门留了两个交替使用的包每次Checkpoint切换一次写入时用版本号标记先后顺序。掉电后挂载时F2FS不需要像传统日志那样恢复整个操作日志只需要读取最新的完整Checkpoint包重建内存中SIT/NAT的对应状态然后继续后续写入。这种设计把“一致性保证”从“每次操作都落盘”变成了“定期落盘崩溃后重放少量状态”所以F2FS在大量小文件fsync场景下表现非常亮眼因为几个fsync可以合并到一次Checkpoint里批量提交而不是每次都刷一串日志。4.3 布局与OP空间、GC代价、discard的联动F2FS布局再精巧也绕不开一个物理事实闪存没有空闲块时GC是躲不掉的。如果主区域被用户数据填满F2FS每次分配新段前都得先回收一个旧段这期间既要读有效块、又要写存活块写放大和延迟都会暴涨。所以mkfs.f2fs默认会为文件系统保留一部分预留空间也就是OPOver-Provisioning比例通常在5%上下具体和卷容量相关。生产环境如果长期高写入建议通过mkfs的-o参数把OP调到7%10%相当于给GC留出缓冲地带性能曲线会稳很多。discard策略也值得单独说。闪存内部的FTL需要知道哪些块已经不被文件系统使用了才能提前擦除。挂载F2FS时可以开启discard让F2FS在段被回收时立刻下发TRIM通知底层也可以不开discard改用定期fstrim批量回收。前者实时性好但可能增加额外开销后者适合固定时间窗内集中做维护的场景。这个选择没有绝对标准要看设备的负载模型。场景建议高频率小文件写入提高OP到7%~10%仔细调整热/温/冷扩展名大文件顺序读写默认OP即可注意GC后台触发频率掉电频繁的环境确保Checkpoint区健康观察fsync延迟使用一段时间掉速明显查看SIT中脏段比例考虑fstrim或调高OP5. 把布局“看”到磁盘上dump.f2fs实证与三个易混淆点5.1 用mkfs和dump.f2fs快速验证布局理论讲再多不如实际格式化一块盘看一眼。F2FS工具链里最常用的验证命令就是dump.f2fs。以/dev/sdb1为例mkfs.f2fs -l f2fs-lab /dev/sdb1 dump.f2fs /dev/sdb1dump.f2fs会输出类似下面的信息具体地址和长度随卷大小变化Info: Superblock: start 0, len 4 Info: Checkpoint: start ... Info: Segment Info Table: start ..., len ... Info: Node Address Table: start ..., len ... Info: Segment Summary Area: start ..., len ... Info: Main Area: start ..., len ...这里面有几件事值得实操验证。一个是看主区域起始位置和总段数验证之前说的“总块数除以每段块数”的换算。另一个是看SIT、NAT、SSA这三个索引区的长度占比能直观理解元数据开销其实很小。还有一个是看不同卷大小下各区域的比例变化会明显发现SIT和NAT区域随容量增长比SSA更明显。挂载之后如果内核开启了CONFIG_DEBUG_FS还可以查看/sys/kernel/debug/f2fs/status里面能看到当前六条活跃日志的段号、各段有效块数、dirty段数量等信息。这些是运行时观察布局状态的一手数据。mount -t f2fs /dev/sdb1 /mnt/f2fs cat /sys/kernel/debug/f2fs/status5.2 写入文件观察数据物理分布想更直观地感受F2FS布局可以在格式化后先记录当前活跃日志段号然后向文件系统里写入一个几百MB的文件再回去看SIT和活跃段变化。你会发现数据块并不是散落在主区域各处的而是按照活跃日志的顺序连续写在某一个段上。段写满后切换到下一个空闲段整体形成一条线性推进的轨迹。这个实验印证了F2FS“顺序追加写”的本质就算应用在随机写文件里的不同位置落到磁盘上时数据也会尽量以线程化段的方式向前推进。这也是为什么F2FS在闪存上能保持稳定吞吐关键不在“把随机写变成顺序写”这个口号而在于布局层面真的为顺序写预留了完整的段管理机制。5.3 学习F2FS布局时最容易绕晕的三个点第一个误区是把F2FS的segment当成NAND物理擦除块。F2FS的2MB segment是文件系统自己定义的管理单位它和SSD内部FTL维护的物理块没有任何固定对应关系。F2FS无法直接控制NAND擦除哪个物理块只能通过逻辑卷的段状态和TRIM指令间接影响底层行为。理解这一点就不会再纠结“F2FS为什么不能保证擦除对齐”。第二个误区是混淆NAT和ext4的inode表。ext4的inode表里直接存放inode结构体而F2FS的NAT只存节点地址inode节点本身是主区里一个普通节点块。这个区别导致F2FS可以灵活迁移inode而不需要大范围更新索引代价是每次读inode都要多一次NAT查询。读路径和磁盘布局的关系往往就是从这种“多一次查询换来的灵活性”里来的。第三个误区是认为主区域里有固定边界的数据区和节点区。真实情况是段类型随着分配和回收动态变化节点段和数据段的初始位置不同但边界一直在浮动。看SIT时如果发现一个原本是数据段的段后来被用作节点段不用惊讶这是正常现象。把主区域理解成“一堆动态段”比理解成“两个固定大分区”准确得多。这段学习看下来我最大的体会是F2FS的布局不是信息的拼盘而是一台完整的循环机器。数据从活跃日志尾巴顺序写入旧块在SIT里被标记为无效GC根据SIT和SSA找到低价值段把有效块搬运回活跃日志腾出的段重新加入空闲池Checkpoint在关键节点把这个循环的进度固定下来。单独背每个区域的起始地址没有意义真正有用的是把这张循环图在脑子里画出来。先理解SIT、NAT、SSA为什么存在再回来看mkfs参数和内核日志很多东西会变得顺理成章。