只读分享解决方案文档原理方案设计📅 2026-10-14🔖 Rev 1.0⏳ 还有3日到期
🔧解决方案&应用市场分析 -> 原理方案设计 -> 电子设计&技术探讨 -> 嵌入式文件系统选型_技术探讨

嵌入式文件系统选型_技术探讨

同系列 · 原理方案设计
扫码打开本页
微信扫码

嵌入式文件系统选型

FatFs / LittleFS / JFFS2 / YAFFS2 / UBIFS / F2FS / ext4 / RomFS / SquashFS 九个系统的横向对照 —— 从介质、掉电承诺、授权约束到资源生态的一份选型方法论参考 | 编制日期 2026-10-14 | 版本号 Rev 1.0

9 个系统
本文横向对照的文件系统
覆盖 MCU 与 Linux 两侧的全部主流选择
4 个问题
选型要按顺序回答的四问
介质 → 掉电承诺 → 授权 → 资源生态
3 个层级
掉电承诺的三个强度等级
数据完整 / 元数据一致 / 事务原子
4 个误区
工程现场最常被当真的错误结论
JFFS2 过时 / F2FS 最好 / ext4 最保险 / 宿主决定能力
报告总结

嵌入式产品要往闪存里存东西时,几乎一定会遇到一个问题:该用哪个文件系统?这个问题看起来简单,实际答案却经常是错的——不是因为选错了算法,而是因为问题本身被问错了。有人问「哪个最快」,有人问「哪个最稳」,有人问「哪个名字最熟」,这三个问题都不构成选型依据。

这份文档给出的是一条固定的提问顺序:先看介质是什么,再看掉电要承诺到哪一层,然后看授权能不能接受,最后才在剩下的候选里比资源占用与生态。顺序不能颠倒,因为后一个问题永远依赖前一个问题的答案。介质没定就谈算法,是把块设备的设计硬套到 NOR 上;掉电承诺没定就谈性能,是拿一个不要求原子提交的系统去和另一个比较写入延迟。

围绕这四问,本文把九个系统逐一拆开:它们为哪种介质设计、用什么机制保证一致性、掉电时承诺到哪一层、资源占用在什么量级、授权条款属于哪一类。结论不落在「谁最好」,而落在「在什么条件下谁是对的」。

结论: 嵌入式文件系统选型不是挑「最好的那个」,而是按顺序回答四个问题——介质是什么、掉电要承诺什么、能不能接受闭源约束、资源与生态哪个优先。 介质是第一性问题:NOR、NAND、块设备各自对应一套不同的候选集合,把块设备系统硬挂到 NOR 上,写放大与各种怪问题都由此而来。 掉电承诺分三层(数据完整 / 元数据一致 / 事务原子),多数选型失败不是算法问题,而是要承诺的层级与介质实际能给的层级不匹配。 授权维度是最容易被忽略的硬约束:随内核分发的系统普遍采用 GPL-2.0 条款,闭源 MCU 产品的现实候选常常只剩 LittleFS 与 FatFs。 而在嵌入式 Linux 上,只读根加可写层往往比在 ext4 与 SquashFS 之间二选一更贴近真实需求。

读者路径

  • 刚开始做方案,要定文件系统:直接看第 11 章(横向对照 + 决策树 + 检查清单),再回头读第 2 章确认介质判断是否正确。
  • NOR Flash 上做数据记录:第 2 章(介质约束)→ 第 4 章(LittleFS)→ 第 6 章(JFFS2),重点是掉电承诺层级的选择。
  • NAND 上做大容量可写分区:第 2 章 → 第 7 章(YAFFS2)→ 第 8 章(UBIFS),重点是预留块与压缩的取舍。
  • 嵌入式 Linux 根文件系统:第 10 章(只读根 + 可写层)→ 第 9 章(F2FS 与 ext4 对照)。
  • 遇到掉电后文件系统损坏:第 3 章(先分清承诺层级)→ 附录 C(按现象排查)。
  • 受授权条款限制:第 11.4 节直接给结论与判断路径,附录 D 说明本文的边界。
  • 只想要速查:附录 B(参数对照)+ 第 12 章(结论与速查)就够了。

1. 文件系统在嵌入式系统里的位置

这一章不比较任何具体系统,只先把「文件系统在嵌入式产品里到底承担什么」讲清楚。很多选型争论之所以谈不拢,是因为双方对「文件系统该做什么」的预期根本不同——有人把它当成目录树,有人把它当成掉电保护机制,有人把它当成寿命管理手段。先把预期对齐,后面的比较才有意义。

1.1 为什么嵌入式产品几乎都要一个文件系统

最早的单片机固件确实不需要文件系统:参数存在固定偏移,日志写进一段预留的数组,升级时把整段固件烧回去。这种做法在功能简单时非常省事,代码量小、没有额外 RAM 开销、行为完全可预测。

但产品一旦越过某个节点,这种做法就开始失效。触发点通常来自下面几类需求中的任意一条:

  • 固件需要 OTA 升级:升级包、备份镜像、版本元数据需要被命名和定位,不能靠硬编码偏移。
  • 需要保存用户数据:配置、计量数据、日志、证书,量不大但必须掉电后还在。
  • 需要存放资源:字库、图片、字体、模型参数,容量上兆字节,手工分配偏移既费事又易错。
  • 需要多任务共享数据:多个线程或多个进程同时读写同一份数据,需要一套统一的加锁与一致性规则。
  • 需要对外交付数据:设备生成的文件要能被上位机工具、上位机操作系统或产线设备直接读取。

这些需求合在一起,指向同一组缺失的能力:把一段无名的字节变成有名字的对象,并且在掉电后还能把名字和内容对应关系恢复出来。这正是文件系统提供的东西。换句话说,文件系统不是因为「嵌入式也需要」而被引入,而是因为手写偏移管理在这些需求面前已经不可维护。

