virtio
virtio 是一种 I/O 半虚拟化解决方案,是一套通用 I/O 设备虚拟化的程序,是对半虚拟化 Hypervisor 中的一组通用 I/O 设备的抽象。提供了一套上层应用与各 Hypervisor 虚拟化设备(KVM,Xen,VMware等)之间的通信框架和编程接口,减少跨平台所带来的兼容性问题,大大提高驱动程序开发效率。
历史背景
qemu支持多种设备,例如网卡有e1000,virtio等,其中e1000属于全虚拟化设备,它模拟了一个真实的硬件设备,提供的访问接口完全遵循硬件手册,当虚拟机使用全虚拟化设备时根本感知不到自身属于虚拟机。而virtio设备是一个专为虚拟化而设计的,早期并没有对应的硬件设备,需要重新编写驱动来使用这个设备,从而一定程度上根据驱动可以判定自身处于虚拟机环境中。相比于半虚拟化设备,访问完全仿真的硬件设备需要更多的陷入/陷出操作和更多的内存拷贝操作。早期的开发者Rusty Russell设计并实现了virtio,之后成为了virtio规范, 经历了0.95, 1.0, 1.1版本的演进。0.95之前称为传统virtio设备,1.0修改了一些PCI配置空间访问方式和virtqueue的优化和特定设备的约定,查看详情。之后虚拟化技术飞速发展,纯粹的virtio软件性能已经满足不了需要,有部分半导体厂商就开始将virtio固化成硬件来提升性能,发现原来为半虚拟化软件实现设计的virtqueue对硬件cache性能不友好,那么干脆找人修改规范,也就有了1.1版本中增加的packed virtqueue支持.
基于PCI的virtio设备
virtio设备可以是基于MMIO,PCI,Channel I/O,目前基于PCI实现最为广泛,下面也主要是基于PCI的virtio 设备解释规范。规范分为两部分,一部分是virtio设备规范,另外一部分是virtio设备接入系统的方式规范,层级关系为:
virtio设备本身的规范
1.1 status,可以判断当前的设备和驱动的状态,主要在驱动初始化时显示状态。
ACKNOWLEDGE:guest已经识别到设备,准备匹配驱动了
DRIVER:guest已经找到设备的驱动
FEATURES_OK:驱动已经和设备协商好feature
DRIVER_OK:驱动已经ready,可以工作了
FAILED:驱动匹配过程中出错了
DEVICE_NEEDS_RESET:设备需要重置
1.2 feature协商:device和driver各自有自己的feature集合,device向driver提供它支持的feature,driver读取device的feature并告诉device它支持的feature子集,这样不同版本的驱动和device可以互相兼容,找一个最大子集进行工作。
0-23:device和driver自定义使用
24-37:给queue和feature协商使用
38+:目前是reserved状态,未使用
1.3 中断和notify
device通过中断通知driver,driver通过notify通知device,还有一个是device配置空间改变时发送中断给driver
1.4 virtqueue
设备和驱动数据通信的方式,每个设备包含1个和多个queue, 又称为vring:Descriptor table,available buffer table, used buffer table;
virtio PCI规范
PCI virtio设备的VID和PID:
Vendor ID:0x1AF4
Device ID: 0x1000- 0x107f;
0x1000- 0x1040是legacy, 0x1040- 0x107f是modern,其中driver识别device ID, id - 0x1040就是传统的virtio device id. 例如网卡可以是0x1000也可以是0x1041
linux kernel中PCI总线通过VID和PID来匹配PCI驱动,通过VID判断是virtio设备,注册到virtio bus;virtio bus根据注册设备的PID来进一步匹配设备驱动
legacy的device id,在此基础上加0x40即是Modern PCI设备的PID,主要是两者规范不同且部分不兼容,在驱动中使用两种方式来操作virtio PCI设备:virtio_pci_modern.c, virtio_pci_legacy.c。
0x1000 network card
0x1001 block device
0x1002 memory ballooning (traditional)
0x1003 console
0x1004 SCSI host
0x1005 entropy source
0x1009 9P transport
virtio ring
split virtqueue
一个vring实际上就包含三个环形的buffer,因为存放的信息都不是数据而是元数据,所以换了个名称:descriptor table,available table,used table。
- 这些表只能被device或driver写,不能两个同时写,因为写要加锁,就需要临界区保护
- 通过读/写顺序保证,不会有两方同时读/写一块区域的情况,免除同步问题
- 允许读写同时操作的区域都是原子操作来保证数据的完整性
- 通过buffer的角度看,driver是生产者,device是消费者,无论是提供空数据的buffer让device写,还是提供数据buffer让device处理。buffer是有driver维护的,device只有读/写数据的权限,对buffer本身是没有任何主权的
- descriptor table
保存有buffer的信息,driver可写 - available table
保存有descriptor table的索引,间接指向buffer,提供buffer给device - used table
保存有descriptor table的索引,间接指向buffer,指示已经消费完成的buffer
逻辑上三个表的关系如上图:
- 每个表的大小都是nr queue
- descriptor table中保存的信息buffer
- 如果一次插入多个多个buffer,buffer的数量不能超过queue的queue_size;有两种方式,如果支持INDIRECT特性,则会申请一个indirect descriptor buffer用来存储buffer的信息,descriptor table指向indirect descriptor table区域并且flag反映这种情况,置位INDIRECT位;如果不支持INDIRECT特性则插入多个descriptor的信息,并置位NEXT位flag表示后续还有buffer,最后一个descriptor table项没有NEXT;两者方式目前不会混用,优先使用indirect descriptor table方式。
- 如果device可写则置位WRITE flag,表示device生产数据,driver消费数据;否则表示device只读,driver生产数据
通知优化
但是在实际使用时,三张表是放在一起的,还额外加了一些东西来优化通知和中断.首先是feature协商过程中,双方是否支持VIRTIO_RING_F_EVENT_IDX特性,如果不支持则每次操作available/used table时都需要通知对方。
如果支持该特性,则每次VM都会发布以下它期望收到中断的used index,只有当host满足条件时才会投递中断;同样的,host发布它期望收到notify的available index, VM每次通知时检查是否满足条件才决定是否通知host。
packed virtqueue
TODO