[FEMU] FEMU Blackbox SSD源码阅读

本文记录FEMU blackbox ssd(ssd对主机透明)的源码阅读。

bb.c

用于注册bbssd,在hw/femu/femu.c>nvme_register_extesions()中执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static int nvme_register_extensions(FemuCtrl *n)
{
    if (OCSSD(n)) {
        ...
    }
  	...
    } else if (BBSSD(n)) {
        nvme_register_bbssd(n);
    }
		...
    return 0;
}

这个FemuCtrl *n结构包含了很多内容,本质上是一个用于线程之间同步/传值的结构

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int nvme_register_bbssd(FemuCtrl *n)
{
    n->ext_ops = (FemuExtCtrlOps) {
        .state            = NULL,
        .init             = bb_init,
        .exit             = NULL,
        .rw_check_req     = NULL,
        .admin_cmd        = bb_admin_cmd,
        .io_cmd           = bb_io_cmd,
        .get_log          = NULL,
    };

    return 0;
}

初始化bbssd的有关结构,最终执行ssd_init()来初始化ssd

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* bb <=> black-box */
static void bb_init(FemuCtrl *n, Error **errp)
{
    struct ssd *ssd = n->ssd = g_malloc0(sizeof(struct ssd));

    bb_init_ctrl_str(n);

    ssd->dataplane_started_ptr = &n->dataplane_started;
    ssd->ssdname = (char *)n->devname;
    femu_debug("Starting FEMU in Blackbox-SSD mode ...\n");
    ssd_init(n);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
static uint16_t bb_admin_cmd(FemuCtrl *n, NvmeCmd *cmd)
{
    switch (cmd->opcode) {
    case NVME_ADM_CMD_FEMU_FLIP:
        bb_flip(n, cmd);
        return NVME_SUCCESS;
    default:
        return NVME_INVALID_OPCODE | NVME_DNR;
    }
}

执行bbssd的I/O操作,调用bb_nvme_rw()将数据写入到DRAM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static uint16_t bb_io_cmd(FemuCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
                          NvmeRequest *req)
{
    switch (cmd->opcode) {
    case NVME_CMD_READ:
    case NVME_CMD_WRITE:
        return bb_nvme_rw(n, ns, cmd, req);
    default:
        return NVME_INVALID_OPCODE | NVME_DNR;
    }
}

ftl.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct ssd {
    char *ssdname;
    struct ssdparams sp;
    struct ssd_channel *ch;
    struct ppa *maptbl; /* page level mapping table */
    uint64_t *rmap;     /* reverse mapptbl, assume it's stored in OOB */
    struct write_pointer wp;
    struct line_mgmt lm;

    /* lockless ring for communication with NVMe IO thread */
    /* 无锁ring寄存器 */
    struct rte_ring **to_ftl;
    struct rte_ring **to_poller;
    bool *dataplane_started_ptr;
    QemuThread ftl_thread;
};

SSD通过ssd_init()函数初始化

ftl.c

init

Ssd_init首先完成对SSD的初始化操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void ssd_init(FemuCtrl *n)
{
    struct ssd *ssd = n->ssd;
    struct ssdparams *spp = &ssd->sp;

    ftl_assert(ssd);

    ssd_init_params(spp);

    /* initialize ssd internal layout architecture */
    ssd->ch = g_malloc0(sizeof(struct ssd_channel) * spp->nchs);
    for (int i = 0; i < spp->nchs; i++) {
        ssd_init_ch(&ssd->ch[i], spp);
    }

    /* initialize maptbl */
    ssd_init_maptbl(ssd);

    /* initialize rmap */
    ssd_init_rmap(ssd);

    /* initialize all the lines */
    ssd_init_lines(ssd);

    /* initialize write pointer, this is how we allocate new pages for writes */
    ssd_init_write_pointer(ssd);

    qemu_thread_create(&ssd->ftl_thread, "FEMU-FTL-Thread", ftl_thread, n,
                       QEMU_THREAD_JOINABLE);
}

初始化流程:

  • 根据设定的参数,分配SSD所使用的内存
  • 初始化$ssd\ Chanel\to nand \ logic \ unit\to plane\to blk\to nand page$
  • 初始化Mapping Table,初始化ppa(物理页的地址)
  • 初始化rmap,反向的映射表Physical 2 Logic,存储在OOB
  • 初始化lines
  • 初始化写指针,指向下一个写入的地址
  • 创建FEMU FTL线程,维持FTL的运行

ftl thread

首先初始化ssd的两个队列

这两个队列会在hw/femu/nvme-io.c>nvme_create_poller()中创建

  • to_ftl:host发送到ssd的nvme req(sq)
  • to_poller:ssd等待host轮询的队列(cq)

ftl thread主要为一个while循环,从femu_ring_dequeue中获取NVMe请求,执行相应的NVMe Write/Read/DSM指令。

在执行NVMe Write/Read时,函数内部会模拟操作的延迟并返回给ftl thread,最终汇总到NVMe request的总延迟中,并将NVMe request加入cq。

每次操作结束后会调用should_gc()来判断是否要执行do_gc()操作。

NVMe Operation

主要执行NVMe Write/Read

先看一下一个NVMe请求的结构:

hw/femu/nvme.h>NvmeRequest

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
typedef struct NvmeRequest {
    struct NvmeSQueue       *sq;
    struct NvmeCQueue       *cq;
    struct NvmeNamespace    *ns;
    uint16_t                status;
    uint64_t                slba;
    uint16_t                is_write;
    uint16_t                nlb;
    uint16_t                ctrl;
    uint64_t                meta_size;
    uint64_t                mptr;
    void                    *meta_buf;
    uint64_t                oc12_slba;
    uint64_t                *oc12_ppa_list;
    NvmeCmd                 cmd;
    NvmeCqe                 cqe;
    uint8_t                 cmd_opcode;
    QEMUSGList              qsg;
    QEMUIOVector            iov;
    QTAILQ_ENTRY(NvmeRequest)entry;
    int64_t                 stime;
    int64_t                 reqlat;
    int64_t                 gcrt;
    int64_t                 expire_time;

    /* OC2.0: sector offset relative to slba where reads become invalid */
    uint64_t predef;

    /* ZNS */
    void                    *opaque;

    /* position in the priority queue for delay emulation */
    size_t                  pos;
} NvmeRequest;

NVMeRequest的cmd.opcode用于判断操作类型


Write

执行Write时,要获取lba(Logical Block Address)、nlb(Number of logic block)

基于lba和nlb和ssd的参数,计算出要写入的起止lpn(Logical Page Number)

执行写入操作前,需要先执行should_gc_high()直到可以执行写入

从start lpn一直到end lpn,对于每个lpn:

  • 获取ppa,判断是否有映射信息,若有则先标记ppa的page status和为Invalid,并移除RMAP映射信息
  • 通过ssd写指针获得写入的ppa
  • 更新mapping table和RMAP,将page标记为valid
  • ssd写指针移动到下一个有效位置
  • 基于nvme req的开始时间,通过ssd_advance_status()获取延迟curlat(current latency)
  • $maxlat= \max(maxlat,curlat)$

最终返回maxlat

Read

执行read操作时,同样获取lba和nlb,计算起止的lpn

后面的过程相比Write流程要简单一些

对于每个lpn:

  • 获取ppa,判断是否有映射信息或当前ppa是否有效,若无信息或当前状态为invalid则跳过该lpn
  • 计算延迟

返回maxlat


注意到,其实这里的Write/Read都没有真正去执行写入,femu中仅模拟延迟,在FEMU issues #84中作者提到,有关数据I/O的操作在NVMe层中进行模拟,位于hw/femu/nvme-io.c>nvme_rw()函数中,最终由backend_rw()通过DMA写入内存。

ssd_advance_status()

更新ssd的状态,根据闪存读写擦的操作来更新SSD的时间戳,即lun的next_lun_avail_time

GC

do_gc()有force参数

首先通过select_victim_line()选取victim line,victim line由line mgmt维护的优先队列中获得,取出队头,若队列为空则返回NULL,若force为False,并且invalid page count in this line小于每个line上的page数量/8,同样返回NULL。进行队列维护操作后返回victim line。

遍历所有的channel,这里没太看懂,主要是推进ssd的状态,以便计算延迟。

最后将该line标记为free。

其他要补充的

对于bbssd,如果要修改sector size,需要手动修改源码

在ftl.c line 239

1
2
3
4
5
6
static void ssd_init_params(struct ssdparams *spp)
{
    spp->secsz = 512;
    spp->secs_per_pg = 8;
    ....
}

https://github.com/vtess/FEMU/blob/ad786ad152e6113057799f2d3edc0ef3295423bf/hw/femu/bbssd/ftl.c#L239

梳理bbssd中的结构

对于我们日常使用的SSD,包含以下几个结构,下图来自ETH Zürich

NAND Packages就是SSD上可以看到的NAND闪存颗粒,每个NAND Packages包含多个LUN/Die

下图来自《深入浅出SSD》

LUN,或称之为Die、Chip,每个LUN上又包含若干个Plane

Plane中则包含SSD擦除的基本单元Block,每个Plane中有寄存器和Cache,用于并行。

Block中包含读写的基本单元Page

Page的几个状态:

  • 无效页 Invalid:数据已删除,但还没有擦除数据
  • 有效页 valid:数据已写入但未删除
  • 空闲页 free:可直接写入数据

不同的LUN之间通过Channel进行连接。

对应femu bbssd ftl中的结构

将SSD划分为多个Channel

  • 每个Channel之中包含多个LUN单元(nluns)

    • 单个Channel之间无法并行,有busy字段(虽然femu有busy字段,但是除了初始化阶段并没有使用过,用next avail time就够了)
    • 有next_ch_avail_time
    • 有gc_endtime
  • 每个LUN之间包含多个Plane单元(npls)

    • 有next_lun_avail_time
    • 有gc_endtime
  • 每个Plane单元包含多个block单元(nblks)

    • Plane之间由于有寄存器,可以并行
  • 每个block单元包含多个page单元(npgs)

    • 包含有效页无效页的统计
      • ipc:invalid page count
      • vpc:valid page count
    • erase_cnt:应该是用于寿命估计/磨损均衡
    • wp:当前的写指针,对于写指针的移动有一套控制算法
  • page单元划分多个logical sector

    • nand_sec_status_t(int):记录每个logical sector状态
    • nsecs:每个page内logical sector的数量,读写的基本单位了
    • status:即page的状态 valid/invalid/free

至此,FEMU bbssd中涉及ssd组成的部分已梳理完毕。

ftl.c还有一个line的概念,这个和NAND Flash中word line和bit line的概念并不相同,根据FEMU issues #86,像是作者为了实现gc自己设计的结构。

1
A line is a superblock, which is essentially striped across blocks from all parallel flash chips with the same offset within the chip, e.g., line 0 is the union of all block 0s from each chip.

一个line包含在一个LUN中所有并行的Plane中block的偏移量,如下图

因此line的结构很简单,维护了相应的offset/block id,和valid/invalid page统计

QTAILQ_ENTRY维护了一个循环链表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define QTAILQ_ENTRY(type)                                              
union {                                                                 
        struct type *tqe_next;        /* next element */                
        QTailQLink tqe_circ;          /* link for circular backwards list */
}
typedef struct line {
    int id;  /* line id, the same as corresponding block id */
    int ipc; /* invalid page count in this line */
    int vpc; /* valid page count in this line */
    QTAILQ_ENTRY(line) entry; /* in either {free,victim,full} list */
    /* position in the priority queue for victim lines */
    size_t                  pos;
} line;

同时还有相应的line management

line主要用来gc选中victim line并执行一些选中、擦除操作。

Reference

  1. FEMU issues #84

  2. ETH Zürich

  3. 《深入浅出SSD》

  4. 固态硬盘基础知识

  5. CMU18-746笔记 NAND-based SSD

0%