芯参谋(95):嵌入式文件系统_开发指南

1 · 文件系统全景与选型

文件系统是软件栈里最容易被「当成系统自带」的一层,却是嵌入式产品里最容易出问题的一层。它的复杂度不来自代码量,而来自它要对物理世界的三条硬约束负责。

五层抽象

1.1 为什么嵌入式文件系统是独立课题

PC 上的文件系统几乎不需要工程师操心,因为三个前提天然成立:介质块大(4 KB 扇区起步)、可靠性高(SSD 有自己的 FTL 与磨损均衡)、应用层只管调 API。嵌入式把这三个前提全部拿掉了。

一颗 64 Mb SPI NOR 的擦除粒度可能是 4 KB,一个页写 200 ms;一次改 16 字节的配置文件,底层要读出整块、修改、擦除、写回------写放大 256 倍。一块 1 Gb 的 NAND 页只有 2 KB、使用中还会长出坏块,需要自己的坏块表与 ECC 策略。这些约束直接进文件系统设计,不是加个驱动就完事。

一个反面案例

某智能终端产品用 FAT32 存配置:一个小文件 100 字节,占 1 个簇。日志每 5 分钟追加一条。每条日志触发「读簇 --- 改 --- 擦簇 --- 写簇」。这颗 NOR 只有 10 万次擦除寿命、簇 4 KB,于是每天约 3 万次擦除,三周就把那一簇写坏,表现为设备运行一段时间后配置读取异常。正确做法是把日志改成环形缓冲 + 批量落盘,或至少做到「攒够 64 KB 再落盘」------写放大立刻降到 1 附近。

1.2 五层抽象:应用、FS、FTL、块设备、介质

NOR FlashNANDeMMC擦除粒度64 KB ~ 256 KB16 KB ~ 256 KB编程粒度1 ~ 8 字节512 B ~ 4 KB 页擦除时间100 ~ 1500 ms1 ~ 10 ms擦写寿命10 万 ~ 100 万次1 千 ~ 10 万次读延迟数十 us数十 us随机读能力强(XIP 可行)弱(需 ECC)是否需要 FTL不必(可直写)必须典型容量1 ~ 512 Mb128 Mb ~ 16 Gb坏块出厂即有,极少增长出厂 + 使用中增长掉电粒度整扇区可能只写一半页 / 多页结论:三类介质不能套同一个文件系统NOR 用 LittleFS 类、NAND 用 FTL + 块层语义、eMMC 直接 ext4/FAT ------ 抽象层选错,成本差一个量级

图 2 · 三类存储介质的特性对照

上图是选型的起点。下面逐层说明每层负责什么、边界在哪里。

第 1 层:应用与框架

业务代码、日志服务、配置中心、数据库。这一层只应该看到 POSIX 接口(open/read/write/lseek/fsync)。如果应用需要知道自己在 NOR 上还是在 eMMC 上,说明抽象已经漏了------那是最需要警惕的设计信号。

第 2 层:文件系统

本文的主体。它负责五件事:

  • 空间分配:哪些块归哪个文件、哪些块空闲;
  • 目录组织:文件名到数据块的映射;
  • 元数据管理:超级块、位图、目录项、inode 的可靠性;
  • 一致性:保证掉电后能恢复到某个一致状态;
  • 寿命与配额:把有限的擦写次数分配给无限的写入请求。
第 3 层:FTL(闪存转换层)

把「逻辑块地址」翻译成「物理页地址」,并在逻辑页被改写时做垃圾回收。eMMC 与 SD NAND 芯片内部自带 FTL,用户态看到的是一个干净的块设备;SPI NOR 与裸 NAND 则通常需要在主机侧自己实现。第 4 章专门讲。

第 4 层:块设备层

提供统一的扇区语义:request(sector, count, buffer)。Linux 上是 blkdev / mtdblk / mmcblk;MCU 上由应用自己实现这个函数指针。关键约定是:单位是 512 字节还是页大小,必须在设计时定死,混用是移植里最常见的错误之一。

第 5 层:物理介质

NOR:可原地改写,但只能 1 变 0擦除一次恢复整块为全 1,因此「擦除 + 写」是改写的前置条件1 1 1 1初始1 1 1 1擦除后0 0 1 1写入 00 0 0 0再次写入想把 0 改回 1?只能擦除整块这就是「写放大」的根源NAND:只能顺序写入到空白页已编程的页不可复写,必须换新页,这就是 FTL 存在的原因页 0已用页 1已用页 2当前页 3空白页 4空白FTL 的活:把逻辑页 2 映射到物理页 3,并记住这个映射映射关系存在 OOB 或映射表中,掉电时必须能重建三条不可违反的物理规则· 擦除只能整块:不能擦半个扇区,这是所有「先读出---改---整块写回」逻辑的根源· 编程有顺序性:NAND 必须编程到空白页;NOR 可原地写但需先擦除· 断电即最终:写操作中断,介质可能处于「一半新一半旧」,这叫撕裂由此推出文件系统的两条设计底线① 任何「看起来只改了 1 字节」的操作,底层可能触发「读整块 --- 改 --- 擦整块 --- 写整块」,写放大由此而来。② 任何可能掉电的操作,都必须能靠介质上已有的信息恢复到「擦除前」或「修改后」的一致状态。这一章的结论文件系统的每一个设计决策------是否用日志、写放大多少、要不要预留空间、元数据放哪------都能追溯到上面这三条物理规则。反过来,看到一个文件系统设计就能推出它假设的介质特性。

图 3 · 擦除与编程的物理过程

这三条规则是整篇文档的地基。请记住它们的实际后果:

  • 「擦除只能整块」→ 文件系统的最小分配单元不能小于擦除粒度,否则每次写都要擦一个大盘;
  • 「编程有顺序性」→ NAND 上必须有 FTL,NOR 上可以不写 FTL 但要用日志式结构;
  • 「断电即最终」→ 一切跨块操作都需要事务性设计。

1.3 块设备与字符设备的分界

这个分界决定了「能否用文件系统」,实践中要先问清楚。

