148 / 163 · C11 · 约 8 分钟
基于闪存的固态硬盘
先记住这句话
本章介绍NAND闪存如何构成现代固态硬盘,重点讲解页编程前必须整块擦除的物理限制、单元磨损问题,以及这些特性如何影响存储系统的设计与实现。
晶体管如何保存数据
闪存单元利用浮栅捕获电子来编码信息。单层单元仅区分有无电荷,多层和三层单元则用多个电压阈值表示两位或三位,从而提升密度但降低速度与寿命。断电后电荷依然存在,因此适合持久存储。
页与擦除块的组织
芯片被划分为平面,平面内含大量擦除块,每块又分成若干4KB左右的页。读写以页为单位进行,但电荷重置只能整块完成。这种粒度差异要求上层软件把闪存当作只能追加写入的介质来管理。
读、擦、编程三种操作
任意页均可在数十微秒内随机读取。编程只能在已擦除的页上进行,耗时数百微秒,将部分1变为0。擦除整块需数毫秒,把所有单元恢复为1,之后才能再次编程。页一旦编程,必须整块擦除才能改写。
性能数字与磨损寿命
SLC闪存读约25微秒、编程200-300微秒、擦除1.5-2毫秒;MLC和TLC更慢。每次擦写都会轻微损伤绝缘层,导致单元最终失效。因此SSD固件必须实现磨损均衡和垃圾回收,才能提供可接受的寿命与稳定性能。
常见误区
- 误把闪存页当成磁盘扇区可直接覆盖
- 擦除前忘记把块内有效页复制到别处
- 忽视不同类型闪存单元的速度与寿命差异
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <stdint.h>
#define NPAGES 4
typedef enum { ST_INVALID, ST_ERASED, ST_VALID } state_t;
typedef struct {
uint8_t data;
state_t st;
} page_t;
static page_t blk[NPAGES];
static void dump(void) {
printf(" ");
for (int i = 0; i < NPAGES; i++) {
const char *s = (blk[i].st == ST_INVALID) ? "INV" :
(blk[i].st == ST_ERASED) ? "ERS" : "VAL";
printf("[%s %02X] ", s, blk[i].data);
}
printf("\n");
}
static void do_erase(void) {
printf("ERASE block\n");
for (int i = 0; i < NPAGES; i++) {
blk[i].data = 0xFF;
blk[i].st = ST_ERASED;
}
dump();
}
static void do_program(int pg, uint8_t val) {
printf("PROGRAM page %d <- %02X\n", pg, val);
if (pg < 0 || pg >= NPAGES || blk[pg].st != ST_ERASED) {
printf(" ERROR: page not erasable\n");
return;
}
blk[pg].data = val;
blk[pg].st = ST_VALID;
dump();
}
static void do_read(int pg) {
printf("READ page %d\n", pg);
if (pg < 0 || pg >= NPAGES || blk[pg].st != ST_VALID) {
printf(" (not valid)\n");
return;
}
printf(" data = %02X\n", blk[pg].data);
}
int main(void) {
for (int i = 0; i < NPAGES; i++) {
blk[i].data = 0x00;
blk[i].st = ST_INVALID;
}
printf("INIT\n");
dump();
do_erase();
do_program(0, 0xA5);
do_program(0, 0x5A);
do_program(2, 0x3C);
do_read(0);
do_read(1);
do_erase();
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-44-ssd.c -o example && ./example预期结果
INIT
[INV 00] [INV 00] [INV 00] [INV 00]
ERASE block
[ERS FF] [ERS FF] [ERS FF] [ERS FF]
PROGRAM page 0 <- A5
[VAL A5] [ERS FF] [ERS FF] [ERS FF]
PROGRAM page 0 <- 5A
ERROR: page not erasable
PROGRAM page 2 <- 3C
[VAL A5] [ERS FF] [VAL 3C] [ERS FF]
READ page 0
data = A5
READ page 1
(not valid)
ERASE block
[ERS FF] [ERS FF] [ERS FF] [ERS FF]
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
若要更新一个已编程页中的数据,必须执行哪些底层步骤?为什么不能直接编程?
查看参考答案
必须先把该块中所有仍需保留的页复制到其他已擦除位置,然后擦除整个块,再对新页进行编程。因为编程只能把1变成0,无法把0变回1,且只能在擦除状态的页上操作。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。