Qemu Vhost Block架构分析(下)

来源:互联网 发布:同步助手数据还原 编辑:程序博客网 时间:2024/05/22 06:28

一. 概述


在上半部已经将GuestOS驱动与QEMU设备交互的过程描述了一下,描述的目的是便于理解Vhost-blk的工作原理,如果想从另外一个角度了解。
分享一个博文链接:http://blog.csdn.net/zhuriyuxiao/article/details/8824735 
结合这篇文章,应该可以更好的理解virtio-block的原理。

言归正传,上部分总结到,GuestOS中virtio-block驱动其实只是一个请求触发,并且要一个请求处理结果,对于GuestOS virtio-blk驱动的对外接口如下:
1. virtio-blk驱动将IO请求通过virtio_queue同步给QEMU后,通过iowrite16写一个pci地址。
2.等虚拟硬件处理完IO请求以后,将请求结果通过virtio_queue同步回来,给GuestOS一个中断,调用中断处理函数,处理IO请求的结果就OK。
既然virtio-blk对外接口我们能确定,就算使用vhost-blk,也要遵循上面的接口,才能让GuestOS驱动正常运行。
后面就围绕着vhost-blk如何完成这些工作描述它的工作原理。

二. Vhost-blk架构


按照惯例,先上图:

如上图,与上部分virtio-blk的架构图有些区别,主要还是在handle_output到Disk的部分.
从GuestOS到kernel 和 kernel到GuestOS驱动 两个黑色箭头无论是否有vhost-blk都时一样的,就是上面介绍的两个接口。
值得关注的是在kernel部分,有一个vhost-blk模块,他是在驱动层(上半部分已经提到过)。
如果QEMU开启vhost-blk,handle_output就会跳过vfs,fs等kernel层,通过vhost-blk模块直接将请求提交给硬件,所以要补充,vhost-blk开启后,QEMU后端只能是block描述符,不能是一个文件,在vhost-blk内核模块中,会检查。
当vhost-blk执行完毕会返回到QEMU,但我用白色箭头表示,意思是vhost-blk将IO请求结果返回给GuestOS,并没有真正等到内核态切回QEMU的用户态再执行,而是直接从vhost-blk层就提交了中断,至于大家好奇,怎么从内核态就能通知QEMU用户态程序触发中断给GuestOS呢,这里QEMU利用了一个巧妙的通知机制,慢慢为大家分享。


三. IO的传送流 


首先,因为上半部分已经为大家介绍过,virtio-blk是通过一个virtio_queue将IO请求同步给QEMU,当开启vhost-blk后,QEMU又将virtio_queue的数据,解析到vhost_queue的结构体中,QEMU通过vhost_queue将IO请求同步给host kernel的vhost_blk模块中,然后vhost_blk将vhost_queue的IO请求解析成一个bio,submit_io提交一个IO请求给硬件,二话不说,再上个图:

上面这个图不需要更多语言描述,看不懂就私信给我,我是不会回的^_^
广说原理也要有点依据,要依据,分享两个函数:

Fall in kernel
vhost_blk_ioctl

Virtio Queue  ----> Vhost Queue
void vhost_virtqueue_start(struct vhost_dev *dev,
                                  struct VirtIODevice *vdev,
                                   struct vhost_virtqueue *vq,
                                   unsigned idx)

Qemu Vhost Queue ---> Kernel Vhost Queue
int vhost_virtqueue_set_addr(struct vhost_dev *dev,
                                      struct vhost_virtqueue *vq,
                                    unsigned idx, bool enable_log)
第一个函数,不用说,QEMU与vhost-blk都要通过ioctl来完成。
第二个函数和第三个函数,在QEMU中,看懂自然明白。
如果您确实把这三个函数看懂了,我再上个图,你会更清晰。

这个图的左半部就是virtio-blk的机制,当增加了vhost-blk,就增加了右半部。
GuestOS与QEMU之间有个VirtioQ,而QEMU与Vhost-blk之间有个VhostQ,VirtioQ通过QEMU转化成VhostQ,用于与Vhost-Blk同步请求。
当Vhost将请求处理完,再将结果放到VhostQ中的used中后,通知QEMU给GuestOS发送一个中断,Guest中断处理函数的处理方法参考上半部分享。
那么要补充的是,VirtioQ与VhostQ的同步也只是地址的同步,并没有数据的同步,所以VhostQ中的数据,也是VirtioQ中的数据。
目前为止,一个IO请求如何通过vhost-blk提交到硬件层,应该有个大致的了解。
上图又引入两个重要函数:
Host_kick与Guest_kick,在第四章为大家分享。


四. 重要函数


1. Vhost_blk重要函数

vhost_blk_handle_guest_kick:当一个IO请求同步到vhost_blk的vhost_queue中时,vhost-blk会调用此函数,将vhost_queue中的IO请求解析成一个bio,通过submit_io提交给硬件层。注意:这个函数在vhost_blk初始化的时候注册给【work_poll->work->fn】

vhost_blk_handle_host_kick:当一个IO请求处理完毕后,会调用此函数,将IO请求的处理结果同步到vhost_queue中,也就是同步到virtio_queue中,并通知QEMU触发一个GuestOS的中断,通知GuestOS调用中断处理函数,处理IO返回的结果。注意:这个函数在vhost_blk初始化的时候注册给【blk->work->fn】

