This programming guide is intended for developers authoring their own block device modules to integrate with SPDK's bdev layer. For a guide on how to use the bdev layer, see Block Device Layer Programming Guide.
A block device module is SPDK's equivalent of a device driver in a traditional operating system. The module provides a set of function pointers that are called to service block device I/O requests. SPDK provides a number of block device modules including NVMe, RAM-disk, and Ceph RBD. However, some users will want to write their own to interact with either custom hardware or to an existing storage software stack. This guide is intended to demonstrate exactly how to write a module.
Block device modules are located in subdirectories under module/bdev today. It is not currently possible to place the code for a bdev module elsewhere, but updates to the build system could be made to enable this in the future. To create a module, add a new directory with a single C file and a Makefile. A great starting point is to copy the existing 'null' bdev module.
The primary interface that bdev modules will interact with is in include/spdk/bdev_module.h. In that header a macro is defined that registers a new bdev module - SPDK_BDEV_MODULE_REGISTER. This macro take as argument a pointer spdk_bdev_module structure that is used to register new bdev module.
The spdk_bdev_module structure describes the module properties like initialization (
module_init) and teardown (
module_fini) functions, the function that returns context size (
get_ctx_size) - scratch space that will be allocated in each I/O request for use by this module, and a callback that will be called each time a new bdev is registered by another module (
examine_disk). Please check the documentation of struct spdk_bdev_module for more details.
New bdevs are created within the module by calling spdk_bdev_register(). The module must allocate a struct spdk_bdev, fill it out appropriately, and pass it to the register call. The most important field to fill out is
fn_table, which points at this data structure:
The bdev module must implement these function callbacks.
destruct function is called to tear down the device when the system no longer needs it. What
destruct does is up to the module - it may just be freeing memory or it may be shutting down a piece of hardware.
io_type_supported function returns whether a particular I/O type is supported. The available I/O types are:
For the simplest bdev modules, only
SPDK_BDEV_IO_TYPE_WRITE are necessary.
SPDK_BDEV_IO_TYPE_UNMAP is often referred to as "trim" or "deallocate", and is a request to mark a set of blocks as no longer containing valid data.
SPDK_BDEV_IO_TYPE_FLUSH is a request to make all previously completed writes durable. Many devices do not require flushes.
SPDK_BDEV_IO_TYPE_WRITE_ZEROES is just like a regular write, but does not provide a data buffer (it would have just contained all 0's). If it isn't supported, the generic bdev code is capable of emulating it by sending regular write requests.
SPDK_BDEV_IO_TYPE_RESET is a request to abort all I/O and return the underlying device to its initial state. Do not complete the reset request until all I/O has been completed in some way.
SPDK_BDEV_IO_TYPE_NVME_IO_MD are all mechanisms for passing raw NVMe commands through the SPDK bdev layer. They're strictly optional, and it probably only makes sense to implement those if the backing storage device is capable of handling NVMe commands.
get_io_channel function should return an I/O channel. For a detailed explanation of I/O channels, see Message Passing and Concurrency. The generic bdev layer will call
get_io_channel one time per thread, cache the result, and pass that result to
submit_request. It will use the corresponding channel for the thread it calls
submit_request function is called to actually submit I/O requests to the block device. Once the I/O request is completed, the module must call spdk_bdev_io_complete(). The I/O does not have to finish within the calling context of
Integrating a new bdev module into the build system requires updates to various files in the /mk directory.
A User can build their own bdev module and application on top of existing SPDK libraries. The example in test/external_code serves as a template for creating, building and linking an external bdev module. Refer to test/external_code/README.md and Linking to Shared Objects for further information.
Block devices are considered virtual if they handle I/O requests by routing the I/O to other block devices. The canonical example would be a bdev module that implements RAID. Virtual bdevs are created in the same way as regular bdevs, but take the one additional step of claiming the bdev.
The module can open the underlying bdevs it wishes to route I/O to using spdk_bdev_open_ext(), where the string name is provided by the user via an RPC. To ensure that other consumers do not modify the underlying bdev in an unexpected way, the virtual bdev should take a claim on the underlying bdev before reading from or writing to the underlying bdev.
There are two slightly different APIs for taking and releasing claims. The preferred interface uses
spdk_bdev_module_claim_bdev_desc(). This method allows claims that ensure there is a single writer with
SPDK_BDEV_CLAIM_READ_MANY_WRITE_ONE, cooperating shared writers with
SPDK_BDEV_CLAIM_READ_MANY_WRITE_SHARED, and shared readers that prevent any writers with
SPDK_BDEV_CLAIM_READ_MANY_WRITE_NONE. In all cases,
spdk_bdev_open_ext() may be used to open the underlying bdev read-only. If a read-only bdev descriptor successfully claims a bdev with
SPDK_BDEV_CLAIM_READ_MANY_WRITE_SHARED the bdev descriptor is promoted to read-write. Any claim that is obtained with
spdk_bdev_module_claim_bdev_desc() is automatically released upon closing the bdev descriptor used to obtain the claim. Shared claims continue to block new incompatible claims and new writers until the last claim is released.
The non-preferred interface for obtaining a claim allows the caller to obtain an exclusive writer claim with
spdk_bdev_module_claim_bdev(). It may be be released with
spdk_bdev_module_release_bdev(). If a read-only bdev descriptor is passed, it is promoted to read-write. NULL may be passed instead of a bdev descriptor to avoid promotion and to block new writers. New code should use
spdk_bdev_module_claim_bdev_desc() with the claim type that is tailored to the virtual bdev's needs.
The descriptor obtained from the successful spdk_bdev_open_ext() may be used with spdk_bdev_get_io_channel() to obtain I/O channels for the bdev. This is likely done in response to the virtual bdev's
get_io_channel callback. Channels may be obtained before and/or after claiming the underlying bdev, but beware there may be other unknown writers until the underlying bdev has been claimed.
When a virtual bdev module claims an underlying bdev from its
examine_config callback, it causes the
examine_disk callback to only be called for this module and any others that establish a shared claim. If no claims are taken by
examine_config callbacks, all virtual bdevs'
examine_disk callbacks are called.