维度 块设备 字符设备 对文件系统的影响
访问单位 固定大小扇区(512 B / 4 KB) 字节流 块设备可挂文件系统;字符设备不能
随机寻址 支持,可跳着读写 不支持,只能顺序 FAT 依赖随机寻址
典型设备 mmcblk、mtdblk、nvme、ublock 串口、GPIO、传感器 MTD 原始设备是字符设备,需 mtdblock 转块设备
MTD 分区 MTD 上仍以字符设备暴露(mtdblockN) mtd0 直接是字符设备 NOR 上常用 mtdblock 挂 LittleFS
能否直接改 1 字节 不能(需读改写整块) 能 决定写放大量级
典型文件系统 ext4、FAT32、LittleFS、NTFS 无 ---

实践中最容易踩的一条:在 SPI NOR 上用 mtdblock 挂文件系统,写放大会大到不可接受。因为 MTD 的写是「按最小可写单位(NAND 上是页、NOR 上是 1 字节)」,而擦除是「按擦除单位(4 KB ~ 256 KB)」。文件系统以为自己在改 1 KB,实际触发了一次 64 KB 的擦除。正确路径是让文件系统直接跑在 MTD 之上(LittleFS 就是这么设计的),由它把「改了多少」与「擦多少」的关系算清楚。

1.4 本文覆盖范围与前置约定

层次 本文讲什么 本文不讲什么
文件系统 FAT、日志式、LittleFS、SPIFFS、KV 存储的原理与选型 ---
FTL 映射策略、GC 算法、坏块替换的设计取舍 芯片内部 FTL 的实现细节
块设备 接口约定与移植要点 块设备驱动的寄存器级实现
介质 物理特性对上层的影响、参数表 电气时序、电压、封装(见电路设计指南)
系统 挂载顺序、只读根、OTA 空间规划 内核调度、进程管理(见内核 BSP 与系统篇)

阅读约定:本文的路径以 Linux 为主线(/dev/mtdblock0、/dev/mmcblk0p1、mount),MCU 平台在每处给出对应写法。命令示例中的参数以常见默认值示意,实际必须按手册与实测调整。

2 · 存储介质与硬件基础

这一章看起来像是「硬件内容」,但它是文件系统设计的输入。跳过这一章直接抄一个文件系统跑起来,是绝大多数移植失败的起点。

2.1 NOR、NAND 与 eMMC 的本质差异

介质标称寿命10 万次擦除(数据手册值)实际单块可用次数受保留块、温度与均衡质量影响总可用擦除次数单块次数 ×可用块数每次逻辑写消耗写放大系数(通常 2 ~ 10)可支撑总写入量总擦除次数 ×擦除粒度 / 写放大换算成寿命总写入量 ÷日均写入量算例:256 Mb SPI NOR,分成 4 KB 簇的 LittleFS 区域标称寿命 10 万次/块 · 保留 4 个块做磨损均衡与元数据 · 擦除粒度 4 KB · 典型写放大 2.5每块可安全擦 10 万次,容量 32 MB 相当于 8192 个 4 KB 块,扣除保留块后约 8188 块步骤计算式结果总可用擦除次数8188 块 × 10 万次≈ 8.19 亿次块擦除折算总擦除字节数8.19 亿次 × 4 KB≈ 33.5 TB扣除写放大 2.533.5 TB ÷ 2.5≈ 13.4 TB 逻辑写入日均写入 8 MB 时13.4 TB ÷ 8 MB≈ 4.9 年日均写入 80 MB 时13.4 TB ÷ 80 MB≈ 5 个月这张表要说明的事:寿命不是介质的固定属性,而是「介质 × 文件系统 × 使用方式」三者的乘积。把写放大从 8 降到 2,等于把寿命翻两番------这通常比换一颗更大容量的芯片便宜得多。

图 4 · 从介质参数到寿命的计算链路

三类介质的差异可以归到三条根上:能否原地改写、擦除粒度多大、会不会长坏块。

特性 NOR Flash NAND Flash eMMC
可否 XIP 就地执行 可以(但速度远低于 SRAM) 不可以 不可以
单元字节数 1 字节(可位级改写) 页(512 B ~ 4 KB) 内部已封装
擦除单位 扇区 64 ~ 256 KB 块 16 ~ 256 KB 内部已封装
编程可否原地 需先擦除同块 不可,只能写空白页 内部已封装
读延迟 几十 us 几十 us us 级
坏块 出厂即有,极少增长 出厂有,擦写中还会新增 内部管理并重映射
ECC 不需要 必须(Hamming / BCH / LDPC) 内部处理
OOB 空间 无 每页有(通常 1/8 ~ 1/16) 不可见
标称寿命 10 万 ~ 100 万次擦除 1 千 ~ 10 万次擦除 按容量摊薄,TBW 指标
典型容量 512 KB ~ 512 Mb 128 Mb ~ 32 Gb 2 ~ 512 GB
常见形态 SPI NOR / QSPI / 并行 NOR SPI NAND / PPI NAND eMMC 封装
主机侧是否要 FTL 通常不要,但要日志式 FS 必须 不要

三点最容易被忽略:

  • NOR 也有坏块 ,只是出厂时就标好了、增长极慢。但 NOR 的「坏块」一般不是整块不可用,而是某些位在擦除后无法归 1。厂家会在出厂时扫描并标记,文件系统要尊重这个标记,不能随意擦除被标记的块。
  • NAND 的 OOB 空间是生命线。它不只放 ECC,还放坏块标记与 FTL 映射信息。写 OOB 时必须严格遵守「只允许把 1 写成 0」的规则,否则映射表会静默损坏。
  • eMMC 的 TBW 指标才是真正该看的参数。它直接给出「在标称负载下能写多少 TB」,比「擦除次数 × 容量」更贴近实际。但要注意 TBW 是按厂商自己的负载模型测的,若你的负载是高频小写入,寿命会明显短于 TBW 标称。

2.2 擦除粒度与编程粒度

这两个参数决定文件系统「最小分配单元」的下限,也决定了一致性原语的可用粒度。

