SPDK的块设备抽象层,从一个简单的示例程序讲起

438 阅读5分钟

最早的SPDK仅仅是一个NVMe驱动,但现在的SPDK已经不是原来的SPDK了,其功能涵盖了整个存储栈。为了能够实现丰富的功能,SPDK实现了一个块设备抽象层,其功能与Linux内核的块设备层类似,这个块设备抽象层称为BDEV。

块设备抽象层BDEV在整个SPDK栈中的位置如图所示,它位于中间位置。向下实现对多种不同类型块设备驱动的管理,除了NVMe外还有malloc (ramdisk),Linux AIO,virtio-scsi,Ceph RBD和Pmem等。向上则为协议层提供访问设备的统一接口。同时,在其内部则实现了一个公共的功能,比如快照、克隆和加密等功能。

我们自上而下简要分析一下BDEV,BDEV向上层主要提供访问的API。SPDK BDEV的API与Linux文件系统的API非常类似,主要是打开关闭设备和进行数据读写等功能。我们这里先简要介绍一下各个API的原型,后面会详细介绍每个API的实现。

首先我们看一下BDEV这些API的函数原型,主要API如下所示。以打开BDEV设备为例,这里主要参数是设备名称,打开后会返回BDEV的设备描述结构体指针,这个指针类似Linux中的文件句柄。

int spdk_bdev_open_ext(const char *bdev_name, bool write, spdk_bdev_event_cb_t event_cb,

      void *event_ctx, struct spdk_bdev_desc **_desc)

void spdk_bdev_close(struct spdk_bdev_desc *desc)

int spdk_bdev_read(struct spdk_bdev_desc *desc, struct spdk_io_channel *ch,

    void *buf, uint64_t offset, uint64_t nbytes,

    spdk_bdev_io_completion_cb cb, void *cb_arg)

int spdk_bdev_write(struct spdk_bdev_desc *desc, struct spdk_io_channel *ch,

    void *buf, uint64_t offset, uint64_t nbytes,

    spdk_bdev_io_completion_cb cb, void *cb_arg)

其他如数据读写的API,其第一个参数都是打开设备API返回的指针,第二个参数是一个IO通路(channel),IO通路与硬件的队列相对应,第三到第五个参数则是数据缓冲区、访问的位置和数据的长度,这几个参数跟Linux的read和write函数很像。最后两个参数是回调函数及其参数,这个参数与SPDK异步处理相关。

BDEV的中间层许多附件的存储功能,比如逻辑卷、快照、克隆、QoS和加密等。这些功能在传统的存储系统中都有类似的实现,本文暂时不再赘述,后面会详细介绍。

BDEV的下层实现对不同种类型设备的支持,在BDEV子系统初始化的时候会对调用模块初始化的函数指针进行模块的初始化,如图是基本函数调用。

其中bdev_modules_init的具体实现如图所示,通过该函数可以看出所有的模块通过一个全局变量g_bdev_mgr进行管理。在模块初始化的时候,会变量该全局变量中的所有模块,并调用模块的初始化函数进行初始化。

前文已述,BDEV的底层模块是通过全局变量g_bdev_mgr来管理的。每个模块都有一个注册的过程,此时该模块将会添加到这个全局变量当中。我们以malloc模块为例来看一下模块是如何注册的。每个模块都要定义一个如图所示的全局变量,其中包括模块初始化和反初始化的API。

但是这个模块是如何添加到全局变量g_bdev_mgr中的呢?我们可以看到这个全局变量的下面有一个名为SPDK_BDEV_MODULE_REGISTER的宏定义,具体定义如下。这个宏定义里面关键的是__attribute__((constructor)),该属性可以保证函数在main函数执行前被调用。换而言之,在main函数执行之前,malloc模块就被添加到全局变量g_bdev_mgr中了。

上面我们从实现层面介绍了BDEV的API和底层对各种模块的支持,接下来我们以SPDK提供的一个实例来介绍一下具体如何使用及其访问的基本流程。这个实例就是hello_bdev,是一个非常简单的例程。具体运行方法如下所示。

./examples/bdev/hello_world/hello_bdev -c ./bdev.json

上述命令行中bdev.json是一个配置文件,其源代码在实例的源代码目录中,配置文件内容如下所示。该配置文件是json格式,通过其结构可以看出例程会启动一个bdev子系统,然后调用bdev_malloc_create方法来创建一个malloc类型的块设备。

{
    "subsystems":[
        {
            "subsystem":"bdev",
            "config":[
                {
                    "method":"bdev_malloc_create",
                    "params":{
                        "name":"Malloc0",
                        "num_blocks":32768,
                        "block_size":512
                    }
                }
            ]
        }
    ]
}

我们回到该例程的具体实现,看看其main函数的具体实现。如图所示,在main函数中主要调用两个函数,一个spdk_app_parse_args,用于解析配置文件;另外一个是spdk_app_start用于启动一个应用程序,在该函数中主要是实现应用程序的初始化工作,比如子系统的初始化、创建线程和启动reactor等。完成应用初始化后会触发回调函数hello_start,也就是图中的第三步。

回调函数hello_start是本例的主要是实现代码,核心步骤如图所示。其中第一步是打开一个bdev块设备,第二步是获取该块设备的一个IO通路,第三步则是调用封装的写数据接口hello_write实现写数据的过程。

我们再具体看一下封装的写数据函数hello_write,该函数的具体实现如图所示。在该函数中主要是调用bdev的API来写数据,这里面需要注意的是该函数会传入一个回调函数作为参数。当写数据完成时会调用该回调函数。

在回调函数中又会触发一个读数据的流程,这部分逻辑本身比较简单,这里就不再赘述了。基于BDEV设备进行数据读写的流程我们暂时分析到这里,希望大家能有一个基本的认识。后面我们会从BDEV的整体架构和实现层面对BDEV进行更进一步的介绍。