通过MMIO的方式实现VIRTIO-BLK设备(一)
背景知识
什么是VIRTIO
使用完全虚拟化,Guest不加任何修改就可以运行在任何VMM上,VMM对于Guest是完全透明的。但每次I/O都将导致CPU在Guest模式与Host模式间切换,在I/O操作密集时,这个切换是影响虚拟机性能的一个重要因素。对于通过软件方式模拟的虚拟化而言,完全可以制定一个更加高效简洁地适用于软件模拟环境下的驱动和模拟设备交互的标准,于是Virtio诞生了。与完全虚拟化相比,使用Virtio标准的驱动和模拟设备的交互不再使用寄存器等传统的I/O方式,而是采用了Virtqueue的方式传输数据。这种设计降低了设备模拟器实现的复杂度,I/O不再受数据总线宽度、寄存器宽度等因素的影响,一次I/O传递的数据量不受限制,减少了CPU在Guest模式与Host模式之间的切换,提高了虚拟化的性能。
作为一个统一的标准,越来越多的操作系统(例如Linux与Windows)已经提供了对Virtio的支持。
本文将根据Virtio1.0文档讲述VIRTIO的实现。
Dirver 与 Device
在VIRTIO中,Driver实现在虚拟机,是VIRTIO的前端;Device实现在虚拟机监控器,是VIRTIO的后端。
描述符表
Virtqueue是VIRTIO中数据传输的载体,是VIRTIO的核心部分。Virtqueue主要包括三个部分,分别是描述符表(Descriptor Table),可用描述符区域(Available Ring),已用描述符区域(Used Ring)。
VIRTIO要求描述符表,可用描述符表,已用描述符表分别在GPA上连续。
这三个表可以通过下图表示:
从左到右依次是,可用描述符表,描述符表,已用描述符表。
描述符表
描述符表是Virqqueue的核心,它包括Queue Size个描述符(Queue Size由driver决定,必须是2的指数,它的值保存在QueueSize寄存器上)。
每个描述符会指向一块共享内存,如果这块内存是驱动写给设备的数据,则称这个描述符为out类型的,如果这块内存是设备写给驱动的数据,则称这个描述符为in类型。
描述符并不是单独存在的,它们可以通过指针组成描述符链,一个描述符表中会有多条描述符链,一条描述符链记录一次I/O事件。
描述符有4个字段,如上图所示。描述符通过addr指向一块保存有I/O数据的共享内存,需要注意的是addr保存的是GPA,当后端需要根据通过addr读写该块共享内存时,需要视虚拟机监控器的实现将GPA转换成HVA或者HPA。len表示该块共享内存的长度。flags标识了描述符的属性,当flags * F_NEXT成立,则描述符可以通过next指向下一个描述符;当flags * F_WRITE成立,则这个描述符属于in类型,否则则是out类型;当flags * F_INDIRECT成立时,共享内存上将不是直接保存数据,而是保存一连串描述符。next指针则指向描述符链中的下一个描述符。
可用描述符表
driver将数据写入描述符记录的共享内存后,需要让device知道哪些描述符可以消费(可用)。可用描述符负责完成这个任务。
可用描述符中的ring是一个数组,因virtqueue中最多可能有Queue Size可用描述符链,ring的大小是Queue Size。ring中每个元素都记录了对应的描述符链的第一个描述符的ID,因此ring中一个元素对应一条描述符链,也即对应一次I/O事件。可用描述符中idx变量记录的是driver下一个填充的可用描述符,与之对应的是device将在变量last_avail_idx中记录上一个处理完的可用描述符,因此在last_avail_idx到idx之间是等待device处理的可用描述符。
已用描述符表
device将已经处理好的IO请求对应的描述符记录在已用描述符中,从这里可以看出,可用,已用这两个概念都是对device而言的。一个需要注意的点是,可用描述符和已用描述符都是指向描述符链,它们只是说明该条描述符链的状态,并不是代表描述符链的in/out类型。
与可用描述符不同的是,已用描述符的数组中每个元素的大小是8byte,它不仅记录了描述符链第一个描述符的ID,还记录了device向描述符链中写入的byte数。已用描述符通过idx和last_used_idx记录了等待driver回收的描述符链。idx由设备维护,表示设备下一个处理完的描述符链将记录在已用描述符表中的位置,last_used_idx由驱动维护,记录的是驱动上一个回收的描述符链在已用描述符表中的位置。
VIRTIO MMIO寄存器(部分
在MMIO实现VIRTIO的情况下,每个VIRTIO设备都有一个MMIO REGION。这个REGION在设备树中的声明如图:
上图表示GPA 0x1e000 到 0x1e200 的地址段是这个virtio_block设备的MMIO REGION。这个REGION中分布着VIRTIO MMIO寄存器。
42是这个virtio设备对应的中断号,注意42需要加上SPI中断的基础值:32,因此这个virtio driver实际上能够识别的中断号是74。
一些重要的MMIO 寄存器如下:
DeviceFeatures & DeviceFeaturesSel
设备通过DeviceFeatures寄存器告诉驱动设备支持的一些机制,比如VIRTIO_RING_F_INDIRECT_DESC这个bit就是告诉driver:device支持virtqueue通过indirection扩大共享内存区域。driver只能够在device提供的机制上工作,不能够在device没有提供该机制的情况下运行对应的代码。由于DeviceFeatures的区域要大于4bytes,driver需要通过DeviceFeaturesSel寄存器用查看DeviceFeatures的部分bits。
DriverFeatures & DriverFeaturesSel
驱动通过DriverFeatures寄存器告诉设备,驱动支持了设备的哪些机制,DriverFeaturesSel则用于设备查看DriverFeatures。
QueueSel
对于某些virtio设备,比如virtio-net,virtio-console会包括多个virtqueue。为了让设备知道该对在哪条virtqueue上进行处理,driver会通过QueueSel寄存器告诉驱动后续的操作是在哪条virtqueue进行的。
QueueReady
driver会通过写QueueReady寄存器通知设备,当前virtqueue已经初始化好了,设备可以通过读描述符寄存器来获得virtqueue的地址。
QueueNotify
当driver准备了新的可用描述符时,会通过写QueueNotify寄存器通知device进行处理。
InterruptStatus
Virtio设备可以通过发送中断通知虚拟机,每个virtio设备有一个对应的中断号(这个中断号在设备树中声明),虚拟机在收到中断后,会根据中断号找到对应的driver,driver则需要通过InterruptStatus寄存器搞清楚产生这次中断的事件是什么,比如bit 0表示已用描述符更新,bit 1表示设备配置空间更新。
QueueDescLow & QueueDescHigh
driver通过写这两个寄存器告诉device描述符表的GPA。由于每个MMIO寄存器只有32个bit,因此需要两个寄存器。
QueueAvailLow & QueueAvailHigh
driver通过写这两个寄存器告诉device可用描述符表的GPA。
QueueUsedLow & QueueUsedHigh
driver通过写这两个寄存器告诉device已用描述符表的GPA。
Config
Config不是一个寄存器,而是一个区域,这个区域由device进行配置,每种device会有不一样的配置区域。
下图展示的就是block设备配置空间的数据结构。
参考资料
《深度探索Linux系统虚拟化:原理与实现》
《Virtual I/O Device Version 1.0》
《Linux虚拟化KVM-Qemu分析(十一)之virtqueue》
https://github.com/minosproject/minos/
下一期将介绍实现VIRTIO-BLK设备时,虚拟机image,rootfs,dtb文件的制作