FTL地址映射:逻辑地址到物理地址的映射表管理
各位做固件的朋友,今天我们来聊聊FTL的核心——地址映射。说白了,就是让主机发来的逻辑地址,找到NAND Flash上真正的物理位置。这个映射表要是管不好,整个SSD的性能就全毁了。
我刚开始做FTL时,觉得映射不就是个查表嘛,有啥难的?后来踩了坑才明白,这里面门道深着呢。映射粒度、表的大小、缓存策略,每一个选择都直接影响性能。
映射粒度:4KB还是8KB?
映射粒度,就是每个映射条目管理的数据块大小。主流方案有两种:4KB和8KB。怎么选?我直接说结论。
| 映射粒度 | 映射表大小(1TB SSD) | 随机写性能 | 写放大 |
|---|---|---|---|
| 4KB | 256MB | 高 | 低 |
| 8KB | 128MB | 中 | 中 |
4KB粒度,映射表大,但随机写性能好。8KB粒度,表小一半,但写放大可能增加。我个人习惯,消费级SSD用4KB,企业级用8KB。为什么?消费级用户对随机性能敏感,企业级更看重容量和成本。
核心原则:映射粒度越小,灵活性越高,但表越大。粒度越大,表越小,但碎片化风险增加。
我在项目中遇到过一个问题:某款SSD用4KB粒度,DRAM只有256MB,1TB容量时映射表就占了256MB,其他功能没空间了。后来改成8KB,表降到128MB,问题解决。但随机写性能掉了约15%。这就是取舍。
映射表缓存策略
映射表全放DRAM里?成本太高。全放NAND里?性能太差。所以需要缓存策略。我常用的方案是两级缓存:SRAM做L1,DRAM做L2。
我的经验:L1缓存用SRAM,放最热门的映射条目。L2用DRAM,放次热门的。冷数据直接查NAND上的映射表。
具体怎么设计?看这个流程图:
这个流程你仔细看。L1命中直接返回,延迟最低。L2命中更新L1,下次就快了。L2都没命中,只能去NAND查,这是最慢的路径。
注意:缓存替换策略很关键。我常用LRU,但要注意热点数据保护。曾经有个项目,频繁的冷数据把热点数据挤出了L1缓存,随机读性能暴跌。后来加了访问计数,热点数据锁定在L1里,问题解决。
映射表管理实战
嗯,这里要讲点实际的。映射表怎么组织?我推荐哈希表+链表的结构。
// 映射条目结构
struct map_entry {
uint64_t lba; // 逻辑块地址
uint64_t pba; // 物理块地址
uint32_t ref_count; // 引用计数
uint32_t flags; // 标志位
struct list_head list; // 链表节点
};
// 哈希桶
struct hash_bucket {
struct list_head head;
spinlock_t lock;
};
// 映射表
struct ftl_map {
struct hash_bucket *buckets;
uint32_t bucket_count;
struct kmem_cache *entry_cache;
};
这段代码看着简单,但有几个坑。第一,哈希函数要均匀分布。我见过用LBA低几位做哈希的,结果连续LBA全挤到一个桶里,性能惨不忍睹。第二,链表操作要加锁,但锁粒度要细。我习惯每个桶一把锁,并发性能好。
避坑指南:我曾经用全局锁保护整个映射表,结果多核CPU下性能只有单核的1.2倍。改成桶级锁后,性能线性提升到4倍。锁的粒度,直接影响并发性能。
映射表持久化
掉电后映射表不能丢。怎么搞?定期刷到NAND里。我常用的策略是增量更新。
- 全量备份:每30分钟或每100万次写操作,全量刷一次
- 增量日志:每次映射变更,记录到日志区
- 恢复流程:上电先加载全量备份,再回放增量日志
这个方案我用了很多年,稳定可靠。但要注意日志区大小,别写满了。我一般预留映射表大小的10%做日志区,够用。
关键点:增量日志要保证原子性。每条日志写完要加CRC校验,防止部分写导致数据损坏。我吃过这个亏,掉电后日志区有条坏记录,恢复时直接崩溃。后来每条日志前加魔数,后加CRC,中间加长度,三重保护。
好了,映射表这块就讲这么多。记住核心:粒度选对,缓存分层,持久化可靠。做到这三点,你的FTL就稳了。