在前文中(参考上面的推荐阅读),我们提到为了便于用户使用用户态的block device, SPDK中开发了用户态、无锁、轮询的block device (bdev) layer。
Block device layer 主要由SPDK中spdk/lib/bdev目录中的代码实现,而为了实现一些必要的块设备及其相应的功能,spdk/module/bdev/中已经实现的一些常见的bdev module。
$ ll lib/bdev
bdev.c bdev_internal.h bdev_rpc.c bdev_zone.c
Makefile part.c scsi_nvme.c vtune.c
$ ll module/bdev/
aio/ compress/ crypto/ delay/ error/ ftl/ gpt/
iscsi/ lvol/ Makefile malloc/ null/ nvme/ ocf/
passthru/ pmem/ raid/ rbd/ rpc/ split/ uring/
virtio/ zone_block/
SPDK的block device layer为用户提供了很多实用的功能,同时也抽象出了一套适合所有块设备的设计:
-
在内存已经用完或者(设备的I/O)队列已满的情况下,会自动将新的I/O请求组成队列。
-
即使设备正在处理I/O, 也可以热插拔。
-
统计块设备的I/O数据比如带宽和延迟。
-
支持设备重启和I/O超时追踪。
除了这些特点之外,本文将详细剖析SPDK bdev layer:讲解bdev layer作为SPDK application中的一个subsystem,SPDK是如何初始化它的。在bdev这个subsystem中,每一个bdev module 以及其中的bdev又是如何初始化的。
注意,本文中使用了bdev layer是表示整个bdev 抽象层,而 bdev subsystem是SPDK中确切存在的一个subsystem。另外,本文所讲的初始化不涉及RPC。
SPDK bdev layer的初始化:
从subsystem的初始化开始
通常我们使用SPDK bdev 都是在SPDK application的框架下。SPDK application开始运行后,程序在进入main函数之前会首先注册application会用到的几个SPDK现有的subsystem: copy, bdev, iscsi, nbd,interface, net framework, nvmf, scsi, vhost, vmd。这些subsystem的注册是通过C语言的函数属性(主要是” attribute __constructor__”)来实现的。
在完成注册之后,SPDK application首先会解析输入的参数。然后在当前线程上创建一个spdk_thread:在SPDK中,只有创建了spdk_thread之后才能初始化bdev subsystem。此后注册的subsystems将会被依次初始化,本文仅仅关注bdev subsystem的初始化,而不会详细讨论SPDK 所有subsystems的初始化及他们之间的初始化顺序。
Bdev subsystem的初始化由函数spdk_bdev_initialize()实现,在这之前,我们需要认识一个管理整个bdev subsystem的变量:g_bdev_mgr,该变量的类型为structspdk_bdev_mgr,其数据成员如下表1所示:
成员变量 |
功能 |
struct spdk_mempool *bdev_io_pool |
存放整个subsystem可用的spdk_bdev_io的内存池 |
struct spdk_mempool *buf_small_pool; |
小 read buffer的内存池。该变量和bdev 的read请求相关,分配空间用以存放read 请求的数据 |
struct spdk_mempool *buf_large_pool; |
大read buffer的内存池。该变量和bdev 的read请求相关,分配空间用以存放read 请求的数据 |
void *zero_buffer; |
和write_zero请求相关,快速将指定区域的数据置为0时使用 |
TAILQ_HEAD(bdev_module_list, spdk_bdev_module) bdev_modules |
SPDK application运行时的bdev_modules的列表,记录了所有的bdev module |
struct spdk_bdev_list bdevs; |
SPDK application运行时的bdevs的列表, 记录了所有用到的bdev, 无论虚拟与否 |
bool init_complete |
指示变量,用来表示整个bdev subsystem是否完成初始化 |
bool module_init_complete |
指示变量,用来表示bdev_modules_init()函数是否成功返回 |
pthread_mutex_t mutex |
针对bdevs list的锁 |
表1:g_bdev_mgr的成员变量
g_bdev_mgr负责管理整个bdev subsystem,它会记录SPDK所有的bdev modules, 同时也会记录所有注册在SPDK application中的bdevs(无论是建立在其他SPDK bdev上的vbdev (virtual bdev),还是直接建立在物理设备上的bdev)。
另外它还负责管理spdk_bdev_io的内存池,负责分配和回收所有bdev进行I/O时所使用的spdk_bdev_io。SPDK中为了提高I/O的效率,为每个线程都设置有一个spdk_bdev_io的cache,如果cache中有未使用的I/O就不必再通过bdev_io_pool申请,一程度上减少了繁琐的共享资源竞争。
同时,g_bdev_mgr还会负责管理两种的buffer的内存池, 这两种buffer在进行读操作时会用来存放读出的数据。如果读取的数据量超过某一阈值(具体可见spdk_bdev_io_get_buf()函数),则会分配大buffer, 否则分配小buffer。
图1:spdk_bdev_initialize()函数的主要步骤
图1就是bdev subsystem初始化的几个主要步骤,接下来本文将深入其中,继续探究bdev subsystem的一些异步初始化的方法。
SPDK bdev layer的初始化:
bdev module的异步初始化
SPDK中的bdev modules是顺序初始化的,且有的module是依次完成初始化,有的module是采用异步初始化,即不等待其完成初始化,先继续其他的bdev module的初始化;某些module之间可能是存在层次关系的,比如split bdev可能会用malloc bdev 作为底层的设备,又或者一个raid bdev 可能既使用了malloc bdev 又使用了nvme bdev。那么在初始化时,SPDK是如何做到让高层次的bdev module一定能找到配置的底层bdev module?该部分主要聚焦于这些问题。
首先概括一下SPDK初始化bdev module的一些准备步骤。在SPDK application通常有一个输入的configuration,里面会配置对应的module。因此bdev module在初始化时的第一步都是搜索configuration,匹配关键字,如果没有在configuration中配置对应的module,那么这个module就不需要初始化任何设备;否则,module就会依次将configuration文件中,对应module section下的设备依次初始化。大概流程如下图:
图2:单个bdev module 初始化的预备步骤
1. 异步初始化的bdev module
SPDK中目前bdev module中,有些采用异步初始化,比如ftl bdev module。异步初始化不同的是,不必等待它们内部的初始化逻辑完成,就可以继续初始化其他的bdev module。以ftl为例,在开始初始化ftl module之前,会首先将控制管理该module的变量中的成员变量internal.action_in_progress加1,表示当前该bdev module中正在进行的动作增加一个。其意义在于告知管理模块g_bdev_mgr,当前某个bdev module正在进行某样动作。之后通过spdk_thread_send_msg()函数,将ftl module初始化过程中的下一个步骤通过回调函数的方式传递,当前线程不再进行ftl module的初始化,而是继续初始化接下来的bdev module。等到其余的同步初始化module都完成之后,SPDK reactor再一次轮询当前所有的SPDK thread, thread就会执行之前通过回调函数形式传递的ftl初始化步骤,继续进行ftl的初始化。
等到这最终的初始化完成之后,ftl会通过函数spdk_bdev_module_init_done()告知g_bdev_mgr,之后g_bdev_mgr会检查是否所有的bdev module都已初始化,如果没有则会继续等待其余的异步操作完成;否则就直接通知上层的subsystem管理模块“bdev module subsystem已经完成初始化”。
2. 存在依赖关系的bdev module的初始化
无论是异步初始化,还是同步初始化,它们的共同点就是:扫描configuration,如果有依赖的底层设备,则通过设备名来搜索该设备。如果搜索不到,也就是底层设备没有完成初始化,则跳过并继续扫描configuration,继续进行初始化。这样一来,很多高层的设备其实并没有真正的完成初始化,而是处于等待底层设备的状态。然而此后没有机会再一次调用它们的初始化函数,因此,当底层设备完成初始化时,需要设计一种机制来通知高层设备。
之前曾提到过,在spdk_bdev_module中有两个examine函数:examine_config()和examine_disk()。它们在函数bdev_start()中被调用,而bdev_start()函数被spdk_bdev_register()函数调用。在spdk_bdev_register()函数中,当一个bdev成功完成基本的初始化之后,就会调用bdev_start()函数,该函数的伪代码大致如下:
static void bdev_start(struct spdk_bdev *bdev)
{
将参数中的bdev加入全局的bdev列表中(加入g_bdev_mgr的bdev list中)。
遍历所有的bdev module,调用examine_config()函数(若有则调用,没有则跳过)检查该bdev是否被声明占用,完成相应的虚拟bdev初始化。
如果该bdev确实被虚拟bdev占用,调用examine_disk()函数(如果有则调用后返回,没有则函数直接返回)完成一些后续操作。
如果该bdev目前仍没有被,则再次遍历所有的bdev module,调用examine_disk()函数检查该函数是否被声明占用,完成相应虚拟bdev的初始化,并完成一些后续操作。
}
examine_config()和examine_disk()函数功能看上去差别不大,但是二者之间有着不小的差距,具体的异同点如下所示:
图3:examine_config()与examine_disk()
结束语
本文简短地介绍了SPDK bdev module的初始化方式,读者在使用或自己编程集成新的bdev module时,可以参考本文的内容,在设计新的module时,仔细考虑初始化地方式,更加高效有序地完成bdev module的初始化。SPDK 不仅仅局限于NVMe driver,建立在此基础上的bdev layer同样有着非常可观的使用价值。
随着SPDK项目的不断发展,越来越多实用高效的bdev module将会加入到SPDK的代码中,读者也可以经常关注SPDK block device layer的功能更新。
原文链接:https://mp.weixin.qq.com/s/qBN8QNb_r8ofotX7aWUMpA
学习更多dpdk视频
DPDK 学习资料、教学视频和学习路线图 :https://space.bilibili.com/1600631218
Dpdk/网络协议栈/ vpp /OvS/DDos/NFV/虚拟化/高性能专家 上课地址: https://ke.qq.com/course/5066203?flowToken=1043799
DPDK开发学习资料、教学视频和学习路线图分享有需要的可以自行添加学习交流q 君羊909332607备注(XMG) 获取