virtio最初的设计目的就是在guest和host之间建立一个通道,达到更高效的数据传输。传统的全虚拟化实现方式,guest不会感知,依赖于qemu的模拟操作,但是guest每次向设备写入数据都需要做一次VM-exit,效率极低;virtio称为半虚拟化的方式,guest是会感知的,因为他需要专门的内核驱动(前端VIRTIO驱动),而不是原本的网卡驱动。
在guest侧,virtio模拟的设备显示出来是virtio-net/virtio-blk这样的设备类型。
所以本质上,VIRTIO就是一种协议,目的就是令guest上的virtio前端驱动和qemu里的virtio后端通过一种方式高效的通信。而共享内存就是通信数据的通道,一般称为数据面,就是virtqueue,承载了guest和host之间的数据传输;而在数据传输之前,识别virtio设备、识别上层的virtio-net/blcok设备、协商设备属性、建立共享内存等一系列的动作,称为控制面。对于virtio-pci的传输层来说,控制面的数据通道利用了pcie设备的config空间和bar空间。
VIRTIO主要在使用的有0.95、1.0和1.1版本。从VIRTIO0.95到VIRTIO1.0,config空间和bar空间的拓扑结构有所不同,增加了一些capability和feature bit,控制面有了很大的改动。VIRTIO1.1主要是在VIRTIO1.0的基础上引入了packed queue,当然也增加了一些feature bit。
1、VIRTIO0.95
首先说明,在spec和virtio驱动代码中,时常有legacy和modern的用法,virtio0.95的协议称为legacy模式,1.0之后的协议称为modern模式。
1、VENDOR_ID和DEVICE_ID
1)传统模式,DEVICE_ID采用0x1000/0x1001,分别表示net和block设备;traditional模式必须提供subsystem_device_id为0/1(分别表示net和block设备)。
2)非传统模式,DEVICE_ID采用0x1040/0x1041,分别表示net和block设备;不需要提供subsystem_device_id。
两种模式都是支持的,向下兼容。
2、配置信息——BAR0
对于VIRTIO设备,legacy模式默认使用BAR0作为VIRTIO的配置寄存器存储区域。所以驱动和设备都按协议操作BAR0的相应位置。 整个拓扑结构如下:
对于VIRTIO层的配置,包括以下内容(代码来源于qemu,内核有相同的定义):
/* A 32-bit r/o bitmask of the features supported by the host */
#define VIRTIO_PCI_HOST_FEATURES 0
/* A 32-bit r/w bitmask of features activated by the guest */
#define VIRTIO_PCI_GUEST_FEATURES 4
/* A 32-bit r/w PFN for the currently selected queue */
#define VIRTIO_PCI_QUEUE_PFN 8
/* A 16-bit r/o queue size for the currently selected queue */
#define VIRTIO_PCI_QUEUE_NUM 12
/* A 16-bit r/w queue selector */
#define VIRTIO_PCI_QUEUE_SEL 14
/* A 16-bit r/w queue notifier */
#define VIRTIO_PCI_QUEUE_NOTIFY 16
/* An 8-bit device status register. */
#define VIRTIO_PCI_STATUS 18
/* An 8-bit r/o interrupt status register. Reading the value will return the
* current contents of the ISR and will also clear it. This is effectively
* a read-and-acknowledge. */
#define VIRTIO_PCI_ISR 19
/* MSI-X registers: only enabled if MSI-X is enabled. */
/* A 16-bit vector for configuration changes. */
#define VIRTIO_MSI_CONFIG_VECTOR 20
/* A 16-bit vector for selected queue notifications. */
#define VIRTIO_MSI_QUEUE_VECTOR 22
所以这是一片连续的地址空间,如果使能了MSI-X中断,才会有VIRTIO_MSI_CONFIG/QUEUE_VECTOR两个字段,则配置空间有24字节长度;如果没有MSI-X中断,则配置空间有20字节长度。
在通用的配置区间之后,还会有不同的net/block设备相关的配置信息,比如net设备和block设备。
net设备的配置区间如下,比较典型的是mac地址、mtu等,guest从这个字段获取mac地址和设备的最大mtu信息。
struct virtio_net_config {
/* The config defining mac address (if VIRTIO_NET_F_MAC) */
__u8 mac[ETH_ALEN];
/* See VIRTIO_NET_F_STATUS and VIRTIO_NET_S_* above */
__virtio16 status;
/* Maximum number of each of transmit and receive queues;
* see VIRTIO_NET_F_MQ and VIRTIO_NET_CTRL_MQ.
* Legal values are between 1 and 0x8000
*/
__virtio16 max_virtqueue_pairs;
/* Default maximum transmit unit advice */
__virtio16 mtu;
/*
* speed, in units of 1Mb. All values 0 to INT_MAX are legal.
* Any other value stands for unknown.
*/
__le32 speed;
__u8 duplex;
/* maximum size of RSS key */
__u8 rss_max_key_size;
/* maximum number of indirection table entries */
__le16 rss_max_indirection_table_length;
/* bitmask of supported VIRTIO_NET_RSS_HASH_ types */
__le32 supported_hash_types;
} __attribute__((packed));
block设备的配置空间如下,比较典型的是capacity、size等,guest从这个空间获取磁盘的容量、块大小等。
struct virtio_blk_config {
/* The capacity (in 512-byte sectors). */
__virtio64 capacity;
/* The maximum segment size (if VIRTIO_BLK_F_SIZE_MAX) */
__virtio32 size_max;
/* The maximum number of segments (if VIRTIO_BLK_F_SEG_MAX) */
__virtio32 seg_max;
/* geometry of the device (if VIRTIO_BLK_F_GEOMETRY) */
struct virtio_blk_geometry {
__virtio16 cylinders;
__u8 heads;
__u8 sectors;
} geometry;
/* block size of device (if VIRTIO_BLK_F_BLK_SIZE) */
__virtio32 blk_size;
/* number of vqs, only available when VIRTIO_BLK_F_MQ is set */
__virtio16 num_queues;
/*...... */
__u8 unused1[3];
} __attribute__((packed));
3、VIRTIO通用配置信息解析
首先是32bit宽度的host feature和guest feature,具体的feature字段可以参见协议或者代码。这些feature bit是来协商具体的属性,host feature表示设备支持的feature,guest前端驱动读取该字段记为guest_feature,然后结合前端支持的driver_feature,两者相与生成共同支持的feature,写回到设备的guest feature;guest feature才是最终协商好的;比如host feature的VIRTIO_F_VERSION_1置位,表示设备使用virtio1.0及以上的协议;VIRTIO_NET_F_MAC表示设置支持修改MAC地址。
pci status字段也是很重要的字段,用于控制面的状态配置,比较重要的是VIRTIO_DRIVER_S_OK这个bit置位的时候,表示virtio控制面协商完成,这时候设备需要做好准备,host要启动数据面的消息传输了。另外,status字段写入0是一个reset信号,这时候设备需要reset队列的idx指针。
然后就是virtqueue初始化需要的queue_sel、queue_num、queue_pfn字段,实际的使用流程,guest首先写queue_sel,这个字段是用来说明后续的操作针对哪个队列;然后操作queue_num/queue_pfn,分别读取queue_sel指定队列的size、写入指定队列vring的起始地址gpa。
queue_notify字段用于队列的中断通知,方向是主机向设备发送中断,写入的数值指明队列index。
msi_config_vector和msi_queue_vector用于设备向主机方向的中断,主机写入的数值表示相应中断在设备所有msix中断向量中的索引,只有使能了msi-x中断的设备才有这个两个字段。msi_config_vector是config changed中断的索引,通知设备config信息变化,比如net设备mac地址变化,block设备容量修改;msi_queue_vector字段是每个队列的中断向量,使用queue_select和msi_queue_vector配合写入区分不同的队列。
对于使能了msix中断的设备,默认是分配(队列个数+1)个中断向量。因为某些设备队列个数比较多,所以host首先判断最大支持的msix中断个数,如果msix_max >= (队列个数+1),则0号中断配置给config changed中断,第(1——qnum)个中断分别配置给n个队列;msix_max < (队列个数+1),则分配2个中断向量,0号中断配置给config changed中断,n个队列全都配置1号中断,也就是队列的中断时共享的。
2、VIRTIO1.1
VIRTIO1.0+协议的控制数据拓扑结构与VIRTIO0.95有很大不同,所以有了legacy和modern的区别。modern模式同样是利用pcie传输层的config空间和bar空间,但它的数据拓扑结构要更加复杂,而且使用了pcie的capability,capablity中并不存储真实的配置数据,而是提供指向配置数据的指针信息(bar index及offset)。
1、设备ID
UEST如何识别VIRTIO设备以及具体的VIRTIO-NET或者VIRTIO-BLK设备呢,是通过PCI设备config空间的VENDOR_ID和DEVICE_ID来实现的,对于virtio-pci设备,VENDOR_ID采用0x1af4,modern模式中,DEVICE_ID采用0x1000/0x1001+0x40,分别表示net和block设备。
2、配置信息格式
modern模式一共开放了4种类型的通用配置信息,每种配置信息都对应一个capabiility,如果设备不需要某种类型的配置信息,则不提供相应的capability即可。配置信息分别是:
1)common config,通用的配置信息,包括队列的初始化、feature bit、status,对应legacy模式下的common config,必须支持;
2)isr config,中断配置信息;
3)notify config,也是中断相关的配置信息,isr和notify至少需要支持一种;
4)device config,设备自有的配置信息,对应virtio0.95中提到的net/block设备配置信息,有些设备不需要单独的配置信息,则不提供相应的capability;
下面是qemu相关的实现代码,注册四种配置信息的mem_region并创建相应的capability。
virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
virtio_pci_modern_mem_region_map(proxy, &proxy->notify, ¬ify.cap);
相应的capability和memory region的内存空间分布图如下,
3、virtio common配置信息
截取QEMU里的common配置结构体,如下:
/* Fields in VIRTIO_PCI_CAP_COMMON_CFG: */
struct virtio_pci_common_cfg {
/* About the whole device. */
uint32_t device_feature_select; /* read-write */
uint32_t device_feature; /* read-only */
uint32_t guest_feature_select; /* read-write */
uint32_t guest_feature; /* read-write */
uint16_t msix_config; /* read-write */
uint16_t num_queues; /* read-only */
uint8_t device_status; /* read-write */
uint8_t config_generation; /* read-only */
/* About a specific virtqueue. */
uint16_t queue_select; /* read-write */
uint16_t queue_size; /* read-write, power of 2. */
uint16_t queue_msix_vector; /* read-write */
uint16_t queue_enable; /* read-write */
uint16_t queue_notify_off; /* read-only */
uint32_t queue_desc_lo; /* read-write */
uint32_t queue_desc_hi; /* read-write */
uint32_t queue_avail_lo; /* read-write */
uint32_t queue_avail_hi; /* read-write */
uint32_t queue_used_lo; /* read-write */
uint32_t queue_used_hi; /* read-write */
};
首先是feature bit的处理,因为modern模式新增了feature bit,所以feature bit扩展到了64位;应该是考虑到后续的扩展,可能有>64bit的feature需求,所以采用了类似于queue配置的方式,增加了device_feature_select和guest_feature_select字段,配合device_feature和guest_feature使用。
msix_config字段,同legacy模式的msix_config_vector;
num_queues表示现在最多支持多少个队列;
device_status字段同legacy模式;
config_generation字段,device每次config空间变化通知主机的时候,config_generation会同步自加1;
queue_msix_vector,同legacy模式;
队列ring空间的设置分为3个部分,descriptor、avail ring和used ring;
增加了queue_enable字段,用于使能队列;
queue_notify_off用于主机向设备发送中断,是结合notify配置信息使用的。在legacy模式下,guest向设备发送中断是向queue_notify字段写入队列index实现的。在modern模式下,是有专门的notify区间,通过notify capability定义这部分空间的地址;notify空间里对每个队列都对应一个elem,而queue_notify_off是表示当前队列对应elem的索引值,当guest向设备发送某个队列的中断时,就写入相应队列的elem空间。