产品需求没有文件系统时的做法引入文件系统后代价
固件 OTA 升级硬编码偏移 + 固定长度按文件名定位镜像与元数据多一次挂载与空间管理
保存配置与计量数据预留固定区域 + 手动校验按路径读写,自动分配与回收元数据本身需要被保护
存放字库与资源数组常量烧进代码段作为文件随镜像分发占用可写空间或需只读挂载
多线程共享数据自建互斥量与缓冲池统一的打开、读写、关闭语义调用开销与栈占用增加
对外交付数据文件自定义二进制格式标准格式,上位机可直接读格式兼容性与簇链碎片

1.2 文件系统解决的三件事:命名、持久化、介质抽象

剥掉所有实现差异,文件系统做的无非是三件事。理解这三件事的分工,比记住任何一个文件系统的特性列表都有用。

第一件是命名:把「一段字节」和「一个路径」绑定起来,并且让这个绑定关系可以查询、遍历、修改。它带来目录、文件、长度、属性这些概念,让上层不必自己记录「这段数据存在哪」。命名是文件系统最古老也最核心的功能——今天所有文件系统的复杂度,很大一部分都是从「目录树在掉电后如何保持一致」这一件事长出来的。

第二件是持久化:把命名关系和内容真正写进介质,并且在断电后还能恢复。这一件是嵌入式场景里最难的,因为介质只保证「写进去的比特能读出来」,不保证「写到一半断电后结构还是合法的」。文件系统的绝大部分代码量都花在处理这一件事上,日志、影子页、事务、原子提交都是为了它。

第三件是介质抽象:向上层提供统一的按字节或按块读写接口,屏蔽「NOR 可以随机改写单字、NAND 必须先擦除整块、块设备完全不告诉你内部结构」这些差异。这一件做得好,上层应用就不用关心底层是哪种介质;做得不好,写放大、寿命消耗、异常行为都会渗透到应用层。

往下一层介质的物理特性逐层向上传导,往上一层文件系统的抽象逐层向下兑现应用层读写文件、创建与删除、按偏移随机访问字节流文件系统层命名、目录、空间分配、掉电一致性(本文主角)闪存管理层磨损均衡、垃圾回收、坏块重映射、FTL 映射介质层NOR Flash / NAND Flash / SD 卡 / eMMC本文讨论的这一层命名、分配、回收以及掉电后的可恢复性最容易出事的一层元数据改动与数据写入不在同一个时刻落盘选型时最容易被跳过的一层闪存管理层的实现质量直接决定上层表现示意图只表达分层关系,不代表任何具体产品的实际层数;带控制器的介质内部还有自己的固件层。
图 1 · 文件系统在嵌入式系统里的分层位置:它夹在应用与介质之间
职责解决的问题做不好的后果各系统的差异主要在哪
命名字节与路径的绑定关系应用要自己记偏移,代码难维护目录结构扁平度、长文件名支持方式
持久化断电后绑定关系仍成立掉电即损坏,可能整盘不可挂载日志、影子页、事务边界的实现策略
介质抽象屏蔽擦除与坏块差异写放大失控、寿命提前耗尽磨损均衡与回收策略、是否压缩

1.3 嵌入式文件系统的四个特殊约束

通用文件系统(ext4、F2FS 这类)为服务器和 PC 设计,那两个环境有大量资源可以挥霍。嵌入式环境的约束完全不同,以下四条会直接淘汰掉大量方案。

约束具体表现被它淘汰的做法仍然可行的做法
资源受限RAM 紧张,代码段与 RAM 都要省为每个文件维护内存索引树把索引放在介质上,读时按需加载
掉电频繁使用中拔电、低压复位是常态假设一次写入总能完整落盘所有元数据更新都要有掉电可恢复路径
介质不完美出厂坏块、擦除不均、寿命有限忽略预留与磨损均衡预留空间 + 动态均衡写放大
生命周期长设备在工作现场可能跑很多年把 RAM 索引全部固化在镜像里紧凑编码 + 按需恢复的元数据

这四条约束里,「掉电频繁」和「生命周期长」这一对是最容易被低估的。很多团队在实验室里用不断电的调试环境开发,文件系统在台式机上跑得好好的,一到现场频繁掉电就出问题。原因不在算法写错,而在于算法假设了一个比现场更温和的环境。

另一条容易被低估的是「介质不完美」。闪存的出厂坏块不是异常,而是规格的一部分;擦除次数有限也是物理事实。一个不处理坏块、不控制写放大的文件系统,哪怕功能完全正确,也会在寿命上先垮掉。

一个常见的错位预期

把通用文件系统直接裁剪后搬到 MCU 上,是一条看起来很自然、实际上很难走通的路。原因不在代码量,而在于设计前提不同:通用文件系统假设「块设备上的一个块被写入后可以被原地改写」,而 NOR 与 NAND 都做不到;通用文件系统假设「有足够的 RAM 缓存目录与索引」,而 MCU 常常只有几十 KB。

裁剪能省下代码,但省不掉这些前提。要真正可用,必须换成从一开始就是为闪存和小型设备设计的方案,而不是把为块设备设计的方案改小。

1.4 本文覆盖的九个系统与各自定位

本文横向对照九个系统。它们不是同一类东西:有的是为 MCU 写的库,有的是随内核分发的文件系统,有的是只读镜像格式。放在一张表里对照之前,先明确各自站在什么位置。