参数 定义 对文件系统的影响
擦除粒度 / erase_size 一次擦除能清空的最小字节数 文件系统块大小不宜远小于它,否则写放大失控
编程粒度 / write_size 一次编程能写入的最大字节数 决定跨页写入时需要多少条命令、是否需要读改写
页大小 / page_size NAND 上一次读写的最小单位 块设备扇区大小通常与页对齐
OOB 大小 每页附带的元数据区 ECC 容量决定可纠错位数;FTL 映射表放这里
擦除超时 厂商保证的最大擦除时间 所有忙等都要以此为上限,超时应报错而非卡死
编程超时 厂商保证的最大编程时间 同上;FRAM 等器件此值为 0(写入即完成)

最高频的移植事故:把时间参数单位搞错

Zephyr 的 flash_parameters.erase_size 与 write_size 单位是 bit ,填 4 KB 要写 12 而不是 4096。Linux 的 SPI-NOR binding 里则是字节。填错的典型表现是:设备初始化时算出扇区数与实际容量差 8 倍(4096 被当成 4096 bit = 512 byte),文件系统挂载时报「设备太小」或读到大量全 0xFF 扇区。

另一个常见坑是「擦除时间取典型值而非最大值」。典型值 200 ms 的器件,最大值可能 2 s。忙等按典型值设超时,遇到坏块(擦除慢)就误判失败;按最大值设,正常时只是多等几轮,不影响正确性。超时一律按数据手册的最大值设定。

2.3 寿命、写入放大与磨损均衡

介质标称寿命10 万次擦除(数据手册值)实际单块可用次数受保留块、温度与均衡质量影响总可用擦除次数单块次数 ×可用块数每次逻辑写消耗写放大系数(通常 2 ~ 10)可支撑总写入量总擦除次数 ×擦除粒度 / 写放大换算成寿命总写入量 ÷日均写入量算例:256 Mb SPI NOR,分成 4 KB 簇的 LittleFS 区域标称寿命 10 万次/块 · 保留 4 个块做磨损均衡与元数据 · 擦除粒度 4 KB · 典型写放大 2.5每块可安全擦 10 万次,容量 32 MB 相当于 8192 个 4 KB 块,扣除保留块后约 8188 块步骤计算式结果总可用擦除次数8188 块 × 10 万次≈ 8.19 亿次块擦除折算总擦除字节数8.19 亿次 × 4 KB≈ 33.5 TB扣除写放大 2.533.5 TB ÷ 2.5≈ 13.4 TB 逻辑写入日均写入 8 MB 时13.4 TB ÷ 8 MB≈ 4.9 年日均写入 80 MB 时13.4 TB ÷ 80 MB≈ 5 个月这张表要说明的事:寿命不是介质的固定属性,而是「介质 × 文件系统 × 使用方式」三者的乘积。把写放大从 8 降到 2,等于把寿命翻两番------这通常比换一颗更大容量的芯片便宜得多。

图 5 · 从介质寿命到整机寿命的计算链路与算例

图 4 给出的算例说明:寿命是三者相乘的结果。介质寿命是分子,写放大是分母,使用方式是系数的来源。工程上真正能动的只有两个:降低写放大、改善使用方式。

概念 定义 典型量级
写放大 (WAW) 写进介质的数据量 ÷ 应用写入的数据量 FAT32 小文件 100 ~ 500;日志式 2 ~ 5;良好设计 1 ~ 2
擦除放大 一次逻辑写引发的擦除次数 理想 1;日志式最坏可达 100
磨损不均 某些块被擦得比其他块多得多 良好均衡下不超过均值 ±20%
TBW 厂商标称可承受的总写入量 eMMC 常见 300 TBW / 600 TBW / 1200 TBW
P/E 循环 单块的编程擦除次数 NOR 10 万次以上;消费级 TLC 约 1 千次

TLC 的「SLC 缓存耗尽陷阱」

消费级 TLC eMMC/NAND 有 SLC 缓存(缓存满了以 SLC 速度写一小块区域),缓存写满后写入速度会掉到原来的十分之一甚至更低。更严重的是:持续写入超过缓存容量后,厂商会在后台做隐式垃圾回收,这段时间内掉电可能导致数据损坏。所以 eMMC 做「大量顺序写」比「频繁随机写」更安全;关键数据(配置、凭据)不要依赖 eMMC 的隐式 GC,要有自己的双副本或日志保护。

2.4 掉电行为的物理本质

形态 1 · 整块写完成介质上是完整的旧数据或完整的新数据新数据旧数据最理想,但只在大块顺序写时出现形态 2 · 块内部分页写入前半是新数据,后半是旧数据,读出来是垃圾新数据旧数据NAND 页编程时的典型形态形态 3 · 元数据已改、数据未改文件大小变了但内容是旧的,或反之新数据旧数据最危险:文件系统自认为一致,实际不一致为什么必须由文件系统来处理· 介质只保证「块内编程是原子的」,不保证「跨块事务是原子的」------它不知道你要做什么;· FTL 能保证自己的一致性,但那只覆盖「已分配页的映射」,覆盖不了「文件系统语义」上的事务;· 只有文件系统知道「这次操作要改哪几个块、顺序是什么、哪个状态是合法的」,所以一致性责任必然落在它身上。工程结论:fsync 不是「更保险一点」,而是「保证跨块原子性」的唯一手段高频写日志的场景不 fsync,掉电后文件系统结构损坏的概率随写入次数线性上升;这个坑要到现场掉电才暴露,实验室常温常压下测不出来。

图 6 · 掉电撕裂的三种形态与责任归属

图 5 的三种形态里,第三种最危险:文件系统的元数据说「文件有 4 KB」,实际内容却是旧的或半新半旧的。程序读到「长度合法但内容错误」的数据,不会报错、静默往下走,最终以诡异的方式崩溃。

工程上对掉电的处理遵循一条原则:介质只保证「单次编程的原子性」,跨块原子性必须由文件系统用「日志」或「双写元数据」自己造出来。第 6 章与第 10 章分别讲机制与工程落地。

2.5 从介质参数反推文件系统设计

