半虚拟化Virtio
virtio是一种半虚拟化的设备抽象接口规范。与宿主机纯软件模拟I/O设备相比,Virtio可以获得更好的I/O性能。
缺点是必须要求客户机安装特定的Virtio驱动使其知道运行在虚拟化环境中。
Virtio使用场景
现代数据中心大量采用虚拟化技术,设备的虚拟化是其中重要的一环。
Virtio作为一种标准化的设备接口,主流的操作系统和应用都逐渐加入了对Virtio设备的直接支持,这给数据中心的运维带来了很多方便。
Virtio同I/O透传技术相比,目前在网络吞吐率、时延以及抖动上尚不具优势,相关的优化工作正在进行当中。
I/O透传的一个典型问题是从物理网卡接收到的数据包将直接送达客户机的接收队列,或者从客户机发送队列发出的包将直接到达其他客户机(比如同一个PF的VF)的接收队列或者直接从物理网卡发出,绕过了宿主机的参与。
但是在很多应用场景下,有需求要求网络包必须先经过宿主机的处理(防火墙、负载均衡等),再传递给客户机。
另外I/O透传技术不能从硬件上支持虚拟机的动态迁移以及缺乏足够灵活的流分类规则。
上图是Virtio设备的典型应用场景,宿主机使用虚拟交换机连通物理网卡和虚拟机。虚拟交换机内部有一个DPDK vhost,实现了Virtio的后端网络设备驱动程序逻辑。虚拟机里有DPDK的Virtio前端网络设备驱动。前端和后端通过Virtio的虚拟机队列交换数据。这样虚拟机里的网络数据可以发送到虚拟交换机中,然后经过转发逻辑,可以经由物理网卡进入外部网络。
Virtio规范和原理
Virtio规范主要有两个版本,0.95和1.0,其规定的实现接口有PCI、MMIO(内存映射)和Channel IO方式,而Channel IO方式是1.0规范中新加的。
PCI是现代计算机系统中普遍使用的一种总线接口,最新的规范是PCI-e。
在一些系统中(例如嵌入式系统中),可能没有PCI接口,Virtio可使用内存映射方式。
IBM S/390的虚拟机系统既不支持PCI接口也不支持内存映射方式,只能使用特有的Channel IO方式。
DPDK目前只支持Virtio PCI接口方式。
Virtio在PCI(传输层)的结构之上还定义了Virtqueue(虚拟队列)接口,他在概念上将前端驱动程序连接到后端驱动程序。
驱动程序可以使用一个或多个队列,具体的数量取决于需求。例如,Virtio网络驱动程序使用两个虚拟队列(一个用于接收,另一个用于发送),而Virtio块驱动程序则使用一个虚拟队列。
Virtio用PCI接口实现时,宿主机会使用后端驱动程序模拟一个PCI的设备,并将这个设备添加在虚拟机配置中。
设备的配置:
-
设备的初始化
-
手工重启设备状态,或者是设备上电时的自动启动后,系统发现设备。
-
客户机操作系统设置设备的状态为Acknowledge,表示当前已经识别到设备。
-
客户机操作系统设置设备的状态为Driver,表明客户操作系统已经找到合适的驱动程序。
-
设备驱动的安装和配置:进行特性列表的协商,初始化虚拟队列,可选的MSI-X的安装,设备专属的配置等。
-
设置设备状态为Driver_OK,或者如果中途出现错误,则为Failed。
-
设备的发现
通过Virtio Device ID来识别各个Virtio设备。
-
传统模式Virtio的配置空间
传统模式使用PCI设备的BAR0来对PCI设备进行配置。如果传统设备配置了MSI-X中断,则在Bits后添加两个域。紧接着,这些通常的Virtio参数可能会有指定设备(例如网卡)专属的配置参数。
-
现代模式Virtio的配置空间
和传统设备固定使用BAR0不同,现代设备通过标准的PCI配置空间中的能力列表,可以指定配置信息的存储位置(使用哪个BAR,从BAR空间开始的偏移地址等)。
1.0规范定义了四种配置信息:通用配置、提醒、中断服务状态、设备专属配置。
设备状态:
Device Status 域主要由 guest 来更新,表示当前 drive 的状态。状态包括:
0:写入 0 表示重启该设备
1:Acknowledge,表明 guest 已经发现了一个有效的 virtio 设备
2:Driver,表明 guest 已经可以驱动该设备,guest 已经成功注册了设备驱动
3:Driver_OK,表示 guest 已经正确安装了驱动,准备驱动设备
4:FAILED,在安装驱动过程中出错
128: 在安装驱动过程中出错
8: FEATURES_OK,表示驱动程序和设备特性协商成功
64:DEVICE_NEEDS_RESET,表示设备遇到错误,需要重启。
特性列表:
设备和驱动都有单独的特性列表,现代设备特性列表有64字位,传统设备只支持32字位。通过特性列表,设备和驱动都能提供自己致辞的特性集合。设备在初始化过程中,驱动程序读取设备的特性列表,然后挑选其中自己能够支持的作为驱动的特性列表。这就完成了驱动和设备之间的特性协商。
中断配置:
现代设备和传统设备都支持两种中断源(设备中断和队列中断)和两种中断放式(INTx和MSI-X)。每个设备中设备中断源只有一个,队列中断源则可以每个队列一个。但具体有多少个中断还取决于中断方式。
INTx方式下,一个设备只支持一个中断,所以设备中断源和队列中断源必须共享这一个中断。
MSI-X支持多个中断,每个单独中断也称为中断向量。
设备的专属性配置:
此配置空间包含了特定设备(例如网卡)专属的一些配置信息,可由驱动读写。
虚拟队列的配置
虚拟队列(Virtqueue)是连接客户机操作系统中Virtio设备前端驱动和宿主机后端驱动的实际数据链路。
虚拟队列主要由描述符列表(descriptor table)、可用环表(available ring)、已用环表(used ring)组成。
描述符列表指向的是实际要传输的数据。两个环表指向的是描述符列表,分别用来标记前端和后端驱动对描述符队列表中描述符的处理进度。
在传统网卡设备中,对描述符的处理进度,一般用两个指针就可以标记:前端指针指向网卡驱动在描述符列表的处理位置,后端指针指向 网卡设备处理的位置。
网卡设备中双指针方案有一个缺点:描述符只能顺序执行,前一个 描述符处理完之前,后一个描述符就只能等待。
Virtio设备中的虚拟队 列则不存在这个限制,队列的生产者(前端驱动)将生产出来的描述符放在可用环表中,而消费者(后端驱动)消费之后将消费过的描述符放 在已用环表中。前端驱动可以根据已用环表来回收描述符以供下次使用。这样即使中间有描述符被后端驱动所占用,也不会影响被占用描述 符之后其他描述符的回收和循环使用。
-
初始化虚拟队列
-
选择虚拟队列的索引,写入队列选择寄存器(Queue Select)
-
读取队列容量寄存器(Queue Size),获得虚拟队列的大小,如 果是0的话,则表示这个队列不可用。(传统设备中,队列容量只能由设备指定,而现代设备中,如果驱动可以选择写入一个小一些的值到队列容量寄存器来减少内存的使用。)
-
分配队列要用到的内存,并把处理后的物理地址写入队列地址寄存器(Queue Address)。
-
如果MSI-X中断机制启用,选择一个向量用于虚拟队列请求的中断,把对应向量的MSI-X序号写入队列中断向量寄存器(Queue Vector),然后再次读取该域以确认返回正确值。
-
描述符列表
描述符列表中每一个描述符代表的是客户虚拟机这侧的一个数据缓冲区,供客户机和宿主机之间传递数据。如果客户机和宿主机之间一次 要传递的数据超过一个描述符的容量,多个描述符还可以形成描述符链 以共同承载这个大的数据。
每个描述符有4个属性:
-
Address:数据缓冲区的客户机物理地址。
-
Len:数据缓冲区的长度。
-
Next:描述符链中下一个描述符的地址。
-
Flags:标志位,表示当前描述符的一些属性,包括Next是否有效 (如无效,则当前描述符是整个描述符链的结尾),和当前描述符对设 备来说是否可写等。
-
可用环表
可用环表是一个指向描述符的环型表,是由驱动提供(写入),给设备使用(读取)的。设备取得可用环表中的描述符后,描述符所对应 的数据缓冲区既可能是可写的,也可能是可读的。可写的是驱动提供给 设备写入设备传送给驱动的数据的,而可读的则是用于发送驱动的数据 到设备之中。
有3个属性:
-
ring:存储描述符指针(id)的数组。
-
index:驱动写入下一个可用描述符的位置。
-
Flags:标志位,表示可用环表的一些属性,包括是否需要设备在使 用了可用环表中的表项后发送中断给驱动。
-
已用环列表
已用环表也是一个指向描述符的环型表,和可用环表相反,它是由设备提供(写入),给驱动使用(读取)的。设备使用完由可用环表中 取得的描述符后,再将此描述符插入到已用环表,并通知驱动收回。
已用环表的表项有3个属性:
-
ring:存储已用元素的数组,每个已用元素包括描述符指针(id) 和数据长度(len)
-
index:设备写入下一个已用元素的位置。
-
Flags:标志位,表示已用环表的一些属性,包括是否需要驱动在回收了已用环表中的表项后发送提醒给设备。
设备的使用
设备使用主要包括两部分过程:驱动通过描述符列表和可用环表提供数据缓冲区给设备用,和设备使用描述符后再通过已用环表还给驱 动。例如,Virtio网络设备有两个虚拟队列:发送队列和接收队列。驱动添加要发送的包到发送队列(对设备而言是只读的),然后在设备发送完之后,驱动再释放这些包。接收包的时候,设备将包写入接收队列中,驱动则在已用环表中接收处理这些包。
1.驱动向设备提供数据缓冲区:客户机操作系统通过驱动提供数据缓冲区给设备使用,具体包括以下步骤:
1)把数据缓冲区的地址、长度等信息赋值到空闲的描述符中。
2)把该描述符指针添加到该虚拟队列的可用环表的头部。
3)更新该可用环表中的头部指针。
4)写入该虚拟队列编号到Queue Notify寄存器以通知设备。
2.设备使用和归还数据缓冲区:设备使用数据缓冲区后(基于不同种类的设备可能是读取或者写入,或是部分读取或者部分写入),将用过的缓冲区描述符填充已用环表,并通过中断通知驱动。具体的过程如下:
1)把使用过的数据缓冲区描述符的头指针添加到该虚拟队列的已用环表的头部。
2)更新该已用环表中的头部指针。
3)根据是否开启MSI-X中断,用不同的中断方式通知驱动。
Virtio网络设备驱动设计
Virtio网络设备是Virtio规范中到现在为止定义的最复杂的一种设备。Linux内核和DPDK都有相应的驱动,Linux内核版本功能比较全面,DPDK则更注重性能。
virtio网络设备Linux内核驱动设计
Virtio网络设备Linux内核驱动主要包括三个层次:底层PCI-e设备层,中间Virtio虚拟队列层,上层网络设备层。
底层PCI-e设备层:底层PCI-e设备层负责检测PCI-e设备,并初始化设备对应的驱动程序。
中间Virtio虚拟队列层:中间Virtio虚拟队列层实现了Virtio协议中的虚拟队列。
上层网络设备:上层网络设备层实现了两个抽象类:Virtio设备 (virtio_net_driver::virtio_driver)和网络设备(dev::net_device)。
基于DPDK用户空间的Virtio网络设备驱动设计及性能优化
其主要实现是在目录drivers/net/virtio/下,也是包括三个层次:底层PCI-e设备层,中间Virtio虚拟队列层,上层网络设备层。
底层PCI-e设备层的实现更多的是在DPDK公共构件中实现,virtio_pci.c和virtio_pci.h主要是包括一些读取PCI-e中的配置等工具函数。
中间Virtio虚拟队列层实 现在virtqueue.c,virtqueue.h和virtio_ring.h中,vring及vring_desc等结构定义和Linux内核驱动也都基本相同。
上层的网络设备层实现的是rte_eth_dev的各种接口,主要在 virtio_ethdev.c和virtio_rxtx.c文件中。virtio_rxtx负责数据报的接受和发送,而virtio_ethdev则负责设备的设置。
DPDK用户空间驱动充分利用了DPDK在构架上的优势 (SIMD指令,大页机制,轮询机制,避免用户和内存之间的切换等)和只需要针对网卡优化的特性,虽然实现的是和内核驱动一样的Virtio协议,但整体性能上有较大的提升。
-
关于单帧mbuf的网络包收发优化
如果一个数据包能够放入一个mbuf的结构体中,叫做单帧mbuf。
在QEMU/KVM的Virtio实现中,vring描述符的个数一般设置成 256个。对于接收过程,可以利用mbuf前面的HEADROOM作为virtio net header的空间,所以每个包只需要一个描述符。对于发送过程,除了需 要一个描述符指向mbuf的数据区域,还需要使用一个额外的描述符指向 额外分配的virtio net header的区域,所以每个包需要两个描述符。
-
Indirect特性在网络包发送中的支持
Indirect特性指的是Virtio前端和后端通过协商后,都支持 VIRTIO_F_RING_INDIRECT_DESC标示,表示驱动支持间接描述符表。