系统宿主设计出发点一句话定位
FatFsMCU / RTOS / 裸机兼容 FAT 生态,向上提供标准磁盘结构通用性与兼容性优先的轻量方案
LittleFSMCU / RTOS / 裸机为闪存特性重做设计,资源占用极低小资源闪存场景的默认候选
JFFS2Linux在 MTD 上做日志式可写文件系统NOR 与 NAND 上都很成熟的日志式选择
YAFFS2Linux为 NAND 的页与块结构重新设计NAND 专用日志式,写入效率高
UBIFSLinux在 UBI 逻辑卷(MTD 之上)做日志式与压缩大容量可写分区的常用主力
F2FSLinux用段与检查点适配闪存访问模式顺序写为主场景的闪存优化方案
ext4Linux通用块设备上的成熟日志式文件系统默认选项,通用性强
RomFSLinux把镜像直接编进内存,不做动态管理极简只读根文件系统
SquashFSLinux压缩只读镜像,按需解压读取容量效率优先的只读根文件系统

1.5 本文要回答的问题与主线

本文的组织方式就是一条提问顺序。每一章回答其中一个问题,附录给出速查与排查。

顺序要回答的问题答错的后果对应章节
第一问介质是什么:NOR、NAND 还是块设备把块设备系统硬挂到闪存上,写放大失控第 2 章
第二问掉电要承诺到哪一层承诺强度与介质实际能力不匹配第 3 章
第三问能否接受闭源授权约束闭源 MCU 产品只剩两个候选第 11.4 节
第四问资源占用与生态哪个优先在等量候选里选错方向第 11 章
为什么顺序不能颠倒

四问之间是严格的依赖关系,不能挑着问。

介质决定候选集合,因为文件系统为哪种介质设计是写在它的结构里的,不是配置项。为块设备设计的系统在闪存上要靠介质抽象层打补丁,写放大与根因都由此而来。先谈算法等于在没有候选的地方比较优劣。

掉电承诺决定机制要求。承诺「元数据一致」的系统可以只做日志与影子页;承诺「事务原子」的系统必须处理写时复制与回滚。不问清要承诺什么,就无法判断某个机制是必需还是浪费。

授权约束决定剩余候选。这一问不涉及技术,却可能在最后一步把候选砍到只剩两个,此时前面所有技术比较都变成背景。

资源与生态是最后一层取舍,只有在前面三问筛完之后才有意义——对一个已经被排除的系统讨论 RAM 占用,是无效工作。

2. 第一性问题:介质决定候选集合

这一章确立全文的第一个筛选器:介质是什么,直接决定哪些文件系统可以进入候选,哪些必须被排除在集合之外。这一章要说明的不是「哪个介质更好」,而是「不同的物理特性如何改写文件系统的设计前提」,以及为什么把块设备系统硬挂到闪存上一定会付出代价。

介质先定候选集合:先归类介质,再在对应集合里挑系统NOR Flash- 随机读,无需擦除即可改写- 擦除粒度较小,坏块很少- 容量通常不大,常外挂 SRAMLittleFSFatFsJFFS2RomFS闭源产品优先看前两个NAND Flash- 首读需整页读出再缓存- 出厂即有坏块,需预留- 擦除粒度大,页与块不对称YAFFS2UBIFSJFFS2LittleFS预留块策略是关键分歧点块设备- 按块地址寻址- 控制器已处理 ECC 与坏块- 掉电语义由设备缓存决定F2FSext4UBIFSSquashFS只读与可写常常分开挂第一步只做排除介质不属于这一类就直接从候选里划掉第二步才谈承诺同一介质下不同系统的差别在这里第三步看约束授权与工具链会砍掉剩下的候选候选集合只是起点:介质只排除,不决定最终答案,后三问仍需逐个回答。
图 2 · 介质类型到候选文件系统集合:介质是筛选器,不是答案

2.1 NOR 与 NAND 的差别如何改写文件系统设计

NOR 与 NAND 都是非易失存储,但两者在读取方式、擦除粒度、坏块特性三个维度上的差别,会直接改写文件系统的结构。理解这三处差别,就理解了为什么面向 NOR 的文件系统与面向 NAND 的文件系统长得完全不一样。

  • 读取方式:NOR 支持随机读,可以像内存一样按地址直接读出任意位置;NAND 必须先把一整页读进页缓存,再从缓存里取字节。这个差别意味着:NAND 上的文件系统不能假设「读一个字节只要一次寻址」,任何按需加载的机制都要按页为单位设计。
  • 擦除粒度:NOR 的擦除单位通常较小,且写入可以先擦除再写,也可以就地把 1 编程为 0(任意改写仍需整块擦除);NAND 的擦除单位是块,量级远大于页,且改写前必须先擦除整块。这意味着 NAND 上「原地修改一个已有块」是不存在的操作,文件系统必须永远写到新块。
  • 坏块特性:NOR 出厂时坏块很少或没有;NAND 出厂即允许存在坏块,且随擦写次数增加可能新增。这意味着 NAND 上的文件系统必须自带预留块与替换机制,否则写到坏块上就是永久丢数据。

把这三条合起来看:NOR 上的文件系统可以把「块」当作可以随机改写的空间来管理,NAND 上的文件系统必须把「块」当作只能顺序写、只能整块擦除的容器来管理。前者的复杂度集中在掉电一致性与目录组织,后者的复杂度还额外包含预留、回收与寿命均衡。

维度NOR FlashNAND Flash对文件系统设计的影响
随机读支持按地址直接读需整页读入缓存后取按需加载必须以页为单位设计
写操作NOR 可就地把 1 编程为 0(无需擦除);任意改写仍需整块擦除须先擦除整块再写不存在原地修改,写操作必须前进
擦除粒度较小较大,按块块大小决定空间分配与回收的最小单位
坏块很少或无出厂即有,随使用增加必须预留并做坏块替换
页与块关系通常无明显页概念页远小于块写入以页推进,擦除以块为单位
典型形态代码存储、小容量数据大容量数据存储前者要省资源,后者要控寿命

2.2 擦除粒度与块大小的关系

