SPDK block device 及其编程的简单介绍
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:= $(abspath $(CURDIR)/../../..)include$(SPDK_ROOT_DIR)/mk/spdk.common.mkCFLAGS+= -I$(SPDK_ROOT_DIR)/lib/bdev/C_SRCS=xxx.cLIBNAME= yyyinclude$(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 rpcBDEV_DEPS= log util $(JSON_LIBS) bdevBDEV_DEPS_CONF= $(BDEV_DEPS) confBDEV_DEPS_THREAD= $(BDEV_DEPS) threadBDEV_DEPS_CONF_THREAD= $(BDEV_DEPS) conf thread
根据新bdev module的实际情况选择合适的依赖。
之后的文段将以spdk/module/bdev/raid为例子(实现了raid0),来具体讲解如何实现一个自定义的bdev_module。
03
SPDK raid bdev 的实现
首先来看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_bdev。
通过配置文件,为已经创建好的raid_bdev逐一添加basebdevs。
通过配置文件,移除一个已经创建好的raid_bdev的所有basebdevs。
还有一些其他的基本操作在这里没有列出,这些接口都是根据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,
};
详细分析其中一些主要接口的具体实现:
· int raid_bdev_init(void)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函数中:
该函数一般由bdev_nvme层调用,用来检查输入的bdev能否被raid所声明并占用。其中主要的函数是bool raid_bdev_can_claim_bdev():
static boolraid_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;} · int raid_bdev_destruct (void* ctxt)这个函数相当于raid_bdevmodule 的析构函数,ctxt就是指向要被析构的raid_bdev的指针。static intraid_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;}· void raid_bdev_submit_request ()此函数完成raid_bdev向更低层次的设备(basebdevs)提交I/O请求的功能。static voidraid_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 intraid0_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
04
结束语
关于spdk block device以及spdk block device module的介绍就大致如上。在spdk 中,bdev module还有许多更强更复杂的功能(compress, crypto等等),spdk的bdev层提供的API也远不止上面所提到的内容。本文不过是抛砖引玉,带读者初步了解spdk bdev层的大致内容以及编写spdk bdev module的基本方式,若是想更深入的了解spdk的功能或者想用spdk编写出符合复杂需求的bdev module,可以详细的阅读spdk 官方的documentation(https://spdk.io/doc/) 以及参考spdk源码(https://github.com/spdk/spdk)中更多spdk bdev module的实现。
本文介绍了如何编写新的SPDK bdev module后,下一篇文章中我们将会介绍SPDK 在bdev 层的具体设计:SPDK 是如何初始化不同的bdev, 它们的资源分配机制是怎样的,以及SPDK是如何优化bdev的I/O。通过了解SPDK在bdev层的设计逻辑,更好地掌握如何使用SPDK bdev。
原文链接:https://mp.weixin.qq.com/s/GkC-mNOhIFZJAbzwcM8Y1Q
学习更多dpdk视频
DPDK 学习资料、教学视频和学习路线图 :https://space.bilibili.com/1600631218
Dpdk/网络协议栈/ vpp /OvS/DDos/NFV/虚拟化/高性能专家 上课地址: https://ke.qq.com/course/5066203?flowToken=1043799
DPDK开发学习资料、教学视频和学习路线图分享有需要的可以自行添加学习交流q 君羊909332607备注(XMG) 获取
SPDK block device 及其编程的简单介绍相关推荐
- Socket编程之简单介绍 - 蓝天下的雨 - 博客园
Socket编程之简单介绍 - 蓝天下的雨 - 博客园 Socket编程之简单介绍 - 蓝天下的雨 - 博客园 Socket编程之简单介绍 2013-03-19 15:27 by 蓝天下的雨, 878 ...
- SPDK: Block Device Layer Programming Guide 块设备层编程指南
文章目录 前言 Target Audience 目标受众 Introduction 简介 Basic Primitives 基本原语 Initializing The Library Library初 ...
- Socket编程之简单介绍
一:套接字编程相关知识点 Socket概念:套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机进行,也可以跨网络进行. 网络中的进程是通过socket来通信的.socket ...
- 非阻塞式编程 php,简单介绍PHP非阻塞模式
非阻塞模式是指利用socket事件的消息机制,Server端与Client端之间的通信处于异步状态. 让PHP不再阻塞当PHP作为后端处理需要完成一些长时间处理,为了快速响应页面请求,不作结果返回判断 ...
- IOS学习之 网络编程(10)--简单介绍ASI框架的使用
转载自 http://www.cnblogs.com/wendingding/p/3950027.html 说明:本文主要介绍网络编程中常用框架ASI的简单使用. 一.ASI简单介绍 ASI:全称是A ...
- python利器怎么编程-bluepy 一款python封装的BLE利器简单介绍
1.bluepy 简介 bluepy 是github上一个很好的蓝牙开源项目,其地址在 LINK-1, 其主要功能是用python实现linux上BLE的接口. This is a project t ...
- TTS技术简单介绍和Ekho(余音)TTS的安装与编程
TTS技术简单介绍和Ekho(余音)TTS的安装与编程 zouxy09@qq.com http://blog.csdn.net/zouxy09 一.TTS技术简单介绍: TTS技术,TTS是Text ...
- OpenCV 编程简单介绍(矩阵/图像/视频的基本读写操作)
PS. 因为csdn博客文章长度有限制,本文有部分内容被截掉了. 在OpenCV中文站点的wiki上有可读性更好.而且是完整的版本号,欢迎浏览. OpenCV Wiki :<OpenCV 编程简 ...
- 简单介绍Javascript匿名函数和面向对象编程
忙里偷闲,简单介绍一下Javascript中匿名函数和闭包函数以及面向对象编程.首先简单介绍一下Javascript中的密名函数. 在Javascript中函数有以下3中定义方式: 1.最常用的定义方 ...
最新文章
- ACMNO.43 C语言-成绩排序 利用结构体解决,是一个进步啦!
- 状态码302.。。。
- 深入了解 TabNet :架构详解和分类代码实现
- mac 安装node_node 服务端部署
- when and where is createContent called
- Erasing Zeroes CodeForces - 1303A
- MYSQL性能调优及架构设计学习笔记-影响MYSQL性能的相关因素之实例分析
- windows doc快捷键
- asp.net设置元素css的属性
- 如何不显示index.php,tp如何隐藏index.php
- 行政区域村级划分数据库_两区划定数据库规范(试行)
- 互联网常见34个术语解释
- 计算机类证书之微软厂商认证分享
- android系统测试模式,Framework基础:手机如何进入meta测试模式
- 某同学:1年经验和1本软考证书,很迷茫~
- 计算机相关期刊阅读,计算机领域的所有SCI一区期刊,这是最顶级期刊了.doc
- Word文档或PDF转图片
- 计算机一级西溪2,我的西溪研学日记(二)——不一样的课堂,不一样的精彩...
- 核烧写及UBOOT调试经验总结
- Parcelable的使用