背景
在spdk 中, bdev 承上启下,隔离了具体的设备和IO模式,对vdev 等上层抽象出了统一的框架。了解它对外的接口、功能、框架,才可能分析它的优缺点。
spdk bdev对外的接口
实现spdk_bdev_write/spdk_bdev_read/spdk_bdev_open/spdk_bdev_close 等操作。
参考:https://spdk.io/doc/bdev.html
接口代码在: bdev.h 中
spdk bdev的重要数据结构
参考:https://spdk.io/doc/bdev_module.html
和注册驱动类似,首先需要实现系列操作接口。以bdev libaio接口为例,相关的重要接口包括:
bdev 公共接口: spdk_bdev_module
tatic struct spdk_bdev_module aio_if = {
.name = "aio",
.module_init = bdev_aio_initialize,
.module_fini = bdev_aio_fini,
.config_text = bdev_aio_get_spdk_running_config,
.get_ctx_size = bdev_aio_get_ctx_size,
};
这里类似注册Linux 驱动接口。
bdev 操作接口: spdk_bdev_fn_table
static const struct spdk_bdev_fn_table aio_fn_table = {
.destruct = bdev_aio_destruct,
.submit_request = bdev_aio_submit_request,
.io_type_supported = bdev_aio_io_type_supported,
.get_io_channel = bdev_aio_get_io_channel,
.dump_info_json = bdev_aio_dump_info_json,
.write_config_json = bdev_aio_write_json_config,
};
同样,这里类似注册了驱动设备的一组操作。具体针对linux 块设备的libaio读写操作,被放到了上面 bdev_aio_submit_request 的操作中。比如bdev_aio_submit_request最终会调用libaio的io_submit()操作。那么收割操作放到哪呢?
spdk bdev 的框架流程
上面问题实际就涉及到了spdk bdev 的IO 提交、事件完的框架了。
框架中IO如何提交
主要的调用关系如下:(上面的被下面调用)
* bdev->fn_table->submit_request(ch->channel, bdev_io);
* _spdk_bdev_qos_io_submit(struct spdk_bdev_channel *ch, struct spdk_bdev_qos *qos)
* _spdk_bdev_io_submit
* spdk_bdev_io_submit
* spdk_bdev_read_blocks/spdk_bdev_readv_blocks/spdk_bdev_write_blocks/spdk_bdev_writev_blocks/spdk_bdev_write_zeroes_blocks/spdk_bdev_unmap_blocks
框架中IO如何收割
- io_getevents (bdev_aio.c)
- bdev_aio_group_poll
- bdev_aio_group_create_cb:ch->poller = spdk_poller_register(bdev_aio_group_poll, ch, 0);
这里特别需要注意:上面基于收割函数注册了一个spdk poller。spdk 的poller 可以理解成一个定时执行的Timer线程。这通过它数据结构可以看到:
struct spdk_poller {
TAILQ_ENTRY(spdk_poller) tailq;
/* Current state of the poller; should only be accessed from the poller's thread. */
enum spdk_poller_state state;
uint64_t period_ticks;
uint64_t next_run_tick;
spdk_poller_fn fn;
void *arg;
};
上面spdk_poller_fn 就是前面注册的bdev_aio_group_poll, 而period_ticks、next_run_tick决定了下次该函数执行的时刻。
收割完的IO的回调如何执行
- bdev_io->internal.cb(bdev_io, bdev_io->internal.status == SPDK_BDEV_IO_STATUS_SUCCESS,..);
- _spdk_bdev_io_complete(void *ctx)
- spdk_bdev_io_complete()
- spdk_bdev_io_complete_scsi_status() or spdk_bdev_io_complete_nvme_status()
以spdk_bdev_io_complete_nvme_status为例,它就是用来执行完一个bdev io,并且带回一个返回值和一个完成队列里的一个entry。
spdk bdev框架的主要原理
SPDK的一个重要理念就是run-to-completion的,保证IO尽量在一个线程上处理。通过分析IO提交和收割(poller)操作如果在一个线程上执行,能够帮助我们理解这一点。
spdk run-to-completion的基础:spdk_thread
理解spdk thread 是理解bdev 机制的关键。而bdev里最需要理解的数据结构是spdk_thread:
struct spdk_thread {
TAILQ_HEAD(, spdk_io_channel) io_channels;//用来表示特定于线程的队列
TAILQ_ENTRY(spdk_thread) tailq;
char *name;
uint64_t tsc_last;
struct spdk_thread_stats stats;
/*
* Contains pollers actively running on this thread. Pollers
* are run round-robin. The thread takes one poller from the head
* of the ring, executes it, then puts it back at the tail of
* the ring.
*/
TAILQ_HEAD(, spdk_poller) active_pollers;//执行各种定期的完成IO收割、统计操作,这个和收割相关
/**
* Contains pollers running on this thread with a periodic timer.
*/
TAILQ_HEAD(timer_pollers_head, spdk_poller) timer_pollers;
struct spdk_ring *messages; // 用来接受其他线程传递过来的IO请求,加入待处理队列, 这个和IO提交相关
SLIST_HEAD(, spdk_msg) msg_cache;
size_t msg_cache_count;
};
bdev IO提交的时候最后通常会走到
spdk_thread_send_msg,它把传过来的 (const struct spdk_thread *thread, spdk_msg_fn fn, void *ctx)三元组,组装成下面的数据结构,最后插入到上面的messages 队列。 (最关键的点就在这里:
1. 通过把操作放入队列实现了请求的异步执行;2. 由于队列是无锁队列,实现了无锁并发)
struct spdk_msg {
spdk_msg_fn fn;
void *arg;
SLIST_ENTRY(spdk_msg) link;
};
而在bdev初始化的时候,会把实际设备或者IO模式的收割函数(比如libaio的get_event())注册到下面的poller 数据结构:
struct spdk_poller {
TAILQ_ENTRY(spdk_poller) tailq;
/* Current state of the poller; should only be accessed from the poller's thread. */
enum spdk_poller_state state;
uint64_t period_ticks;
uint64_t next_run_tick;
spdk_poller_fn fn;
void *arg;
};
基于spdk_thread实现run-to-completion
bdev 提供了异步提交和收割的线程模型。基于类似定时器的spdk poller机制,可以实现周期地收割完成到IO事件;基于SPDK 消息和无锁队列机制,提交线程通过spdk_thread_send_msg()把IO请求加入处理队列,然后在统一的线程(spdk_thread_poll(struct spdk_thread *thread, uint32_t max_msgs, uint64_t now)中批量提交(msg_count = _spdk_msg_queue_run_batch(thread, max_msgs);)。提交一批即开始收割一批完成的IO事件(timer_rc = poller->fn(poller->arg);)。
由于提交和收割都在同一个函数里面,因此这也就实现了run-to-completin。