块大小与文件系统的块大小之间有一个必须注意的比例关系:文件系统块不能大于介质擦除块。否则一次文件系统级的写入会横跨多个介质擦除块,要么无法原子完成,要么需要额外的读-改-写流程,把写放大推到不可接受的水平。

反过来,文件系统块远小于擦除块也是问题。举一个量级上的例子:当文件系统块远小于介质擦除块时,一次改写一个小文件的数据,会把整个擦除块读出、修改、再整块写回。写入量是实际修改量的很多倍,这就是写放大的最朴素来源。

因此「文件系统块大小」这个参数从来不是随便设的,它必须在空间利用率、索引粒度、掉电原子性、写放大之间取平衡。不同系统给出的默认值不同,但设计的取舍逻辑是同一个:让文件系统块与介质擦除块建立整数倍关系,让一次数据落盘尽量对齐到一次擦除操作。

具体到数值,本文只给量级判断:介质擦除块在数十 KB 到数百 KB 量级,文件系统块通常取其整数分之一或与之相当,具体取值以介质手册与系统文档为准。任何「推荐设为某个固定值」的说法都应结合实际介质核对。

2.3 块设备的三个假设哪些可以去掉

为块设备(SD 卡、eMMC)设计的文件系统,建立在三条假设之上。这三条假设在闪存介质上都不成立,所以直接挂载必然出问题。

第一条假设:块设备上的块可以被原地改写。SD 卡与 eMMC 对主机呈现的是块设备语义,主机可以读一个块、改几个字节、再写回同一个块。这个模型之所以成立,是因为卡内控制器维护了 FTL(映射表),把主机的逻辑改写转成「写到别处、更新映射」。把文件系统挂到裸闪存上时,这个 FTL 并不存在——要么你自己实现一个(那就是 YAFFS2/UBIFS 之类在做的事),要么让文件系统自己去处理「不能原地改写」这件事。后者如果没设计好,写放大和寿命问题会同时出现。

第二条假设:块地址直达介质,不需要预留。块设备已经把坏块处理掉了,文件系统不需要预留空间。而裸 NAND 上,坏块标记本身要占空间,垃圾回收要占空间,文件系统必须自己划出一部分容量作为「工作空间」,这部分容量对用户不可见。跳过预留直接挂载,是最常见的现场事故来源之一。

第三条假设:介质抽象层可以不管。ext4 这类系统假设下面有一个已经把磨损均衡、坏块重映射、TRIM 都做好的块设备。如果下面是裸闪存,它不会为裸闪存的特性做任何优化,写放大与寿命消耗会直接体现在应用层。

反过来说,为闪存设计的文件系统(LittleFS、JFFS2、YAFFS2、UBIFS、F2FS)挂到块设备上通常也是浪费——它们自己实现了介质管理,块设备的 FTL 又做了一遍,两层叠加会让写放大不降反升。每层只做自己该做的事,这是嵌入式存储栈的基本纪律。

块设备的假设为什么在裸闪存上不成立直接挂载的表现正确做法
块可原地改写闪存改写前须先擦除整块写入不生效或静默丢失用闪存原生文件系统,或自己实现 FTL
无需预留空间裸 NAND 有坏块与回收开销写到坏块或空间莫名不足按机制要求的比例预留,比例以文档为准
下层已做介质管理裸闪存没有 FTL写放大高、寿命快速消耗让文件系统或中间层负责其中一层

2.4 介质抽象层与挂载方式

同一套文件系统代码,能跑在 MCU 的外部 SPI NOR 上,也能跑在 Linux 的 eMMC 上,差别在于中间的抽象层。这个抽象层把「怎么把一段字节写进介质」这件事从文件系统里剥出来。

Linux 侧的常见做法是:裸闪存之上放一层设备映射(把物理块组织成逻辑卷),文件系统再挂在逻辑卷上。这层映射负责坏块替换与磨损均衡。UBIFS 这类系统的定位正是「逻辑卷之上的文件系统」,它假设下面那层已经把物理块的问题解决了;F2FS 与 ext4 也走同一条路径。

MCU 侧的常见做法不同:文件系统直接读写闪存的页与块,内部自己做介质管理。LittleFS 就是这样——它只要求你提供「读一个块」「擦除一个块」「写一个块」三个原语,剩下全由它负责。FatFs 更保守:它只要求你提供「按扇区读写」,介质管理完全推给下面的驱动。

这三种形态对应三种不同的责任划分:FatFs 把介质管理完全推出去,LittleFS 只做块级抽象、F2FS 与 ext4 把全部介质管理交给下层、而 YAFFS2 与 UBIFS 自己做了其中一部分。选型时先确认这一层的责任归属,比比较功能列表更能预测实际行为。

形态代表系统文件系统要求下层做什么谁负责磨损均衡与坏块移植工作量
扇区接口FatFs按固定大小读写扇区、给出容量使用者(写驱动时补齐)最小,但要自己补的最多
块级原语LittleFS擦除一块、读一块、写一块文件系统自己做一部分中等,需理解块与页的关系
块级原语(更完整)YAFFS2擦除、读页、写页、查询坏块文件系统与下层分工中等,页语义必须准确
逻辑卷UBIFS提供逻辑卷与其上的空闲块管理中间层(设备映射)较大,需先把逻辑卷搭起来
块设备F2FS / ext4标准块设备语义设备内部控制器最小,但前提是设备已就绪

2.5 按介质给出的候选集合

把前四节合起来,按介质给出一个初始候选集合。这个集合只做排除,不做推荐——它排除掉那些结构上不可能在这个介质上正常工作的系统,剩下的还要继续回答后面三问。

