• SPDK block device及其编程的简单介绍


    SPDK基于用户态,轮询、异步、无锁的NVMe驱动,封装且提供了一层关于块设备 (bdev) 的库。同时,块设备支持多层抽象与集成从而实现块设备组件 (bdev module) ,因此用户也可以根据自己的需求,编写出需要的bdev module。本文将聚焦于SPDK的块设备层 (bdev layer) 和块设备组件两个部分,并且以bdev raid module 为例,让读者更深入的认识SPDK bdev。

    一. 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_channelspdk_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 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, 内部的内容大致为

    1. SPDK_ROOT_DIR:= $(abspath $(CURDIR)/../../..)
    2. include$(SPDK_ROOT_DIR)/mk/spdk.common.mk
    3. CFLAGS+= -I$(SPDK_ROOT_DIR)/lib/bdev/
    4. C_SRCS=xxx.c
    5. LIBNAME= yyy
    6. 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常用的依赖库:

    1. JSON_LIBS:= json jsonrpc rpc
    2. BDEV_DEPS= log util $(JSON_LIBS) bdev
    3. BDEV_DEPS_CONF= $(BDEV_DEPS) conf
    4. BDEV_DEPS_THREAD= $(BDEV_DEPS) thread
    5. BDEV_DEPS_CONF_THREAD= $(BDEV_DEPS) conf thread

    根据新bdev module的实际情况选择合适的依赖。

    之后将以spdk/module/bdev/raid为例子(实现了raid0),来具体讲解如何实现一个自定义的bdev_module。

    三. SPDK raid bdev的实现

    首先来看bdev_raid.h头文件中的内容。这其中包含了4个比较主要的结构体:

    1. struct raid_base_bdev_info {
    2. /* 指向basebdev 的指针*/
    3. struct spdk_bdev*bdev;
    4. /* 指向 base bdev 的描述符的指针*/
    5. struct spdk_bdev_desc *desc;
    6. /*and so on…… */
    7. };

    该结构体记录了组成raid的base bdev的信息。

    1. struct raid_bdev {
    2. /*代表raid bdev设备, raid bdev在bdev层的数据结构*/
    3. struct spdk_bdev bdev;
    4. /* 指向raid bdev的config文件数据结构的指针*/
    5. struct raid_bdev_config *config;
    6. /* 数组,存有 raid bdev的base bdevs的信息*/
    7. struct raid_base_bdev_info *base_bdev_info;
    8. /* raid bdev的strip size,以块(block)为单位表示*/
    9. uint32_t strip_size;
    10. /* and so on…… */
    11. };

    记录raid_bdev的主要信息。

    1. struct raid_bdev_io {
    2. /* …… */
    3. /* 本次I/O 原本所使用的 channel*/
    4. struct spdk_io_channel *ch;
    5. /* and so on…… */
    6. };

    记录raid_bdev的I/O的格式。

    1. struct raid_bdev_io_channel {
    2. /*base bdevs 的 I/O channel */
    3. struct spdk_io_channel **base_channel;
    4. /*上面I/O channel数组的大小,也是 I/O channel的数量*/
    5. uint8_t num_channels;
    6. };

    记录raid_bdev的I/O channel的格式。

    raid_bdev中还提供了一系列对raid_bdev的基本操作:

    1. int raid_bdev_create
    2. (struct raid_bdev_config *raid_cfg);
    3. int raid_bdev_add_base_devices
    4. (struct raid_bdev_config *raid_cfg);
    5. void raid_bdev_remove_base_devices
    6. (struct raid_bdev_config *raid_cfg,
    7. raid_bdev_destruct_cb cb_fn, void*cb_ctx);
    8. /* and so on…… */

    这些函数分别实现了以下的操作:

    • 通过一个配置文件创建一个raid_bdev。
    • 通过配置文件,为已经创建好的raid_bdev逐一添加basebdevs。
    • 通过配置文件,移除一个已经创建好的raid_bdev的所有basebdevs。

    还有一些其他的基本操作在这里没有列出,这些接口都是根据raid的性质实现的。用户在实现自己的bdevmodule时,也应该根据实际情况自行设置一些基本操作。

    再看raid实现了哪些上一部分提到的接口:

    1. static struct
    2. spdk_bdev_module g_raid_if = {
    3. .name = "raid",
    4. .module_init = raid_bdev_init,
    5. .fini_start = raid_bdev_fini_start,
    6. .module_fini = raid_bdev_exit,
    7. .get_ctx_size = raid_bdev_get_ctx_size,
    8. .examine_config = raid_bdev_examine,
    9. .config_text = raid_bdev_get_running_config,
    10. .async_init =false,
    11. .async_fini =false,
    12. };
    13. static const struct
    14. spdk_bdev_fn_table g_raid_bdev_fn_table = {
    15. .destruct = raid_bdev_destruct,
    16. .submit_request = raid_bdev_submit_request,
    17. .io_type_supported = raid_bdev_io_type_supported,
    18. .get_io_channel = raid_bdev_get_io_channel,
    19. .dump_info_json = raid_bdev_dump_info_json,
    20. .write_config_json = raid_bdev_write_config_json,
    21. };

    详细分析其中一些主要接口的具体实现:

    • int raid_bdev_init(void)
    1. static int
    2. raid_bdev_init(void)
    3. {
    4. int ret;
    5. /* 分析raid_bdev的config文件,bdev层的
    6. 配置文件一般在 spdk App启动的时候就会完成
    7. 读取。分析的过程就由此函数完成 */
    8. ret = raid_bdev_parse_config();
    9. if (ret <0) {
    10. SPDK_ERRLOG("raid bdev init failed parsing\n");
    11. raid_bdev_free();
    12. return ret;
    13. }
    14. SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,
    15. "raid_bdev_init completed successfully\n");
    16. return 0;
    17. }

    该函数的主要功能就是解析raid_bdev的config文件,为后续通过config文件创建raid_bdev做准备。在其中的raid_bdev_parse_config函数中:

    static int

    1. raid_bdev_parse_raid(struct spdk_conf_p *conf_p)
    2. {
    3. /* ………… some codes …………*/
    4. /* 检测输入的conf_p,读出raid的基础数据如:raid name, stripsize, number of base bdevs等,然后以此创一个raid_bdev_config */
    5. rc= raid_bdev_config_add(raid_name, strip_size, num_base_bdevs, raid_level,
    6. &raid_cfg);
    7. if (rc !=0) {
    8. SPDK_ERRLOG("Failedto add raid bdev config\n");
    9. return rc;
    10. }
    11. /* 继续检测输入的conf_p,读取base bdevs的信息,并将它们依次添加到raid_bdev_config中 */
    12. for (i =0; true; i++) {
    13. base_bdev_name= spdk_conf_p_get_nmval(conf_p, "Devices", 0, i);
    14. if(base_bdev_name ==NULL) {
    15. break;
    16. }
    17. if (i >= num_base_bdevs){
    18. raid_bdev_config_cleanup(raid_cfg);
    19. SPDK_ERRLOG("Numberof devices mentioned is more than count\n");
    20. return-EINVAL;
    21. }
    22. rc= raid_bdev_config_add_base_bdev(raid_cfg, base_bdev_name, i);
    23. if (rc !=0) {
    24. raid_bdev_config_cleanup(raid_cfg);
    25. SPDK_ERRLOG("Failedto add base bdev to raid bdev config\n");
    26. return rc;
    27. }
    28. }
    29. if (i != raid_cfg->num_base_bdevs){
    30. raid_bdev_config_cleanup(raid_cfg);
    31. SPDK_ERRLOG("Numberof devices mentioned is less than count\n");
    32. return-EINVAL;
    33. }
    34. /* 根据之前得到的raid_bdev_config(也就是raid_cfg变量),创建一个raid_bdev。创建好的raid_bdev的地址会记录在raid_cfg中。*/
    35. rc= raid_bdev_create(raid_cfg);
    36. if (rc !=0) {
    37. raid_bdev_config_cleanup(raid_cfg);
    38. SPDK_ERRLOG("Failedto create raid bdev\n");
    39. return rc;
    40. }
    41. /* 创建好raid_bdev之后,还要手动将raid_cfg中记录的base bdevs加入到raid中。*/
    42. rc= raid_bdev_add_base_devices(raid_cfg);
    43. if (rc !=0) {
    44. SPDK_ERRLOG("Failedto add any base bdev to raid bdev\n");
    45. }
    46. return 0;
    47. }
    • void raid_bdev_exit(void)

    该函数主要调用了voidraid_bdev_free()函数:

    static void

    1. raid_bdev_free(void)
    2. {
    3. struct raid_bdev_config *raid_cfg, *tmp;
    4. SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,"raid_bdev_free\n"); TAILQ_FOREACH_SAFE(raid_cfg,&g_raid_config.raid_bdev_config_head, link,tmp) {
    5. /* 遍历所有的raid_bdev_config, 将它们依次清空删除。注意上面的数据结构中,g_raid_config是一个全局变量,它存放了程序中所有raid_bdev_config(一个raid 一个config)。*/
    6. raid_bdev_config_cleanup(raid_cfg);
    7. }
    8. }
    • void raid_bdev_examine(struct spdk_bdev *bdev)

    该函数一般由bdev_nvme层调用,用来检查输入的bdev能否被raid所声明并占用。其中主要的函数是bool raid_bdev_can_claim_bdev():

    static bool

    1. raid_bdev_can_claim_bdev(const char*bdev_name, struct raid_bdev_config **_raid_cfg,
    2. uint8_t*base_bdev_slot)
    3. {
    4. /* 该函数接受3个参数,其中bdev_name是需要检查的base bdev的名字,后面两个是返回值,当确认该base bdev可以被声明并占用后,就返回它对应的raid的configuration 以及它在该raid中占有的slot。*/
    5. struct raid_bdev_config *raid_cfg;
    6. uint8_t i;
    7. TAILQ_FOREACH(raid_cfg,&g_raid_config.raid_bdev_config_head, link) {
    8. for (i =0; i < raid_cfg->num_base_bdevs;i++) {
    9. /* 检查的方式是用过遍历轮询每一个raid的每一个base bdev, 搜索匹配的base bdev。*/
    10. if (!strcmp(bdev_name,raid_cfg->base_bdev[i].name)) {
    11. *_raid_cfg = raid_cfg;
    12. *base_bdev_slot= i;
    13. return true;
    14. }
    15. }
    16. }
    17. return false;
    18. }
    • int raid_bdev_destruct (void* ctxt)

    这个函数相当于raid_bdevmodule 的析构函数,ctxt就是指向要被析构的raid_bdev的指针。

    static int

    1. raid_bdev_destruct(void*ctxt)
    2. {
    3. struct raid_bdev *raid_bdev = ctxt;
    4. SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,"raid_bdev_destruct\n");
    5. raid_bdev->destruct_called=true;
    6. for (uint8_t i =0; i < raid_bdev->num_base_bdevs;i++) {
    7. /* 通过关闭base bdev的descriptor的方式,释放所有base bdev的资源。如果某一个base bdev的资源已经被释放则跳过*/
    8. if(g_shutdown_started ||
    9. ((raid_bdev->base_bdev_info[i].remove_scheduled==true) &&
    10. (raid_bdev->base_bdev_info[i].bdev!=NULL))) {
    11. raid_bdev_free_base_bdev_resource(raid_bdev,i);
    12. }
    13. }
    14. if(g_shutdown_started) {
    15. TAILQ_REMOVE(&g_raid_bdev_configured_list,raid_bdev, state_link);
    16. raid_bdev->state =RAID_BDEV_STATE_OFFLINE;
    17. TAILQ_INSERT_TAIL(&g_raid_bdev_offline_list,raid_bdev, state_link);
    18. }
    19. /* 在spdk_thread层面注销raid_bdev,将其作为io_device注销并释放*/
    20. spdk_io_device_unregister(raid_bdev,NULL);
    21. /* 当所有的base bdevs都被移除raid之后,释放raid_bdev所占用的资源*/
    22. if (raid_bdev->num_base_bdevs_discovered==0) {
    23. /* Freeraid_bdev when there are no base bdevs left */
    24. SPDK_DEBUGLOG(SPDK_LOG_BDEV_RAID,"raid bdev base bdevs is 0, going to free all in destruct\n");
    25. raid_bdev_cleanup(raid_bdev);
    26. }
    27. return 0;
    28. }
    • void raid_bdev_submit_request ()

    此函数完成raid_bdev向更低层次的设备(basebdevs)提交I/O请求的功能。

    static void

    1. raid_bdev_submit_request(struct spdk_io_channel *ch, struct spdk_bdev_io *bdev_io)
    2. {
    3. struct raid_bdev *raid_bdev;
    4. raid_bdev= (struct raid_bdev *)bdev_io->bdev->ctxt;
    5. switch (bdev_io->type) {
    6. case SPDK_BDEV_IO_TYPE_READ:
    7. /* 对于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()这个函数*/
    8. spdk_bdev_io_get_buf(bdev_io,raid_bdev_get_buf_cb,
    9. bdev_io->u.bdev.num_blocks* bdev_io->bdev->blocklen);
    10. break;
    11. case SPDK_BDEV_IO_TYPE_WRITE:
    12. /* 对于write请求,直接调用start_rw_reqeust即可,因为不需要主动申请buffer的空间*/
    13. raid_bdev->fn_table->start_rw_request(ch,bdev_io);
    14. break;
    15. /* code of handling other types I/O*/
    16. }

    start_rw_request对应的函数是:voidraid0_start_rw_request()。此函数会计算出raid的start_strip和end_strip并且调用raid0_submit_rw_request()最终完成I/O.

    static int

    1. raid0_submit_rw_request(struct spdk_bdev_io *bdev_io, uint64_tstart_strip)
    2. {
    3. /* codeabout calculating some basic information used by submitting I/O */
    4. if (bdev_io->type ==SPDK_BDEV_IO_TYPE_READ) {
    5. /* 调用之前提到的readv_blocks函数完成数据读。*/
    6. ret= spdk_bdev_readv_blocks(raid_bdev->base_bdev_info[pd_idx].desc,
    7. raid_ch->base_channel[pd_idx],
    8. bdev_io->u.bdev.iovs,bdev_io->u.bdev.iovcnt,
    9. pd_lba, pd_blocks,
    10. raid_bdev_io_completion,
    11. bdev_io);
    12. }else if (bdev_io->type ==SPDK_BDEV_IO_TYPE_WRITE) {
    13. /* 调用之前提到writev_blocks函数完成数据写。*/
    14. ret= spdk_bdev_writev_blocks(raid_bdev->base_bdev_info[pd_idx].desc,
    15. raid_ch->base_channel[pd_idx],
    16. bdev_io->u.bdev.iovs,
    17. bdev_io->u.bdev.iovcnt,
    18. pd_lba, pd_blocks,
    19. raid_bdev_io_completion,
    20. bdev_io);
    21. }else {
    22. SPDK_ERRLOG("Recvdnot supported io type %u\n", bdev_io->type);
    23. assert(0);
    24. }
    25. return ret;
    26. }

    在实现以上这些函数的基础上,raidbdev在源代码目录下新建了一个Makefile:

    1. SPDK_ROOT_DIR := $(abspath $(CURDIR)/../../..)
    2. include $(SPDK_ROOT_DIR)/mk/spdk.common.mk
    3. CFLAGS += -I$(SPDK_ROOT_DIR)/lib/bdev/
    4. C_SRCS = bdev_raid.c bdev_raid_rpc.c
    5. # 左边都是raid 目录下的源代码文件
    6. LIBNAME = bdev_raid
    7. 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

  • 相关阅读:
    等保2.0对云计算有哪些特定的安全要求?
    Bean 的生命周期总结
    Elasticsearch分布式模式下读写流程
    Spring JdbcTemplate(使用详解)
    随机访问文件类
    机器学习算法交叉验证最频繁犯的6个错误
    DevOps流程demo(实操记录)
    心动(GDI+)
    串口工作流程硬核解析,没有比这更简单的了!
    「贪心笔记」通过最少操作次数使得数组的和相等
  • 原文地址:https://blog.csdn.net/lingshengxiyou/article/details/126709905