VFIO设备直通原理(一)

Wang Xingang2020-11-29IOMMUSMMUDMA

现代系统大多提供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, &reg);

    /* 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重映射让用户态驱动访问物理设备能力.

Reference

  • https://www.kernel.org/doc/Documentation/vfio.txt
  • https://kernelgo.org/vfio-introduction.html

【免责声明】本文仅代表作者本人观点,与本网站无关。本网站对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文仅供读者参考,由此产生的所有法律责任均由读者本人承担。