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操作完成以后,完成一些指定的动作。该回调函数的命名无限制,但是其接受的参数有限制:

  • 第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/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 及其编程的简单介绍相关推荐

  1. Socket编程之简单介绍 - 蓝天下的雨 - 博客园

    Socket编程之简单介绍 - 蓝天下的雨 - 博客园 Socket编程之简单介绍 - 蓝天下的雨 - 博客园 Socket编程之简单介绍 2013-03-19 15:27 by 蓝天下的雨, 878 ...

  2. SPDK: Block Device Layer Programming Guide 块设备层编程指南

    文章目录 前言 Target Audience 目标受众 Introduction 简介 Basic Primitives 基本原语 Initializing The Library Library初 ...

  3. Socket编程之简单介绍

    一:套接字编程相关知识点 Socket概念:套接字是一种通信机制,凭借这种机制,客户/服务器系统的开发工作既可以在本地单机进行,也可以跨网络进行. 网络中的进程是通过socket来通信的.socket ...

  4. 非阻塞式编程 php,简单介绍PHP非阻塞模式

    非阻塞模式是指利用socket事件的消息机制,Server端与Client端之间的通信处于异步状态. 让PHP不再阻塞当PHP作为后端处理需要完成一些长时间处理,为了快速响应页面请求,不作结果返回判断 ...

  5. IOS学习之 网络编程(10)--简单介绍ASI框架的使用

    转载自 http://www.cnblogs.com/wendingding/p/3950027.html 说明:本文主要介绍网络编程中常用框架ASI的简单使用. 一.ASI简单介绍 ASI:全称是A ...

  6. python利器怎么编程-bluepy 一款python封装的BLE利器简单介绍

    1.bluepy 简介 bluepy 是github上一个很好的蓝牙开源项目,其地址在 LINK-1, 其主要功能是用python实现linux上BLE的接口. This is a project t ...

  7. TTS技术简单介绍和Ekho(余音)TTS的安装与编程

    TTS技术简单介绍和Ekho(余音)TTS的安装与编程 zouxy09@qq.com http://blog.csdn.net/zouxy09 一.TTS技术简单介绍: TTS技术,TTS是Text ...

  8. OpenCV 编程简单介绍(矩阵/图像/视频的基本读写操作)

    PS. 因为csdn博客文章长度有限制,本文有部分内容被截掉了. 在OpenCV中文站点的wiki上有可读性更好.而且是完整的版本号,欢迎浏览. OpenCV Wiki :<OpenCV 编程简 ...

  9. 简单介绍Javascript匿名函数和面向对象编程

    忙里偷闲,简单介绍一下Javascript中匿名函数和闭包函数以及面向对象编程.首先简单介绍一下Javascript中的密名函数. 在Javascript中函数有以下3中定义方式: 1.最常用的定义方 ...

最新文章

  1. ACMNO.43 C语言-成绩排序 利用结构体解决,是一个进步啦!
  2. 状态码302.。。。
  3. 深入了解 TabNet :架构详解和分类代码实现
  4. mac 安装node_node 服务端部署
  5. when and where is createContent called
  6. Erasing Zeroes CodeForces - 1303A
  7. MYSQL性能调优及架构设计学习笔记-影响MYSQL性能的相关因素之实例分析
  8. windows doc快捷键
  9. asp.net设置元素css的属性
  10. 如何不显示index.php,tp如何隐藏index.php
  11. 行政区域村级划分数据库_两区划定数据库规范(试行)
  12. 互联网常见34个术语解释
  13. 计算机类证书之微软厂商认证分享
  14. android系统测试模式,Framework基础:手机如何进入meta测试模式
  15. 某同学:1年经验和1本软考证书,很迷茫~
  16. 计算机相关期刊阅读,计算机领域的所有SCI一区期刊,这是最顶级期刊了.doc
  17. Word文档或PDF转图片
  18. 计算机一级西溪2,我的西溪研学日记(二)——不一样的课堂,不一样的精彩...
  19. 核烧写及UBOOT调试经验总结
  20. Parcelable的使用

热门文章

  1. 使用Camera和Matrix实现3D效果
  2. 发现个Mac上 好玩的快捷键  苹果图标快捷键
  3. MAC 最小化不显示缩略图标
  4. java URL中含有汉字转码格式
  5. OSChina 周四乱弹 —— 老公你回来啦?
  6. 大家都去荷兰注册公司到底是为了什么?
  7. 课程实验 【八路抢答器】
  8. 社交类APP如何利用多元化业务场景和广告场景实现优质增收
  9. 前端之javascript的节点操作和Event
  10. oracle xfce,Centos7安装配置桌面环境xfce