说到前后端就要提到virtio,virtio是IBM提出的实现虚拟机内部和宿主机之前数据交换的一种方式,与全虚拟化方式比较(即通过qemu完全模拟设备的方式),性能有了较大的提升。
简单来讲,在virtio体系中分为前端驱动和后端驱动两个部分。前端驱动我们一般可以理解为虚拟机内部的虚拟网卡的驱动,当然Windows和Linux的驱动是不同的;后端驱动virtio是宿主机上的部分,实现可能会有不同的方式,如内核模式的vhost,用户态vhost-user等等,但是本质的功能是类似的,就是将前端驱动发出的报文转发到后端虚拟设备上,同理将收到的报文传入实例内的前端驱动。
总的来说,virtqueue 即前后端环形队列进行数据交换的实际数据链路。guest 把 buffers 插入其中,每个 buffer 都是一个分散-聚集数组。驱动调用 find_vqs()来创建一个与 queue 关联的结构体。virtqueue 的数目根据设备的不同而不同。network 设备有 2 个 virtqueue,一个用于发送数据包,一个用于接收数据包。
virtqueue由 描述符列表(descriptor table)、可用环表(available ring)、已用环表(used ring)3部分组成:
struct vring {
unsigned int num;
struct vring_desc *desc;
struct vring_avail *avail;
struct vring_used *used;
}
这三个队列都是固定长度的环形队列,当然实现仅仅是对相应索引号对最大长度去余而已。下面这张图形象地表明三个队列和前后端驱动的关系:
virtqueue:数据结构以及通信机制
我们以 前端的发送队列为例,注意所有的结构信息都是在虚拟机内部可见的,可以通过core dump查看:
struct vring_virtqueue {
vq = {
list = {
next = 0xffff881027e3d800,
prev = 0xffff881026d9b000
},
callback = 0xffffffffa0149450,
name = 0xffff881027e3ee88 "output.0", ->>表明是发送队列
vdev = 0xffff881023776800,
priv = 0xffff8810237d03c0
},
vring = {
num = 256, ->>所有的队列长度
desc = 0xffff881026d9c000, ->> desc队列
avail = 0xffff881026d9d000, ->> avail队列
used = 0xffff881026d9e000 ->> used队列
},
broken = false,
indirect = true,
event = true,
num_free = 0, ->> 队列目前有多少空闲元素了,如果已经为0表明队列已经阻塞,前端将无法发送报文给后端
free_head = 0, ->> 指向下一个空闲的desc元素
num_added = 0, ->>是最近一次操作向队列中添加报文的数量
last_used_idx = 52143, 这是前端记录他看到最新的被后端用过的索引(idx),是前端已经处理到的used队列的idx。前端会把这个值写到avail队列的最后一个元素,这样后端就可以得知前端已经处理到used队列的哪一个元素了。
<> ->> last_avail_idx 前端不会碰,而且前端的virtqueue结构里就没有这个值,这个代表后端已经处理到avail队列的哪个元素了,前端靠这个信息来做限速,后端是把这个值写在used队列的最后一个元素,这样前端就可以读到了。
notify = 0xffffffffa005a350,
queue_index = 1,
data = 0xffff881026d9f078
}
crash> struct vring_avail 0xffff881026d9d000
struct vring_avail {
flags = 0,
idx = 52399, ->> avail队列的下个可用元素的索引
ring = 0xffff881026d9d004 ->> 队列数组
}
crash> struct vring_used
struct vring_used {
__u16 flags;
__u16 idx; ->> used队列的下个可用元素的索引
struct vring_used_elem ring[]; ->> 队列数组
}
下面我们再深入分析下后端驱动的几个重要数据结构:
struct VirtQueue
{
VRing vring;//每个queue一个vring
hwaddr pa;//记录virtqueue中关联的描述符表的地址
/*last_avail_idx对应ring[]中的下标*/
uint16_t last_avail_idx;//上次写入的最后一个avail_ring的索引,下次给客户机发送的时候需要从avail_ring+1
/* Last used index value we have signalled on */
uint16_t signalled_used;
/* Last used index value we have signalled on */
bool signalled_used_valid;
/* Notification enabled? */
bool notification;
uint16_t queue_index;
int inuse;
uint16_t vector;
void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);
VirtIODevice *vdev;
EventNotifier guest_notifier;
EventNotifier host_notifier;
};
与前端类似,HOST和客户机也正是通过VirtQueue来操作buffer。每个buffer包含一个VRing结构,对buffer的操作实际上是通过VRing来管理的。pa是描述符表的物理地址。last_avail_index对应VRingAvail中ring[]数组的下标,表示上次最后使用的一个buffer首个desc对应的ring[]中的下标。这里先介绍VRing:
typedef struct VRing
{
unsigned int num;//描述符表中表项的个数
unsigned int align;
hwaddr desc;//指向描述符表
hwaddr avail;//指向VRingAvail结构
hwaddr used;//指向VRingUsed结构
} VRing;
前面说过,VRing管理buffer,其实事实上,VRing是通过描述符表管理buffer的。究竟是怎么个管理法?这里num表示描述符表中的表项数。align是对其粒度。desc表示描述符表的物理地址。avail是VRingAvail的物理地址,而used是VRingUsed的物理地址。每一个描述符表项都对应一个物理块,参考下面的数据结构,每个表项都记录了其对应物理块的物理地址,长度,标志位,和next指针。同一buffer的不同物理块正是通过这个next指针连接起来的。
typedef struct VRingDesc
{
uint64_t addr;//buffer 的地址
uint32_t len;//buffer的大小,需要注意当描述符作为节点连接一个描述符表时,描述符项的个数为len/sizeof(VRingDesc)
uint16_t flags;
uint16_t next;
} VRingDesc;
{
typedef struct VRingAvail
uint16_t flags;//限制host是否向客户机注入中断
uint16_t idx;
uint16_t ring[0];//这是一个索引数组,对应在描述符表中表项的下标,代表一个buffer的head,即一个buffer有多个description组成,其head会记录到
//ring数组中,使用的时候需要从ring数组中取出一个head才可以
} VRingAvail;
typedef struct VRingUsedElem
{
uint32_t id;
uint32_t len;//应该表示它代表的数据段的长度
} VRingUsedElem;
typedef struct VRingUsed
{
uint16_t flags;//用于限制客户机是否增加buffer后是否通知host
uint16_t idx;//
VRingUsedElem ring[0];//意义同VRingAvail
} VRingUsed;
两个字段基本一致,flags是标识位主要限制HOST和客户机的通知。VRing中的flags限制当HOST写入数据完成后是否向客户机注入中断,而VRingUsed中的flags限制当客户机增加buffer后,是否通知给HOST。这一点在高流量的情况下很有效。就像现在网络协议栈中的NAPI,为何采用中断加轮训而不是采用单纯的中断或者轮询。
二者也都有idx。VRingAvail中的idx表明客户机驱动下次添加buffer使用的ring下标,VRingUsed中的idx表明qemu下次添加VRingUsedElem使用的ring下标。
然后两者都有一个数组,VRingAvail中的ring数组记录的是可用buffer 的head index.即
if ring[0]=2
then desctable[2] 记录的就是一个逻辑buffer的首个物理块的信息。
Available ring 指向 guest 提供给设备的描述符,它指向一个 descriptor 链表的头。Available ring 结构如下图所示。
其中标识 flags 值为 0 或者 1,1 表明 Guest 不需要 device 使用完这些 descriptor 时上报中断。idx 指向我们下一个 descriptor 入口处,idx 从 0 开始,一直增加,使用时需要取模:idx=idx&(vring.num-1)
virtqueue中的last_avail_idx记录ring[]数组中首个可用的buffer头部。即根据last_avail_idx查找ring[],根据ring[]数组得到desc表的下标。然后last_avail_idx++。
每次HOST向客户机发送数据就需要从这里获取一个buffer head。
当HOST完成数据的写入,可能会产生多个VirtQueueElement,即使用多个逻辑buffer,每个VirtQueueElement的信息记录到VRingUsed的VRingUsedElem数组中,一个元素对应一个VRingUsedElem结构,其中id记录对应buffer的head,len记录长度。