本文以PureFlash为例,介绍了如何将一个新的存储类型对接到qemu虚拟化平台下,为虚机提供存储能力。
关于virtio-blk以及其工作原理这里就不介绍了,网上有很多分析的文章。总之就是如果我们想给虚机提供一种新的存储类型(不同于标准的块设备,文件,或者iSCSI等linux内置接口 ),我们就需要为qemu增加新的virtio-blk后端驱动来达到这个目的。典型的比如ceph rbd驱动.
说到virtio协议,其核心是通过virtio queue在guest-host系统之间进行交互,这方面的资料目前还是比较多的。不过这部分对于我们开发后端驱动而言其实不需要触碰,qemu已经定义了一套更易理解的接口,实现方按部就班实现这些API接口就可以了。
virtio-blk后端接口有两种模式,旧的接口使用线程模式,新的接口使用协程模式。这里的新旧是指qemu的演进过程。这里我们使用协程模式。
一、virioblk IO接口
virtio-blk接口的关键是在自己的代码中定义并填充struct BlockDriver 结构体,通过这个结构体向qemu传递自己这个后端实现的信息以及API实现。我们先对这个结构体里的几个关键字段做下说明:
BlockDriver字段 | 解释 |
---|---|
const char *format_name | 驱动名称 |
int instance_size | 实例化一个块设备/虚拟盘,这个设备对象的大小(不是盘的大小,是表示这个盘的内存结构体大小) |
const char *protocol_name | 在qemu启动的命令行上,通过<protocol>:filename 指定一个后端设备后,通过这个名字qemu才能知道对于一个块设备调用哪个驱动。通常这个名字设置成和format_name相同 |
int GRAPH_UNLOCKED_PTR (*bdrv_file_open)( BlockDriverState *bs, QDict *options, int flags, Error **errp); |
用于为qemu打开一个块设备,也就是当qemu需要创建一个新的块设备时首先调用这个函数进行打开操作 |
void (*bdrv_parse_filename)(const char *filename, QDict *options, Error **errp); | 把filename 字符串解析成方便使用的参数字典,即QDict类型的options。 通常qemu会接收一个复杂的字符串作为文件名,比如 "pfbd:test_v1", 其中pfbd是protocol_name, 后半部分是volume name。这个函数将解析后的结果保存到options中。后面这个options被传递给brv_file_open函数使用。 |
int coroutine_fn GRAPH_RDLOCK_PTR (*bdrv_co_preadv)(BlockDriverState *bs, int64_t offset, int64_t bytes, QEMUIOVector *qiov, BdrvRequestFlags flags); |
读操作函数,实现这个函数以提供读操作。 BlockDriver接口里定义了多个读操作函数,实现方只需要实现其中的一个即可。 |
int coroutine_fn GRAPH_RDLOCK_PTR (*bdrv_co_pwritev)( BlockDriverState *bs, int64_t offset, int64_t bytes, QEMUIOVector *qiov, BdrvRequestFlags flags); |
写操作函数,和上面的读操作函数类似原理。 |
BlockDriver接口里定义了多个读写操作函数, 比如 bdrv_aio_preadv, bdrv_co_readv, bdrv_co_preadv, bdrv_co_preadv_part 都可以用来实现读操作,实现方根据自己的情况实现任意一个函数就可以。
这样就明确了,实现virtio-blk后端驱动的重点就是实现一个read函数和一个write函数。本文以实现bdrv_co_preadv、bdrv_co_pwritev为例。正如名字里的co暗示的,这两个函数以协程的模式工作,协程模型也是qemu新版本力推。
相比业界一些专门的协程库(比如 Argobots,...),qemu里的协程是一个非常轻量化的协程实现,只有创建协程,进入协程,出让执行权这几个操作。其他的甚至基本的锁操作都没有。
二、IO代码路径
当虚拟机里的guest OS执行IO操作时,首先把IO请求放入到virtio-queue里面,然后执行一个kick动作。这个kick动作就是一个对PCIe寄存器的写入操作:
但是虚拟设备是没有PCIe硬件存在的,这个写入操作引发VM_EXIT,进入到KVM的代码,KVM之后通过eventfd通知qemu,触发qemu的主事件循环里处理:
其中blk_aio_prwv的代码如下,这里创建IO协程:
我们编写的IO接口函数就是在这个协程里面被调用的:
三、接口实现
前面的IO路径已经跟到了pfbd_co_pwritev函数,作为接口实现的工作就是提供实现这一函数。
PureFlash项目提供了用户态的接口库libs5common.a , 这里提供了volume的打开,读写等操作函数。这些函数都是用户态API,用于对接qemu, TCMU( LIO的用户态后端实现,提供iSCSI接口), nbd, ViveNAS等访问。这些函数中用于进行读写操作函数是pf_io_submit/pf_iov_submit。
/**
* Submit IO vector.
* @param volume, the volume to submit IO
* @param iov, io vector
* @param iov_cnt, iovec count in param iov
* @param length, total length of iovs
* @param offset, offset in volume, of this IO to perform in volume
* @param callback, the call back handler function to call when IO complete_status
* @param cbk_arg, user defined data to passed to callback when IO complete. PureFlash system will not touch this argument
* @param is_write, indict this IO is WRITE or READ. 1 for WRITE IO; 0 for READ IO
* @return 0 on success, negative number to indicate submit error.
* @retval -EINVAL argument invalidate, length too large or offset not aligned on sector(512B)
* @retval -EAGAIN device is temporary busy, caller can retry this IO later
*
*/
int pf_iov_submit(struct PfClientVolume* volume, const struct iovec *iov, const unsigned int iov_cnt, size_t length, off_t offset,
ulp_io_handler callback, void* cbk_arg, int is_write);
和大多数异步操作函数一样,pf_iov_submit的参数里包含了一个回调函数用于在IO完成时调用。但是Qemu的IO接口函数bdrv_co_preadv/bdrv_co_pwritev期望的行为是同步调用。因此接口实现时的一个重要功能就是实现异步/同步行为转换:
//这个函数在Qemu的IO线程中执行
static void pfbd_rw_cb_bh(void* opaque)
{
PfbdCoData* data = opaque;
data->complete=true;
qemu_coroutine_enter(data->co); //唤醒处于等待中的IO 协程
}
//这个回调函数在PureFlash s5common库的事件循环线程中调用
static void pfbd_rw_cb(void* opaque, int ret)
{
PfbdCoData* data = opaque;
data->ret = ret;
//调度一个IO后半段操作(Behand half)用于唤醒协程
aio_bh_schedule_oneshot(bdrv_get_aio_context(data->bds),
pfbd_rw_cb_bh, data);
}
static coroutine_fn int pfbd_co_preadv(BlockDriverState* bs,
int64_t offset, int64_t bytes,
QEMUIOVector* qiov, BdrvRequestFlags flags)
{
int r;
BDRVPfbdState* s = bs->opaque;
PfbdCoData data = {
.ret = -EINPROGRESS,
.co= qemu_coroutine_self() ,
.complete = false,
.bds = bs,
};
// 调用PureFlash的API提交IO操作,传递异步操作回调函数 pfbd_rw_cb。
// IO完成时 pfbd_rw_cb会被调用。可以看到这里不几乎不需要对其他参数做
// 修改适配,因为虚拟机是PureFlash设计适用的重要场景,API的参数已经根
// 据qemu接口做了尽可能适配
r = pf_iov_submit(s->vol, qiov->iov, qiov->niov, bytes, offset, pfbd_rw_cb, &data, 0);
if(r){
error_report("submit IO error, rc:%d", r);
return r;
}
//等待IO完成
while (!data.complete) {
qemu_coroutine_yield(); //出让协程CPU等待唤醒,因此这里不是忙等待
}
return data.ret;
}
通过上面的方法实现了IO路径,接下来要把驱动注册到qemu里
static const char *const qemu_pfbd_strong_runtime_opts[] = {
"volume",
"conf",
"snapshot",
NULL
};
static BlockDriver bdrv_pfbd = {
.format_name = "pfbd",
.instance_size = sizeof(BDRVPfbdState),
.bdrv_parse_filename = qemu_pfbd_parse_filename,
.bdrv_file_open = qemu_pfbd_open,
.bdrv_close = qemu_pfbd_close,
.bdrv_co_get_info = qemu_pfbd_co_getinfo,
.create_opts = &qemu_pfbd_create_opts,
.bdrv_co_getlength = qemu_pfbd_co_getlength,
.protocol_name = "pfbd",
.bdrv_co_preadv = pfbd_co_preadv,
.bdrv_co_pwritev = pfbd_co_pwritev,
.strong_runtime_opts = qemu_pfbd_strong_runtime_opts,
};
static void bdrv_pfbd_init(void)
{
bdrv_register(&bdrv_pfbd);
}
block_init(bdrv_pfbd_init);
三、使用方法
使用下面的命令在启动qemu时使用PureFlash 卷
qemu/build//qemu-system-x86_64 -enable-kvm -cpu host -smp 1 -drive if=ide,file=./centos8.qcow2,cache=none \
-drive if=virtio,file=pfbd:test_v1,cache=none \
-m 4G -vnc :12 \
-nic user,hostfwd=tcp::5555-:22
这个命令里面为qemu挂载了test_v1这个卷作为数据盘。
在虚机运行之前需要启动PureFlash集群(单个容器也可以提供服务),并创建名字为test_v1的卷,这个操作可以参考PureFlash存储系统介绍与上手指南-CSDN博客
本文里完整的代码: block/pfbd.c · cocalele/qemu - 码云 - 开源中国 (gitee.com)