介质优先进入候选结构上不适合排除的理由
NOR FlashLittleFS、FatFs、JFFS2、RomFSext4(无 MTD 直挂路径)4 KB 块远小于擦除块,读改写放大严重
并行 / SPI NANDYAFFS2、UBIFS、JFFS2、LittleFSRomFS(镜像放不进容量)只读格式无法承载运行期写入
SD 卡 / eMMCext4、F2FS、SquashFSJFFS2 / UBIFS(均面向 MTD/UBI,块设备路径不对应)缺 MTD/UBI 层,块设备路径不对应
内存(无掉电)RomFS、SquashFS全部可写系统无掉电约束时一致性机制是纯开销
宿主机文件系统ext4、F2FS全部 MCU 侧库这些系统没有宿主机移植层
这一章的结论,以及它没有回答的问题

结论:介质是筛选器。为块设备设计的系统(ext4、F2FS、UBIFS)挂到 NOR 上,必须靠介质抽象层打补丁;写放大、性能异常与寿命问题都由此而来,不是算法 bug。

这一章没有回答的问题是「要承诺什么」。介质只规定了「能不能工作」,没有规定「工作到什么程度」。同在 NAND 上,要求元数据一致的系统与要求事务原子的系统,需要的机制完全不同。

也没有回答「能不能接受闭源约束」。上面的候选集合里,有相当一部分随内核分发、以 GPL-2.0 条款提供。如果产品必须闭源,这个集合在第三问就会进一步收窄——这一条放在第 11.4 节展开,因为它虽然与技术无关,却常常是最后生效的那一刀。

3. 掉电模型:文件系统承诺什么

掉电是嵌入式文件系统唯一真正困难的场景。本章把「掉电安全」这个笼统的说法拆成三个可以分别选择的承诺层级,并说明每种机制分别能兑现哪一层。本章的核心判断是:选型失败多数不是算法问题,而是要承诺的层级与介质实际能给的层级不匹配。

掉电不是一个事件,而是四个环节里最靠后的那个环节被截断写入请求到达上层把数据交给文件系统尚未改动任何介质状态文件系统改元数据目录项、索引、分配表被更新只改了一半最危险数据写向介质真正把字节写进闪存或块设备写到一半的块是不确定状态介质层落盘写入从介质缓存真正固化这一步之前断电等于没写关键:前三步的断电是「可恢复的」,第四步的断电才是真正丢数据文件系统的一致性机制只能保证「断电后结构仍然合法」,它无法保证「未完成写入的数据还在」。示意图为流程示意,四道环节的先后顺序按常见实现给出;不同系统的具体落盘时机不同。
图 3 · 掉电时到底会坏在哪一层:损坏位置决定了需要哪种一致性机制

3.1 掉电时到底会坏在哪一层

一次文件写入在系统内部要经过若干环节,掉电可能发生在任何一个环节上。不同环节的断电,后果完全不同——把它们混为一谈,是「我们的文件系统应该做到不丢数据」这类需求无法落地的根本原因。

  • 数据还在缓冲区时断电:介质上什么都没变,文件保持旧内容。对文件系统来说这是最轻松的一种断电,什么都不需要恢复。
  • 元数据改了一半时断电:目录项或索引指向了一个还没写完的区域。这是最危险的一种,因为结构本身已经不合法,挂载可能直接失败,或者更糟——挂载成功但读出乱码。
  • 数据写到一半时断电:目标位置的内容是不确定的。如果文件系统之前已经把元数据指向了这里,就会读到半新半旧的数据。
  • 写入已返回、但介质缓存尚未固化时断电:上层认为写成功了,介质上前一个版本还在。这一种最隐蔽,因为没有任何错误码能提示它。

前三种,文件系统可以通过机制设计做到「可恢复」;第四种,取决于介质与控制器是否实现了掉电保护,文件系统层面无能为力,只能依赖下层的Flush 命令语义或带掉电保护的器件。

断电发生的环节介质上的状态文件系统能否恢复需要什么机制
缓冲区未落盘完全未改动无需处理,直接保持旧内容无
元数据改写中途结构不合法可以恢复到上一个一致点日志重放或双副本校验
数据写入中途目标区内容不确定元数据不能指向未完成区域先写数据后提交元数据
返回成功但缓存未固化仍是旧版本文件系统层面无能为力依赖下层掉电保护语义

3.2 三个层级:数据完整、元数据一致、事务原子

「掉电安全」不是一个二元属性。把它的要求拆成三个递进的层级后,选型对话才能变得具体——你可以明确说出产品需要哪一层,而不是笼统地说「要可靠」。

第一层是数据完整:已经写进去的字节,读回来还是对的。这是最基本的承诺,绝大多数文件系统都能做到——只要介质本身可靠。这一层不讲掉电,讲的是介质可靠性与错误校验。

第二层是元数据一致:掉电后重新挂载,文件系统的结构必须仍然是合法的。目录树不能出现悬空指针,索引不能指向不存在的块,分配表不能自相矛盾。这一层是嵌入式文件系统的基本要求,也是日志式与双副本机制真正在解决的问题。

第三层是事务原子:一个「写操作」要么全部生效,要么完全不生效,中间不存在「一半」的可见状态。这一层比前两层严格得多——它要求多个文件、多个块之间的修改作为一个整体提交或放弃,绝大多数文件系统在单文件范围内能做到,跨文件范围就很少有系统提供。

三层的实现代价是递增的。产品实际需要哪一层,取决于业务能接受什么后果:只是记录传感器采样值,第一层足够;维护一份需要长期累积的日志文件,第二层是底线;维护一个需要「要么全做完要么全没做」的配置事务,才需要第三层。

层级承诺内容典型要求实现代价常见误解
数据完整写入的字节读回一致错误校验、坏块处理低以为它等于掉电安全
元数据一致掉电后结构仍然合法日志、影子页、原子提交中以为有了它就丢不了数据
事务原子一组修改要么全成要么全不成写时复制、回滚、多副本高以为所有文件系统都提供
把需求写成层级,而不是写成形容词

