聊聊VirtIO的数据结构 —— Split virt queue

2024-03-11 1091阅读

温馨提示:这篇文章已超过376天没有更新,请注意相关的内容是否还可用!

聊聊VirtIO的数据结构

  • 什么是VirtIO
    • 传统的数据收发包形式
    • VirtIO实现的零拷贝
    • 核心的数据结构
      • Virt Queue的作用
      • Split virtQueue的形成及数据结构
        • desc ring
        • avail ring
        • used ring

          跳槽到芯片公司干着也快小一年了,准备写几篇文章聊聊这一年耗过的技术点,实话说做的并不深入,精力大多可能都花在了跟人打交道上,纯粹技术上的探究反而很少,这里做点总结,避免一年下来啥都不明白,不知道了。

          准备先开几篇小讲讲VirtIO的数据接口,看心情继续往下写写这一年碰到的各种奇葩bug技术点吧。

          什么是VirtIO

          virtIO是Linux平台下的一种半虚拟化的实现方式,是一种实现出来的技术,并最终得到了标准化。为什么需要有VirtIO,本质上其实是虚拟化需要一个高效且标准的生产者-消费者队列实现。

          首先我们需要确认,我们所说的生产者和消费者,即虚拟device和driver是如何进行通信的。

          传统的数据收发包形式

          传统的网络数据收发包形式中,当数据包到达物理主机时,首先会被接收到物理主机的网络设备驱动程序中的内核缓冲区。然后,虚拟机监控程序(如Hypervisor)会从内核缓冲区中将数据包拷贝到虚拟机的内核缓冲区中。接下来,虚拟机的网络协议栈会对数据包进行处理。最后,数据包会从虚拟机的内核缓冲区再次拷贝到虚拟机的用户空间缓冲区,供应用程序使用。

          VirtIO实现的零拷贝

          实际virtio的实现中使用了共享内存的方法,一块由设备和驱动程序可以共同访问的内存。共享内存是VirtIO实现所谓零复制的重要方法,避免了数据的来回转换,多次拷贝。

          数据过来,写在一块内存里,接下来的工作无非是这块内存的地址如果通过hypervisor给到guest,guest能够直接访问设备的内存空间,在DMA已经比较成熟的今天已经有成熟的解决方案了,这个我们不细说。

          另一个VirtIO实现的关键点是减少了内核态和用户态的转换,这也是VirtIO形态多变,vhost、vhost-user、vdpa等等各种内核态用户态反复横跳的形态玩的特别花,这个有空我们单独讲。

          今天我们主要来看看VirtIO的数据结构,讲讲所谓的内存零拷贝是怎么实现的。

          核心的数据结构

          VirtIO是由三部分组成的 —— avail ring, desc ring, used ring

          接下来我们以virtio split为例,看看Virtio具体的设计思路和作用。

          Virt Queue的作用

          翻了一下Virtio的官方文档,发现比自己写的真的好不知道多少倍,这里直接放一下原文内容吧

          聊聊VirtIO的数据结构 —— Split virt queue

          其实最简单的就是网卡的收发包场景:

          1. 收包:driver提供一个buffer,由device进行填写,并通过中断等手段通知driver已经完成写入。
          2. 发包:driver提供一个含有数据的buffer,device(网卡设备)执行发送的指令。

          Split virtQueue的形成及数据结构

          Split virtQueue提供了desc ring, avail ring, used ring三种ring环结构,size如下大小所示,下面会详细讲讲table每个ring中的数据形式,以及数据为什么这么设计。

          聊聊VirtIO的数据结构 —— Split virt queue

          desc ring

          一句话总结:数据存储的指针放在这个环里

          全名其实叫做descripter ring,这个数据是存放真正的数据的地方。

          数据结构

          struct vring_desc {
              /* Address (guest-physical). */
              __virtio64 addr;
              /* Length. */
              __virtio32 len;
              /* The flags as indicated above. */
              __virtio16 flags;
              /* We chain unused descriptors via this, too */
              __virtio16 next;
          };
          

          address是实际的guest侧的物理地址

          length是长度

          flags是标志位,标志这个buffer是否被设备端或驱动端消费,以及其他的一些标志

          next表示下一个buffer的位置,仅在部分情况下启用。

          不难看出,desc ring是真正数据存放的地方,一个64bit的指针指向了数据存放和消费的地址。

          avail ring

          一句话总结:guest driver可用的buffer地址环! host会来这里找buffer索引,最终找到可用的buffer

          avail ring存放Decriptor Table索引,指向Descriptor Table中的一个entry。当Guest Driver向Vring中添加buffer时,可以一次添加一个或多个buffer,所有buffer组成一个Descriptor chain,Guest Driver添加buffer成功后,需要将Descriptor chain头部的地址记录到Avail Ring中,让Host端能够知道新的可用的buffer是从VRing的哪个地方开始的。Host查找Descriptor chain头部地址,需要经过两次索引Buffer Adress = Descriptor Table[Avail Ring[last_avail_idx]],last_avail_idx是Host端记录的Guest上一次增加的buffer在Avail Ring中的位置。Guest Driver每添加一次buffer,就将Avail Ring的idx加1,以表示自己工作在Avail Ring中的哪个位置。Avail Rring是Guest维护,提供给Host用

          数据结构如下:

          struct vring_avail {
             __virtio16 flags;
             __virtio16 idx;
             __virtio16 ring[];
          }; 
          

          实际上这个ring的长度可能会非常长,一般由BAR空间中的参数指定。

          flags:暂时忽视掉它!

          idx:指示Guest下一次添加buffer时的在Avail Ring所处的位置,换句话说,idx存放的ring[]数组索引,ring[idx]存放才是下一次添加的buffer头在Descriptor Table的位置。注意,avaiil ring和used ring中都在头部存放了一个指针,可以直接索引到ring[]中下一次添加buffer的位置!

          ring:存放Descriptor Table索引的环,是一个数组,长度是队列深度加1个。其中最后一个用作Event方式通知机制,见下图。VirtIO实现了两级索引,一级索引指向Descriptor Table中的元素,Avail Ring和Used Ring代表的是一级索引,核心就是这里的ring[]数组成员。二级索引指向buffer的物理地址,Descriptor Table是二级索引

          used ring

          一句话总结:host回写buffer的索引!

          host通过used提供信息,表示已经回写了guest提供的buffer。

          这样所谓avail,used就非常好理解了,avail表示内存是avail的,可以使用的,提供给host方便你填写,used表示内存已经使用过了。

          1 struct vring_avail {
          2    __virtio16 flags;
          3    __virtio16 idx;
          4    __virtio16 ring[];
          5 }; 
          

          flags:again,我们先忽略掉它!

          idx:指示Guest下一次添加buffer时的在Avail Ring所处的位置,换句话说,idx存放的ring[]数组索引,ring[idx]存放才是下一次添加的buffer头在Descriptor Table的位置

          ring:存放Descriptor Table索引的环,是一个数组,长度是队列深度加1个。其中最后一个用作Event方式通知机制,见下图。VirtIO实现了两级索引,一级索引指向Descriptor Table中的元素,Avail Ring和Used Ring代表的是一级索引,核心就是这里的ring[]数组成员。二级索引指向buffer的物理地址,Descriptor Table是二级索引

VPS购买请点击我

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

目录[+]