FTL 映射与写放大
编制日期 2026-10-14 | 版本号 Rev 1.0
存储技术探讨 · 第三篇(控制器层) | 前两篇讲颗粒:NAND 存储单元与 3D NAND、NAND 干扰机制与数据保持;本篇讲控制器怎么把块设备接口翻译到 NAND 的物理约束上,以及写放大是怎么产生、怎么量化、怎么压下去的。
面向 XTX(芯天下)SD NAND / SPI NAND / eMMC 产品线的从业人员,回答五个问题:FTL 到底翻译了什么;映射粒度怎么选、表要多少内存;一次写到底在盘里发生了几件事;写放大 WA 怎么算、跟 OP 和 TRIM 是什么定量关系;以及从 WA 怎么一路算到 TBW 与 DWPD。全文公式均可代入数字手算,不依赖任何厂商私有参数。
写放大不是 NAND 的缺陷,而是「异地更新」加「按块擦除」两条规则叠加后的必然结果:主机写 1 页,盘里很可能要写 1 页新数据 + 若干页 GC 搬移。FTL 做的全部事情——映射、GC、磨损均衡、OP、TRIM、SLC Cache——本质上都是在给这个必然结果买单,或者把它摊薄。压 WA 的唯一总纲是:让盘里少产生无效数据,让产生的无效数据尽快被主机声明为无效。
| 系列篇目 | 关注层级 | 核心问题 | 本篇的衔接点 |
|---|---|---|---|
| 第一篇 · NAND 存储单元与 3D NAND | 晶体管与阵列 | 数据存在哪、怎么存进去、能存多久 | 给出页 / 块 / P/E 这些物理常量 |
| 第二篇 · NAND 干扰机制与数据保持 | 单元间干扰 | 读干扰、写干扰、保持期漏电怎么影响可靠性 | 解释 FTL 为何要额外做回收与刷新 |
| 第三篇 · FTL 映射与写放大(本篇) | 控制器固件 | 块设备语义怎么落到物理约束上,代价如何量化 | 把前两篇的物理常量换算成寿命与性能 |
1. FTL 是什么:块设备语义与 NAND 物理约束之间的翻译层
主机操作系统手里只有一套「块设备」的抽象:一块可以按扇区随机读写、可以反复覆盖、看起来无限寿命的线性空间。NAND 颗粒的实际行为完全不是这样。中间那层把前者翻译成后者的软件(或固件),就是 FTL(Flash Translation Layer)。本章先把两侧的语义摆在一起对照,看清冲突到底在哪。
- 冲突一:主机要覆盖写,NAND 只能换页写,旧页留下变垃圾。
- 冲突二:主机按扇区释放空间,NAND 必须整块擦除才释放。
- 冲突三:主机默认寿命无限,NAND 的擦写次数有限且不可恢复。
后面所有关于 GC、写放大、OP、TRIM 的讨论,都是从这三处冲突推出来的。理解 FTL 的捷径不是记住它的模块划分,而是记住这三个它必须补上的窟窿。
1.1 主机看到的盘:可覆盖写、按扇区寻址
主机侧的接口从几十年前的机械硬盘继承而来,语义非常朴素:整个盘是 0 ~ N-1 号逻辑块(LBA)组成的线性数组,读第 n 块就是把第 n 块的内容取出来,写第 n 块就是把第 n 块的内容换掉。文件系统(ext4、FAT、NTFS、F2FS)建立在这套语义之上,它默认三件事:
- 可以原地覆盖:改文件里一个字节,就直接把那个扇区重写一遍,不需要先擦除,也不影响别的扇区。
- 最小单位是扇区:读写粒度是 512 B 或 4 KB,跟「块」「页」这些物理概念没有任何约定关系。
- 删除只是改元数据:删掉一个文件,文件系统把它的块标记为空闲,并不要求存储设备立刻做什么动作,更不会通知设备「这几块以后不用了」。
第三点是后面所有麻烦的伏笔:文件系统知道哪些逻辑块已经没用了,但它默认不会告诉存储设备。设备这边只能看到「主机又写了这个 LBA」,根本无从判断上一次写的那份数据是不是已经作废。
1.2 NAND 真实的样子:按页写、按块擦、有寿命
NAND 这边是另一套规则,而且是硬约束,没有商量余地:
- 写以页为单位:一次编程的最小单位是一页,典型值 4 KB 到 16 KB(大容量 TLC 器件常见 16 KB)。写之前该页必须是已擦除状态。
- 擦以块为单位:擦除的最小单位是一个块,典型值由数百页组成(例如 256 页到 1024 页,对应几 MB 到十几 MB)。不能只擦一页。
- 不能原地改写:一个页写完之后,想改里面的内容,唯一办法是把整个块擦掉再写。这就是「异地更新(out-of-place update)」的物理根源。
- 擦写次数有限:每个块有 P/E 次数上限,SLC 量级数万次,MLC 数千到一万,TLC 数百到数千,QLC 更低;超过之后误码率快速上升,块被判废。
- 读本身也有副作用:反复读同一个块会干扰同块其它页(读干扰),需要计数并在阈值时把数据搬走重写。
把这些约束和主机语义并排放,冲突一目了然:主机想「改一个字节」,NAND 只能「换个地方写一整页,旧页留着,等攒够一整块的垃圾再一起擦」。
顺带解释一个常被追问的问题:擦除块为什么不能做小?因为擦除是靠在衬底与阱之间加高压、把整个块的浮栅(或电荷俘获层)里的电子一次性拉出来。块的物理边界由共享的阱与源线结构决定,做小意味着每个块都要配套一套高压选择管,面积开销会急剧上升。所以块做得大是电路结构权衡的结果,也正因如此,擦除一次的耗时在毫秒量级,远大于编程一页的几百微秒——这中间的差距,就是第 5 章里前台 GC 造成写延迟尖峰的物理根源。
| 对照项 | 主机 / 文件系统侧 | NAND 颗粒侧 | FTL 必须补上的动作 |
|---|---|---|---|
| 写入粒度 | 512 B / 4 KB 扇区 | 4 KB ~ 16 KB 一页(典型值) | 小写入要做读改写合并或缓存对齐 |
| 改写方式 | 原地覆盖同一 LBA | 不能原地改,必须换页写 | 维护 LBA 到物理页的映射表 |
| 空间释放 | 删除文件只改元数据 | 必须整块擦除才释放 | 垃圾回收:搬有效页 + 整块擦 |
| 寿命 | 默认无限次 | P/E 次数有限(典型值千级) | 磨损均衡,把擦写摊平到全盘 |
| 坏块 | 接口里没有坏块概念 | 出厂即有坏块,使用中新增 | 坏块表 + 备用块替换 |
| 读取副作用 | 读不改变数据 | 读干扰累积,需刷新 | 读计数触发回收与重写 |
1.3 翻译层的落点:谁来实现 FTL
「FTL 在哪」这个问题在不同产品形态上答案完全不同,做选型时这是第一个要问清楚的点,因为它直接决定主机端要承担多少工作。
| 产品形态 | FTL 落点 | 主机看到的接口 | 主机端负担 | 对 XTX 产品线的含义 |
|---|---|---|---|---|
| SD NAND / eMMC / UFS | 封装内控制器固件自带 | 块设备(SD / eMMC / UFS 协议) | 只管读写,无需感知 NAND | FTL 质量直接决定产品竞争力 |
| SPI NAND / Raw NAND | 无内置 FTL,需主机实现 | 原始命令(页读 / 页编程 / 块擦) | 自行实现映射、GC、均衡、坏块 | 要配套提供参考 FTL 或评估板 |
| SSD(SATA / NVMe) | 盘内主控固件 + DRAM | 块设备(ATA / NVMe) | 通过 TRIM 等命令配合 | 与 eMMC 同一套原理,规模更大 |
| UFS / NVMe 的 FTL 增强 | 主控 + Host Performance Booster | 块设备 + 主机可借出内存 | 可选借出 DRAM 给盘用 | 小容量方案的低成本路径 |
对 SD NAND 与 eMMC 这类自带控制器的产品,主机工程师往往感觉不到 FTL 的存在,但盘的行为——写入掉速、寿命长短、掉电后数据是否完整——几乎全部由 FTL 决定。反过来,SPI NAND 这类裸颗粒方案把 FTL 完全交给主机,好处是透明可控、成本更低,代价是软件工作量与验证工作量都要自己承担。
| 要问的问题 | 为什么重要 | 典型答案形态 | 问不到答案时的对策 |
|---|---|---|---|
| FTL 在盘内还是在主机侧 | 决定主机端工作量与可控性 | 内置固件 / 需主机实现 | 裸颗粒方案要预留 FTL 开发工时 |
| 是否支持 TRIM / Discard | 直接影响稳态写放大 | 支持 / 不支持 / 部分支持 | 不支持就靠分区留白补 |
| OP 比例是多少、是否可调 | 决定 WA 上界与寿命基数 | 出厂固定 / 命令可调 | 按标称与裸容量的关系反推 |
| 稳态写带宽怎么标 | 决定长时间写入的性能下限 | 稳态值 / 峰值 / 未标 | 实测:写满全盘取后段平台值 |
| 掉电保护策略是什么 | 决定元数据与数据的完整性 | journal / 电容 / 无保护 | 无保护时上层必须做事务保护 |
这张表里的五个问题,建议在选型评审的第一轮就全部问完。它们共同决定了后面所有性能与寿命讨论的边界:FTL 在主机侧意味着写放大可以被自己控制,也意味着所有代价都要自己承担;FTL 在盘内则相反,可控性换成了集成度。
FTL 的存在意味着「主机看到的容量」与「颗粒的实际容量」从来就不相等,也意味着「主机写了多少」与「颗粒写了多少」从来就不相等。前者差的那部分叫 OP 预留空间,后者差的倍数叫写放大 WA。后面第 6、7 章会分别把这两个量算清楚。
2. FTL 的核心职责与支撑模块
FTL 的工作可以拆成三大核心职责加三个支撑模块。三大职责——地址映射、垃圾回收、磨损均衡——直接决定盘的性能与写放大;三个支撑模块——坏块管理、ECC 与读重试、读干扰与数据保持——不直接参与地址翻译,但决定这块盘能不能长期稳定工作。
- 地址映射决定「写到哪」:它是 FTL 的主索引,也是一切灵活性的来源。
- 垃圾回收决定「什么时候要为写入付出额外代价」:它是写放大的直接来源。
- 磨损均衡决定「代价摊在哪些块上」:它决定全盘是同时老化还是局部先写穿。
三个支撑模块(坏块管理、ECC 与读重试、读干扰与保持)虽然不参与地址翻译,但它们的动作几乎都表现为额外的 NAND 写入,做写放大预算时不能漏掉。
2.1 地址映射:LBA 到 PPN 的翻译
映射表是 FTL 的主索引结构。正向表的输入是逻辑页号 LPN(由 LBA 换算而来),输出是物理页号 PPN;反向表反过来,从 PPN 查回 LPN,主要用于 GC 搬移时找到「这个物理页属于哪个逻辑页」,以便更新映射。
| 概念 | 含义 | 谁来维护 | 典型规模 | 备注 |
|---|---|---|---|---|
| LBA | 主机下发的逻辑块地址 | 主机 | 按容量 / 512 B 或 4 KB | 文件系统视角的地址 |
| LPN | 逻辑页号,映射表的行号 | FTL | 裸容量 / 页大小 | LBA 按页大小换算得到 |
| PPN | 物理页号,含通道/芯片/块/页 | FTL | 同 LPN 量级 | 写入时才最终确定 |
| 正向表 | LPN → PPN | FTL | 常驻 RAM + NAND 备份 | 查询路径,掉电要能恢复 |
| 反向表 | PPN → LPN | FTL | 可放 NAND,部分常驻 | GC 搬移时更新映射用 |
| 有效位图 | 每块每页是否有效 | FTL | 页数 × 1 bit | GC 选块与搬移的依据 |
映射表必须在掉电后能重建,所以它需要周期性地写回 NAND(checkpoint / journal),这本身就产生额外写入——只是这部分通常计入固件元数据写,在 WA 统计里可以单独列出来。
2.2 垃圾回收:把失效页变成可用块
一个块里只要还有一页是有效数据,就不能整块擦除。GC 的做法是:挑一个「垃圾比例最高」的块,把里面残余的有效页读出来,写到别的空闲块里,更新映射表,然后把原块整块擦除,还回空闲池。搬走的那些有效页,对主机而言没有任何意义——主机从未要求写它们——但它们实实在在消耗了 NAND 的写入带宽和 P/E 预算。
| GC 触发方式 | 触发条件 | 时机 | 优点 | 代价 |
|---|---|---|---|---|
| 前台 GC(被动) | 写入时找不到空闲块 | 写路径上同步执行 | 实现简单,保证有块可写 | 主机可见的写延迟尖峰 |
| 后台 GC(主动) | 空闲块数低于水位线 | 主机空闲时异步执行 | 延迟平滑,用户无感 | 需要空闲窗口,忙时补不上 |
| 强制 GC(磨损均衡) | 某些块被冷数据长期占用 | 定期触发 | 把冷数据腾到热块 | 额外搬移,抬高 WA |
| 读干扰回收 | 块的读计数超阈值 | 读路径上统计后触发 | 抑制读干扰导致的误码 | 同样是额外写 |
前台 GC 是「写入掉速」的直接原因:主机写请求到达时盘里已经没有现成的干净块,必须先现场完成一次搬移 + 擦除才能落盘,延迟立刻从几百微秒涨到几毫秒甚至更高。
GC 的另一个关键设计点是「选哪个块」。最朴素的做法是 greedy——挑垃圾最多的块,直觉上搬移量最小。但 greedy 有个已知缺陷:它会反复回收那些「写入密集但数据很快失效」的块,而把装着冷数据的高擦写次数块一直留着,长期看反而加速磨损不均。改进做法是成本收益策略(cost-benefit):把块的「年龄」也计入成本,收益用「可释放空间 × 块已放置时间」来衡量,于是装着冷数据的老块即使垃圾比例不是最高,也可能被优先回收,从而把冷数据腾到别处、让磨损自然均衡。
2.3 磨损均衡:把擦写次数摊平
磨损均衡要解决的是「热点块先写穿」的问题。它分两类:
- 动态磨损均衡:每次分配新块时,优先挑擦写次数少的块。这只解决了「新写到哪」,不解决「老数据占着不动」。
- 静态磨损均衡:定期把长期不更新的冷数据从低擦写次数的块搬到高擦写次数的块,把低次数块腾出来给热数据用。这一步是纯额外写,会抬高 WA,但不做的话全盘寿命会被冷数据「锁死」。
工程上通常设一个阈值:当最大擦写次数与最小擦写次数之差超过某个值(典型值为几百到几千次,取决于器件 P/E 规格)时才启动静态均衡,避免过于频繁地搬冷数据。
| 类型 | 作用对象 | 触发条件 | 是否产生额外写 | 对 WA 的影响 |
|---|---|---|---|---|
| 动态磨损均衡 | 新分配的写入块 | 每次需要空闲块时 | 否(只改变分配选择) | 几乎为零 |
| 静态磨损均衡 | 长期不动的冷数据块 | 擦写次数差超阈值 | 是(搬冷数据) | 抬高 WA,幅度取决于触发频率 |
| 全局均衡 | 全盘所有块 | 定期或按差值触发 | 是 | 与静态均衡同量级 |
| 分区内均衡 | 按区域分别均衡 | 区域内差值超阈值 | 是 | 比全局略低 |
一个实用的判断方法:如果业务里存在大片写入后基本不更新的数据(固件镜像、只读资源、历史归档),那么静态磨损均衡就是必须的,而且这部分额外写要在 WA 预算里单独留出来。反过来,如果数据几乎全部是热数据、全盘都在持续更新,静态均衡的触发频率会很低,它对 WA 的贡献可以忽略。
2.4 三个支撑模块:坏块管理、ECC、读干扰
| 支撑模块 | 要解决的问题 | 主要手段 | 失效后果 | 与 WA 的关系 |
|---|---|---|---|---|
| 坏块管理 | 出厂坏块 + 使用中新增坏块 | 坏块表 + 备用块池替换 | 容量损失,严重时盘变只读 | 替换本身不产生写放大 |
| ECC 与读重试 | 原始误码率随 P/E 与保持时间上升 | BCH / LDPC 纠错 + 参考电压扫描 | 不可纠错误码(UNC)导致丢数据 | 读重试不写,但触发的回收要写 |
| 读干扰与保持 | 读干扰累积、保持期电荷流失 | 读计数触发回收、定期扫描刷新 | 静默数据错误 | 刷新是额外写,计入 WA |
表列之外还有第四个支撑模块——掉电保护:要解决的是掉电时映射表与缓存数据丢失,主要手段是元数据 journal 加掉电电容或安全写,失效后果是映射表损坏、整盘不可读,其 journal 写同样是额外写。
这四个模块加起来的共同特点是:它们的动作几乎都表现为「额外的 NAND 写入」。所以在做写放大预算时,不能只算 GC 的搬移,还要把静态均衡、读干扰刷新、元数据 checkpoint 这些一并计入,尤其是在小容量、低 OP 的嵌入式方案里,这几项的占比往往比想象中高。
3. 映射粒度:块级、页级与混合
映射表按什么粒度建,是 FTL 设计里最根本的一次取舍:粒度越粗,表越小、越省内存,但小写入越吃亏;粒度越细,随机写越灵活,代价是表项数量爆炸。本章给出三种粒度的原理、代价,以及一个可以直接套用的 RAM 开销算式。
- 块级映射:表小到可以放 SRAM,但一次小写入可能触发整块搬移。
- 页级映射:写入量与主机请求严格 1:1,代价是表要几百 MB 量级。
- 混合映射:主体按块、随机写走日志块,是嵌入式器件的主流折中。
这三者没有优劣,只有匹配与否:先算出表大小,再看控制器有多少 RAM,答案通常就唯一了。
3.1 块级映射:省表项,怕小写入
块级映射的表项是「逻辑块 → 物理块」,块内页偏移保持不变。表项数量等于块数,非常小:1 TB 裸容量按 16 MB 一块算,只有约 6.5 万项,4 B 一项也才 0.26 MB,完全可以放进控制器的 SRAM。
块级映射还有第二个好处:表的结构稳定、掉电重建快,因为表项少、变更频率低,元数据写也少。对 MCU 直接驱动 SPI NAND 这类资源紧张的方案,这两点往往比随机写性能更重要。
问题在于小写入。假设主机只改某个逻辑块里的 1 个页:由于页偏移固定,这个页不能直接写到新块的任意位置,必须把整个逻辑块的其它页也一起搬过去(读改写),然后擦掉旧块。一次 4 KB 的随机写,实际可能触发几 MB 的搬移,写放大直接飙到几十甚至上百倍。所以块级映射只适合写入模式高度顺序、或者容量很小的场景。
3.2 页级映射:最灵活,表也最大
页级映射的表项是「逻辑页 → 物理页」,任意逻辑页可以落到任意物理页。主机改一个页,就只写一个页,旧页标记失效——写入量与主机请求量严格 1:1,GC 之前的 WA 就是 1。这是大容量 SSD 的标准做法,也是第 6 章推导顺序写 WA约等于1 的前提。
代价是表项数等于页数。1 TB 裸容量、16 KB 页时约 6711 万项,4 B 一项需要 268 MB;如果页是 4 KB,表直接涨到 1 GB 量级。这也就是为什么大容量 SSD 必须配 DRAM,以及为什么「DRAM-less」方案要改用混合映射或把表放在主机内存(HMB)。
| 落地形态 | 映射表放在哪 | 查表延迟 | 容量上限(典型值) | 代价与风险 |
|---|---|---|---|---|
| 带 DRAM | 盘上 DRAM,全表常驻 | 纳秒级,一次访问 | 可做到数 TB | BOM 成本与掉电保护成本 |
| DRAM-less | 部分表在 SRAM,其余在 NAND 换入换出 | 换入时微秒到毫秒级 | 通常 1 TB 以内 | 随机读延迟抖动,掉电重建复杂 |
| HMB(借用主机内存) | 主机划出一段 DRAM 给盘用 | 接近带 DRAM 方案 | 取决于主机借出多少 | 依赖主机驱动支持,掉电即失 |
| 混合映射 | 块表常驻 + 日志区表常驻 | 低,日志区命中即返回 | 覆盖多数嵌入式容量段 | 合并操作会带来额外写 |
对 eMMC 与 SD NAND 这类成本敏感的器件,主流选择是最后两种:要么把表压缩到片内 SRAM 能装下的规模(靠混合映射或粗粒度表项),要么向主机借内存。选择哪种,取决于主机能否提供稳定的内存资源,以及产品对掉电重建时间的要求。
3.3 混合映射:log-block 与 superblock
混合映射的思路是「主体粗、局部细」:数据区按块级映射建表,另外划出一小部分块作为日志块(log block),按页级映射吸收随机写;日志块写满后再合并(merge)回数据块。合并分几种情形:
- 全顺序合并(switch merge):日志块刚好被顺序写满,与数据块内容一一对应,直接把日志块变成新的数据块,旧块整块擦除,一次搬移都不用。
- 部分合并(partial merge):日志块只有一部分页有效,需要搬走有效页再擦,产生搬移写。
- 完全合并(full merge):日志块和数据块的有效页要合并到新块,搬移量最大,写放大也最高。
三种合并的代价差别很大,工程上的目标就是尽量让合并落在第一类:
- switch merge(最优):日志块恰好被顺序写满,与数据块一一对应,直接互换角色,搬移量为零,只消耗一次擦除。
- partial merge(中等):日志块只有一部分页有效,且位置恰好对齐,只需搬这一部分,再擦原块。
- full merge(最差):日志块与数据块的有效页要一起搬到新块,搬移量等于两者的有效页之和,还要擦两个块。
这解释了为什么「顺序化写入」对混合映射方案的收益特别大:只要主机写入是顺序的,日志块就能被顺序写满,合并几乎总是落在第一类,写放大直接回到 1 附近;反之随机写会让 full merge 频繁出现,写放大迅速恶化。
superblock 是另一种常见做法:把多个物理块绑成一个逻辑大块统一管理(便于多通道并行与磨损均衡),内部仍按页映射。它在现代大容量 TLC / QLC 器件上几乎是标配,因为单块的页太多,一次性擦除与搬运的粒度过大。
| 映射粒度 | 表项数(1 TB,16 KB 页 / 16 MB 块) | 表大小(4 B/项) | 随机写表现 | 典型适用 |
|---|---|---|---|---|
| 块级 | 约 6.5 万 | 约 0.26 MB | 差,小写入触发整块搬移 | 小容量、写入高度顺序 |
| 页级 | 约 6711 万 | 约 268 MB | 好,写入量与请求量 1:1 | 大容量 SSD,配 DRAM |
| 混合(log-block) | 块表 + 日志区表 | 数 MB 量级(典型值) | 中等,取决于合并类型 | SD NAND / eMMC / DRAM-less |
| 混合(superblock) | 按 superblock 建表 | 介于两者之间 | 好,利于并行与均衡 | 现代 TLC / QLC 大容量器件 |
3.4 映射表到底占多少 RAM
算式本身很简单,关键是选对三个输入:裸容量、页(或块)大小、每个表项的字节数。
映射表大小 = 表项数 × 每表项字节数
表项数(页级) = 裸容量 / 页大小
表项数(块级) = 裸容量 / 块大小
每表项 4 B(32 bit 即可寻址 4 G 个页,多数方案够用)
代入 1 TB 裸容量(按 1 TB = 1024^4 B 估算):
- 页级,16 KB 页:1024^4 / 16384 约等于 6.71×10⁷ 项,× 4 B 约等于 268 MB。
- 页级,4 KB 页:1024^4 / 4096 约等于 2.68×10⁸ 项,× 4 B 约等于 1074 MB(超过 1 GB)。
- 块级,16 MB 块:1024^4 / 16×1024^2 约等于 6.55×10⁴ 项,× 4 B 约等于 0.26 MB。
工程上常用的一条经验法则是「每 1 GB NAND 配 1 MB 表内存」,即约 0.1% 的表开销,对应页级映射在 16 KB 页下的实际量级。对小容量的 SD NAND / eMMC,控制器片内 SRAM 通常只有几百 KB 到几 MB,这就反向决定了它只能走块级或混合映射,或者把部分表放到 NAND 上按需换入换出(代价是查表延迟与额外读)。
| 做法 | 怎么省 | 省下多少(典型值) | 代价 | 适用 |
|---|---|---|---|---|
| 缩短表项位宽 | 用 32 bit 甚至变长编码存 PPN | 可省四分之一到一半 | 寻址范围受限,需分区管理 | 中小容量方案 |
| 按需换入换出 | 表放 NAND,热部分常驻 SRAM | 可省一个数量级 | 换入延迟与额外读 | DRAM-less 方案 |
| 粗粒度 + 日志区 | 主体块映射,随机写走日志块 | 可省一个数量级以上 | 合并操作的额外写 | 嵌入式主流做法 |
| 按块建二级表 | 先查块级索引再查块内页表 | 介于两者之间 | 两级查表延迟 | 大容量折中方案 |
实践里这四种做法经常组合使用。判断标准不是「表能压到多小」,而是「在最坏随机读路径上,查表会不会成为瓶颈」——映射表省下来的内存,最终会以查表延迟和额外读的形式还回去一部分。
拿到一颗器件的页大小与块大小,先用上面的算式估一遍表大小,再对照控制器可用 RAM,就能在一分钟内判断这个方案能不能走页级映射。如果表放不下而被迫降级到块级,就要同步评估随机写场景下的写放大是否还能接受——这一步在方案评审阶段经常被跳过,等到实测掉速才发现。
4. 一次主机写入的完整旅程
把一笔最普通的主机写请求从接口一路跟到物理页,就能看清写放大是在哪一步被「制造」出来的。本章按时间顺序拆成五步,并说明每一步各自产生多少 NAND 写入。
- 看链路:主机写 1 页,盘里立即写 1 页数据页 + 少量元数据,此刻 WA 还看不出来。
- 看伏笔:旧页被标记无效,它不产生写,但制造了将来 GC 必须搬移的垃圾。
- 看一致性:映射表本身也要落盘,掉电保护策略决定了这部分额外写有多少。
写完这一章,再看第 5 章的 GC,写放大的来源就只剩下一条:为擦除而做的搬移。
4.1 从 LBA 到物理页的五步链路
以一次 4 KB 的主机写为例(页大小按 16 KB 的典型值讨论时,控制器会先做读改写合并,逻辑相同):
- LBA 到达:主机通过 SD / eMMC / NVMe 接口下发写命令,携带起始 LBA 与数据。此时盘只知道逻辑地址。
- 映射查询:FTL 用 LPN 查正向表。若该 LPN 已有映射,说明之前写过,本次是覆写;若无映射,是首次写。
- 分配新页:从空闲块池取一个已擦除的干净物理页,把数据编程进去。覆写场景下的旧 PPN 暂时不动。
- 旧页失效:把旧 PPN 在有效位图里标记为无效,它所占的物理页从此不属于任何 LPN,成为垃圾。
- 更新映射:把 LPN 指向新的 PPN,并把映射变更写入元数据日志(journal),保证掉电后能恢复。
五步里只有第 3 步是主机真正要求的写入,第 5 步的元数据写是固件开销,第 4 步不写但埋下了 GC 的伏笔。所以主机写 1 页,盘里立即发生的写入是 1 页数据页 + 少量元数据页,写放大在这一刻还看不到,要等 GC 时才显现。
这里有一个对工程实践很有用的推论:把小写合并成大块写,收益不只是少写几次接口,更是让每次覆写产生的垃圾页在空间上聚拢。垃圾页聚拢,将来 GC 选中该块时的有效页占比 v 就低,搬移量就小。所以「合并写」这个动作在第 4 步就能看到收益,在第 5 章的 GC 里会再收获一次。
| 链路步骤 | 是否产生 NAND 写 | 写入量(典型值) | 计入 WA 吗 | 备注 |
|---|---|---|---|---|
| ① LBA 到达 | 否 | 0 | 不计 | 仅接口与命令解析 |
| ② 映射查询 | 否(可能读 NAND) | 0 写 | 不计 | 表未命中时要从 NAND 换入 |
| ③ 分配新页 | 是 | 1 页(主机数据) | 计为分子的一部分 | 这是主机认可的写入 |
| ④ 旧页失效 | 否 | 0 写 | 不计,但制造垃圾 | 后续 GC 成本的来源 |
| ⑤ 更新映射 | 是 | 数十到数百 B(典型值) | 计入元数据写 | 批量提交可摊薄 |
4.2 覆写为什么不是原地改
这是整个 FTL 存在的核心理由。主机把同一个 LBA 写了两遍,物理上发生的是:第一遍写在 PPN-A,第二遍写在 PPN-B,PPN-A 变成垃圾。LBA 没变,物理位置变了——这就是「异地更新」。
| 场景 | 主机视角 | NAND 实际动作 | 副作用 |
|---|---|---|---|
| 首次写 LBA 100 | 写一个扇区 | 分配并编程一个物理页 | 无 |
| 覆写 LBA 100 | 覆盖同一个扇区 | 另分配一个新页编程,旧页标记失效 | 旧页变垃圾,等待 GC |
| 覆写 100 次 | 始终是写 1 个扇区 | 产生 100 个物理页写入 + 99 个垃圾页 | GC 压力随覆写次数线性上升 |
| 删除文件 | 改元数据,不通知设备 | 设备完全不知情,旧页仍是有效页 | 若不发 TRIM,GC 要白白搬移 |
覆写还有一层容易被忽略的代价:读改写(Read-Modify-Write)。当主机下发的写入长度小于一个页、或者起止位置没有对齐到页边界时,控制器不能只写那一部分——页是编程的最小单位。它必须先把整页读出来,把要改的部分替换进去,再把整页写到新物理页。于是一次 512 B 的写入,实际产生了「读一整页 + 写一整页」两个动作,写入量被放大了页大小 / 请求大小倍。这正是第 11 章把「对齐写」列为主机侧第一优先手段的原因。
注意最后一行:文件系统删除文件这件事,NAND 是完全不知道的。被删文件占用的那些物理页,在 FTL 眼里依然是「有效数据」,GC 遇到它们还得老老实实搬一遍。这正是第 8 章 TRIM 要解决的问题。
4.3 映射表的一致性与掉电风险
映射表本身也是数据,也要存在 NAND 上。这里有一个绕不开的矛盾:每写一次数据页就要更新一次映射,如果每次都同步落盘,元数据写会爆炸;如果不落盘,掉电时映射表可能丢失。
| 策略 | 做法 | 掉电后果 | 元数据写开销 | 适用 |
|---|---|---|---|---|
| 全表定期落盘 | 攒够一批变更写一次快照 | 丢失最后一次快照之后的映射 | 低 | 小容量、掉电有保护的方案 |
| 日志(journal) | 变更先写日志,再批量合并 | 重放日志即可恢复 | 中 | 多数 eMMC / SSD 主控 |
| 每次同步落盘 | 写完数据页立刻写映射 | 几乎不丢 | 高,显著抬高 WA | 只在对一致性要求极高的小分区 |
| 掉电电容 + 缓存回写 | 靠硬件能量把缓存写完 | 可做到完整落盘 | 额外硬件成本 | 企业级与工业级盘 |
还有一个实践中的细节:元数据写的大小虽然只有几十到几百字节,但它不能单独写一页——同样要占一个物理页(或者与其他元数据合并后占一页)。所以「攒够一批再提交」几乎是所有方案的标配动作,区别只在攒多少。攒得多,掉电窗口大、元数据写少;攒得少,掉电丢失少、元数据写多。这个权衡点应该和掉电保护能力一起决定:有掉电电容的方案可以放心攒,没有的方案要把窗口压小。
把「数据页写成功」当成「这次写完成」是错的。只有映射表也安全落盘之后,这次写才算真正完成。对 SD NAND / SPI NAND 这类可能工作在掉电频繁环境的器件,选型时一定要确认主控的掉电保护策略,并在验证阶段用真实掉电测试覆盖「写一半掉电」的场景。