介质手册里的每一行,都对应文件系统的某个设计选择做移植前先把参数抄进参数表,再逐条对照右列------漏掉任何一条,后期都会以「莫名其妙的问题」形式暴露擦除粒度 256 KB文件块不宜过小FAT 簇 ≥ 128 KB,或改用大簇擦除时间 1.5 s阻塞式擦除不可接受配擦写队列,OS 要能睡等寿命 10 万次预留块比例上调LittleFS 保留块设为块数的 2% ~ 5%有 ECC读错误可纠正NAND 上的日志式 FS 可用无 XIP 支持代码不能就地执行XIP 分区只放数据或改用加密掉电粒度 = 块原子性可做在块级提交标记 + 元数据双写足够手册参数设计约束落地做法反过来也成立:看到文件系统的行为,可以反推它假设的介质FAT32 假设块设备能提供原子扇区写;ext4 假设块设备有可靠的中断与请求合并;LittleFS 假设擦除粒度大、擦除慢但次数多、可以整块擦;SPIFFS 假设 NOR 可位级改写且 RAM 紧张。

图 7 · 从介质参数反推文件系统设计决策

这张图建议在移植前填成一张表,逐行确认。顺序上先做介质参数表(照抄手册),再逐条确定文件系统侧的后果,两者对齐后再动手写配置。反过来------先抄一份 CONFIG_LITTLEFS=y 再去想为什么挂了------是嵌入式里最常见的顺序错误。

参数表要抄哪些项(可直接当模板)

  • 容量、页大小、擦除粒度、编程粒度、OOB 大小
  • 擦除时间典型值 / 最大值、编程时间典型值 / 最大值
  • 擦写寿命(P/E 次数)、数据保持年限(如 10 年 @ 85 °C)
  • 出厂坏块数、允许的增长上限
  • 工作电压范围、待机电流(决定低功耗策略)
  • 顺序读 / 随机读 / 页编程 / 块擦除 四类操作的实测时间

最后一项最好自己实测一遍:手册值是典型设备的表现,具体到你的板子(走线长度、去耦、时钟)会有偏差,而文件系统层的超时设置需要用实测的最大值。

3 · 分区、挂载与命名空间

分区解决「一块介质怎么切成多块用」,挂载解决「多块怎么变成一棵目录树」。这一章的内容在 PC 上很少被关心,但在嵌入式上每一处都会影响寿命与可靠性。

3.1 分区表:MBR、GPT 与固定分区

一块 8 Gb eMMC 上的典型分区布局分区不是「切块」,而是给每类数据不同的可靠性与寿命契约MBR / GPT16 KB分区表与元数据,只写一次boot8 MB内核 + initramfs,只读,可靠性最高rootfs A48 MB根文件系统,ext4 或 squashfsrootfs B48 MBOTA 的目标槽位data512 MB用户数据,可读写,写放大可接受log32 MB运行日志,环形覆盖,允许丢失cache64 MB可随时丢弃,不计入寿命预算剩下未分配留作扩容或数据分区分区划分的真正目的:让「可以丢的数据」不要占用「不能丢的数据」的寿命预算日志与 cache 每天写入几十 MB,若与 rootfs 共享 FTL 与寿命预算,会把整机寿命拖垮;分区之后,它们可以独立挂载、独立格式化、甚至独立加密与丢弃。

图 8 · 整块存储上的典型分区布局

三种分区方式并存,选哪种取决于介质类型与平台能力。

方式 原理 容量上限 表项数上限 适用场景
MBR 主引导记录在 0 扇区,含 4 个主分区 + 扩展分区链表 2 TB(实际约 2 GB 因 32 位扇区数) 4 个主分区 eMMC、小容量 SD;PC 兼容性最好
GPT 保护 MBR + 全局唯一标识 + 分区表项数组(默认 128 项) 9.4 PB(64 位扇区号) 128 项 大容量 eMMC;需 UEFI 或 hybrid 模式
固定分区 在 DTS / 设备树或 bootloader 里硬编码偏移与长度 无 自定义 SPI NOR + MCU;无 GPT 支持时的唯一选择
MTD 分区 由 mtdparts 分区表定义,解析后注册为多个 MTD 设备 受介质限制 无硬限制 NOR / NAND;Zephyr 与 U-Boot 都支持
固定分区:MCU 上的默认选择

MCU 平台没有 GPT 工具链,常见做法是把分区写进设备树:

复制代码
&flash0 {
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        boot_partition: partition@0 {
            label = "mcuboot";
            reg = <0x00000000 0x0000C000>;   /* 48 KB 引导 */
        };
        storage_partition: partition@c000 {
            label = "storage";
            reg = <0x0000C000 0x00314000>;   /* 3.1 MB 文件系统 */
        };
    };
};

分区表的三个常见错误

  • 偏移未按擦除粒度对齐:分区起点不对齐到 4 KB 或 64 KB,会导致该分区首个块无法使用,或读写耗时暴涨。
  • 大小未留对齐余量 :reg 的 size 必须是擦除粒度的整数倍,否则最后一个块跨界,访问时行为未定义。
  • 改了分区表但没动应用:应用里写死的偏移量与新分区表不一致,读到的是错误位置的数据。这类问题在 OTA 升级后集中爆发。

3.2 挂载点与挂载选项

第 1 层 · 设备节点/dev/mmcblk0p2 · 或 /dev/mtdblock1内核提供的设备入口第 2 层 · 超级块ext4 magic 0xEF53 · FAT BPB 签名文件系统在该块上的签名与几何参数第 3 层 · 挂载表/etc/fstab · 或 mount 命令「哪个设备挂到哪个目录」第 4 层 · 挂载点/data · 目录必须存在应用看到的路径排查挂载失败时的四个问题① 设备节点存在吗?② 超级块签名对吗?③ 分区号与分区表一致吗?④ 文件系统类型匹配吗?⑤ 挂载点目录存在吗?⑥ 内核编译了这个 FS 吗?常见失败与原因对照wrong fs type→ 用错 fs 名或未挂载bad superblock→ 分区偏移错或未格式化device busy→ 有进程占用,需先 umountno such file→ 挂载点目录不存在

图 9 · 挂载的四层解析链路与排查顺序

图 9 右侧的六个问题按顺序排查,九成以上的挂载失败在前四个就能定位。

