SPDK基于用户态,轮询、异步、无锁的NVMe驱动,封装且提供了一层关于块设备 (bdev) 的库。同时,块设备支持多层抽象与集成从而实现块设备组件 (bdev module) ,因此用户也可以根据自己的需求,编写出需要的bdev module。本文将聚焦于SPDK的块设备层 (bdev layer) 和块设备组件两个部分,并且以bdev raid module 为例,让读者更深入的认识SPDK bdev。
块设备是一种支持固定大小数据块读写的存储设备。通常一个块 (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操作完成以后,完成一些指定的动作。该回调函数的命名无限制,但是其接受的参数有限制: 第1个参数是本次写操作所用到spdk_bdev_io,在函数回调函数中必须要释放其所占用的空间,一般是使用spdk_bdev_free_io()函数完成。 第2个参数是一个bool值,代表本次写操作的完成情况,true代表完成,false代表失败。 第3个参数是一个void指针,指向和回调函数一起传入的参数cb_arg。 |
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 App的编程规范来启动已经配置好的spdk_bdev,否则spdk App将无法使用这些spdk_bdev。
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:= $(abspath $(CURDIR)/../../..)
- include$(SPDK_ROOT_DIR)/mk/spdk.common.mk
- CFLAGS+= -I$(SPDK_ROOT_DIR)/lib/bdev/
- C_SRCS=xxx.c
- LIBNAME= yyy
- include$(SPDK_ROOT_DIR)/mk/spdk.lib.mk
这里xxx就是新bdev module的名字(下面的内容也会用xxx标新bdevmodule的名字)。
2. 然后,修改位于spdk/module/bdev/目录下的Makefile文件,只需要修改其中的一行
DIRS-y+= delay error gpt lvol malloc null nvme passthru raid rpc split zone_block xxx
3. 最后,修改位于spdk/mk/目录下的两个文件:
1) 修改文件spdk.modules.mk:
在BLOCKDEV_MODULES_LIST变量下添加新的bdevmodule 比如:
BLOCKDEV_MODULES_LIST += xxx
2) 修改文件spdk.lib_deps.mk
在该文件中需要指定新bdevmodule所依赖的库,因此需要添加一个变量:
DEPDIRS-yyy:= ……
这里的yyy就是之前在第1部分提到的LIBNAME等号后就是新bdev module依赖的库。在spdk.lib_deps.mk中已经指定了一般情况下bdev常用的依赖库:
- JSON_LIBS:= json jsonrpc rpc
- BDEV_DEPS= log util $(JSON_LIBS) bdev
- BDEV_DEPS_CONF= $(BDEV_DEPS) conf
- BDEV_DEPS_THREAD= $(BDEV_DEPS) thread
- BDEV_DEPS_CONF_THREAD= $(BDEV_DEPS) conf thread
根据新bdev module的实际情况选择合适的依赖。
之后将以spdk/module/bdev/raid为例子(实现了raid0),来具体讲解如何实现一个自定义的bdev_module。
首先来看bdev_raid.h头文件中的内容。这其中包含了4个比较主要的结构体:
- struct raid_base_bdev_info {
- /* 指向basebdev 的指针*/
- struct spdk_bdev*bdev;
- /* 指向 base bdev 的描述符的指针*/
- struct spdk_bdev_desc *desc;
- /*and so on…… */
- };
该结构体记录了组成raid的base bdev的信息。
- struct raid_bdev {
- /*代表raid bdev设备, raid bdev在bdev层的数据结构*/
- struct spdk_bdev bdev;
- /* 指向raid bdev的config文件数据结构的指针*/
- struct raid_bdev_config *config;
- /* 数组,存有 raid bdev的base bdevs的信息*/
- struct raid_base_bdev_info *base_bdev_info;
- /* raid bdev的strip size,以块(block)为单位表示*/
- uint32_t strip_size;
- /* and so on…… */
- };
记录raid_bdev的主要信息。
- struct raid_bdev_io {
- /* …… */
- /* 本次I/O 原本所使用的 channel*/
- struct spdk_io_channel *ch;
- /* and so on…… */
- };
记录raid_bdev的I/O的格式。
- struct raid_bdev_io_channel {
- /*base bdevs 的 I/O channel */
- struct spdk_io_channel **base_channel;
- /*上面I/O channel数组的大小,也是 I/O channel的数量*/
- uint8_t num_channels;
- };
记录raid_bdev的I/O channel的格式。
raid_bdev中还提供了一系列对raid_bdev的基本操作:
- int raid_bdev_create
- (struct raid_bdev_config *raid_cfg);
- int raid_bdev_add_base_devices
- (struct raid_bdev_config *raid_cfg);
- void raid_bdev_remove_base_devices
- (struct raid_bdev_config *raid_cfg,
- raid_bdev_destruct_cb cb_fn, void*cb_ctx);
- /* and so on…… */
这些函数分别实现了以下的操作:
还有一些其他的基本操作在这里没有列出,这些接口都是根据raid的性质实现的。用户在实现自己的bdevmodule时,也应该根据实际情况自行设置一些基本操作。
再看raid实现了哪些上一部分提到的接口:
- static struct
- spdk_bdev_module g_raid_if = {
- .name = "raid",
- .module_init = raid_bdev_init,
- .fini_start = raid_bdev_fini_start,
- .module_fini = raid_bdev_exit,
- .get_ctx_size = raid_bdev_get_ctx_size,
- .examine_config = raid_bdev_examine,
- .config_text = raid_bdev_get_running_config,
- .async_init =false,
- .async_fini =false,
- };
- static const struct
- spdk_bdev_fn_table g_raid_bdev_fn_table = {
- .destruct = raid_bdev_destruct,
- .submit_request = raid_bdev_submit_request,
- .io_type_supported = raid_bdev_io_type_supported,
- .get_io_channel = raid_bdev_get_io_channel,
- .dump_info_json = raid_bdev_dump_info_json,
- .write_config_json = raid_bdev_write_config_json,
- };
详细分析其中一些主要接口的具体实现:
- static int
- raid_bdev_init(void)
- {
- int ret;
- /* 分析raid_bdev的config文件,bdev层的
- 配置文件一般在 spdk App启动的时候就会完成
- 读取。分析的过程就由此函数完成 */
- ret = raid_bdev_parse_config();
- if (ret <0) {
- SPDK_ERRLOG("raid bdev init failed parsing\n");
- raid_bdev_free();
- return ret;
- }
- SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,
- "raid_bdev_init completed successfully\n");
- return 0;
- }
该函数的主要功能就是解析raid_bdev的config文件,为后续通过config文件创建raid_bdev做准备。在其中的raid_bdev_parse_config函数中:
static int
- raid_bdev_parse_raid(struct spdk_conf_p *conf_p)
- {
- /* ………… some codes …………*/
- /* 检测输入的conf_p,读出raid的基础数据如:raid name, stripsize, number of base bdevs等,然后以此创一个raid_bdev_config */
- rc= raid_bdev_config_add(raid_name, strip_size, num_base_bdevs, raid_level,
- &raid_cfg);
- if (rc !=0) {
- SPDK_ERRLOG("Failedto add raid bdev config\n");
- return rc;
- }
- /* 继续检测输入的conf_p,读取base bdevs的信息,并将它们依次添加到raid_bdev_config中 */
- for (i =0; true; i++) {
- base_bdev_name= spdk_conf_p_get_nmval(conf_p, "Devices", 0, i);
- if(base_bdev_name ==NULL) {
- break;
- }
- if (i >= num_base_bdevs){
- raid_bdev_config_cleanup(raid_cfg);
- SPDK_ERRLOG("Numberof devices mentioned is more than count\n");
- return-EINVAL;
- }
- rc= raid_bdev_config_add_base_bdev(raid_cfg, base_bdev_name, i);
- if (rc !=0) {
- raid_bdev_config_cleanup(raid_cfg);
- SPDK_ERRLOG("Failedto add base bdev to raid bdev config\n");
- return rc;
- }
- }
- if (i != raid_cfg->num_base_bdevs){
- raid_bdev_config_cleanup(raid_cfg);
- SPDK_ERRLOG("Numberof devices mentioned is less than count\n");
- return-EINVAL;
- }
- /* 根据之前得到的raid_bdev_config(也就是raid_cfg变量),创建一个raid_bdev。创建好的raid_bdev的地址会记录在raid_cfg中。*/
- rc= raid_bdev_create(raid_cfg);
- if (rc !=0) {
- raid_bdev_config_cleanup(raid_cfg);
- SPDK_ERRLOG("Failedto create raid bdev\n");
- return rc;
- }
- /* 创建好raid_bdev之后,还要手动将raid_cfg中记录的base bdevs加入到raid中。*/
- rc= raid_bdev_add_base_devices(raid_cfg);
- if (rc !=0) {
- SPDK_ERRLOG("Failedto add any base bdev to raid bdev\n");
- }
- return 0;
- }
该函数主要调用了voidraid_bdev_free()函数:
static void
- raid_bdev_free(void)
- {
- struct raid_bdev_config *raid_cfg, *tmp;
- SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,"raid_bdev_free\n"); TAILQ_FOREACH_SAFE(raid_cfg,&g_raid_config.raid_bdev_config_head, link,tmp) {
- /* 遍历所有的raid_bdev_config, 将它们依次清空删除。注意上面的数据结构中,g_raid_config是一个全局变量,它存放了程序中所有raid_bdev_config(一个raid 一个config)。*/
- raid_bdev_config_cleanup(raid_cfg);
- }
- }
该函数一般由bdev_nvme层调用,用来检查输入的bdev能否被raid所声明并占用。其中主要的函数是bool raid_bdev_can_claim_bdev():
static bool
- raid_bdev_can_claim_bdev(const char*bdev_name, struct raid_bdev_config **_raid_cfg,
- uint8_t*base_bdev_slot)
- {
- /* 该函数接受3个参数,其中bdev_name是需要检查的base bdev的名字,后面两个是返回值,当确认该base bdev可以被声明并占用后,就返回它对应的raid的configuration 以及它在该raid中占有的slot。*/
- struct raid_bdev_config *raid_cfg;
- uint8_t i;
- TAILQ_FOREACH(raid_cfg,&g_raid_config.raid_bdev_config_head, link) {
- for (i =0; i < raid_cfg->num_base_bdevs;i++) {
- /* 检查的方式是用过遍历轮询每一个raid的每一个base bdev, 搜索匹配的base bdev。*/
- if (!strcmp(bdev_name,raid_cfg->base_bdev[i].name)) {
- *_raid_cfg = raid_cfg;
- *base_bdev_slot= i;
- return true;
- }
- }
- }
- return false;
- }
这个函数相当于raid_bdevmodule 的析构函数,ctxt就是指向要被析构的raid_bdev的指针。
static int
- raid_bdev_destruct(void*ctxt)
- {
- struct raid_bdev *raid_bdev = ctxt;
- SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,"raid_bdev_destruct\n");
- raid_bdev->destruct_called=true;
- for (uint8_t i =0; i < raid_bdev->num_base_bdevs;i++) {
- /* 通过关闭base bdev的descriptor的方式,释放所有base bdev的资源。如果某一个base bdev的资源已经被释放则跳过*/
- if(g_shutdown_started ||
- ((raid_bdev->base_bdev_info[i].remove_scheduled==true) &&
- (raid_bdev->base_bdev_info[i].bdev!=NULL))) {
- raid_bdev_free_base_bdev_resource(raid_bdev,i);
- }
- }
- if(g_shutdown_started) {
- TAILQ_REMOVE(&g_raid_bdev_configured_list,raid_bdev, state_link);
- raid_bdev->state =RAID_BDEV_STATE_OFFLINE;
- TAILQ_INSERT_TAIL(&g_raid_bdev_offline_list,raid_bdev, state_link);
- }
- /* 在spdk_thread层面注销raid_bdev,将其作为io_device注销并释放*/
- spdk_io_device_unregister(raid_bdev,NULL);
- /* 当所有的base bdevs都被移除raid之后,释放raid_bdev所占用的资源*/
- if (raid_bdev->num_base_bdevs_discovered==0) {
- /* Freeraid_bdev when there are no base bdevs left */
- SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,"raid bdev base bdevs is 0, going to free all in destruct\n");
- raid_bdev_cleanup(raid_bdev);
- }
- return 0;
- }
此函数完成raid_bdev向更低层次的设备(basebdevs)提交I/O请求的功能。
static void
- raid_bdev_submit_request(struct spdk_io_channel *ch, struct spdk_bdev_io *bdev_io)
- {
- struct raid_bdev *raid_bdev;
- raid_bdev= (struct raid_bdev *)bdev_io->bdev->ctxt;
- switch (bdev_io->type) {
- case SPDK_BDEV_IO_TYPE_READ:
- /* 对于read请求,首先通过 spdk_bdev_io_get_buf()为bdev_io中存放数据的buffer申请空间,然后通过raid_bdev_get_buf_cb()这个回调函数,完成I/O的提交。最终提交I/O部分依然使用了raid_bdev->fn_table->start_rw_request()这个函数*/
- spdk_bdev_io_get_buf(bdev_io,raid_bdev_get_buf_cb,
- bdev_io->u.bdev.num_blocks* bdev_io->bdev->blocklen);
- break;
- case SPDK_BDEV_IO_TYPE_WRITE:
- /* 对于write请求,直接调用start_rw_reqeust即可,因为不需要主动申请buffer的空间*/
- raid_bdev->fn_table->start_rw_request(ch,bdev_io);
- break;
- /* code of handling other types I/O*/
- }
start_rw_request对应的函数是:voidraid0_start_rw_request()。此函数会计算出raid的start_strip和end_strip并且调用raid0_submit_rw_request()最终完成I/O.
static int
- raid0_submit_rw_request(struct spdk_bdev_io *bdev_io, uint64_tstart_strip)
- {
- /* codeabout calculating some basic information used by submitting I/O */
- if (bdev_io->type ==SPDK_BDEV_IO_TYPE_READ) {
- /* 调用之前提到的readv_blocks函数完成数据读。*/
- ret= spdk_bdev_readv_blocks(raid_bdev->base_bdev_info[pd_idx].desc,
- raid_ch->base_channel[pd_idx],
- bdev_io->u.bdev.iovs,bdev_io->u.bdev.iovcnt,
- pd_lba, pd_blocks,
- raid_bdev_io_completion,
- bdev_io);
- }else if (bdev_io->type ==SPDK_BDEV_IO_TYPE_WRITE) {
- /* 调用之前提到writev_blocks函数完成数据写。*/
- ret= spdk_bdev_writev_blocks(raid_bdev->base_bdev_info[pd_idx].desc,
- raid_ch->base_channel[pd_idx],
- bdev_io->u.bdev.iovs,
- bdev_io->u.bdev.iovcnt,
- pd_lba, pd_blocks,
- raid_bdev_io_completion,
- bdev_io);
- }else {
- SPDK_ERRLOG("Recvdnot supported io type %u\n", bdev_io->type);
- assert(0);
- }
- return ret;
- }
在实现以上这些函数的基础上,raidbdev在源代码目录下新建了一个Makefile:
- SPDK_ROOT_DIR := $(abspath $(CURDIR)/../../..)
- include $(SPDK_ROOT_DIR)/mk/spdk.common.mk
- CFLAGS += -I$(SPDK_ROOT_DIR)/lib/bdev/
- C_SRCS = bdev_raid.c bdev_raid_rpc.c
- # 左边都是raid 目录下的源代码文件
- LIBNAME = bdev_raid
- include $(SPDK_ROOT_DIR)/mk/spdk.lib.mk
同时修改了spdk/mk/目录下的两个文件:
spdk.lib_deps.mk: 添加了bdev_raid的依赖库
DEPDIRS-bdev_raid :=$(BDEV_DEPS_CONF_THREAD)
spdk.modules.mk: 在bdev moduleslist中添加了raid:
BLOCKDEV_MODULES_LIST +=bdev_raid
学习地址:http://ke.qq.com/course/5066203?flowToken=1043717
更多DPDK学习资料有需要的可以自行添加进入学习交流君 羊 793599096 免费获取,或自行报名学习,免费订阅,永久学习,关注我持续更新哦!!!
原文链接:https://blog.csdn.net/weixin_37097605/article/details/103397197