概述
本文基于SPDK v23.1版本的hello_world
示例来说明SPDK的nvme命令处理流程,代码架构如下:
example\nvme\hello_world.c
int main(int argc, char **argv)
{
spdk_env_opts_init(&opts);
rc = parse_args(argc, argv, &opts); // 参数解析
opts.name = "hello_world";
if (spdk_env_init(&opts) < 0) { // spdk环境初始化,最终调用的是dpdk的环境初始化
}
// 扫描设备,并将驱动和设备绑定,调用用户提供的回调`probe_cb`和`attach_cb`
rc = spdk_nvme_probe(&g_trid, NULL, probe_cb, attach_cb, NULL);
hello_world(); // IO qpair创建、nvme的读写
cleanup(); // 资源释放
return rc;
}
标准的NVMe处理涉及到NVMe子系统、HOST CPU、HOST 内存三方面,下图展示这三者之间的关系:
- NVMe子系统作为PCIe总线的Endpoint存在,可以直接与RC连接,也可以通过一个Switch连接到PCIe总线;
- NVMe命令存放在HOST内存的SQ中,命令处理完NVMe子系统会生成一个完成命令并放到HOST内存的CQ中;
-
NVMe协议对于SQ/CQ的个数并没有要求一一对应,但是SPDK中是一一对应的都放在一个qpair中
nvme子系统.png
初始化SPDK环境
接口:int spdk_env_init(const struct spdk_env_opts *opts)
这里需要的参数opts
可以通过接口spdk_env_opts_init(&opts)
来设置,以及通过当前程序提供的参数来修改parse_args(argc, argv, &opts)
默认的opts
参数配置如下:
[ DPDK EAL parameters: hello_world --no-shconf -c 0x1 --huge-unlink --log-level=lib.eal:6 --log-level=lib.cryptodev:5 --log-level=user1:6 --iova-mode=pa --base-virtaddr=0x200000000000 --match-allocations --file-prefix=spdk_pid76450 ]
最终调用DPDK的接口rte_eal_init
来完成SPDK环境的初始化,为后面的操作做好准备工作
设备查找
设备的注册
SPDK对设备的管理类似Linux的设备驱动模型:包含bus
、device
、driver
三个部分。如下:
/**
* Structure describing the PCI bus
*/
struct rte_pci_bus {
struct rte_bus bus; /**< Inherit the generic class */
RTE_TAILQ_HEAD(, rte_pci_device) device_list; /**< List of PCI devices */
RTE_TAILQ_HEAD(, rte_pci_driver) driver_list; /**< List of PCI drivers */
};
另外,SPDK对于传输使用的协议或者总线虚拟化成一个transport
,主要包含PCIE
、TCP
、Fabric
、RDMA
等类型。本文是基于example\nvme\hello_world
来说明,此示例使用的是PCIE
类型的transport
在main
函数执行之前会进行设备的注册,SPDK中使用的是gnu
的attribute
特性来实现
-
bus
注册RTE_REGISTER_BUS(pci, rte_pci_bus.bus); #define RTE_REGISTER_BUS(nm, bus) \ RTE_INIT_PRIO(businitfn_ ##nm, BUS) \ {\ (bus).name = RTE_STR(nm);\ rte_bus_register(&bus); \ // TAILQ_INSERT_TAIL(&rte_bus_list, bus, next);,将bus放到rte_bus_list链表中,pci对应的bus为rte_pci_bus.bus } #define RTE_INIT_PRIO(func, prio) \ static void __attribute__((constructor(RTE_PRIO(prio)), used)) func(void)
-
driver
注册前半部分SPDK_PCI_DRIVER_REGISTER(nvme, nvme_pci_driver_id, SPDK_PCI_DRIVER_NEED_MAPPING | SPDK_PCI_DRIVER_WC_ACTIVATE); #define SPDK_PCI_DRIVER_REGISTER(name, id_table, flags) \ __attribute__((constructor)) static void _spdk_pci_driver_register_##name(void) \ { \ spdk_pci_driver_register(#name, id_table, flags); \ // 会将driver加到链表g_pci_drivers中 }
-
transport
注册SPDK_NVME_TRANSPORT_REGISTER(pcie, &pcie_ops); .name = "PCIE", .type = SPDK_NVME_TRANSPORT_PCIE,
注册使用的宏如下(黄色部分保证宏在main之前执行,这种用法在spdk中很多,用到时再记录):
#define SPDK_NVME_TRANSPORT_REGISTER(name, transport_ops) \ static void __attribute__((constructor)) _spdk_nvme_transport_register_##name(void) \ { \ spdk_nvme_transport_register(transport_ops); \ } spdk_nvme_transport_register(transport_ops); //在g_spdk_transports中申请一个空间存放这种transport对应的ops,并且把申请的空间放到链表g_spdk_nvme_transports中 ```c
-
在
main
函数执行过程中的spdk初始化中会做设备的注册以及driver的后半部注册,将设备和驱动都注册到bus中
![](https://img.haomeiwen.com/i4789207/1e7c0cd7f4967454.png)
两个回调接口
probe_cb
: 找到NVMe controller之后进行回调,hello_world示例中只做了一条日志打印。
attach_cb
: NVMe controller连接到用户空间驱动程序后调用,hello_world示例中做了两件事,一是将初始化好的controller连接到g_controllers中;二是将NS注册到controller中。
设备查找流程
总体入口为SPDK接口
int
spdk_nvme_probe(const struct spdk_nvme_transport_id *trid, void *cb_ctx,
spdk_nvme_probe_cb probe_cb, spdk_nvme_attach_cb attach_cb,
spdk_nvme_remove_cb remove_cb)
其中三个callback参数都可以用户自定义,在hello_world示例中使用了probe_cb
和attach_cb
整体调用关系如下图所示
![](https://img.haomeiwen.com/i4789207/a0787df719d92d69.png)
- 调用DPDK的接口
dpdk_bus_probe
将device和对应的driver做绑定操作 - 调用用户自定义
probe_cb
- 创建
controller
、admin qpair
、controller bar
空间等 - 初始化
controller
- 调用用户自定义的
attach_cb
其中QPair
是SPDK的一种结构如下图所示(Admin和IO是一样的结构):
![](https://img.haomeiwen.com/i4789207/97cea43179ce6b2f.png)
- SQ和CQ内存区域既可基于DPDK所管理的大页内存来构建,也可基于CMB进行构建。
- 为了便于请求对象的复用管理,每个QP会引入一个free_req对象池来缓存nvme_request对象实例,同时还会引入一个free_tr对象池(缓存nvme_tracker对象,索引为cmdId),来跟踪每个nvme_request的执行情况(在其执行结束时触发相应的回调),在nvme_request的内部主要维护了spdk_nvme_cmd数据结构,由于其和SQ采用不同的物理空间,因此在提交命令的时候需要做一次数据拷贝。
- 针对执行失败的请求,QP并不会将其丢弃,而是先加入queued_req队列,以便后续做retry处理,当queued_req不为空的时候,后续新的请求都会提交到该队列,以保证之前失败的请求先做执行。
- 最后,每个QP还会绑定两个doorbell寄存器(每个doorbell占用4字节空间),以便向设备控制通知cmd的就绪情况,以及cpl的完成情况(基于MMIO方式更新)
IO处理
创建IO qpair
根据NVMe协议要求:先创建IO CQ
再创建IO SQ
,如下图所示
![](https://img.haomeiwen.com/i4789207/1e63597379861a33.png)
-
SPDK使用polling机制来检查CQ,而不是使用MSI/MSI-X等中断形式,在创建CQ时对DW11的IV/IEN字段都设置为0,只设置了PC字段为1(即使用连续地址)
代码如下:
int nvme_pcie_ctrlr_cmd_create_io_cq(struct spdk_nvme_ctrlr *ctrlr, struct spdk_nvme_qpair *io_que, spdk_nvme_cmd_cb cb_fn, void *cb_arg) { ...... req = nvme_allocate_request_null(ctrlr->adminq, cb_fn, cb_arg); ...... cmd->cdw11_bits.create_io_cq.pc = 1; cmd->dptr.prp.prp1 = pqpair->cpl_bus_addr; return nvme_ctrlr_submit_admin_request(ctrlr, req); }
NVMe协议描述如下:
image-20230315155753473.png
-
通过循环检查CQE中的
Phase Tag(P)
字段来确定哪些是新来的CQE代码如下:
int32_t nvme_pcie_qpair_process_completions(struct spdk_nvme_qpair *qpair, uint32_t max_completions) { ...... pqpair->stat->polls++; while (1) { cpl = &pqpair->cpl[pqpair->cq_head]; ...... next_cpl = &pqpair->cpl[next_cq_head]; next_is_valid = (next_cpl->status.p == next_phase); if (next_is_valid) { __builtin_prefetch(&pqpair->tr[next_cpl->cid]); } ...... tr = &pqpair->tr[cpl->cid]; /* Prefetch the req's STAILQ_ENTRY since we'll need to access it * as part of putting the req back on the qpair's free list. */ __builtin_prefetch(&tr->req->stailq); pqpair->sq_head = cpl->sqhd; if (tr->req) { nvme_pcie_qpair_complete_tracker(qpair, tr, cpl, true); } else { SPDK_ERRLOG("cpl does not map to outstanding cmd\n"); spdk_nvme_qpair_print_completion(qpair, cpl); assert(0); } if (++num_completions == max_completions) { break; } } ...... return num_completions; }
NVMe协议对CQE中
Phase Tag
的描述如下
![](https://img.haomeiwen.com/i4789207/a71f48fd3ccb8af2.png)
- 在
Create IO CQ
命令提交时SPDK设置了一个回调接口,在收到此命令的CQE之后调用此回调函数,而在此回调函数中做了Create IO SQ
的命令处理,命令处理过程与Create IO CQ
类似
构造IO读写
SPDK中的命令处理流程为:一个命令执行之前会添加此命令执行完成之后的回调接口,也即当此命令执行完并且收到对应CQE时会调用回调接口。
所以Hello_world示例通过此特性来实现了先写再读的操作,在写命令时设置回调write_complete
,而在write_complete
里面执行nvme的读操作,操作流程与前面的create_io_cq
和create_io_sq
类似
![](https://img.haomeiwen.com/i4789207/6ba580dd0a157c9e.png)
网友评论