把「我们要掉电安全」直接交给文件系统选型,几乎得不到有用的回答。改成下面三句,每一句都能对应到具体的机制判断:

  • 第一句:设备运行中可能被直接断电,断电后设备必须能自动重启并挂载成功,不能要求人工干预。这对应第二层(元数据一致),是绝大多数嵌入式产品的实际要求。
  • 第二句:断电时正在写的那个文件,允许丢失,也可以退回到上一个完整版本。这仍然是第二层,只是明确了边界。
  • 第三句:断电不能损坏已经关闭的文件,也不能让已确认保存的数据消失。这仍然主要是第二层,但要求「提交」边界清晰。
  • 只有当业务确实需要「跨多个文件的一次操作不能做一半」时,才进入第三层的讨论。

把层级讲清楚之后,选型就变成一个可以查表的问题:这个系统的机制覆盖到第几层,覆盖不到的代价是什么。

3.3 日志式:把变化先顺序追加

日志式的核心思想只有一句:不要原地修改任何东西,把「我要做什么」按顺序追加写到介质末尾,重启时从头重放一遍。因为写入是纯追加的,每个块在被写满之前不会被重复写,所以掉电最多丢掉最后一条不完整的记录,前面的记录依然有效。

这个思路把「修改一个已存在的结构」变成了「追加一条描述这个修改的记录」,后者在顺序写的介质上几乎免费。它的代价同样明显:

  • 需要额外空间:日志区要占容量,而且必须保证日志区本身写满前有空间可用。
  • 挂载要重放:重启时要把日志从头扫一遍,介质上数据越多,挂载越慢。
  • 写放大:日志满了之后要做垃圾回收,把仍然有效的数据合并写回,这一步是主要写放大的来源。

JFFS2、YAFFS2、UBIFS、F2FS 都属于这一类。它们之间的差别,不在「要不要日志」这个根本选择上,而在日志的记录粒度、垃圾回收的触发策略、以及是否压缩这三个细节上。

同样的目标,两条不同路径:把变化记在别处,还是留一份旧的日志式:先追加,再落地变化先按顺序追加到日志区操作1已固化操作2已固化操作3半截恢复时从头重放,遇到不一致就丢弃写放大来自重放与合并代表:JFFS2、YAFFS2、UBIFS、F2FS写入顺序固定,不追求原地改写双副本:留一份旧的当备份同一份关键结构存两份,靠序号定新旧副本 A序号 n同一结构副本 B序号 n-1同一结构写新副本时不覆盖旧副本挂载时读序号更大的一份代表:LittleFS 元数据对、部分引导块需要预留副本空间,但恢复快日志式的代价写放大更明显需要额外日志空间双副本的代价需要预留副本空间更新次数受限两者的共同点都在元数据层做文章都不保护未写完的数据两种机制可以叠加使用;实际系统常同时具备日志区与多份关键元数据。
图 4 · 日志式与双副本:实现一致性机制的两条主要路径

3.4 双副本与影子页:靠冗余而不是靠日志

另一条路径是双副本:同一份关键结构存两份,写的时候只改其中一份,另一份保持完整;两份都不可信时才用第三份或退回默认值。恢复时读出所有副本,选出校验通过且计数最大的那一份。

这条路径的特点是恢复极快——不需要从头扫描介质,只要读几个已知位置的块,比较计数即可。这对启动时间敏感的设备(电机、控制器、仪表)很有价值:挂载耗时基本与文件数量无关。

它的代价也很清楚:

  • 需要预留副本空间,这部分容量不可用于存数据。
  • 更新次数受限:只有两份副本时,写满两份才能回收最旧的一份,这个循环要求有额外的空闲块存在,否则文件系统只能进入只读状态。
  • 只保护被显式复制的结构:目录项与文件数据本身如果不在副本范围内,仍然需要别的机制保护。

LittleFS 是把这条路径用到比较彻底的系统:它的每一个文件与目录都有一对元数据块,因此挂载开销与文件总数基本无关,掉电恢复也只需要读块头。

3.5 原子写与写时复制

前面两种机制解决的是「结构不损坏」,但还有一个更细的问题:能不能让一次修改在外部看来是不可分割的。这就是原子写。

实现原子写的手段基本只有一种:写时复制。把要改的内容写到一份新的副本上,写完之后再用一个原子的动作把「指向哪一份」切换过去。旧副本在切换成功之前一直是有效的,因此任何时刻掉电,要么看到旧的,要么看到新的,不存在中间态。

写时复制的实现代价是空间与时间都要翻倍:修改一个已有文件需要额外空间存放新副本,写完还要清理旧副本。对嵌入式产品来说,这意味着两件事——容量要留出余量,写延迟会增加。

需要区分的是两种不同粒度的原子性:

  • 文件内原子写:一个文件的内容要么是旧版本要么是新版本。多数日志式文件系统都能做到,因为文件内容本身就是按顺序写的。
  • 跨文件原子写:多个文件的修改作为一个整体提交。实现这个需要在文件之上再加一层事务机制,多数通用文件系统并不提供。

F2FS 提供了一种较特别的机制:同一文件内的多次小写入可以在写时复制配合回滚的情况下表现为一次原子更新,这也是它被大量用于手机存储的原因之一。但这个能力的边界仍然是文件内,不是跨文件。

原子性粒度能做到的系统实现代价典型需求
单次写入不撕裂全部九个系统低,写入本身按最小单位完成单条记录写入
单文件内多次写作为一次F2FS、部分日志式系统中,需版本号与回滚频繁改写同一配置文件
单文件更新可回退到旧版F2FS中,需保留旧版本位置状态文件、计数器累加
跨文件事务需更上层机制高,需应用层或专门机制多个配置项要么全改要么全不改
跨文件加跨目录事务极少数系统,需定制很高目录与内容必须同时切换
机制保护的对象恢复方式主要代价典型代表
日志重放目录、索引、分配表从头扫描并重放挂载慢、写放大JFFS2 / YAFFS2 / UBIFS
多副本被显式复制的关键结构比较计数选最新预留空间LittleFS 元数据对
写时复制被替换的旧数据切换指向即可空间与延迟翻倍带原子写支持的日志式系统
校验 + 纠错数据内容本身读时纠正额外开销依赖下层 ECC