挂载选项 作用 适用场景 代价
ro 只读挂载 根文件系统、boot 分区 无法写日志
noatime 不更新访问时间 SD 卡 / eMMC 常开 失去 atime 追踪
relatime 相对时间戳 通用默认 精度略低
discard 删除时立即丢弃块 SSD / eMMC 增加 GC 压力
data=ordered 日志式默认 ext4 通用 写延迟略高
data=writeback 放宽写序 追求吞吐 掉电风险上升
sync 每次写入立即落盘 关键配置分区 性能显著下降
noatime,commit=600 批量提交 日志分区 最多丢 10 分钟日志

noatime 不是可选项

默认情况下每次 read() 都会更新文件的 atime(访问时间),这意味着「只读」也会触发写操作。在 64 MB 的 NOR 分区上,一个每小时读 1 MB 配置文件、每次读都触发 atime 更新的系统,会在几个月内把那个文件所在的块擦到寿命上限。

所有嵌入式挂载都必须加 noatime。这是嵌入式 Linux 最容易被遗漏、代价又最直接的一个挂载选项。

3.3 挂载顺序与依赖关系

挂载顺序错了会表现为「偶发启动失败」------因为根文件系统所在的分区还没挂上,init 就去读它配置。

顺序 挂载点 依赖 缺失时的后果
1 /dev(devtmpfs) 无 内核无法创建设备节点,应用报 no such file
2 /proc 无 ps / top / 大量工具不可用
3 /sys 无 udev 无法枚举设备,动态库加载可能失败
4 /boot(只读) 根设备可用 内核启动参数缺失
5 /(根,只读) boot 分区挂好 init 找不到 /sbin/init
6 /data(可写) 根已挂载 应用无法写配置,表现为只读错误
7 /tmp(tmpfs) 根已挂载 大量程序无法启动
8 /run /var(tmpfs) 根已挂载 日志写入失败

在 systemd 体系下,挂载顺序由 fstab 的依赖与 unit 的 After= 声明保证;在 BusyBox initramfs 里则要手工按上表顺序写 mount。一条实用的自检规则:把上面这张表做成脚本,逐项检查挂载点是否存在且挂载类型正确,作为开机自检的第一项。

3.4 只读挂载与 rootfs 切换

  1. OverlayFS内核联合文件系统只读 lowerdir + 可写 upperdir + 工作目录 workdir。写操作只落在 upperdir,升级时只换 lowerdir。适用:最常用;需要内核 CONFIG_OVERLAY_FS;MCU 上通常没有。2. 只读挂载 + tmpfsmount -o remount,ro根以只读挂载,/tmp、/var、/run 挂 tmpfs。所有写入只落在内存,重启即丢。适用:最简单;需要足够 RAM;日志要外发到非易失区。3. 快照 / COW只读底层 + 写时复制底层是只读镜像,写时复制到另一分区。本质是 OverlayFS 的手工实现。适用:适合无 OverlayFS 但有 eMMC 的场景。为什么嵌入式特别需要只读根① 根分区只读 = 系统文件不可能被应用写坏,坏掉了重启就好;② 写放大只落在小块可写区;③ 升级只需替换只读镜像,配合 A/B 分区实现原子回滚;④ 安全启动校验的对象是只读镜像,不受运行时污染。

图 10 · 只读根与可写层的三种实现

只读根的四条收益在第 6 章还会再展开:

  • 系统文件不可被应用写坏------写坏了重启即可,不需要重刷;
  • 写放大只落在小块可写区------boot 与 rootfs 变成一次性写入;
  • 升级 = 替换只读镜像------配合 A/B 分区实现原子回滚;
  • 安全启动校验对象不被运行时污染------校验的是只读镜像,攻击者改不动。

3.5 tmpfs、ramfs 与内存文件系统

特性 tmpfs ramfs 说明
大小 可限制(size=) 不限制,吃满即 OOM 生产环境一律用 tmpfs 并限制大小
是否占 Swap 可写入 swap 不占 tmpfs 内容可换出到 swap
支持 O_DIRECT 不支持 不支持 需要直接 I/O 时应改用块设备直写
是否计入内存 计入页缓存 占连续物理内存 ramfs 的连续分配容易碎片化
掉电 丢失 丢失 关键数据绝不能只放这里
典型用途 /tmp /run /var/run 早期 initramfs 现代系统基本只用 tmpfs

tmpfs 用满的表现不是报错,而是诡异故障

tmpfs 写满时,写调用返回 ENOSPC,但更麻烦的是:某些进程(如 systemd 的 journald、udev、dbus)会因写不进去而反复重试并吃掉 CPU。典型现象是「系统越来越慢,重启就好,但过几天又复现」。

规避方法:给每个 tmpfs 明确 size= 上限;给 /var/log 单独挂 tmpfs 并设 size=16M;journald 设 SystemMaxUse=16M。这些上限之和必须小于可用 RAM 的 20%。

4 · FTL 与闪存转换层

FTL 是「块设备」与「闪存物理特性」之间的翻译器。它决定了文件系统看到的块设备有多「干净」,也决定了寿命被消耗得有多快。

4.1 FTL 要解决的核心问题

一句话:把「可随机覆盖的块」伪装成「只能顺序写的页」。具体要解决四件事。

问题 现象 FTL 的解法
不能原地覆写 写过的页不能重写 映射到新页,旧页标记为无效
不能局部擦除 擦除单位远大于写单位 把「逻辑块」映射到「空闲的擦除块」
有坏块 部分块永久不可用 坏块池 + 备用块替换 + 标记管理
断电会丢映射 映射表在 DRAM 里就全丢 映射表持久化(OOB 或双份表 + CRC)

4.2 逻辑块与物理页的映射结构

逻辑页号(文件系统看到的)连续的逻辑地址,被翻译成物理上分散的页L0L1L2L3L4L5L6L7P5P2P9P1P12P7P3P15物理页号(介质上真实的)逻辑上连续的 8 个页,在物理上散布于整个芯片------这正是「顺序写」的物理基础FTL 必须维护的三张表映射表 L2P逻辑页 → 物理页 · 每次读都要查反向表 P2L物理页 → 逻辑页 · FTL 内部使用状态表每块已用多少页 · GC 时需要映射表存在哪里,直接决定掉电会不会丢文件· 存 OOB:与数据同页掉电,不一致风险最低,但 OOB 空间紧张、不能放太多信息;· 存独立表区域:容量大,但该区域自身也要可靠性保护(通常双份 + CRC);· 存 DRAM:最快,但掉电即全丢,除非有电池或 supercap 支撑。实践建议:MCU 平台优先选 OOB 或双份表;Linux 上交给内核 MTD 层处理,不要自己造。

