SPDK初识之hello_world程序分析
首先是hello_world程序整体框架分析
int main(int argc, char **argv)
{
rc = parse_args(argc, argv, &opts);
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;
}
初始化SPDK环境
int spdk_env_init(const struct spdk_env_opts *opts)
函数 spdk_env_init
用于初始化SPDK环境,它接受一个指向 spdk_env_opts
结构体的指针作为参数。这个结构体包含了SPDK环境配置的各种选项。在调用 spdk_env_init
之前,通常需要先通过 spdk_env_opts_init
函数来初始化这个结构体,设置一些基本的配置选项。
最终调用DPDK的接口rte_eal_init来完成SPDK环境的初始化。
扫描设备
SPDK对于传输使用的协议或者总线虚拟化成一个transport,主要包含PCIE、TCP、Fabric、RDMA等类型。
SPDK对设备的管理类似Linux的设备驱动模型:包含总线、设备、驱动三个部分。
SPDK先后注册总线、驱动和transport。
之后提供了两个回调接口。
两个回调接口
probe_cb:
probe_cb 是在SPDK发现新的NVMe控制器后调用的回调函数。
在hello_world示例中,当SPDK发现一个新的NVMe控制器时,它仅打印了一条日志消息来确认控制器的发现。
attach_cb:
attach_cb 是在NVMe控制器成功连接到用户空间驱动程序后调用的回调函数。
在hello_world示例中,attach_cb 做了两件事情:
将初始化好的NVMe控制器添加到全局控制器列表 g_controllers 中,以便SPDK可以跟踪和管理所有已发现的控制器。
将命名空间(Namespace,NS)注册到控制器中。在NVMe中,命名空间是逻辑存储单元,它包含存储设备上的数据。注册命名空间意味着SPDK可以开始对它进行操作。
Qpair
首先明白SPDK中的关键概念:提交队列(Submission Queue)和完成队列(Completion Queue)。
在NVMe(非易失性内存表达)协议中,SQ和CQ是两个关键的概念,用于管理NVMe设备上的I/O(输入/输出)操作。
SQ: SQ是NVMe设备上的一个队列,用于存放待处理的I/O请求。 主机(HOST)将I/O请求打包成命令,并放入SQ中。 NVMe子系统会从SQ中读取命令,并处理这些I/O请求。
CQ: CQ是NVMe设备上的另一个队列,用于存放处理完成的I/O请求。当NVMe子系统完成一个I/O请求时,它会生成一个完成命令,并将其放入CQ中。 主机从CQ中读取完成命令,以确认I/O操作的成功或失败。
在NVMe协议中,SQ和CQ的个数并没有要求一一对应。也就是说,一个SQ可以对应多个CQ,或者一个CQ可以对应多个SQ。这种设计增加了系统的灵活性,可以根据具体需求配置SQ和CQ的数量。
然而,在SPDK中,SQ和CQ通常是一一对应的,并且都包含在一个名为“qpair”的结构中。这种做法简化了SPDK的编程模型,使得开发者可以更容易地管理和同步I/O请求的提交和完成。
QPair是SPDK(Storage Performance Development Kit)中的一种结构,用于管理NVMe(非易失性内存表达)设备的I/O操作。如下图所示:
QPair包括两个主要部分:SQ(Submission Queue,提交队列)和CQ(Completion Queue,完成队列)。
为了更有效地管理请求对象,每个QPair都会包含一个free_req对象池,用于缓存nvme_request对象实例。同时,还会包含一个free_tr对象池,用于缓存nvme_tracker对象,每个对象都关联一个cmdId(命令标识),以跟踪每个请求的执行情况。当请求完成时,相应的回调会被触发。
nvme_request对象内部主要维护了spdk_nvme_cmd数据结构,由于SQ和CQ使用不同的物理内存空间,因此在提交命令时需要进行一次数据拷贝。
对于执行失败的请求,QPair不会直接丢弃它们,而是先加入到queued_req队列中,以便后续进行重试处理。当queued_req队列不为空时,新的请求会先提交到这个队列中,确保之前失败的请求先得到处理。
在SPDK中,每个QPair(队列对)会绑定两个Doorbell寄存器,每个寄存器占用4个字节的空间。这些寄存器用于通知NVMe控制器关于I/O操作的状态,并且基于基于MMIO(内存映射I/O)方式更新。具体来说:
第一个Doorbell寄存器用于告知NVMe控制器,提交队列(SQ)中新的I/O命令已准备好执行。
第二个Doorbell寄存器用于通知NVMe控制器,完成队列(CQ)中I/O命令的执行状态已更新。
IO处理
创建IO qpair
创建IO qpair时先创建CQ再创建SQ。
IO读写
SPDK的命令在执行前,每个命令都会附带一个完成后的回调函数。这意味着一旦命令完成并收到对应的完成队列条目(CQE),就会触发这个回调函数。
因此,Hello_world示例利用了这一特性,实现了先写入数据再读取的操作流程。在发送写命令时,它定义了一个回调函数write_complete,并在该函数内部执行了NVMe的读操作,在发送读命令时,定义一个回调函数read_complete,在该函数内部打印数据,并将sequence.is_completed标志设置为1。
在Hello_world函数里,主程序一个while死循环,在循环体内周期性地调用spdk_nvme_qpair_process_completions() 函数。
这个函数会检查NVMe设备的完成队列(CQ),以确定是否有新的完成事件(CQEs)到达。
如果CQ中有新的完成事件,函数会处理这些事件,并调用相应的回调函数。在Hello_world示例中回调函数将sequence.is_completed标志位设置为1,于是死循环退出。
//轮询I/O队列对,等待写操作完成
while (!sequence.is_completed) {
spdk_nvme_qpair_process_completions(ns_entry->qpair, 0);
}