作者简介:
刘岚峰 Intel 实习生
专注于开源存储SPDK
存储软件性能优化的工作
//
SPDK基于用户态,轮询、异步、无锁的NVMe驱动,封装且提供了一层关于块设备 (bdev) 的库。同时,块设备支持多层抽象与集成从而实现块设备组件 (bdev module) ,因此用户也可以根据自己的需求,编写出需要的bdev module。本文将聚焦于SPDK的块设备层 (bdev layer) 和块设备组件两个部分,并且以bdev raid module 为例,让读者更深入的认识SPDK bdev。
/
01
SPDK bdev layer
块设备是一种支持固定大小数据块读写的存储设备。通常一个块 (Block) 的大小是512或者4096字节 (512B or 4KiB) 。一个块设备可以是逻辑上的设备,也可以对应一个物理上的存储设备,比如NVMe SSD。SPDK中的bdev layer集成在目录 spdk/lib/bdev之中,主要头文件为spdk/include/spdk/bdev.h,其中包含了与bdev进行交互的所有函数的声明。下面两张表分别是在操作bdev过程中涉及的主要数据结构和函数(Commit ID=ae3a9b8f08de94e95f6ee700d4901903bc898bd9)。
struct spdk_bdev |
代表bdev的数据结构,记录一个bdev的名称,块大小,编号等基本属性,也记录有bdev在活跃期间的一些数据比如I/O总数,另外还记录有bdev所属的组件 (module) 以及和bdev操作相关的一张 function table. |
struct spdk_bdev_desc |
一个描述符,代表bdev的一个handle,通过descriptor可以获得对应bdev的指针或者打开一个bdev,类似于UNIX系统中的文件描述符一,个bdev上可以挂载多个spdk_bdev_desc,因此不同的线程可以使用同一个bdev,对应的,在关闭bdev时,需要保证没有bdev_desc挂载在bdev上。 |
struct spdk_bdev_io |
代表发送给bdev的异步I/O。每一个I/O都需要通过spdk_io_channel 来传递。I/O中数据的封装形式主要是struct iovec。spdk_bdev_io 也有多种类型,其中最常用的就是两种类型:read 和write。 |
struct spdk_io_channel |
spdk_thread(线程) 和io_device(设备)进行I/O的通道,是spdk中抽象出的一种通信机制,spdk_bdev是一种较常用的io_device。通常一个spdk_io_channel只对应一个线程和一个块设备。spdk_bdev的I/O操作都是通过spdk_io_channel传递的。 |
上面四个结构体的关系图大致如下:
void spdk_bdev_initialize() |
初始化spdk_bdev的函数,但是在调用前必须先初始化一些bdev的options, 该函数一般在初始化SPDK环境时调用。用以初始化配置文件中的bdev。 |
void spdk_bdev_open() 或 void spdk_bdev_open_ext() |
打开一个spdk_bdev获得它的I/O操作权限。在打开时可以指定对该spdk_bdev的读写权限: 通过指定参数 write的值,如果为true,则该spdk_bdev可读/写,如果为false则只可读。该函数通过参数返回一个spdk_bdev_desc, 指向对应打开的spdk_bdev。 |
void spdk_bdev_close() |
关闭一个spdk_bdev设备,或者归还一个spdk_bdev_desc的使用权。传入的参数是一个spdk_bdev_desc, 即spdk_bdev的描述符。如果程序不再使用某spdk_bdev或者程序即将结束时可调用该函数,归还当前进程对该spdk_bdev的使用权。 |
void spdk_bdev_get_io_channel() |
通过传入spdk_bdev_desc, 获得对应的spdk_bdev的io_channel。如果当前线程已经存在一个为当前bdev设置的io_channel, 则返回该io_channel(线程和I/O channel的关系详见之前的微信文章);否则当前线程为该bdev创建一个io_channel并绑定到该线程。 |
void spdk_bdev_write() 或void spdk_bdev_writev()
|
函数的参数中指定写入的bdev、对应的io_channel、存放写数据的buffer、buffer中数据的位置(偏移量)与长度,以及一个可选的回调函数及其参数cb_arg。该函数会将buffer中的数据转化为块数据,以此来适应bdev读写,并调用spdk_bdev_write_blocks() 或spdk_bdev_writev_blocks()函数。这两个函数的区别在于后者可以支持使用scatter gather list的块设备。 参数中提到的回调函数的主要作用是在write操作完成以后,完成一些指定的动作。该回调函数的命名无限制,但是其接受的参数有限制:
|
void spdk_bdev_write_blocks()或void spdk_bdev_writev_blocks() |
函数的参数中指定写入的bdev、对应的io_channel、存放写数据的buffer、存放meta data的mdbuffer(可选)、buffer中数据偏移量和大小(均以块个数为单位),以及一个回调函数及其参数。两个函数的区别同上,所接受的回调函数的作用也同上所述。 |
void spdk_bdev_read()或void spdk_bdev_readv() |
和spdk_bdev_write(spdk_bdev_writev)相对应的函数,完成读数据的功能。
|
void spdk_bdev_read_blocks()或 void spdk_bdev_readv_blocks() |
和spdk_bdev_write_blocks(spdk_bdev_writev_blocks)相对应的函数,完成读数据块的功能。 |
void spdk_bdev_free_io() |
函数的参数中指定需要释放的spdk_bdev_io。该函数是spdk提供的规范地释放spdk_bdev_io资源的函数。 |
用户在使用spdk编程的过程中,通过以上接口,就可以简单的操作一个块设备。注意,通常在使用spdk_bdev前,我们需要手动写一个配置文件来配置物理块设备(具体的配置方式可以参考spdk/etc中的模板)。同时,我们需要遵守spdk App的编程规范来启动已经配置好的spdk_bdev,否则spdk App将无法使用这些spdk_bdev。
02
SPDK bdev module
SPDK不仅实现了直接操作块存储设备的接口,还提供了一套抽象接口:通过实现这些抽象接口,用户可以利用SPDK设计自己想要的满足特定需求的bdev module。在spdk/module/bdev/目录下,有一些已经实现好的bdev module供用户直接使用,比如raid, compress等等。
下两图展示了前文提到的一套实现bdev module 的抽象接口。
这两组接口更具体信息可以在spdk/include/spdk/bdev_module.h中查看。如果用户想要实现一套自己的bdev module, 至少需要将上面两图中的基本接口实现,因为spdk App(或其他的spdk组件) 必须通过这两组接口与bdev module 进行交互。在此基础上,用户还需要提供一些基本的bdev module的操作,比如创建bdev module。
同时,用户还应该修改对应的makefile,这样spdk项目在编译时,会将新编写的bdev module一同编译链接;否则用户将无法正常使用新的bdev module。
1. 首先用户应该在新bdev module的源文件目录下创建一个Makefile, 内部的内容大致为
SPDK_ROOT_DIR