图 11 · FTL 的逻辑页到物理页映射与三张必备表

图 11 的上半部分是映射本身:逻辑上连续的页,在物理上可以散布在整个芯片。这正是「顺序写」能高效的前提------GC 只需要搬数据,不需要原地改。

下半部分是三张必备表。其中映射表的存放位置是掉电安全的核心,图中的三种方案各有取舍:

方案 优点 缺点 适用
OOB 区 与数据同页掉电,不一致窗口最小 空间紧张,通常只能存少量映射 主机侧 FTL、SPI NAND
独立表区域 容量大,可存更多元数据 该区自身需双份 + CRC + 原子更新 大容量 NAND 量产方案
DRAM 速度最快 掉电全丢,必须有后备(电池 / supercap) 工业级高可靠方案

eMMC 上你看不到 FTL,但它的影响一样存在

使用 eMMC 时,FTL 由芯片内部实现,应用看到的是干净的块设备。但它仍然会:做垃圾回收、消耗寿命、在 SLC 缓存耗尽时掉速、在后台 GC 期间掉电丢数据。所以 eMMC 上的开发仍然要做寿命预算(第 9 章)与关键数据保护(第 13 章),只是不能也不需要自己写 FTL。

4.3 垃圾回收的三种策略

  1. 贪心 / 立即回收写到一个已用页就立即找新页并回收旧页权衡:写延迟最稳定;回收频繁;空闲块始终充足空闲块2. 惰性 / 按需回收直到空闲块低于阈值才开始 GC权衡:写延迟有尖峰;平时几乎零开销;空闲块少时集中回收空闲块3. 预留块 / 冷热分离留出专用块,热数据不与冷数据混在同一块权衡:磨损最均衡;需要额外空间;实现最复杂空闲块三种策略的共同前提:擦除只能整块,且块内数据必须全部有效才能回收· 块内有效页比例低 → 回收收益差,这就是碎片化的代价;· 回收时要读出有效页、写到新块、校验、再擦旧块 ------ 一次回收的写放大可能是数据量的数倍;· 回收过程本身也可能掉电中断,因此 FTL 必须保证「回收未完成」与「回收已完成」两种状态都可恢复。

图 12 · 三种垃圾回收策略与共同前提

三种策略的差别本质上是**「写延迟稳定性」与「平均效率」之间的取舍** 。对嵌入式产品,评价标准通常不是平均吞吐,而是最坏情况下的写延迟------因为一次超长的 GC 停顿可能直接触发看门狗或用户可感知的卡顿。

策略 写延迟 平均效率 空闲块需求 适合
贪心 / 立即回收 最稳定 低(回收频繁) 高 硬实时要求、eMMC 常用
惰性 / 按需回收 有尖峰 中 中 通用嵌入式,默认选择
冷热分离 较稳定 高 高(需额外空间) 长期运行、日志与数据混存

4.4 坏块管理与备用块池

  1. 出厂坏块厂商在出厂时扫描并标记文件系统必须跳过,不得尝试擦除(擦除会加剧失效甚至扩成大坏块)2. 增长坏块使用中擦除失败或校验出错标记为坏块,从可用池中移除记录到坏块表(FTL 或 MTD 层)3. 备用块池预留若干块专门用于替换坏块出现时用备用顶上延迟介质失效的时间点三条必须遵守的规则① 出厂标记的坏块永远不要擦------手册会明写「do not erase」,擦它可能把局部失效扩大成整块失效;② 坏块表本身要可靠:每个记录加 CRC,掉电后能识别损坏;Linux 上用 MTD 的 bad_blocks 机制;③ 坏块增长要有告警:NAND 从 0 增长到几十个是正常的,突然上百说明存储条件(温度、电压、磨损)有问题。

图 13 · 坏块的三种处理方式与三条铁律

坏块处理的核心原则是**「宁可少一块,不能用坏块」**。一条坏块的错误处理如果不当(比如反复重试到超时才标记),会浪费大量时间并可能扩大损伤。

4.5 FTL 的一致性保证

FTL 层面的掉电一致性问题与文件系统正交但相关。掉电可能发生在 GC 的任意一步:

复制代码
/* GC 的四个阶段与掉电后的可恢复性 */
1. 选择牺牲块 V(有效页最少)
2. 把 V 中有效页复制到空闲页 P        <- 掉电:V 与 P 同时存在,重复占用空间但可恢复
3. 更新映射表,把逻辑页指向 P       <- 掉电:映射表未更新,数据仍在 V,可重做
4. 标记 V 无效,加入空闲块池       <- 掉电:V 未标记,下次 GC 会重做,不丢数据

关键:第 3 步的映射表更新必须是「原子提交」,
     否则会出现「映射指向 P,但 P 里数据没写完」的错误指针。

FTL 一致性的设计原则

  • 宁可重复,不可丢失:掉电后允许同一份数据占两个物理块,这只浪费空间;反过来「丢失」无法恢复。
  • 映射表提交要原子:用「双份表 + 序号 + CRC」或「写 OOB + ECC 保护」实现。
  • GC 全程可重入:每个阶段开始时都能判断「上次做到哪」,重做而不是从头推断。

5 · FAT 文件系统族

FAT 是嵌入式里最常见、也最被误解的文件系统。它技术上不先进,但生态兼容性压倒性优势,让它在「需要被别的设备读取」的场景里仍是首选。

5.1 FAT12/16/32 的结构与差异

保留扇区0 号扇区,BPB 就在这里1 个FAT 区文件分配表,FAT12/16/32 表项 12/16/32 位2 份 + 备用根目录区FAT32 根目录不占固定区,存放在数据区FAT12/16 固定数据区簇的集合,簇 = 若干连续扇区剩余全部以 512 MB、簇 32 KB 的 FAT32 为例保留扇区32 KB1 扇区 = 512 BFAT1约 2 MB簇数 × 4 BFAT2约 2 MB与 FAT1 完全相同(备份)根目录不存在固定区32 位簇号可指向任意簇数据区约 508 MB约 1.6 万个 32 KB 簇FAT32 与 FAT16 的根本差别不是「支持大文件」,而是「根目录不再有固定位置」FAT16 根目录固定在数据区开头且项数受限,容量上限 2 GB;FAT32 用簇号链解决,大容量才成为可能。

