virtio技术(3)virtqueue机制
virtio的关键技术是virtqueue机制,其提供了一套统一的用于virito前端和后端的通信机制。virtqueue的核心数据结构是vring,这是virtio前端驱动和后端Hypervisor虚拟设备之间传输数据的载体。
vring数据结构
vring数据结构示意图如下:
vring主要由三个部分组成:描述符表(Descriptor Table)、可用描述符表(Avaliable Ring)和已用描述符表(Used Ring)。virtio协议规定,vring结构关联的内存由客户机中的前端驱动负责分配和回收。Linux内核virtio前端驱动定义的vring数据结构如下:
struct vring {
unsigned int num; /* vring的队列深度,表示一个VRing有多少个buffer */
struct vring_desc *desc; /* 指向Descriptor Table */
struct vring_avail *avail; /* 指向Avaliable Ring */
struct vring_used *used; /* 指向Used Ring */
};
描述符表
描述符表用于保存一系列描述符,每一个描述符都被用来描述客户机内的一块内存区域。对于这块内存区域,如果存放的是前端驱动写给设备的数据,称这个描述符为out类型的;如果存放的是前端驱动从设备读取的数据,称这个描述符为in类型的。描述符数据结构定义如下:
struct vring_desc {
__virtio64 addr;
__virtio32 len;
__virtio16 flags;
__virtio16 next;
};
描述符通过以下字段指定内存区域的各个属性:
字段 | 作用 |
---|---|
addr | 表示内存区域在客户机物理地址空间中的起始地址 |
len | 该字段的意义取决于该内存区域的读写属性。如果该区域是只写的,数据传递方向只能从后端设备到前端驱动,此时len表示设备最多可以向该内存块写入的数据长度。反之,如果该区域是只读的,此时len表示后端设备必须读取的来自前端驱动的数据量。 |
flag | 用于标识描述符自身的特性,一共有三种可选值。VRING_DESC_F_WRITE表示当前内存区域是只写的,即该内存区域只能被后端设备用来向前端驱动传递数据。VRING_DESC_F_NEXT表明该描述符的next字段是否有效。VRING_DESC_F_INDIRECT表明该描述符是否指向一个中间描述符表。 |
next | 驱动和设备的一次数据交互往往会涉及多个不连续的内存区域。通常的做法是将描述符组织成描述符链表的形式来表示所有的内存区域。next字段便是用来指向下一个描述符。通过flag字段中的值VRING_DESC_F_NEXT,就可以间接地确定该描述符是否为描述符链表的最后一个。 |
可用描述符表
可用描述符表用于保存前端驱动提供给后端设备且后端设备可以使用的描述符。可用描述符表由一个flags字段、idx索引字段以及一个以数组形式实现的环组成。可用描述符表数据结构定义如下:
struct vring_avail {
__virtio16 flags;
__virtio16 idx;
__virtio16 ring[];
};
各个字段作用描述如下:
字段 | 作用 |
---|---|
flags | 标志位,表示可用描述符表的一些属性,包括是否需要设备在使用了可用描述符表中的表项后发送中断给驱动。 |
idx | 用于索引ring数组中下一个可用的位置 |
ring | 用于存放描述符链表中作为链表头的描述符在描述符表中的索引 |
已用描述符表
已用描述符表用于保存后端已经处理并且尚未反馈给驱动的描述。与可用描述符表不同的是,已用描述符表中数组ring的每个元素不仅包含后端设备已经处理的描述符链表的头部描述符在描述符表中的索引,而且由于后端设备可能会向前端驱动写回数据或需要告知驱动写操作的状态,还需要包括一个len字段来记录设备写回数据的长度。已用描述符区域数据结构定义如下:
struct vring_used {
__virtio16 flags;
__virtio16 idx;
struct vring_used_elem ring[];
};
各个字段作用描述如下:
字段 | 作用 |
---|---|
flags | 标志位,表示已用描述符表的一些属性,包括是否需要驱动在回收了已用描述符表中的表项后发送通知给设备 |
idx | 用于索引ring数组中下一个已用元素的位置 |
ring | 存储已用元素的数组,每个已用元素包括描述符索引和数据长度 |
使用virtqueue的通信流程
设备使用virtqueue主要包括两部分过程:驱动通过描述符列表和可用描述符表提供数据缓冲区给设备用,和设备使用描述符后再通过已用描述符表归还给驱动。
前端驱动写入
客户机操作系统通过驱动提供数据缓冲区给设备使用,具体包括以下步骤:
- 把数据缓冲区的地址、长度等信息赋值到空闲的描述符中;
- 把该描述符指针添加到该虚拟队列的可用环表的头部;
- 更新该可用环表中的头部指针;
- 写入该虚拟队列编号到Queue Notify寄存器以通知设备。
后端设备使用
每次后端设备取用可用描述符时,需要知道剩余可用描述符在数组ring中的起始位置。后端设备会维护一个变量last_avail_idx,用来标记这个位置。当切换到主机中时,后端设备将检查last_avail_idx和idx的值,数组ring中位于last_avail_idx和idx-1之间的部分就是可供后端设备使用的区域。
设备使用数据缓冲区后(基于不同种类的设备可能是读取或者写入,或是部分读取或者部分写入),将用过的缓冲区描述符填充已用环表,并通过中断通知驱动。具体的过程如下:
- 把使用过的数据缓冲区描述符的头指针添加到该虚拟队列的已用环表的头部;
- 更新该已用环表中的头部指针;
- 根据是否开启MSI-X中断,用不同的中断方式通知驱动
前端驱动回收
当设备驱动回收已用的设备描述符时,需要知道剩余已用标识符在数组ring中的起始位置,前端驱动会维护一个变量last_used_idx,用来标记这个位置。当切换到虚拟机中时,前端驱动将检查last_used_idx和idx的值,数组ring中位于last_used_idx和idx-1之间的部分便是可供前端驱动回收的区域。
相关参考
- 《深入浅出DPDK》
- 《深入浅出系统虚拟化:原理与实践》
- 《深度探索Linux系统虚拟化:原理与实现》