C++ / a working model

148 / 163   ·   C11   ·   约 8 分钟

基于闪存的固态硬盘

先记住这句话

本章介绍NAND闪存如何构成现代固态硬盘,重点讲解页编程前必须整块擦除的物理限制、单元磨损问题,以及这些特性如何影响存储系统的设计与实现。

本篇内容
  1. 晶体管如何保存数据
  2. 页与擦除块的组织
  3. 读、擦、编程三种操作
  4. 性能数字与磨损寿命
  5. 运行示例
  6. 动手练习

官方章节 PDF

晶体管如何保存数据

闪存单元利用浮栅捕获电子来编码信息。单层单元仅区分有无电荷,多层和三层单元则用多个电压阈值表示两位或三位,从而提升密度但降低速度与寿命。断电后电荷依然存在,因此适合持久存储。

页与擦除块的组织

芯片被划分为平面,平面内含大量擦除块,每块又分成若干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,且只能在擦除状态的页上操作。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录