3.6 「不丢数据」与「不丢已提交的数据」不是一回事

这是本章最重要的一条,也是需求沟通里最容易出错的地方。

「不丢数据」的字面含义是:任何时刻断电,之前写过的每一个字节都还在。文件系统无法兑现这个要求,因为写入可能根本没有落到介质上——数据还在缓冲区里,或者在介质控制器的缓存里,断电就没了。这不是文件系统实现得不好,是物理事实。

「不丢已提交的数据」是一个可兑现的要求:凡是已经向调用方返回「写成功」的写入,即使随后立刻断电,内容也必须读得回来。这个要求能够满足,但代价是:每次写入都必须真正固化到介质,操作会明显变慢,而且这个保证的强度取决于介质与控制器是否实现了掉电保护。

两者的差距就是「返回成功」到「真正固化」之间的那一段时间。消除这段差距的唯一办法是在返回前强制固化,而这正是同步写模式的代价。

把要求写进设计文档的正确说法

不要写「文件系统必须保证掉电不丢数据」。这句话无法验收,因为没人能定义「返回成功」的确切时刻。

改成下面这种写法,就能直接查表验收:

  • 允许丢失的范围:正在写入中的文件内容、尚未调用关闭的文件、未同步的缓冲区,明确列为可丢。
  • 必须保留的范围:已经关闭的文件、已经返回成功的配置写入,明确列为不可丢。
  • 一致性要求:断电重启后必须能自动挂载,不允许出现「需要格式化才能使用」的情况。
  • 边界条件:掉电发生在写入中途时的期望行为,是退回上一个版本还是整个文件损坏,两种都可以接受,但要选定一种并验证。

这样写出来的需求,可以直接对应到文件系统需要提供的机制层级,也可以在验收时用断电测试逐条验证。而「掉电安全」四个字什么都验证不了。

4. LittleFS:面向闪存的小型文件系统

LittleFS 是 MCU 侧最常被推荐的闪存文件系统,本章拆开它的四个关键设计。它的价值不在于某项指标最优,而在于把「掉电可恢复、磨损均衡、极小 RAM 占用」这三件难事用同一套机制同时做到了——这个机制就是元数据对。理解了它,后面所有特性都能顺下来。

4.1 设计目标与定位

LittleFS 从设计之初就明确了三件事,因此它的形态与其他系统差别明显。

  • 为闪存设计,不是为块设备裁剪。它假设介质只能整块擦除、只能顺序写、没有原地改写。这不是妥协,而是把它做对的前提。
  • RAM 占用与文件数量无关。目录、索引、属性全部存放在介质上的元数据块里,内存里只保留当前访问路径的少量缓存。这一点在小内存 MCU 上是决定性的——传统设计要为每个文件在 RAM 里留结构体,文件一多就撑不住。
  • 掉电安全来自结构本身,不是来自附加的日志。元数据成对存放、写新不写旧,使得任何时刻断电都能读出一个完整版本。

它的宿主形态是一个可以被链接进固件的库,不依赖操作系统,不依赖多线程保护(同一实例不可并发访问),这一点与 Linux 侧的文件系统有本质区别。

整个文件系统被组织成若干「元数据对」,每个文件或目录至少占一对一个元数据对:两个块轮换写,靠块头里的计数定新旧块 1计数 12块 2计数 11配对块 3数据块 4数据普通数据块块 5计数 12块 6计数 11配对块 7数据块 8数据普通数据块写入时只往计数较旧的那一块写;两块都写完才擦除最旧的一份为什么成对写一块时另一块保持完整掉电最多丢一次未提交的写磨损均衡从哪来每次更新换一块写天然把擦除摊到整个介质空间代价小文件多时元数据占比高块越大越明显这一层设计带来的直接结果目录、文件、属性全部存放在这些块里,挂载时按需读入内存,因此 RAM 占用与文件数基本无关。示意图只表示组织关系;块大小、预留块比例等参数以所用版本的文档为准。
图 5 · LittleFS 的块与元数据对:成对轮换写是它掉电安全与磨损均衡的同一个来源

4.2 块与元数据对的组织

LittleFS 把介质切成等大的块,然后把两个块绑定成一个「元数据对」。每个文件、每个目录都至少占用一对。目录本身也是一种元数据,存放在元数据对里,这解决了「目录在哪里」的问题。

元数据对的核心规则只有一条:更新时永远往计数较旧的那一块写,写完把计数加一;两块都写过之后,才擦除最旧的一份开始新的一轮。这条规则同时产生了三个效果:

  • 掉电安全:写新块时旧块仍然完整,读到旧版本就是一个合法的一致状态。
  • 磨损均衡:同一份元数据被反复更新时,写入位置在两块之间来回轮换,之后又扩展到整个介质,擦除分布天然均匀。
  • 恢复简单:挂载时只需要读块头比较计数,不需要扫描整个介质。

元数据块里除了计数,还带有头部标记与内容校验。校验的作用是区分「这一块写完了」和「这一块写到一半断电了」——没有校验就无法判断该信任哪一份。

4.3 动态磨损均衡

静态磨损均衡(周期性搬移冷数据,使长期不写的块也参与磨损分摊)实现简单但效果有限;LittleFS 走的是动态路线:磨损均衡不靠周期性搬移冷数据,而是内建于「元数据对的轮换写」本身。

这个设计带来一个实际后果:小文件频繁更新的场景下,写放大被有效控制住,不会出现「改一个字节要擦一整块」的情况。因为元数据块本身已经很大,一次更新只写一个元数据块而不是整个文件。

