# 第 44 章实验:用 C 写一个小型 FTL 模拟器 真实 SSD 的 FTL 藏在固件里,我们看不到;但可以用几百行 C 把它的核心逻辑模拟出来, 亲眼看到"直接映射为什么糟糕""日志结构 + 垃圾回收为什么好""预留空间和磨损均衡的代价"。 ## 文件 | 文件 | 说明 | |---|---| | `ftlsim.c` | FTL 模拟器:`direct`(直接映射)、`log`(页映射日志结构 + 贪心 GC)、`log+wl`(再加静态磨损均衡) | | `Makefile` | `make` 编译;`make run` 跑默认实验;`make sweep` 额外做预留空间扫描 | ## 怎么跑 ```bash cd labs/44-ssd make ./ftlsim # 4 种负载 × 3 种 FTL ./ftlsim -op # 再加"预留空间比例 vs 写放大"扫描 ./ftlsim -b 128 -p 32 -n 100000 -s 7 # 改块数、每块页数、写次数、随机种子 ``` ## 模型 - 闪存:默认 256 块 × 64 页。页有三种状态 INVALID / ERASED / VALID;只能对 ERASED 页编程(program), 擦除(erase)以块为单位。程序里如果对非 ERASED 页编程会直接报 BUG 退出——保证模拟遵守闪存规则。 - `direct`:逻辑页 N 固定在物理页 N。覆盖写 = 读出块内其余有效页 → 擦除整块 → 把旧页和新页全部重新编程。 - `log`:写入总是追加到"当前块"的下一个空闲页,映射表记录逻辑页 → 物理页;旧版本变成垃圾。 空闲块少于 2 个时做 GC:贪心挑"存活页最少"的块,把存活页搬到日志末尾,再擦除它。 逻辑容量比物理容量少 10%(预留空间,overprovisioning)。 - `log+wl`:每次 GC 后检查,如果某个块的擦除次数比最老的块少 30 次以上(说明它装着不动的冷数据), 就把它的数据搬走,让它重新参与写入轮转。 - 每个实验先把整个逻辑空间顺序写满一遍(预热),再统计之后 N 次写的代价。 - 时间估算用原书 Figure 44.2 的 MLC 数字:读 50µs、编程 750µs、擦除 3000µs(没有考虑多芯片并行)。 ## 该观察到什么(示例输出,你的机器上结果应完全相同,因为模拟是确定性的) ``` workload FTL host prog erase read WA wear time(s) seq direct 200000 12800000 200000 12600000 64.00 768/832 10830.0 seq log 200000 200000 3125 0 1.00 0/14 159.4 random direct 200000 12800000 200000 12600000 64.00 708/882 10830.0 random log 200000 1010167 15785 810167 5.05 56/69 845.5 cold90 log 200000 246984 3861 46984 1.23 0/84 199.2 cold90 log+wl 200000 722492 11290 522492 3.61 37/68 601.9 ``` 1. **direct 的写放大 = 每块页数(64)**:每写 1 页都要擦除一次、重编程 64 页。这正是原书说的 "写放大与块内页数成正比",而且每次写都擦除,热点块(`cold90` 行 wear 最大值 7978)会很快磨损坏。 2. **log 对顺序写近乎完美(WA=1.00)**:整块数据一起失效,GC 只需擦除空块、不用搬数据。 3. **log 对随机写 WA≈5**:块里混着存活页和垃圾页,GC 必须先搬存活页。`./ftlsim -op` 显示预留空间越多, GC 选到的受害块越"空",写放大越低——这就是厂商做预留空间的原因。 4. **磨损均衡有代价**:`cold90` 负载下纯 log FTL 的写放大只有 1.23,但冷数据所在块一次都没擦过(wear 最小值 0), 而热块被擦了 84 次;加上磨损均衡后各块擦除次数变均匀(37/68),代价是写放大涨到 3.61。 原书:"This process of wear leveling increases the write amplification of the SSD"。 5. 顺序负载下 log 的 wear 最小值是 0:贪心 GC 在多个"全空"块中总挑编号最小的,某些块一直闲置—— 说明仅靠日志结构不能保证均匀磨损,需要显式的磨损均衡策略。 ## 动手改一改 - 把 `ftl_new` 里的预留比例(`op`)改成 0.02,看看 GC 是否还能工作(空闲块不够时会报"设备空间耗尽")。 - 把 `maybe_wear_level` 里的 `THRESH` 改大/改小,观察写放大和 wear 最小/最大值的取舍。 - 把贪心 GC 换成"按块号轮转挑受害者",比较随机负载下的写放大。