qemu-virtio基本原理

virtio是相当复杂的,网上写virtio原理解析的文章也不少,这里我想通过最简练易懂的方式来解释一下virtio的原理。一方面也完善一下自己对virtio的理解,文中含有大量个人理解,如果发现有错误的地方欢迎与我交流。

virtio整体流程是怎样的?

盗用网上的一张图:
在这里插入图片描述

一开始我也看不明白这里面都画了些啥东西,后面慢慢抽丝剥茧吧。

管道的原理

国际惯例先简单介绍一下管道的基本实现原理,其实管道就是一个基本的生产者消费者模型,一般都是基于共享内存来实现的,在两个进程中映射好共享内存的地址然后处理好同步问题即可。在各种管道中尤其以环形队列的同步开销最小,在单生产者的情况下甚至可以做到无锁。在虚拟化环境下的管道与平时用的管道稍稍有点不同,其实也就是共享内存的访问方式不同而已,具体可以参考我的另一篇文章goldfish pipe,这个算是最简单的虚拟化管道了。

virtio是如何实现高性能的?

大家都知道virtio以共享内存的零拷贝实现高性能的,理解virtio内存共享是理解virtio的基础。
virtio实现共享内存基于两个基本的条件:

  • qemu或者说host可以访问guest的任意内存地址的内容。
  • qemu知道guest中物理地址在host内存中的转换关系。

基于以上两点,只需要在guest中申请一块连续的内存空间,然后将内存首地址与长度告诉qemu,之后qemu就知道怎样获取这块内存的数据了。这里又基于一个前提条件:连续的内存空间。我们知道在虚拟化中、内存被分割的最小单位是页,所以只要按页对其且不超过页大小申请到的内存在host中也是连续的。所以在guest内核中按页申请内存buffer即可。

介绍一下virtio的主要组成部分吧

virtio主要由以下几个部分组成:
buffer:数据传输的载体
descriptor table:一个数组,包含buffer地址和下个元素的index。
avail ring:抽象管道前端,guest中维护的一个结构,host不可见
used ring:抽象管道后端,host中维护的一个结构,guest不可见

如果将virtio比作一个双向管道,那个管道的组成部分如下:
管道A端:avail ring
管道B端:used ring
管道的传输载体:buffer

所以这里引出virtio的另一个组成部分:
virtqueue:virtio的一个管道,或者说是一个channel,一个virtio可以有多个virtqueue组成,上限是1024个

下面先通过几幅图慢慢的勾勒出virtio的样子。

最简单的管道

在这里插入图片描述
这个管道我画得像goldfish pipe了,可以参考()[],这里只要host拿到guest matedata的起始地址就可以进行数据传输了。实现方式比较简单粗暴。

环形队列管道

上面的简单管道在做数据传输的时候不能实现异步通信,guest必须每次等到host处理完管道中的数据之后才可以进行下一次传输,下面我们使用环形队列来改善这种状况。
在这里插入图片描述

数据分离的环形队列管道

我们继续来抽象这个管道,吧环形队列分离出来
在这里插入图片描述
这里修改了一些对象的名字,让这幅图慢慢接近virtio的样子。图中吧环单独抽离出来成为avail ring,avail ring中包含最基本的两个元素:idx,ring[]。idx指向最新ring[]中的下标。ring[]中的元素又指向vring_desc数组中的下标,所以这里不再是直接采用地址指向了,而是采用下标的方式。virtio就是采用了这种两级索引的方式。将环形队列单独抽离出来以后avail ring就自成一体,vring_desc就不再是环形队列了,这时候vring_desc退化成为一个可用内存池。

完整的数据分离的环形队列管道

上面的环形管道少了消费者这一端的环,下面把它补上。
在这里插入图片描述

这里已经比较接近virtio实现的样子了,在host中有自己的used ring来描述自己的队列状态。在used ring中多了一个len字段,用来标识现在工作在buffer的那个位置了。avail ring的所有数据host都可以很容易的感知到,但是used ring的状态guest却无法感知,只能通过vm_enter的时候将used ring的idx带入虚拟机。下面看一下virtio最终的实现样子。

virtio的环形队列管道

为了贴近最开头那张图,我画了下面这幅。其实这里面缺少了很多信息,导致这张图有很多错误,具体我就不解释这张图了。
在这里插入图片描述

既然最开头那张图缺少细节,那我就给它添加亿点细节吧。

既然最开头那张图缺少细节,那我就给它添加亿点细节吧。既然最开头那张图缺少细节,那我就给它添加亿点细节吧。在这里插入图片描述

这就是最终的样子了,guest和host中的内存状态都画出来了。首先virtio通过virtqueue保存管道,前后端通过id号保持管道的连通性guest中保存了avail ring和used ring两个环形队列,通过vring将环形队列和buffer池子组织在了一起。管道的编号最终抽离到了virtqueue中,这里面当然不止包含index,这里我忽略了亿点细节。最后vring_virtqueue将vring和virtqueue组织在了一起。guest到host的通信是靠向pci的一个config空间写入管道id引起vmexit到host的。host中使用管道id找到正确的VirtQueue,host中的VRing保存了desc、used、avail也就是内存池、使用队列、可获得队列的地址,这里我使用的虚线来画的,因为这里保存的地址是guest中的物理地址,也就是GPA。为了方便对guest中desc、used、avail的访问,qemu在VRing中添加了一个caches来保存他们在主机上的虚拟地址,也就是HVA,所以VRingMemoryRegionCaches中的desc、used、avail是转换过的HVA,我直接用实线表示的。到这里大概会对virtio大致原理有一个基本的了解了。

virtio基本的数据传输过程是怎样的?

  1. host中先初始化好virtqueue,里面描述了管道的队列长度等信息
  2. guest收到virtqueue后开始前端初始化
  3. guest填充buffer并更新avail ring
  4. host取用buffer写入数据更新used ring
  5. guest读取到host写入的数据

guest与host是怎样进行元数据交换的?

我们知道虚拟机一次vm-exit最多只能带出64位数据,最多一个uint64大小。那么virtio这样一个环形队列肯定有很多数据需要同步,如果通过vm-exit这个通道肯定太窄了,VCPU一次vm-exit代价相当高的。所以virtio的guest与host的数据同步其实是通过vm-exit传递virt-queue编号,也就是管道号,或者channel号。之后host通过共享内存的方式从guest中读取出descriptor table、avail、used等其他数据。

介绍一下virtio使用中常用的数据传输结构

iov信息:iov是host中描述buffer的一种数据结构,保存了buffer在的起始地址、大小、和所有的内存页。
buffer的拷贝:在host中使用iov_to_buf函数通过iov信息将buffer内容从guest内存拷贝到host内存中。除此之外你也可以使用iov信息自己读取buffer中的数据实现零拷贝。

再完整解释一下virtio的全部数据结构吧

buffer是guest中事先申请好的,avail ring与used ring都存在guest内存中,通过virtqueue中的指针就可以访问,对于qemu来说这个指针的地址需要转换一下,整个流程我再补一张图。

  • 4
    点赞
  • 55
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值