红色部分是一个遗留问题,后面为大家讲解如何在内核中通知用户程序的QEMU触发中断的。
先看vhost-blk是如何调用这两个函数的?引入了一个重要的内核线程【Vhost Worker thread】,再上个图:


如上图所示:首先紫色模块vhost_dev_set_owner,创建一个vhost_worker的线程(调用kthread_create),但不是一直运行的,使用通过唤醒函数进行唤醒。
这个线程的主要工作就是不停的在work_list取出之前被注册进来的work,调用work->fn函数,那整个流程如下:
1. 当IO请求被同步到vhost_queue中时,QEMU通过ioctl通知vhost_blk调用vhost_poll_start。
2. vhost_poll_start将poll->work通过vhost_work_queue注册给work_list,并调用唤醒函数唤醒vhost_worker线程。
3. vhost_worker取出work_list头的work,并调用work的fn,这个work->fn就是vhost_blk_handle_guest_kick。
4. 当vhost_blk_handle_guest_kick后,会调用vhost_blk_req_done。
5. vhost_blk_req_done函数将blk->work通过vhost_work_queue添加到work_list的队尾,并启动进程。
6. 与第四步一样,此时work->fn就是vhost_blk_handle_host_kick。

2. QEMU中的重要函数

到目前位置,已经描述了Vhost-blk最重要的两个函数,其中vhost_blk_handle_host_kick在处理完请求后,需要通知QEMU向GuestOS发送一个中断,那么这个通知机制是如何完成的呢?
请看下面两个函数
virtio_queue_host_notifier_read:当GuestOS的virtio_blk把IO请求同步到virtio_queue中时,会调用此函数,此函数实际就是调用handle_output去处理IO请求。
virtio_queue_guest_notifier_read:当Vhost-blk处理完请求时,通过vhost_blk_handle_host_kick发送信号,让Qemu调用此函数,为GuestOS发送一个中断。

这里必须要引入一个概念eventfd。
它是一个系统调用,它会返回一个描述符,描述符实际是一个对象,这个对象包含一个由内核维护的计数器。
当read这个描述符的时候,如果这个描述符的对象计数器大于0,read将会返回,并且将计数器清0。
当write这个描述符的时候,就是将一个uint64的数值写到计数器中。
当poll这个描述符的时候,如果计数器大于0,poll会认为它是可读的,如果计数器等于0,poll认为它是不可读的。(select亦如此)

QEMU中有个轮询io_handler的主循环,还是要贴一个链接:http://blog.csdn.net/zhuriyuxiao/article/details/8835593
简而言之,这个主循环在poll这个io_handler列表中所有的描述符,当poll返回值为真的,就会调用该描述符对应的函数。

说回来,QEMU就是分别创建了一个host_notifier和一个guest_notifier两个eventfd,并将这两个eventfd绑定上面的两个函数:
virtio_queue_host_notifier_read -> host_notifier
virtio_queue_guest_notifier_read -> guest_notifier
并将这两个eventfd注册到io_handler里面。

最关键的是,QEMU将host_notifier通过ioctl注册给内核vhost-blk中的kick。
                                    将guest_notifer通过ioctl注册给内核vhost-blk中的call。

到这,大家是不是看出点门道,当在内核vhost-blk中,给call的计数器做+1操作,QEMU中的guest_notifier就会被poll认为是可读的,io_handler主循环中就会调用virtio_queue_guest_notifier_read给GuestOS发中断。
那么vhost-blk的kick又是干什么的呢?
每次vhost-blk启动vhost_worker线程之前,都要检测一下kick是不是可读,如果可读,才会启动线程,处理vhost_queue中的IO请求,而在QEMU中的virtio_queue_host_notifier_read函数中,调用handle_output之前,需要将host_notifier的计数器清零,言外之意,就是QEMU中的handle_output与vhost-blk的vhost_blk_handle_guest_kick不能同时进行,因为他们都会操作vhost_queue,这是保证一个vhost_queue处理的完整性。


五. 总结

总结必不可少,那就是vhost-blk到底优化空间又多少,先把两张老图拿出来对比下:


第一张图是没有vhos-blk架构图,红色箭头发出一次IO请求,要走完第三张图的vfs层一直到Block Device层,而请求处理完毕后,要返回到QEMU,将IO请求的处理结果填充到Virtio_queue中,再触发中断,让GuestOS中断处理函数对IO请求的结果进行处理。

第二张图是有vhost-blk的架构图,与红色箭头处对应的是,直接跳过其他层,到Block Device Driver层,提交IO请求,当IO请求的结果到Vhost-blk驱动中,在block device driver层将请求结果填充到vhost_queue中,就通知QEMU触发一次中断,让GuestOS中断处理函数对IO请求结果进行处理。

也就是说开启vhost-blk的IO路径 从请求发出到请求结果返回,两次都有缩短。

但是!!但是优化空间到底有多少?
在上文提到过,vhost-blk开启后,QEMU后端必须是block设备(可以是逻辑卷LV),那么上面第三个图,表示QEMU后端为一个Raw文件的IO路径。
这样是不在一个基准线上的竞争,假如不开启vhost-blk,QEMU的virtio-blk后端为一个LV的话,我们知道LVM是建立在硬盘和分区之上的一个逻辑层,接近于驱动层。

大家应该明白我的结论,我接触QEMU不久,而且以上都是基于代码分析出来的,没有官网给出结论,所以有任何不同意见,欢迎提出来,大家一起把这个社区没接受的 非官方的 大家都好奇的vhost-blk搞清楚。

0 0
原创粉丝点击