图 14 · FAT 文件系统的磁盘布局与 FAT32 的关键改变

维度FAT12FAT16FAT32exFAT簇数上限约 4084约 65524约 2×10⁶约 2×10⁹表项位宽12 位16 位32 位32 位最大容量约 16 MB约 2 GB约 2 TB约 16 EB单文件上限约 32 MB约 2 GB约 2 TB约 16 EB根目录固定区固定区数据区内数据区内文件名长度8.38.3255(VFAT)255卷标 / 簇链单簇链单簇链单簇链单簇链 + FAT 镜像簇数 > 65525不支持不支持必须启用必须启用嵌入式使用度很少用少量最多U 盘 / 大容量卡FAT32 与 exFAT 都要看「簇数是否超过 65525」这一个条件簇数由「容量 ÷ 簇大小」决定。512 MB 用 32 KB 簇时簇数约 1.6 万,可以;但 512 MB 若用 4 KB 簇,簇数变成 12.5 万,超过 65525 必须开 FAT32 位。很多「FAT32 无法格式化」的报错,根因就是簇选太小。

图 15 · FAT12 / FAT16 / FAT32 / exFAT 的逐项对比

图 15 里最实用的一条是最后一行与「簇数 > 65525」那一行。格式化失败报 Attempting to create a too large filesystem 或 invalid argument 时,九成是簇选太小导致簇数超限。

格式化失败的三种典型原因

  • 簇太小 :容量 ÷ 簇大小 > 65525(FAT32)。512 MB 用 4 KB 簇就会触发;解法是指定 -s 8(8 个扇区 = 4 KB)或直接 -s 64(32 KB)。
  • 分区类型不匹配 :MBR 分区类型字节应为 0x0C(FAT32 LBA)或 0x0B(FAT32 LBA32),写 0x06(FAT16)会被识别成 FAT16。
  • 容量超过 32 GB 仍用 FAT16 类型:FAT16 结构上支持不了,必然格式化失败或挂载失败。

5.2 BPB 与关键字段

BIOS Parameter Block 位于 0 号扇区,文件系统识别全靠它。写 FTL 或做底层格式化时,这些字段必须逐个对齐。

字段 偏移 长度 说明 常见错误
BytsPerSec 11 2 每扇区字节数,通常 512 填 1024 会导致容量计算全错
SecPerClus 13 1 每簇扇区数,必须是 2 的幂 填 3(非法值)
RsvdSecCnt 14 2 保留扇区数,FAT32 通常 32 填 1 在大容量下不够用
NumFATs 16 1 FAT 表个数,必须 ≥ 2 填 1 = 无备份,损坏即全丢
FATSz16 22 2 FAT16 表大小,为 0 表示用 FAT32 字段 与实际不符导致挂载错分区
TotSec32 32 4 总扇区数,FAT32 必填 填 0 会算出 0 容量
SecPerFAT32 36 4 每个 FAT 的扇区数 算小则 FAT 表被截断
FSVer 42 2 版本号,FAT32 应为 0 填非 0 会被部分 OS 拒绝

NumFATs 必须 ≥ 2,这不是可选项

FAT 表是整个卷唯一的「文件名到簇链」索引。第一份 FAT 损坏 = 卷上所有文件名与簇链信息丢失。保留第二份 FAT 的唯一目的就是备份。

部分格式化工具默认只写一份(某些嵌入式工具链为了省空间会省掉)。写入自己的格式化工具时,绝不要省这个字段------它省下的几百 KB 空间,换来的是整个卷一次性报废的风险。

5.3 目录项与短文件名

0-10文件名11属性12-21时间戳22-23起始簇24-27文件大小文件名(0-10):11 字节,8.3 格式,空格补位;长文件名用 VFAT 扩展项属性(11):只读 / 隐藏 / 系统 / 目录 / 卷标 / 归档,共 12 位有效时间戳(12-21):创建 / 访问 / 写入时间,各 4 字节(时/分/秒 + 年/月/日)起始簇(22-23):文件第一个簇的簇号,FAT32 用 4 字节文件大小(24-27):4 字节,FAT32 必用;FAT16 高 2 字节必须为 0两个必须注意的实现细节① 文件大小字段在 FAT16 中只用低 2 字节,高 2 字节必须写 0。写成随机值会让部分系统认为文件超过 2 GB 而拒绝访问。② 目录项里的「起始簇」与「文件大小」是两个独立字段,删除文件时必须一起清零(0xE5 + 大小 0),否则会出现「目录项还在但簇链已丢」的悬挂条目。FAT 在掉电场景下的三个脆弱点① FAT 表本身是唯一的数据结构,它损坏 = 整个卷的文件名与簇链全丢;② 目录项是多字节结构,写一半就是垃圾字节;③ 文件内容写了一半但大小字段已更新 → 读到「长度对、内容半新半旧」的文件,这正是第 2 章讲的形态 3。缓解办法只有一个:把 FAT 区与目录项的更新顺序设计成「先数据、后元数据」,并配合第 6 章的日志思想。

图 16 · 目录项的 32 字节结构与三个脆弱点

短文件名(8.3)的规则要记清,因为它解释了为什么很多设备生成的文件名在 PC 上显示异常:

  • 文件名最多 8 字符,扩展名最多 3 字符,大写字母;
  • 不允许空格,特殊字符如 +=,.;[]_ 会被替换或省略;
  • 不支持中文与长文件名,需要 VFAT 扩展项(LFN)配合;
  • 尾部以 0x20(空格)填充,扩展名不足 3 位也要补。

5.4 FAT32 的簇链与簇管理