但它也有一个明确的短板:数据块本身不是元数据对的一部分。如果某个文件的数据部分反复被覆写,LittleFS 会通过「写新块 + 更新元数据指向 + 回收旧块」来处理,回收不完全时会产生写放大。在数据量远大于元数据量的场景里,这一点比元数据侧的均衡更值得实测关注。

4.4 掉电恢复与预留块

掉电恢复的过程很短:读每个元数据对的两个块头,比较计数与校验,选出可信的那一份。如果两个都不可信,说明这一对已经彻底损坏——在正常设计下这不应该发生,因为两份同时写坏需要连续两次故障。

这里引出一个必须理解的约束:LittleFS 需要预留一部分空闲块,作用是给回收过程提供落脚的临时位置。预留不足的直接表现是文件系统进入只读状态——它检测到没有足够空间完成一次安全的元数据更新,于是拒绝写入以避免破坏自身。

所以「预留块」在 LittleFS 里不是可选优化,而是保证系统还能继续运行的前提条件。介质被写满时,只有预留块还在,文件系统就还能正常卸载、正常重新挂载。把这个行为误解成「文件系统坏了」是很常见的现场误判。

预留不足时的表现系统实际发生了什么该怎么处理不该怎么处理
挂载后变成只读文件系统拒绝写入以自保检查剩余容量与预留比例反复重试写入
卸载时报错无法完成最后一次元数据提交清理数据后重新挂载强行擦除介质
新建文件失败没有可用空间完成元数据更新按产品需求清理旧数据降低预留比例硬撑
重新挂载正常说明只是瞬时空间不足增加数据清理策略认为文件系统有 bug

4.5 RAM 占用与静态镜像生成流程

LittleFS 的 RAM 占用主要由两部分构成:文件读写缓冲区(每个打开的文件需要一份)与挂载时的少量全局状态。由于目录与索引不常驻内存,RAM 占用基本不随文件数量增长——这是它在小内存 MCU 上最实际的价值。

LittleFS 还有一个容易被忽略但对产品化很关键的特性:可以把整个文件系统生成成一个静态镜像数组,编译进固件。这样一来,固件里就带着一份已经格式化好的文件系统,开机直接使用,连首次格式化的时间都省掉了。

生成流程大致是:在 PC 上运行一份宿主工具,把源目录的内容打包成 C 数组文件,然后在固件编译时把它链接进去。这套流程的价值在于——资源文件(字库、默认配置、证书)的更新不需要额外的下载流程,直接随固件升级一起走。

步骤在哪台机器上做做什么常见坑
准备源目录PC按目标路径组织好资源文件路径层级与运行时不一致
生成镜像PC(宿主工具)把目录转成可写入介质的镜像文件工具版本与固件侧不匹配
转成数组PC(宿主工具)把镜像编码成 C 源文件生成后源目录又改了,未重生成
编译链接固件构建把数组与固件一起编译数组体积撑爆 RAM
运行时挂载设备直接挂载,不需要格式化误以为能在设备上重新生成

4.6 优势、限制与典型陷阱

优势集中在三点:RAM 占用与文件数无关;掉电恢复只读块头,速度快且不依赖文件数量;结构上不需要额外的日志区,介质利用率较高。

限制同样明确:

  • 不是 FAT 格式。PC、相机、手机无法直接读取。这意味着通过 USB 导出文件时需要额外的转换层。
  • 单实例不可并发。同一文件系统实例不能被多线程同时操作,跨线程共享需要自己加锁。
  • 需要预留块。空间利用率不是满的,介质接近满时的行为需要提前设计好应对。
  • 大容量与小文件场景要实测。目录层级很深或文件数量极大时,元数据对的组织会带来额外开销,具体影响随配置变化。
LittleFS 的三个典型陷阱

陷阱一:以为它是 FAT。LittleFS 不产生 PC 能读的目录。把含 LittleFS 分区的存储卡插到电脑上,操作系统会认为它没有文件系统。如果产品需要用户直接取数据,必须在导出环节做格式转换,或者改用 FatFs。

陷阱二:忘记预留块。预留不足时文件系统进入只读,看起来像「存储坏了」。正确做法是在设计阶段就按使用模式估算峰值占用,把清理策略一起设计进去。

陷阱三:多线程直接共享实例。LittleFS 的设计假设是单线程访问。多线程场景下不加锁直接调用,出现的是随机的元数据损坏,排查成本很高。要么在封装层加一把全局锁,要么每个线程用独立实例。

对比项LittleFSFatFs选型提示
介质假设闪存原生,整块擦除扇区读写,介质无关前者更适合裸闪存
掉电机制元数据对轮换写FAT 双备份 + 写入顺序两者都能到元数据一致层
RAM 占用与文件数基本无关与文件数与目录项相关小内存优先前者
格式兼容自有格式标准 FAT需要 PC 直读选后者
静态镜像支持生成 C 数组需自行实现资源随固件分发的场景前者省事
授权条款BSD 类官方自定义免费条款均适合闭源, specifics 以官方条款为准
相关报告 · 交叉引用 2 器件全部展开收起 ▾
点开类目即可展开:具体文档 / 章节(点章直接跳转到该章)
eMMC
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查
SPI NOR
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查2
系统设计指南
其他提及器件(共 1)
SPI NAND
电路设计指南
软件设计规范
Linux 驱动开发
样机问题排查
量产问题排查4
全志平台 SPI NAND NOR
章节(点章跳转,共 11 章)
海思平台 SPI NAND 量产常见失败
章节(点章跳转,共 11 章)
烧录成功但不开机 SPI NAND
章节(点章跳转,共 12 章)
瑞芯微平台 SPI NAND eMMC
章节(点章跳转,共 11 章)
系统设计指南3