嵌入式文件系统开发指南
编制日期 2026-10-05 | 版本号 Rev 1.4
面向 NOR / NAND / eMMC 与 MCU、MPU 平台的存储软件栈 · 覆盖分区挂载、FTL、FAT 与日志式文件系统、LittleFS 与 KV 持久化、掉电一致性、寿命预算、移植与验收 · 内核态与用户态双视角
一句话结论:嵌入式文件系统的难点从来不是「把文件存进去」,而是在介质物理特性受限的前提下,保证掉电后数据仍可恢复、把擦写次数花在真正需要的地方。NOR 的扇区大、擦除慢、寿命十万次量级,NAND 的页小、擦除快、需要坏块与 ECC,eMMC 有内部 FTL 与磨损均衡——三套介质特性直接决定了三套不同的文件系统设计。选错抽象层,写放大能从 1 倍放大到 10 倍以上;漏掉掉电原子性,一次掉电就是一批配置全丢。
这份文档解决什么
本文是文件系统层的专项指南:从介质物理特性出发,讲清分区挂载、FTL 映射与垃圾回收、FAT 族结构、日志式一致性模型、LittleFS 与 SPIFFS 的取舍、键值存储布局、磨损均衡与寿命预算、掉电原子性、加密存储,最后给出一套可执行的移植流程与验收规范。 存储器件的电气设计(时序、电压、封装、信号完整性)由各器件的电路设计指南承接;内核设备树、内核态驱动开发由《Linux 内核开发指南(BSP Kernel)》承接;具体 Flash 芯片的驱动实现由《SPI NOR / SPI NAND / PPI NAND / eMMC 驱动开发指南》系列承接。本文站在它们之上,只讲文件系统。
读者路径
路线 1(移植一个新器件):第 2.5 节从介质参数反推设计 → 第 3.1 节分区 → 第 7.3 节三个回调 → 第 12.2 节移植前十个问题 → 附录 A.1 流程十步。
路线 2(排查线上问题):第 18 章故障手册 → 第 10.3 节原子重命名 → 第 8.3 节 NVS 布局 → 第 16.2 节调试命令。
路线 3(做容量与寿命规划):第 2.3 节寿命与写放大 → 第 9.1 节寿命预算算法 → 第 11.3 节小 IO 差异 → 第 13.3 节预算平衡。
路线 4(做选型):第 1.2 节五层抽象 → 第 5.6 节 FAT 固有缺陷 → 第 6.1 节日志思想 → 第 7.2 节 LittleFS 取舍 → 附录 B.2 对比表。
版本基线与引用约定
- 文件系统以 Linux 6.1 / 5.15 LTS 与 LittleFS v2.9 / v3.x、SPIFFS 3.0 为基线;
- 介质参数以各厂商数据手册为唯一依据,本文数值仅为示例量级,实际设计必须回到手册;
- 命令与路径在 MCU 上为
/mnt、/data等挂载点,在 Linux 上为/dev/mmcblk0p1等块设备,两套都给出; - 寄存器级时序参数见《SPI NOR / eMMC 电路设计指南》,本文只讲它们对文件系统软件栈的影响。
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、块设备、介质
上图是选型的起点。下面逐层说明每层负责什么、边界在哪里。
第 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 层:物理介质
这三条规则是整篇文档的地基。请记住它们的实际后果:
- 「擦除只能整块」→ 文件系统的最小分配单元不能小于擦除粒度,否则每次写都要擦一个大盘;
- 「编程有顺序性」→ 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 与系统篇) |
2 · 存储介质与硬件基础
这一章看起来像是「硬件内容」,但它是文件系统设计的输入。跳过这一章直接抄一个文件系统跑起来,是绝大多数移植失败的起点。
2.1 NOR、NAND 与 eMMC 的本质差异
三类介质的差异可以归到三条根上:能否原地改写、擦除粒度多大、会不会长坏块。
| 特性 | 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 寿命、写入放大与磨损均衡
图 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 eMMC/NAND 有 SLC 缓存(缓存满了以 SLC 速度写一小块区域),缓存写满后写入速度会掉到原来的十分之一甚至更低。更严重的是:持续写入超过缓存容量后,厂商会在后台做隐式垃圾回收,这段时间内掉电可能导致数据损坏。所以 eMMC 做「大量顺序写」比「频繁随机写」更安全;关键数据(配置、凭据)不要依赖 eMMC 的隐式 GC,要有自己的双副本或日志保护。
2.4 掉电行为的物理本质
图 5 的三种形态里,第三种最危险:文件系统的元数据说「文件有 4 KB」,实际内容却是旧的或半新半旧的。程序读到「长度合法但内容错误」的数据,不会报错、静默往下走,最终以诡异的方式崩溃。
工程上对掉电的处理遵循一条原则:介质只保证「单次编程的原子性」,跨块原子性必须由文件系统用「日志」或「双写元数据」自己造出来。第 6 章与第 10 章分别讲机制与工程落地。
2.5 从介质参数反推文件系统设计
这张图建议在移植前填成一张表,逐行确认。顺序上先做介质参数表(照抄手册),再逐条确定文件系统侧的后果,两者对齐后再动手写配置。反过来——先抄一份 CONFIG_LITTLEFS=y 再去想为什么挂了——是嵌入式里最常见的顺序错误。
- 容量、页大小、擦除粒度、编程粒度、OOB 大小
- 擦除时间典型值 / 最大值、编程时间典型值 / 最大值
- 擦写寿命(P/E 次数)、数据保持年限(如 10 年 @ 85 °C)
- 出厂坏块数、允许的增长上限
- 工作电压范围、待机电流(决定低功耗策略)
- 顺序读 / 随机读 / 页编程 / 块擦除 四类操作的实测时间
最后一项最好自己实测一遍:手册值是典型设备的表现,具体到你的板子(走线长度、去耦、时钟)会有偏差,而文件系统层的超时设置需要用实测的最大值。
3 · 分区、挂载与命名空间
分区解决「一块介质怎么切成多块用」,挂载解决「多块怎么变成一棵目录树」。这一章的内容在 PC 上很少被关心,但在嵌入式上每一处都会影响寿命与可靠性。
3.1 分区表:MBR、GPT 与固定分区
三种分区方式并存,选哪种取决于介质类型与平台能力。
| 方式 | 原理 | 容量上限 | 表项数上限 | 适用场景 |
|---|---|---|---|---|
| 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 挂载点与挂载选项
图 9 右侧的六个问题按顺序排查,九成以上的挂载失败在前四个就能定位。
| 挂载选项 | 作用 | 适用场景 | 代价 |
|---|---|---|---|
| ro | 只读挂载 | 根文件系统、boot 分区 | 无法写日志 |
| noatime | 不更新访问时间 | SD 卡 / eMMC 常开 | 失去 atime 追踪 |
| relatime | 相对时间戳 | 通用默认 | 精度略低 |
| discard | 删除时立即丢弃块 | SSD / eMMC | 增加 GC 压力 |
| data=ordered | 日志式默认 | ext4 通用 | 写延迟略高 |
| data=writeback | 放宽写序 | 追求吞吐 | 掉电风险上升 |
| sync | 每次写入立即落盘 | 关键配置分区 | 性能显著下降 |
| noatime,commit=600 | 批量提交 | 日志分区 | 最多丢 10 分钟日志 |
默认情况下每次 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 切换
只读根的四条收益在第 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 写满时,写调用返回 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 逻辑块与物理页的映射结构
图 11 的上半部分是映射本身:逻辑上连续的页,在物理上可以散布在整个芯片。这正是「顺序写」能高效的前提——GC 只需要搬数据,不需要原地改。
下半部分是三张必备表。其中映射表的存放位置是掉电安全的核心,图中的三种方案各有取舍:
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| OOB 区 | 与数据同页掉电,不一致窗口最小 | 空间紧张,通常只能存少量映射 | 主机侧 FTL、SPI NAND |
| 独立表区域 | 容量大,可存更多元数据 | 该区自身需双份 + CRC + 原子更新 | 大容量 NAND 量产方案 |
| DRAM | 速度最快 | 掉电全丢,必须有后备(电池 / supercap) | 工业级高可靠方案 |
使用 eMMC 时,FTL 由芯片内部实现,应用看到的是干净的块设备。但它仍然会:做垃圾回收、消耗寿命、在 SLC 缓存耗尽时掉速、在后台 GC 期间掉电丢数据。所以 eMMC 上的开发仍然要做寿命预算(第 9 章)与关键数据保护(第 13 章),只是不能也不需要自己写 FTL。
4.3 垃圾回收的三种策略
三种策略的差别本质上是「写延迟稳定性」与「平均效率」之间的取舍。对嵌入式产品,评价标准通常不是平均吞吐,而是最坏情况下的写延迟——因为一次超长的 GC 停顿可能直接触发看门狗或用户可感知的卡顿。
| 策略 | 写延迟 | 平均效率 | 空闲块需求 | 适合 |
|---|---|---|---|---|
| 贪心 / 立即回收 | 最稳定 | 低(回收频繁) | 高 | 硬实时要求、eMMC 常用 |
| 惰性 / 按需回收 | 有尖峰 | 中 | 中 | 通用嵌入式,默认选择 |
| 冷热分离 | 较稳定 | 高 | 高(需额外空间) | 长期运行、日志与数据混存 |
4.4 坏块管理与备用块池
坏块处理的核心原则是「宁可少一块,不能用坏块」。一条坏块的错误处理如果不当(比如反复重试到超时才标记),会浪费大量时间并可能扩大损伤。
4.5 FTL 的一致性保证
FTL 层面的掉电一致性问题与文件系统正交但相关。掉电可能发生在 GC 的任意一步:
/* GC 的四个阶段与掉电后的可恢复性 */
1. 选择牺牲块 V(有效页最少)
2. 把 V 中有效页复制到空闲页 P <- 掉电:V 与 P 同时存在,重复占用空间但可恢复
3. 更新映射表,把逻辑页指向 P <- 掉电:映射表未更新,数据仍在 V,可重做
4. 标记 V 无效,加入空闲块池 <- 掉电:V 未标记,下次 GC 会重做,不丢数据
关键:第 3 步的映射表更新必须是「原子提交」,
否则会出现「映射指向 P,但 P 里数据没写完」的错误指针。
- 宁可重复,不可丢失:掉电后允许同一份数据占两个物理块,这只浪费空间;反过来「丢失」无法恢复。
- 映射表提交要原子:用「双份表 + 序号 + CRC」或「写 OOB + ECC 保护」实现。
- GC 全程可重入:每个阶段开始时都能判断「上次做到哪」,重做而不是从头推断。