现代系统大多提供DMA和中断重映射功能来确保I/O设备在有限的范围内运行,比如x86平台的AMD-Vi和Intel VT-d。VFIO(Virtual Function I/O)是一个可以将设备I/O、中断和DMA等能力安全的暴露到用户态空间,从而使用用户态驱动实现设备驱动的框架。通过VFIO进行设备直通到虚拟机,可以获得更高的设备I/O性能。
实现用户态驱动最关键的问题在于如何安全可控的将设备的DMA能力暴露到用户空间,IOMMU的出现可以限制设备对内存的访问,恶意的设备不能直接读写物理内存,经过IOMMU映射之后才能使用IOVA或者虚拟地址进行访存,由IOMMU来保证访存的安全性。
VFIO内核组件
VFIO内核组件主要包括如下图所示,通过设备文件向用户态提供统一访问接口vfio interface层,包括:
- VFIO container
- VFIO group
- VFIO device
+-----------------------------------------+
| vfio interface |
+-----------------------------------------+
| vfio_iommu_driver | vfio_pci |
+--------------------+--------------------+
| iommu | pci_bus |
+--------------------+--------------------+
vfio interface 封装了vfio\_iommu\_driver和vfio\_pci分别和底层的IOMMU、PCI驱动进行交互,vfio\_iommu\_driver为VFIO提供了IOMMU重映射驱动,向用户态暴露DMA操作,主要是vfio\_iommu\_type1驱动,利用IOMMU管理IO页表的能力来进行IO重映射。vfio\_pci模块封装pci设备驱动并和用户态程序进行配合完成用户态的设备配置模拟、Bar空间重定向及中断重映射等功能。
VFIO框架中比较重要的几个概念包括:Container、Group和Device,其相互之间的关系如图所示,一个container可以理解为实际的物理资源集合,每个container中可以有多个group,group描述了设备在物理上的划分,一个group可以有多个device,划分的逻辑取决于硬件上的IOMMU拓扑结构。
container
+------------------------+
| group0 group1 |
| +-------+ +------+ |
| | dev0 | | dev2 | |
| | dev1 | +------+ |
| +-------+ |
+------------------------+
可以结合内核中vfio.txt文件来理解Container、Group、Device和IOMMU之间的关系。
VFIO Container
// container: /dev/vfio/vfio
struct vfio_container {
struct kref kref;
struct list_head group_list;
struct rw_semaphore group_lock;
struct vfio_iommu_driver *iommu_driver;
void *iommu_data;
bool noiommu;
};
Container是管理内存资源,和IOMMU、DMA及地址空间相关,可以通过打开设备文件/dev/vfio/vfio来获取container对应的文件描述符,在内核vfio/vfio.c中有对应该vfio设备文件的具体操作实现,ioctl主要是可以获取IOMMU相关的信息,vfio会将用户态对IOMMU相关操作发给底层的vfio_iommu驱动进行操作,通过vfio ioctl提供的接口如下:
- 获取API versio
- 设置IOMMU的类型,如设置为常用的VFIO\_TYPE1\_IOMMU
- 获取IOMMU的信息
- 分配空间并进行DMA映射
int container, group, device, i;
struct vfio_iommu_type1_info iommu_info = { .argsz = sizeof(iommu_info) };
struct vfio_iommu_type1_dma_map dma_map = { .argsz = sizeof(dma_map) };
/* Create a new container */
container = open("/dev/vfio/vfio", O_RDWR);
if (ioctl(container, VFIO_GET_API_VERSION) != VFIO_API_VERSION)
/* Unknown API version */
if (!ioctl(container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU))
/* Doesn't support the IOMMU driver we want. */
/* Enable the IOMMU model we want */
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU);
/* Get addition IOMMU info */
ioctl(container, VFIO_IOMMU_GET_INFO, &iommu_info);
/* Allocate some space and setup a DMA mapping */
dma_map.vaddr = mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
dma_map.size = 1024 * 1024;
dma_map.iova = 0; /* 1MB starting at 0x0 from device view */
dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;
ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
VFIO Group
// group: /dev/vfio/%group_id
struct vfio_group {
struct kref kref;
int minor;
atomic_t container_users;
struct iommu_group *iommu_group;
struct vfio_container *container;
struct list_head device_list;
struct mutex device_lock;
struct device *dev;
struct notifier_block nb;
struct list_head vfio_next;
struct list_head container_next;
struct list_head unbound_list;
struct mutex unbound_lock;
atomic_t opened;
wait_queue_head_t container_q;
bool noiommu;
struct kvm *kvm;
struct blocking_notifier_head notifier;
};
Group是IOMMU进行DMA隔离的最小硬件单元,设备属于哪个group取决于IOMMU和设备的物理结构,在设备直通时需要将一个group里的所有设备都分配给一个虚拟机,其实就是多个group可以从属于一个container,而group下的所有设备也随着该group从属于该container。这样能够做到DMA隔离,避免一个container里的device通过DMA来攻击获取另一个container里的数据。
对于一个PCI设备0000:06:0d.0::,通过readlink可以在sys文件目录下获取其iommu_group,比如该PCI设备在ID为26的IOMMU group中。
$ readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group
../../../../kernel/iommu_groups/26
设备挂载在pci bus下,可以使用 vfio-pci 来管理这个group。使用vfio-pci来管理设备时,首先从原来的驱动里unbind该PCI设备,然后将id写入新的vfio-pci路径下,会为这个group创建一个字符设备。
$ lspci -n -s 0000:06:0d.0
06:0d.0 0401: 1102:0002 (rev 08)
$ echo 0000:06:0d.0 > /sys/bus/pci/devices/0000:06:0d.0/driver/unbind
$ echo 1102 0002 > /sys/bus/pci/drivers/vfio-pci/new_id
当设备绑定到vfio之后,在/dev/vfio/路径下面会产生一个新的group id,通过该id可以获取到group,完成以下操作:
- 查询group状态,是否所有设备都绑定到vfio驱动
- 设置group的container
- 根据设备的BDF号为设备分配一个文件描述符
struct vfio_group_status group_status =
{ .argsz = sizeof(group_status) };
/* Open the group */
group = open("/dev/vfio/26", O_RDWR);
/* Test the group is viable and available */
ioctl(group, VFIO_GROUP_GET_STATUS, &group_status);
if (!(group_status.flags & VFIO_GROUP_FLAGS_VIABLE))
/* Group is not viable (ie, not all devices bound for vfio) */
/* Add the group to the container */
ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
/* Get a file descriptor for the device */
device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");
VFIO Device
struct vfio_device {
struct kref kref;
struct device *dev;
const struct vfio_device_ops *ops;
struct vfio_group *group;
struct list_head group_next;
void *device_data;
};
为了兼顾platform和pci设备,vfio统一对外提供struct vfio_device
来描述vfio设备,并用device_data来指向如struct vfio_pci_device
。Device即设备,但与真正的物理设备有区别的是,对于一个在硬件上独立的设备,单独构成一个iommu group,而如果是multi-function的设备,多个function之间是互联的,相互可以访问对方的数据,所以必须放到一个group里面。
通过group的ioctl操作和设备的的BDF号获取到设备描述符之后,在vfio_pci中有对应描述符的内核操作vfio_pci_ops,这个ops是在vfio_pci设备驱动vfio_pci_probe调用的时候注册到PCI设备的,probe的时候还会将设备加入到对应的group中。vfio_pci设备的ops中比较重要的是 vfio_pci_ioctl函数,提供了如下功能:
- VFIO_DEVICE_GET_INFO:获取设备信息,region数量、irq数量等
- VFIO_DEVICE_GET_REGION_INFO:获取vfio_region的信息,包括配置空间的region和bar空间的region等
- VFIO_DEVICE_GET_IRQ_INFO:获取设备中断相关的信息
- VFIO_DEVICE_SET_IRQS:完成中断相关的设置
- VFIO_DEVICE_RESET:设备复位
- VFIO_DEVICE_GET_PCI_HOT_RESET_INFO:获取PCI设备hot reset信息
- VFIO_DEVICE_PCI_HOT_RESET:设置PCI设备 hot reset
- VFIO_DEVICE_IOEVENTFD:设置ioeventfd
要暴露设备的能力到用户态空间,要让用户态能够直接访问设备配置空间并处理设备中断,对于PCI设备而言,其配置其配置空间是一个VFIO region,对应着一块MMIO内存,通过建立dma重映射让用户态能够直接访问设备配置空间,另外还需要建立中断重映射以让用户态驱动处理设备中断事件。
struct vfio_device_info device_info = { .argsz = sizeof(device_info) };
/* Get a file descriptor for the device */
device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");
/* Test and setup the device */
ioctl(device, VFIO_DEVICE_GET_INFO, &device_info);
for (i = 0; i < device_info.num_regions; i++) {
struct vfio_region_info reg = { .argsz = sizeof(reg) };
reg.index = i;
ioctl(device, VFIO_DEVICE_GET_REGION_INFO, ®);
/* Setup mappings... read/write offsets, mmaps
* For PCI devices, config space is a region */
}
for (i = 0; i < device_info.num_irqs; i++) {
struct vfio_irq_info irq = { .argsz = sizeof(irq) };
irq.index = i;
ioctl(device, VFIO_DEVICE_GET_IRQ_INFO, &irq);
/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQS */
}
/* Gratuitous device reset and go... */
ioctl(device, VFIO_DEVICE_RESET);
Container,group和device绑定
1.VFIO_SET_IOMMU: Container 绑定 IOMMU:
首先,VFIO的Container和IOMMU之间的绑定,通过在用户态通过ioctl调用VFIO_SET_IOMMU完成,绑定意味着将container管理的所有group都attach到IOMMU中,最终会将每个group中的每个设备都attach到IOMMU中,这意味着为设备建立IO页表完成初始化
ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU)
vfio_ioctl_set_iommu
list_for_each_entry(group, &container->group_list, container_next) {
ret = driver->ops->attach_group(data, group->iommu_group);
__iommu_attach_group
ret = __iommu_group_for_each_dev(group, domain,
iommu_group_do_attach_device);
__iommu_attach_device
arm_smmu_attach_dev
建立设备的IO页表
2.VFIO\_GROUP\_SET_CONTAINER: 将Group设置到对应的Container:
VFIO提供接口由用户态指定Group绑定到哪个Container中,这个绑定操作会将group记录到container的链表中进行管理,并且如果已经设置好了vfio\_iommu\_driver,会进行group的attach操作,并进而完成该group中的设备的IO页表初始化
VFIO_GROUP_SET_CONTAINER:
vfio_group_set_container
driver = container->iommu_driver;
if (driver) {
ret = driver->ops->attach_group(container->iommu_data,
group->iommu_group);
if (ret)
goto unlock_out;
}
group->container = container;
container->noiommu = group->noiommu;
list_add(&group->container_next, &container->group_list);
3.Device和Group之间的绑定关系源自设备和IOMMU的物理拓扑结构
小结
VFIO内核组件的实现与Linux内核的IOMMU、设备模型等紧密相连,通过抽象出VFIO的概念来完成对Linux内核组件的封装。本文主要通过VFIO的用户态接口的使用来介绍了VFIO的几个基本概念,包括VFIO Container、Group和Device。要让物理设备通过VFIO驱动暴露给用户态,需要完成以下步骤:
- 首先将设备与原有驱动进行解绑,并重新绑定到VFIO驱动,VFIO驱动会为设备指定对应的group,设备属于哪个IOMMU group与设备和IOMMU的物理拓扑结构有关。
- 完成上述绑定之后,用户态驱动就可以通过
/dev/vfio/vfio
获取到VFIO 的container,设置vfio_iommu_driver的类型,通过container可以间接访问IOMMU完成dma映射。 - 然后可以通过
/dev/vfio/%group_id
获取到设备所属的group,通过ioctl将该group上的所有设备加入到container中。 - 然后通过group和设备BDF号可以获取到VFIO device的fd,并通过vfio提供的接口访问设备的配置空间和irq信息等,完成在用户态访问物理设备。
VFIO设备直通有几个关键问题需要关注,如何访问直通设备的IO地址空间,如何完成中断重映射和DMA重映射让用户态驱动访问物理设备能力.
文章来源:openEuler
推荐阅读
- DAOS ARM64调测之旅
- Virtio协议概述
- 回顾2022年计算机视觉领域最激动人心的进展
- 量化部署篇 | Vision Transformer应该如何进行PTQ量化?这个方法或许可以参考!
- 自制深度学习推理框架-前言-第一课
- YOLOv6精度提升的又一根稻草,感知量化的重参再设计,QARep值得内卷
更多Arm服务器干货请关注Arm服务器专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。