spdk探秘-----块设备开发指导
这里的块设备是一种存储设备,它支持在固定大小的块中读写数据。这些块通常是512或4096字节。这些设备可能是软件中的逻辑结构,或者对应于像NVMe ssd这样的物理设备。
通用库的公共头文件是bdev.h,它是与任何类型的块设备交互所需的全部API。
除了为所有块设备提供一个通用的抽象之外,bdev层还提供了许多有用的特性:
1、I/O请求自动排队,以响应队列满或内存不足的情况
2、热删除支持,即使有I/O流量时。
3、I/O统计信息,如带宽和延迟
4、设备重启和I/O超时跟踪
struct spdk_bdev(bdev)表示一个通用块设备。struct spdk_bdev_desc,称为描述符,表示给定块设备的句柄。描述符用于建立和跟踪使用底层块设备的权限,非常文件描述符。对块设备的请求是异步的,并由spdk_bdev_io对象表示。请求必须在关联的I/O通道上提交。将从消息传递和并发两个方面阐述了I/O通道的设计和动机。
Bdevs可以分层,这样一些Bdevs通过将请求路由到其他Bdevs来服务I/O。这可以用于实现缓存、RAID、逻辑卷管理等。将I/O路由到其他bdev的bdev通常称为虚拟bdev,或简称vbdev。
初始化库
bdev层依赖于头文件include/spdk/thread.h提供的通用消息传递基础设施。最重要的是,只能通过spdk_thread_create()分配的线程才能调用bdev库。
在分配的线程中,可以通过调用spdk_bdev_initialize()来初始化bdev库,这是一个异步操作。在调用完成回调之前,不能调用其他bdev库函数。相反的要卸载bdev库,可以调用spdk_bdev_finish()。
发现块设备
所有块设备都有一个简单的字符串名称。在任何时候都可以通过调用spdk_bdev_get_by_name()获得指向设备对象的指针,也可以使用spdk_bdev_first()和spdk_bdev_next()迭代整个bdevs集合。
一些块设备也可能被赋予别名,这也是字符串名称。别名的行为类似符号链接——它们可以与真名互换使用,以查找块设备。
块设备准备
为了向块设备发送I/O请求,必须首先通过调用spdk_bdev_open_ext()来打开它。这将返回一个描述符。多个用户可能同时打开一个bdev,用户之间读写的协调必须由bdev层之上的机制来处理。如果虚拟bdev模块claimed了bdevA,那么使用写权限打开bdevA可能会失败。因为虚拟bdev模块实现像RAID或逻辑卷管理这样的逻辑,并将它们的I/O转发给较低级别的bdevA,因此它们将这些较低级别的bdevA标记为claimed的,以防止外部用户发出写操作。
当块设备打开时,必须提供回调和上下文,当bdev触发异步事件(如bdev删除)时,将使用适当的spdk_bdev_event_type enum作为参数调用它们。例如,当NVMe SSD热拔时,将对物理NVMe SSD支持的bdev的每个打开的描述符调用回调。在这种情况下,可以将回调看作是关闭描述符的请求,以便释放其他内存。当描述符存打开时不能卸载bdev,因此需要提供回调。
当用户使用完描述符时,他们可以通过调用spdk_bdev_close()来释放它。
描述符可以同时传递给多个线程并在多个线程使用。但是,对于每个线程,必须通过调用spdk_bdev_get_io_channel()来获得一个单独的I/O通道。这将为每个线程分配必要的资源,以便在不获取锁的情况下向bdev提交I/O请求。要释放一个通道,请调用spdk_put_io_channel()。在所有相关联的通道被销毁之前,描述符不能被关闭。
发送I / O
一旦获得了描述符和通道,就可以通过调用各种I/O提交函数(如spdk_bdev_read())来发送I/O。这些调用都接受一个回调函数作为参数,在后面会使用spdk_bdev_io对象的句柄来调用该回调函数。为了响应这个完成,用户必须调用spdk_bdev_free_io()来释放资源。在这个回调中,用户还可以使用函数spdk_bdev_io_get_nvme_status()和spdk_bdev_io_get_scsi_status()来获取他们选择的格式的错误信息。
I/O提交是通过调用spdk_bdev_read()或spdk_bdev_write()等函数来执行的。存储需要写入device的数据的内存必须通过spdk_dma_malloc()或它的变体来分配。在用户空间的直接内存访问(DMA)中会介绍为什么必须使用DMA来分配。在一般情况下,内存中的数据将被直接传输到块设备。这个过程零拷贝。
所有的I/O提交函数都是异步的和非阻塞的。它们不会因为任何原因阻塞或停止线程。但是,I/O提交函数可能有两种失败。首先,它们可能会立即失败并返回一个错误代码。在这种情况下,提供的回调将不会被调用。其次,它们可能异步失败。在这种情况下,相关的spdk_bdev_io将被传递给回调函数,它将报告错误信息。
有些I/O请求类型是可选的,可能不被给定的bdev支持。要查询bdev支持的I/O请求类型,请调用spdk_bdev_io_type_supported()。
重置块设备
为了处理意外的故障条件,bdev库提供了一种通过调用spdk_bdev_reset()来重置的机制。这将向bdev存在I/O通道的每个其他线程传递消息,暂停它,然后将一个复位请求转发给底层bdev模块,并等待完成。完成后,I/O通道将恢复,重置将完成。bdev模块中的特定行为是特定于模块的。例如,NVMe设备将删除所有队列对,执行一个NVMe重置,然后重新创建队列对并继续。最重要的是,无论设备类型是什么,对块设备的所有I/O都将在重置完成之前完成。
编写自定义块设备模块
这个指南是为编写自己的块设备模块以与SPDK的bdev层集成的开发人员准备的。
在SPDK中,块设备模块相当于传统操作系统中的设备驱动程序。模块提供了一组函数指针,用于服务块设备I/O请求。SPDK提供了许多块设备模块,包括NVMe、RAM-disk和Ceph RBD。但是,有些用户希望编写自己的存储,以便与自定义硬件或现有的存储软件堆栈交互。本指南旨在准确地演示如何编写模块。
创建新模块
块设备模块现在位于lib/bdev的子目录中。目前还不能将bdev模块的代码放在其他地方。要创建模块,添加一个带有单个C文件和Makefile的新目录。一个很好的途径是复制现有的“null”bdev模块。
bdev模块将与之交互的主要接口在include/spdk/bdev_module.h中。在这个头文件中定义了一个宏SPDK_BDEV_MODULE_REGISTER来注册一个新的bdev模块。这个宏采用一个指针spdk_bdev_module结构作为参数,该结构用于注册新的bdev模块。spdk_bdev_module结构描述了模块属性初始化(module_init)和卸载(module_fini)函数。可以查看struct spdk_bdev_module的文档了解更多细节。
创建Bdevs
通过调用spdk_bdev_register()在模块中创建新的bdev。新模块必须分配struct spdk_bdev,并初始化,并将它传递给寄存器调用。要初始化的最重要的字段是fn_table,它指向这个数据结构:
/*
* Function table for a block device backend.
*
* The backend block device function table provides a set of APIs to allow
* communication with a backend. The main commands are read/write API
* calls for I/O via submit_request.
*/
struct spdk_bdev_fn_table {
/* Destroy the backend block device object */
int (*destruct)(void *ctx);
/* Process the IO. */
void (*submit_request)(struct spdk_io_channel *ch, struct spdk_bdev_io *);
/* Check if the block device supports a specific I/O type. */
bool (*io_type_supported)(void *ctx, enum spdk_bdev_io_type);
/* Get an I/O channel for the specific bdev for the calling thread. */
struct spdk_io_channel *(*get_io_channel)(void *ctx);
/*
* Output driver-specific configuration to a JSON stream. Optional - may be NULL.
*
* The JSON write context will be initialized with an open object, so the bdev
* driver should write a name (based on the driver name) followed by a JSON value
* (most likely another nested object).
*/
int (*dump_config_json)(void *ctx, struct spdk_json_write_ctx *w);
/* Get spin-time per I/O channel in microseconds.
* Optional - may be NULL.
*/
uint64_t (*get_spin_time)(struct spdk_io_channel *ch);
};
bdev模块必须实现这些函数回调。
当系统不再需要该设备时,调用析构函数来拆除该设备。destruct所做的是由模块决定:可能只是释放内存,也可能是关闭一块硬件。
io_type_supported函数返回是否支持特定的I/O类型。可用的I/O类型有:
enum spdk_bdev_io_type {
SPDK_BDEV_IO_TYPE_INVALID = 0,
SPDK_BDEV_IO_TYPE_READ,
SPDK_BDEV_IO_TYPE_WRITE,
SPDK_BDEV_IO_TYPE_UNMAP,
SPDK_BDEV_IO_TYPE_FLUSH,
SPDK_BDEV_IO_TYPE_RESET,
SPDK_BDEV_IO_TYPE_NVME_ADMIN,
SPDK_BDEV_IO_TYPE_NVME_IO,
SPDK_BDEV_IO_TYPE_NVME_IO_MD,
SPDK_BDEV_IO_TYPE_WRITE_ZEROES,
};
对于最简单的bdev模块,只需要SPDK_BDEV_IO_TYPE_READ和SPDK_BDEV_IO_TYPE_WRITE。SPDK_BDEV_IO_TYPE_UNMAP通常被称为“trim”或“deallocate”,它是一个将一组块标记为不再包含有效数据的请求。SPDK_BDEV_IO_TYPE_FLUSH请求使之前完成的所有写操作具有持久性,许多设备不需要flush。SPDK_BDEV_IO_TYPE_WRITE_ZEROES就像常规的写操作,但是不提供数据缓冲区(它只包含所有的0)。如果不支持它,一般的bdev代码可以通过发送常规的写请求来模拟它。
SPDK_BDEV_IO_TYPE_RESET是一个中止所有I/O并将底层设备返回到初始状态的请求。在以某种方式完成所有I/O之前,不要完成重置请求。
SPDK_BDEV_IO_TYPE_NVME_ADMIN、SPDK_BDEV_IO_TYPE_NVME_IO_MD和SPDK_BDEV_IO_TYPE_NVME_IO_MD都是通过SPDK bdev层传递原始NVMe命令的机制。它们是严格可选的,可能只有在后备存储设备能够处理NVMe命令时才有意义。
get_io_channel函数应该返回一个I/O通道。通用bdev层将每个线程调用一次get_io_channel,缓存结果,并将结果传递给submit_request。它将为调用submit_request的线程使用相应的通道。
submit_request函数被调用来实际地向块设备提交I/O请求。I/O请求完成后,模块必须调用spdk_bdev_io_complete()。I/O不必在submit_request的调用上下文中完成。
创建虚拟Bdevs
如果块设备A通过将I/O路由到其他块设备B来处理I/O请求,则认为A是虚拟的。典型的例子是实现RAID的bdev模块就是个虚拟模块。虚拟bdev的创建方式与常规bdev相同,但需要额外的一个步骤。虚拟模块可以使用spdk_bdev_get_by_name()查找它希望将I/O路由到的底层bdevs,其中字符串名称由用户在配置文件中或通过RPC提供。然后通过打开底层bdev获取描述符,并为bdev创建I/O通道(可能是对get_io_channel回调的响应),模块可以正常运行。最后一步是让虚拟模块用其底层块设备描述符来调用spdk_bdev_module_claim_bdev(),表明该虚拟模块正在使用底层的bdev。这将防止其他用户在该底层模块上打开具有写权限的描述符。这有效地将描述符“提升”为写独占,并且这是一个仅对bdev模块可用的操作。
上一篇: [MIT 6.824: Distributed Systems] LEC 1: Introduction之Preparation
下一篇: Introduction NFS, or Network File System, is a distributed filesystem protocol that allows you to m
推荐阅读