本文记录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)
-
每个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
-
FEMU issues #84
-
ETH Zürich
-
《深入浅出SSD》
-
固态硬盘基础知识
-
CMU18-746笔记 NAND-based SSD