文件「report.pdf」占 5 个非连续簇FAT 表记录的是「下一个簇在哪」,靠链式结构把碎片串起来簇 12内容块 1FAT12 = 3簇 3内容块 2FAT3 = 9簇 9内容块 3FAT9 = 7簇 7内容块 4FAT7 = 5簇 5内容块 5FAT5 = EOF簇大小的取舍· 太小(如 4 KB)FAT 表大、簇链长、随机访问慢,FAT32 甚至会超 65525 簇· 太大(如 128 KB)FAT 表小、顺序快,但小文件浪费严重(1 KB 文件占 128 KB)经验值:让「典型文件大小 ÷ 簇大小」落在 4 ~ 32即典型 1 MB 文件配 32 ~ 256 KB 簇簇链方式的代价:读一个文件要串行走完链,随机访问慢且存在「链断一环后面全丢」的风险ext4 用的是 extents(记录连续块区间),一次 I/O 就能定位,不用走链,所以随机访问快得多。但 FAT 的优势是「实现极其简单、PC 端全兼容」------在需要插 PC 读卡器的产品上,这一点常常压倒性能。选择建议需要在 PC 上直读(相机、录音笔、路由器刷机盘)→ FAT32;纯嵌入式自用(配置、日志、缓存)→ LittleFS 或 KV;需要 PC 直读 + 大容量 + 大文件 → exFAT(但 MCU 侧实现极少,建议配一颗专门的 SD 卡座走 USB 读卡)。

图 17 · 簇链遍历与簇大小选择

图 17 给出了选择簇大小的经验规则,这里再补一条判断:如果典型文件远小于 1 MB(比如配置文件只有几 KB),簇就应该小;如果典型文件是几 MB 的音视频,簇要大。这两条规则有时冲突,取「浪费可接受的那一侧」------因为浪费空间只是浪费,簇过大导致的 FAT 表膨胀与簇数超限是硬失败。

5.5 exFAT 与 FAT 的兼容边界

FAT 家族能互通的地方· 数据区是裸簇,无扇区级校验;· 文件名与大小存在目录项里;· 文件内容按簇链顺序排列;· 用 0xE5 标记删除的目录项。不能互通的地方· FAT 表结构完全不同(位图 + 簇堆 + FAT 镜像);· 没有 8.3 短名与 VFAT 扩展;· 不支持软盘 / 早期 OS;· 部分 Linux 内核需显式启用 exFAT 支持。嵌入式侧的现实选择容量需求 < 32 GBFAT32几乎所有平台都支持,兼容性最好需要存 > 4 GB 单文件FAT32 不行只能 exFAT 或换文件系统主机是 Linux 工控机FAT32 或 ext4exFAT 支持看内核版本MCU 侧自读写FAT32 / LittleFSMCU 侧很少实现 exFAT为什么 MCU 生态里 exFAT 几乎缺席① exFAT 专利曾长期存续,Linux 桌面直到 2019 年才内核原生支持,历史包袱影响深远;② 结构比 FAT32 复杂(位图 + 簇堆 + FAT 镜像),MCU 上代码体积与 RAM 开销明显更大;③ 需要 exFAT 的场景(大容量、大文件、U 盘)通常有主机 CPU 参与,MCU 只做数据生成。结论:除非明确要求「大于 4 GB 的单文件且要在 PC 上直读」,否则不要在 MCU 上考虑 exFAT。

图 18 · exFAT 与 FAT 的兼容边界与 MCU 侧现实

5.6 FAT 的固有缺陷与使用建议

  1. FAT 表是单点表损坏 = 文件名与簇链全丢,没有任何冗余2. 无扇区校验数据区没有 ECC 或校验和,静默位翻转无法发现3. 更新顺序脆弱目录项与数据区是两处独立写,掉电易不一致4. 32 位大小字段写坏就是 2 GB 以上的荒谬长度5. 簇浪费簇大小与典型文件大小不匹配时浪费可达 50%6. 簇链遍历随机访问需顺序走链,碎片多时性能骤降但这些缺陷在「需要 PC 直读」的场景下可以接受FAT 的核心价值不是技术先进,而是「生态兼容」------任何 PC、相机、打印机、车机都认。选型时先问「产品需不需要被别的设备读」,需要 → FAT;不需要 → 用更省寿命、更抗掉电的方案。工程补偿手段(用 FAT 时)把 FAT 区做双备份并在挂载时校验;关键配置另存一份 KV 双副本;定期 fsync;避免 4 KB 簇;日志用环形缓冲。

图 19 · FAT 的六个固有缺陷与补偿手段

把这一节浓缩成一句选型结论:

FAT 的选型判据

先问一个问题:这个分区将来需不需要被别的设备读取?

需要(相机存储、路由器刷机盘、给 PC 读的配置盘)→ FAT32,簇按第 5.4 节规则选,务必双 FAT、noatime、关键数据双副本。

不需要(纯设备自用的配置、日志、缓存)→ 不要用 FAT。用 LittleFS(NOR)或 KV 存储(NVS/ZMS),寿命与掉电表现都好得多,代码体积还更小。

需要大文件与大容量又要 PC 直读 → 优先考虑「MCU 只写 FAT32 内的单个文件,PC 侧负责拼接」或「走 USB MSC + 主机侧格式化」,避免 MCU 实现 exFAT。

后续章节请下载「芯参谋」查看

......(篇幅原因, 还有20章· 详见芯参谋→方案设计)

打开路径:🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> 嵌入式文件系统_开发指南

📥 下载芯参谋,免费查看完整报告

网址:www.myxincm.com

相关报告 · 1

相关报告 · 2

相关推荐
@兽血沸腾1 小时前
Windows系统安装Jenkins部署应用至远程Windows服务器操作指南
运维·服务器·jenkins
且从容.4 小时前
Ubuntu安装flume
linux·ubuntu·flume
承渊政道4 小时前
Linux网络学习【Reactor反应堆模式】
linux·网络·学习·ubuntu·reactor
讯捷蓝达6 小时前
戴尔R720xd服务器卡死在 “Configuring Memory...Done“,换了内存还是卡?问题根本不在内存
运维·服务器
且从容.7 小时前
ubuntu安装flink
linux·ubuntu·flink
数字新视界9 小时前
动环监控信创适配在智能机房管理中的应用价值与未来展望
大数据·服务器·数据库·动环监控系统·动力与环境监控系统
爱和冰阔落9 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨9 小时前
WSL2的安装与使用
linux·wsl
微小的鱼9 小时前
Linux的渲染与显示系统链路
android·java·linux