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

跳槽到芯片公司干着也快小一年了,准备写几篇文章聊聊这一年耗过的技术点,实话说做的并不深入,精力大多可能都花在了跟人打交道上,纯粹技术上的探究反而很少,这里做点总结,避免一年下来啥都不明白,不知道了。
准备先开几篇小讲讲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的官方文档,发现比自己写的真的好不知道多少倍,这里直接放一下原文内容吧
在这里插入图片描述
其实最简单的就是网卡的收发包场景:

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

Split virtQueue的形成及数据结构

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

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是二级索引

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值