SPDK Block Device及其编程的简单介绍(二):初始化

在前文中(参考上面的推荐阅读),我们提到为了便于用户使用用户态的block device, SPDK中开发了用户态、无锁、轮询的block device (bdev) layer。

Block device layer 主要由SPDK中spdk/lib/bdev目录中的代码实现,而为了实现一些必要的块设备及其相应的功能,spdk/module/bdev/中已经实现的一些常见的bdev module。​​​

$ ll lib/bdevbdev.c bdev_internal.h bdev_rpc.c bdev_zone.cMakefile 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) 获取
 

相关文章

学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习...
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面...
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生...
Can’t connect to local MySQL server through socket \'/v...
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 ...
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服...