SSD部分

目前维护的版本是76a9d8e333f081a9df30b5f8706e9be81517665c,"Update ZNS support statements"

数据结构

ssd

变量名

变量类型

说明

ssdname

char*

maptbl

page-level的映射表

rmap

uint64_t *

反向映射表,assume it's stored in OOB

to_poller

dataplane_started_ptr

bool*

ftl_thread

ssdparams

  1. 对闪存的配置,例如sector的大小、每个page的sector数、每个block的页数等等。(闪存组织结构是 channel->lun->plane->block->page->sector

  2. 延迟:page读/写延迟、块的擦除延迟以及channel的page传输延迟

  3. 一些计算得到的值

ssd_channel

变量名

变量类型

说明

nluns

int

next_ch_avail_time

uint64_t

初始化为0

busy

bool

初始化为0

gc_endtime

uint64_t

?这个变量没有初始化

nand_lun

变量名

类型

说明

pl

struct nand_plane *

plane级的链表

npls

int

根据用户参数初始化,每个lun的plane数

next_lun_avail_time

uint64_t

初始化为0

busy

bool

初始化为false

gc_endtime

uint64_t

?未初始化

nand_plane

变量名

类型

说明

nblks

int

根据用户参数初始化,每个plane的block数

blk

struct nand_block*

block级的链表

nand_block

变量名

类型

说明

pg

struct nand_page*

page层的指针

npgs

int

根据用户参数进行初始化,就是每个block的页数量

ipc

int

invalid page数量,初始化为0

vpc

int

valid page数量,初始化为0

erase_cnt

int

初始化为0,初始化为0

wp

int

?写指针,初始化为0

nand_page

变量名

类型

说明

sec

int *

记录page内每个sector的状态,都初始化为SEC_FREE(0)

nsecs

int

每个page的sector个数

status

int

page的状态,初始化为PG_FREE(0)

ppa

这里用到了联合,将64位的物理页地址按字段进行划分。

#define BLK_BITS    (16)
#define PG_BITS     (16)
#define SEC_BITS    (8)
#define PL_BITS     (8)
#define LUN_BITS    (8)
#define CH_BITS     (7)

struct ppa {
    union {
        struct {
            uint64_t blk : BLK_BITS;
            uint64_t pg  : PG_BITS;
            uint64_t sec : SEC_BITS;
            uint64_t pl  : PL_BITS;
            uint64_t lun : LUN_BITS;
            uint64_t ch  : CH_BITS;
            uint64_t rsv : 1;
        } g;

        uint64_t ppa;
    };
};

起始不用在意这些ppa的各个字段怎么区分,因为这个版本的femu里不是直接利用ppa去写flash的,而是绕了一步:

  1. 利用写指针write_pointer来实现channel、lun、plane、block、page的改变;

  2. 将对应的字段赋值给ppa

  3. 在建立反向映射表rmap的时候,调用了ppa2pgidx函数,利用如下公式将ppa转化为一个page index

pgidx = ppa->g.ch * spp->pgs_per_ch + ppa->g.lun * spp->pgs_per_lun +
        ppa->g.pl * spp->pgs_per_pl + ppa->g.blk * spp->pgs_per_blk + ppa->g.pg;

write_pointer

变量名

类型

说明

curline

struct line *

ch

int

初始化为0

lun

int

初始化为0

pg

int

初始化为0

blk

int

初始化为0

pl

int

初始化为0

line_mgmt

这个结构体存在于ssd结构体中,用于ssd中line(也就是superblock)的管理;

变量名

类型

说明

lines

ssd中所有line

free_line_list

QTAILQ_HEAD

victim_line_pq

pqueue_t *

应该是用优先队列管理的victim line队列

full_line_list

QTAILQ_HEAD

tt_lines

int

ssd的line总数

free_line_cnt

int

空闲计数器

victim_line_cnt

int

victim line计数器

full_line_cnt

int

full line计数器

rte_ring

这个应该是借鉴了dpdk,实现了一个无锁队列。具体操作见rte环形队列

line

line就是一个superblock,其结构比较简单:

变量名

类型

说明

id

int

与block id一样

ipc

int

这个line的invalid page 数量

vpc

int

这个line的valid page数量

entry

QTAILQ_ENTRY(line)

pos

size_t

然后用QTAILQ_ENTRY将这些line串在某些队列里面。(必在{free,victim,full}其中之一)

NvmeRequest

变量名

类型

说明

sq

struct NvmeSQueue*

cq

struct NvmeCQueue*

ns

struct NvmeNamespace*

status

uint16_t

slba

uint64_t

起始逻辑扇区号

nlb

uint16_t

请求长度(sector)

TailQ链表相关的定义

TailQ似乎是原本就提供的一个数据结构(在linux中叫做list_head),其示意图如下:

TailQ

也就是一个双向的环形链表,这个链表提供了很多的API供使用:

TAILQ_HEAD(name,type)
TAILQ_ENTRY(type)
TAILQ_EMPTY (head)
TAILQ_FIRST(head) //获取队列第一个元素
TAILQ_FOREACH(var,head, field) //遍历队列
TAILQ_INIT(head) //初始化队列
TAILQ_INSERT_AFTER(head, listelm,elm,field) //在指定元素之后插入元素
TAILQ_INSERT_BEFORE( listelm,elm,field) // 在指定元素之前插入元素
TAILQ_INSERT_TAIL(head, elm,field) //在队列尾部插入元素
......
typedef struct QTailQLink {
    void *tql_next;
    struct QTailQLink *tql_prev;
} QTailQLink;

这个变量是整个QList的基础,利用QTailQlink可以创建一个节点,结点包含: 1. 一个void*类型的指针,指向下一个节点; 2. 一个struct QTailQLink,指向前一个结点。

这一点其实让我有些费解,这样区分两个指针类型有什么深意吗?

QTAILQ_ENTRY(type)

#define QTAILQ_ENTRY(type)              
union {
        struct type *tqe_next;        /* next element */
        QTailQLink tqe_circ;          /* link for circular backwards list */
}

rte_ring环形队列

ring的特点:

  • 无锁出/入队

  • 多消费者/生产者同时出入对

rte_ring_init

/*
* 功能:初始化一个ring,这个ring由“r”来索引,memory的大小必须足够大,建议使用rte_ring_get_memsize()来获取足够的size
* 
* ring的大小设置为 *count*,这个值必须为2的幂。实际可用的大小是*count-1*,用来区分* free ring * 和 * empty ring *
* 
* @param r
*   指向ring结构体的指针
* @param name
*   ring的名称
* @param flags
*   
* @return
*   0表示成功,负值表示失败
*/

int rte_ring_init(struct rte_ring *r, const char *name, unsigned count,
    unsigned flags);

rte_ring_create

/* 
作用:创建一个ring对象;
变量:name:ring的名称
      count:ring队列的长度,必须为2的幂
      flags:指定创建的ring的属性:单/多生产者、单/多消费者两者之间的组合
      (0表示使用默认属性(多生产者、多消费者)。不同的属性出入队的操作会有所不同)
*/
struct rte_ring *femu_ring_create(enum femu_ring_type type, size_t count);

其中type有如下三种类型,当前创建的时候传入的是第二种FEMU_RING_TYPE_MP_SC,即多个生产者、单一消费者

FEMU_RING_TYPE_SP_SC,        /* Single-producer, single-consumer */
FEMU_RING_TYPE_MP_SC,        /* Multi-producer, single-consumer */
FEMU_RING_TYPE_MP_MC,        /* Multi-producer, multi-consumer */

rte_ring_dump

/*
* 将ring的状态dump到一个文件中
* 
* @param f
*   指向输出文件的指针
* @param r
*   指向ring结构的指针
*/
rte_ring_dump(FILE *f, const struct rte_ring *r)

rte_ring_free

/*
* @param r
*   需要free的ring
*/
rte_ring_free(struct rte_ring *r)

rte_ring_get_memsize

/*
* 返回ring占用的内存大小
*/
rte_ring_get_memsize(unsigned count)

基本流程

请求队列的创建与管理

femu的相关配置基本都是利用一个名为FemuCtrl的结构体控制的

请求队列的创建

(femu.c 189: femu_create_nvme_poller)

在这个函数调用的时候会传入一个FemuCtrl指针,变量名为n。根据nnum_poller的值,将

SSD的初始化

ssd_init

调用ssd_init函数,传入一个参数FemuCtrl n,这个参数里包含有一个指向ssd的指针n->ssd,之后根据ssd里的sp参数,进行相关配置。

  1. 初始化channel等结构体;

  2. 初始化映射表,所有LPN都指向UNMAPPED_PPA,也就是(~(0ULL))

  3. 初始化反向映射表,所有的PPN都指向INVALID_LPN,也就是(~(0ULL))

  4. 初始化ssd->lm,这里的line相当于plane级别的superblock,。因此这就就是调用ssd_init_lines函数来初始化superblock。

  5. 调用ssd_init_write_pointer来初始化写指针。写指针的移动方式决定了page的写入顺序;

    6.调用qemu_thread_create函数来创建ftl_thread线程。

ssd_init_lines

  1. 使用g_malloc库函数为每个line结构体分配空间,也就是分配了spp->tt_lines(total lines)个line结构体的空间;

  2. 使用QTAILTQ_INIT函数分别初始化free_line_listfull_line_list这两个TIALQ链表;

  3. 调用pqueue_init函数初始化lm->victim_line_pq。这个函数应该是传入了几个函数指针,用来设置、比较、获取优先级,获取、设置位置。

  4. 循环设置每个line的id等参数:每个line的id依次递增;ipc、vpc都等于0。同时初始化lm->free_line_cnt

ssd_init_write_pointer

  1. 使用QTAILQ_FIRST函数来获取lm->free_line_list中的第一个元素,赋值给变量ssd->wp->curline

  2. 使用QTAILQ_REMOVE函数,从lm的free_line_list中移除第一个元素,并维护计数器;

  3. 初始化当前wppch,lunpgblkpl几个值。

qemu_thread_create

这个函数需要传入4个参数:

  1. Thread指针

  2. 一个字符串,也就是线程的名字

  3. 一个函数指针,名为start_routine

  4. void*类型的arg

  5. int类型的mode

线程创建函数的具体细节待日后补充

ssd写流程,ssd_write

这个函数有两个参数:ssdreq。其中req中包含了:① 请求的起始地址,req->slba;② 请求的长度,req->nlb

写的流程如下:

  1. 根据据req的信息和ssd的配置来计算起始页号和结束页号;

  2. 调用should_gc_high判断是否需要垃圾回收。如果需要就调用do_gc函数进行垃圾回收;

  3. MARK 这里是一个待实现的功能,即cache。

  4. 判断当前lpn对应的ppa(未写入的情况下会是UNMAPPED_PPA)是否是一个已经有效的地址。如果是的话:① 旧的ppn置无效,调用mark_page_invalid;② 更新反向映射,调用函数set_rmap_ent

  5. 申请新的ppa,调用get_new_page

  6. 更新映射表set_maptbl_ent、反向映射表set_rmap_ent、页状态mark_page_valid、写指针(ssd_advance_write_pointer);

  7. 最后生成一个nand_cmd,type设置为USER_IO,cmd是NAND_WRITE,time为req->stime,并调用ssd_advance_status来更新时间线。

ssd读流程,ssd_read

参数、请求的处理与写流程类似,最后调用ssd_advance_status来更新时间线。

垃圾回收

should_gc_high

判断free_line_cntssd->sp.gc_thres_lines_high的大小关系,当free line的数量小于阈值时触发GC。

do_gc

这个函数有两个参数,第一个是ssd,第二个是一个布尔类型的force。如果force是true,就会正常进行垃圾回收;而如果force是false,那么仅仅在line的invalid页数达到1/8以上的时候才会进行垃圾回收。

用户的写操作触发GC的时候,会传入true;而ftl线程后台执行GC的时候会传入false

FTL时间线的维护

FTL里读写对于时间线的推进,采用的都是一个函数,即ssd_advance_status

这个函数针对读、写、擦三个请求分别进行了时间线的维护。这个维护是以lun(也就是die)为粒度的。时间线本身的维护也很简单,仅仅是通过一个uint64_t类型的变量next_lun_avail_time来实现的。

读请求的时间推进

  1. 判断①请求的下发时间;②lun下个一可用状态的时间 的大小关系,并取其中较大作为请求开始执行的时间;

  2. 将lun的可用时间调整为:请求的开始执行时间后推一个page read时延;

  3. 请求的latancy为 lun的下一个可用时间-请求的下发时间。

写请求的时间推进

  1. 判断①请求的下发时间;②lun下个一可用状态的时间 的大小关系,并取其中较大作为请求开始执行的时间;

  2. 将lun的可用时间调整为:请求的开始执行时间后推一个page write时延;

  3. 请求的latancy为 lun的下一个可用时间-请求的下发时间。

块擦除的时间推进

  1. 判断①请求的下发时间;②lun下个一可用状态的时间 的大小关系,并取其中较大作为请求开始执行的时间;

  2. 将lun的可用时间调整为:请求的开始执行时间后推一个block erase时延;

  3. 请求的latancy为 lun的下一个可用时间-请求的下发时间。

最终函数的返回值为请求的时延。

用户手册

使用gdb进行debug

sudo gdb x86_64-softmmu/qemu-system-x86_64

set args -name "FEMU-blackbox-SSD" -enable-kvm -cpu host -smp 4 -m 4G -device virtio-scsi-pci,id=scsi0 -device scsi-hd,drive=hd0 -drive file=/home/nvm/images/u14s.qcow2,if=none,aio=native,cache=none,format=qcow2,id=hd0 -device femu,devsz_mb=4096,femu_mode=1 -net user,hostfwd=tcp::8080-:22 -net nic,model=virtio -nographic -qmp unix:./qmp-sock,server,nowait 2>&1 | tee log

其中file=/home/nvm/images/u14s.qcow2根据自己的镜像文件路径进行修改

逻辑空间与物理空间的配置

逻辑空间配置

在运行脚本,如run-blackbox.sh中进行配置,例如配置8GB的逻辑空间:

devsz_mb=8192

物理空间配置

hw/block/femu/ftl/ftl.c源文件中,修改函数static void ssd_init_params(struct ssdparams *spp)

spp->secsz = 512;
spp->secs_per_pg = 8;
spp->pgs_per_blk = 256;
// spp->blks_per_pl = 256; /* 16GB */
spp->blks_per_pl = 160; /* 10GB */
spp->pls_per_lun = 1;
spp->luns_per_ch = 8;
spp->nchs = 8;

SSD的物理空间容量由上述几个参数相乘得到,单位为BYTE。值得注意的是,pls_per_lun参数默认只能为1,如果修改为其他值,在别的地方会报错。例如在static void ssd_advance_write_pointer(struct ssd *ssd)函数中:

/* TODO: assume # of pl_per_lun is 1, fix later */
assert(wpp->pl == 0);

以及在static struct ppa get_new_page(struct ssd *ssd)函数中:

assert(ppa.g.pl == 0);

时延的配置

NAND的时延

femu/bbssd/ftl.h头文件中,

Last updated

Was this helpful?