FEMU

1. FEMU NVMe 协议

1.1 数据结构

提交队列 SQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct NvmeCmd {
uint8_t opcode; // 操作码
uint8_t fuse; // 组合操作FUSE
uint16_t cid; // [15:14]传输数据的方式(PSDT,PRP还是SGL) [13:10]保留 [9:8]组合操作FUSE
uint32_t nsid; // 命令空间标识NSID
uint64_t res1; // 保留
uint64_t mptr; // 元数据指针mptr,只有在命令有元数据,并且元数据没有与数据的逻辑块交织时有效,并且元数据的地址标识方式由cdw0.psdt决定
uint64_t prp1;
uint64_t prp2;
// 数据指针dptr
uint32_t cdw10;
uint32_t cdw11;
uint32_t cdw12;
uint32_t cdw13;
uint32_t cdw14;
uint32_t cdw15;
// 上面的cdw在具体命令中被指定含义
} NvmeCmd;
  1. FUSE:推测可能跟文件系统有关

  2. PRP,SGL:帮助Host告知Controller数据在Host内存中的具体地址

    PRP:由PRP1和PRP2表示传输的物理页地址,当传输的物理页无法有两个PRP表示,则PRP可以表示PRP Lists,并且每个指向的物理页的最后一个prp项指向下一个PRP Lists页。

    SGL:由于PRP只能表示一个个物理页,SGL能表示起始地址+长度,

完成队列 CQ

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct NvmeCqe {
union {
struct {
uint32_t result;
uint32_t rsvd;
} n;
uint64_t res64;
};
uint16_t sq_head; // 完成队列头指针
uint16_t sq_id; // 完成队列表示
uint16_t cid;
uint16_t status;
} NvmeCqe;

1.2 功能函数

创建I/O CQ

1
2
3
4
5
6
7
8
9
10
11
uint16_t nvme_create_cq(FemuCtrl *n, NvmeCmd *cmd)
{
...
}

uint16_t nvme_init_cq(NvmeCQueue *cq, FemuCtrl *n, uint64_t dma_addr,
uint16_t cqid, uint16_t vector, uint16_t size, uint16_t irq_enabled,
int contig)
{
...
}

创建I/O SQ

1
2
3
4
5
6
7
8
9
10
11
uint16_t nvme_create_sq(FemuCtrl *n, NvmeCmd *cmd)
{
...
}

uint16_t nvme_init_sq(NvmeSQueue *sq, FemuCtrl *n, uint64_t dma_addr,
uint16_t sqid, uint16_t cqid, uint16_t size, enum NvmeQueueFlags prio,
int contig)
{
...
}

read / write

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static uint16_t nvme_rw(FemuCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
NvmeRequest *req)
{
// 解析出nvme提交队列项到NvmeReq数据结构里
// 检查cmd,slba,nlb等参数合理性
NvmeRwCmd *rw = (NvmeRwCmd *)cmd;
...
err = femu_nvme_rw_check_req(n, ns, cmd, req, slba, elba, nlb, ctrl, data_size, meta_size);

// 根据req中的参数,通过prp1和prp2获取DMA传输的数据块地址
if (nvme_map_prp(&req->qsg, &req->iov, prp1, prp2, data_size, n)) {
nvme_set_error_page(n, req->sq->sqid, cmd->cid, NVME_INVALID_FIELD,
offsetof(NvmeRwCmd, prp1), 0, ns->id);
return NVME_INVALID_FIELD | NVME_DNR;
}

// 在后端内存存/取数据
ret = femu_rw_mem_backend_bb(&n->mbe, &req->qsg, data_offset, req->is_write);
}

命令提交

1
2
3
4
void nvme_process_sq_io(void *opaque, int index_poller){
// 更新sq_head_db,生成新的nvme_io_cmd
...
}

命令完成

1
2
3
4
static void nvme_post_cqe(NvmeCQueue *cq, NvmeRequest *req){\
// 更新完成队列尾部命令寄存器(cq_tail_db)和执行中断服务例程(isr)
...
}

1.3 NVMe处理流程

  1. 主机将一个至多个执行命令放置在内存中下一个空闲提交队列槽位;
  2. 主机用提交队列尾指针作为新值更新提交队列尾门铃寄存器,告知控制器有一个新的命令被提交,需要处理;
  3. 控制器将提交队列槽位的命令传输至控制器,这里会用到仲裁机制,决定传输哪一个提交队列的命令;
  4. 控制器(可能乱序)执行命令;
  5. 命令执行完成后,控制器将完成队列项放置在与之关联的完成队列的空闲槽位,控制器会移动完成队列项中的提交队列头指针,告知主机最近一次被消耗的提交队列命令, Phase Tag 会被反转,表明完成队列项是新的(这里的理解是,由于完成队列是环形队列,当环形队列由尾部工作至头部时,表明完成一个周期,而 Phase Tag 则表示完成队列项是否完成一个周期);
  6. 控制器可能会产生中断告知主机有一个新的完成队列项要处理;
  7. 主机处理完成队列中的完成队列项(过程 A),这里可能会做相应的错误处理,过程 A 持续直至遇到反转的 Phase Tag 终止(主机处理完成队列项会将同一周期内的处理完);
  8. 主机写完成队列头部门铃寄存器,表明完成队列项已经被处理。

2. FEMU 的数据与逻辑地址分离处理

仿真时,真实数据写入内存,逻辑地址单独处理

nvme_process_sq_io:
1. 生成一个io命令 nvme_io_cmd
2. nvme_rw -> femu_rw_mem_backend_bb写入内存
3. femu_ring_enqueue(n->to_ftl[index_poller], (void *)&req, 1); 取出slba和len传给to_ftl仿真

3. FTL poller的时延仿真