IO(Input/Output)指在内存和外部设备的数据交互过程,设备例如接收输入的键鼠,显示输出的屏幕,存储数据的磁盘,一个功能完备的虚拟机同样需要IO,在虚拟环境真实设备有限,虚拟机管理器对物理设备管理为虚拟机提供设备抽象,截获虚拟机的IO请求,转换为对物理设备的访问,实现对外部设备资源的复用。这用实现方式我们称为设备虚拟化。
将设备直接挂到虚拟机使用叫做设备直通,这种方式可以使虚拟机拥有较好的性能。传统的透传设备方式是QEMU/KVM实现的PCI passthrough,kvm需要和IOMMU、注册中断打交道,kvm完成了设备驱动的部分工作。VFIO是一种用户态驱动框架,利用硬件的IO虚拟化将设备直通给虚拟机,虚拟机具有更好的性能。
1、vfio基本思想和原理
设备直通就是将物理设备直接挂到虚拟机,
传统方式为PCI passthrough,KVM需要处理许多工作,包括IOMMU交互、注册中断处理函数等。
VFIO是安全的、用户态驱动框架,利用的是VT-d/AMD-Vi等硬件io虚拟化能力,用户态QEMU接管所有vm对设备的访问
a、基于DMA映射和隔离的硬件IOMMU:基于IOMMU 组
b、模块化IOMMU和总线驱动支持
已经支持PCI和平台驱动
IOMMU API(type1)和ppc64(SPAPR)模型 ----- type1主要是intel/amd
c、完全的设备访问、DMA和中断支持
设备资源的读、写、映射支持
映射用户内存到IO虚拟地址(iova)
基于信号机制的eventfd和irqfd
用户态、内核态驱动需求都是一样的:设备资源访问,通过IOMMU隔离DMA映射,中断信号支持。
其他用户态驱动:
DPDK、数据平面开发包(NFV)
UNVMe、用户态nvme驱动
rVFIO、Ruby封装的VFIO
VFIO的基本思想包括两个部分,
第一是将物理设备的各种资源分解,并将获取这些资源的接口向上导出到用户空间,QEMU等应用层软件可以利用这些接口获取硬件的所有资源,包括设备的配置空间、BAR空间和中断。
第二就是聚合,也就是将从硬件设备得到的各种资源聚合起来,对虚拟化展示一个完整的设备接口,这种聚合是在用户空间完成的
2、vfio是怎么工作的
2.1、设备驱动怎么控制PCI设备?
控制IO:IN/OUT、read/write;
PCI配置空间
VFIO设备文件描述符:
划分为区域
每个区域映射到一个设备资源,Ex. MMIO BAR、IO BAR、 PCI config space
通过ioctl发现区域信息和计数:文件偏移、可允许访问等等
Eg.
| region0 | region1 | region2 | region3 | region4 | region4| ...
0 --文件偏移→
通过ioctl操作
ioctl(fd, VFIO_DEVICE_GET_INFO, &dev_info);
struct vfio_device_info {
__u32 argsz;
__u32 flags;
#define VFIO_DEVICE_FLAGS_RESET (1 << 0) /* Device supports reset */
#define VFIO_DEVICE_FLAGS_PCI (1 << 1) /* vfio-pci device */
#define VFIO_DEVICE_FLAGS_PLATFORM (1 << 2) /* vfio-platform device */
#define VFIO_DEVICE_FLAGS_AMBA (1 << 3) /* vfio-amba device */
#define VFIO_DEVICE_FLAGS_CCW (1 << 4) /* vfio-ccw device */
#define VFIO_DEVICE_FLAGS_AP (1 << 5) /* vfio-ap device */
__u32 num_regions; /* Max region index + 1 */
__u32 num_irqs; /* Max IRQ index + 1 */
};
VFIO_DEVICE_GET_REGION_INFO
struct vfio_region_info {
__u32 argsz;
__u32 flags;
#define VFIO_REGION_INFO_FLAG_READ (1 << 0) /* Region supports read */
#define VFIO_REGION_INFO_FLAG_WRITE (1 << 1) /* Region supports write */
#define VFIO_REGION_INFO_FLAG_MMAP (1 << 2) /* Region supports mmap */
#define VFIO_REGION_INFO_FLAG_CAPS (1 << 3) /* Info supports caps */
__u32 index; /* Region index */
__u32 cap_offset; /* Offset within info struct of first cap */
__u64 size; /* Region size (bytes) */
__u64 offset; /* Region offset from start of device fd */
};
VFIO_DEVICE_GET_IRQ_INFO
struct vfio_irq_info {
__u32 argsz;
__u32 flags;
#define VFIO_IRQ_INFO_EVENTFD (1 << 0)
#define VFIO_IRQ_INFO_MASKABLE (1 << 1)
#define VFIO_IRQ_INFO_AUTOMASKED (1 << 2)
#define VFIO_IRQ_INFO_NORESIZE (1 << 3)
__u32 index; /* IRQ index */
__u32 count; /* Number of IRQs within this index */
};
2.2、设备怎么给驱动信号?
通过中断,用户态中断是通过eventfd实现的
EVENTFD(2) Linux Programmer's Manual EVENTFD(2)
NAME
eventfd - create a file descriptor for event notification
SYNOPSIS
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);
DESCRIPTION
eventfd() creates an "eventfd object" that can be used as an event wait/notify mechanism by user-
space applications, and by the kernel to notify user-space applications of events.
ioctl(s->device, VFIO_DEVICE_SET_IRQS, irq_set);
struct vfio_irq_set {
__u32 argsz;
__u32 flags;
#define VFIO_IRQ_SET_DATA_NONE (1 << 0) /* Data not present */
#define VFIO_IRQ_SET_DATA_BOOL (1 << 1) /* Data is bool (u8) */
#define VFIO_IRQ_SET_DATA_EVENTFD (1 << 2) /* Data is eventfd (s32) */
#define VFIO_IRQ_SET_ACTION_MASK (1 << 3) /* Mask interrupt */
#define VFIO_IRQ_SET_ACTION_UNMASK (1 << 4) /* Unmask interrupt */
#define VFIO_IRQ_SET_ACTION_TRIGGER (1 << 5) /* Trigger interrupt */
__u32 index;
__u32 start;
__u32 count;
__u8 data[];
};
2.3、设备怎么搬运数据?
直接内存访问–DMA
IO设备可以读写:系统内存(RAM)、每个设备内存
在CPU MMU控制之外,IO需要一个MMU,→IOMMU
传输:I/O虚拟地址空间(iova),以前的IOMMU主要目的
隔离:单设备传输,invalid accesses blocked
IOMMU问题:
DMA替换(混叠?):不是所有设备需要唯一ID
DMA隔离:点对点DMA传输
解决办法:IOMMU groups
IOMMU驱动给设备分组(不是用户配置),组与组间DMA隔离
影响因素有:IOMMU能力、端点设备隔离、总线和互联属性
严重影响VFIO设计
内存问题:
iova page fault不支持端到端,静态映射。
用户内存可以被迁移,交换、合并等等
一些缺点:
固定内存就是锁定内存,用户需要足够的锁定内存限制
防止页面合并和交换
Eg:
三个pci设备,绑定设备到vfio-pci后得到分组节点,一个/dev/vfio/23, 两个/dev/vfio/
open(“/dev/vfio/vfio”) 创建一个container,
open(“/dev/vfio/42”) 创建一个group,
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container)
ioctl(container, VFIO_GROUP_SET_IOMMU, VFIO_TYPE1_IOMMU)
open(“/dev/vfio/23”) 创建一个group2,
ioctl(group2, VFIO_GROUP_SET_CONTAINER, &container)
ioctl(container, VFIO_IOMMU_MAP_DMA, &map)
ioctl(container, VFIO_IOMMU_UNMAP_DMA, &unmap)
#container{ group:/dev/vfio/42(设备1,设备2), group2:/dev/vfio/23(设备3)} <-→ IOMMU <-→ 系统内存lock/unlock
ioctl(group2, VFIO_GROUP_GET_DEVICE_FD, "0000:01:00.0")
上图是VFIO驱动将设备分解
下图是在用户空间QEMU重新聚合
3、QEMU 与vfio
3.1、guest如何操作设备
答:QEMU:MemoryRegions;VFIO:Device file descriptor regions
设备编程
受限于hypercisor(qemu/kvm)
内存区域查找执行,(读写)访问调用
读写 vfio的区域
内存区域分层:
“slow” read/write base layer
“Fast” mmap overlay
“Quirks” to correct device virtualization issues
3.2、设备如何给guest发信号
答:QEMU:EventNotifiers;VFIO/KVM:Eventfds/irqfds configured via ioctls
有选择的操作:
直接 pass-through:通过读写配置区域
虚拟化:MSX/X、BARs、ROM等
中断信号:
QEMU通过ioctl配置vfio设备中断状态
通过eventfd发中断信号,Eventnotifiers 触发qemu设备中断,两步:host –》qemu,qemu-》VM
怎么才能更快呢?通过
irqfd:
eventfds发信号事件,irqfds接收事件信号,eventfds可以发信号给irqfds
KVM支持vm通过irqfd发中断,一步:host-》KVM,在用户态不退出
IRQ硬件加速:
APIC Virtualiztion(Intel APICv):VM更少的中断退出
VT-d 发中断:中断直接给vcpu
3.3、设备怎么传数据
答:QEMU:MemoryListeners;VFIO:IOMMU mapping & pinning ioctls
映射整个虚拟机物理地址
guest看不到IOMMU:DMA是guest physical,host IOMMU映射guest physical 到host physical
通过QEMU memorylistener完成
4、VFIO框架
1、iommu driver是物理硬件实现,如intel/amd/arm的iommu;vfio_iommu是对iommu driver的封装,向上提供功能,如DMA Remapping以及Interrupt Remapping。
2、pci_bus driver是对物理PCI设备的驱动程序; vfio_pci是对设备驱动的封装,用来提供访问设备驱动的功能,如配置空间和模拟BAR
3、VFIO interface接口层,QEMU等用户态程序,可以通过ioctl与vfio设备交互
VFIO对各个设备分区,即使有IOMMU想要以单个设备作为隔离粒度也困难,所以VFIO设备有三个概念来实现(vf)设备粒度的隔离:container/group/device
group是IOMMU能够进行DMA隔离的最小单位,一个group有一个或多个device,取决于硬件IOMMU拓扑
一个group里的设备只能全部直通一个vm,如果部分dev在其他vm或者host,无法做到物理上DMA隔离。(而被利用DMA攻击)
device指IOMMU拓扑的设备,如果设备是一个硬件拓扑上独立的设备,它自己就构成一个IOMMU group;
如果是一个multi-function设备,那么它和其他的function一起组成一个IOMMU group,因为多个function设备在物理硬件上是互联的,它们可以互相访问数据,所以必须放到一个group里隔离起来。
container是由多个group组成,虽然group是VFIO的最小隔离单元,但是有的时候并不是最好的分割粒度。如多个group可能会共享一组页表,通过将多个group组成一个container可以提高系统的性能,也能够方便用户。一般来讲,每个进程/虚拟机可以作为一个container
5、VFIO使用
04:00.0 Ethernet controller:
# readlink /sys/bus/pci/devices/0000:04:00.0/iommu_group #由内核生成的
../../../../../../kernel/iommu_groups/40
# ls /sys/bus/pci/devices/0000:04:00.0/iommu_group/devices/ #这个group只有一个设备
0000:04:00.0
# echo "0000:04:00.0" > /sys/bus/pci/devices/0000:04:00.0/driver/unbind #设备解绑
#lspci -n -s 0000:04:00.0 #查看生产商和设备ID
04:00.0 0200: 19e5:1822 (rev 45)
# echo 19e5 1822 /sys/bus/pci/drivers/vfio-pci/new_id #将设备绑定到vfio-pci驱动,会创建一个新设备节点“/dev/vfio/15”,表示直通设备所属的group文件。
#修改节点的用户组为qemu:qemu
#unlimit -l 2621400 #设置能够锁定的内存,vm内存+io空间,(2048+512)*1024
#qemu-kvm -device vfio-pci,host=0000:04:00.0,id=net0 #qemu启动
6、VFIO API编程
与KVM的dev/vm/vcpu类似,VFIO接口也分三类
container层面,通过打开“/dev/vfio/vfio”设备可以获得一个新的container,可以用在container上的ioctl包括
接口 |
功能 |
VFIOGETAPIVERSION |
用来报告VFIO API的版本 |
VFIO_CHECK_EXTENSION |
用来检测是否支持特定的扩展,如支持哪个IOMMU |
VFIO_SET_IOMMU |
用来指定IOMMU的类型,指定的IOMMU必须是通过VFIO_CHECK_EXTENSION确认驱动支持的 |
VFIO_IOMMU_GET_INFO |
用来得到IOMMU的一些信息,这个ioctl只针对Type1的IOMMU |
VFIO_IOMMU_MAP_DMA |
用来指定设备端看到的IO地址到进程的虚拟地址之间的映射,类似于KVM中的KVM_SET_USER_MEMORY_REGION指定虚拟机物理地址到进程虚拟地址之间的映射。 |
这里IOMMU的类型指定的不同架构的IOMMU实现不一样,能够向
上提供的功能也不一样,所以会有不同类型的IOMMU,如内核针对
Intel VT-d和AMD-Vi的IOMMU就叫作Type1 IOMMU
group层面,通过打开“/dev/vfio/<groupid>”可以得到一个group,group层面的ioctl包括如下几个
接口 |
功能 |
VFIO_GROUP_GET_STATUS |
用来得到指定group的状态信息,如是否可用、是否设置了container |
VFIO_GROUP_SET_CONTAINER |
用来设置container和group之间的管理,多个group可以属于单个container |
VFIO_GROUP_GET_DEVICE_FD |
用来返回一个新的文件描述符fd来描述具体设备,用户态进程可以通过该fd获取文件的诸多信息 |
设备层面,其fd是通过VFIO_GROUP_GET_DEVICE_FD接口返回的,device层面的ioctl包括如下几个。
接口 |
功能 |
VFIO_DEVICE_GET_REGION_INFO |
用来得到设备的指定Region的数据,需要注意的是,这里的region不单单指BAR,还包括 ROM空间、PCI配置空间等 |
VFIO_DEVICE_GET_IRQ_INFO |
得到设备的中断信息 |
VFIODEVICERESET |
重置设备 |
使用过程:
1、创建container,并判断是否支持Type1类型的IOMMU,设置类型为VFIO_TYPE1_IOMMU
2、打开group,得到该group的信息并设置container
3、设置DMA mapping,这里设备视角0-1M映射到进程dma_map.vaddr开始的1M
4、得到直通设备的描述符,获取其各个region信息和irq信息
5、重置设备后,vm就可以使用vf设备了