VFIO【ChatGPT】

发布时间 2023-12-10 19:59:07作者: 摩斯电码

VFIO - "虚拟功能 I/O" [1]

许多现代系统现在提供 DMA 和中断重映射功能,以帮助确保 I/O 设备在它们被分配的边界内运行。这包括具有 AMD-Vi 和 Intel VT-d 的 x86 硬件、具有可分区端点(PEs)的 POWER 系统以及嵌入式 PowerPC 系统,如 Freescale PAMU。VFIO 驱动程序是一个面向 IOMMU/设备的通用框架,用于在安全的、受 IOMMU 保护的环境中向用户空间公开直接设备访问。换句话说,这允许安全的、非特权的用户空间驱动程序。

为什么我们需要这个?当虚拟机配置为实现最高可能的 I/O 性能时,通常会直接访问设备("设备分配")。从设备和主机的角度来看,这简单地将虚拟机转变为用户空间驱动程序,具有显著降低的延迟、更高的带宽和直接使用裸机设备驱动程序的好处。

一些应用程序,特别是在高性能计算领域,也受益于来自用户空间的低开销、直接设备访问。例如,包括网络适配器(通常不基于 TCP/IP)和计算加速器。在 VFIO 出现之前,这些驱动程序必须经过完整的开发周期才能成为适当的上游驱动程序,或者在树外进行维护,或者利用没有 IOMMU 保护概念、中断支持有限,并且需要 root 权限才能访问 PCI 配置空间的 UIO 框架。

VFIO 驱动程序框架旨在统一这些,取代 KVM PCI 特定设备分配代码,并提供比 UIO 更安全、更具功能的用户空间驱动程序环境。

组、设备和 IOMMU

设备是任何 I/O 驱动程序的主要目标。设备通常创建由 I/O 访问、中断和 DMA 组成的编程接口。在不深入讨论每个接口的细节的情况下,DMA 是维护安全环境最关键的方面,因为允许设备对系统内存进行读写访问对整个系统的完整性构成最大风险。

为了帮助减轻这一风险,许多现代 IOMMU 现在将隔离属性纳入到原本只用于转换的接口中(即解决具有有限地址空间的设备的寻址问题)。有了这个功能,设备现在可以相互隔离,也可以与任意内存访问隔离,因此可以将设备安全地直接分配到虚拟机中。

然而,这种隔离并不总是以单个设备为粒度的。即使 IOMMU 有这个能力,设备、互连和 IOMMU 拓扑的属性都可能降低这种隔离。例如,单个设备可能是较大的多功能封装的一部分。虽然 IOMMU 可能能够区分封装内的设备,但封装可能不需要将设备之间的事务传递到 IOMMU。例如,一个具有功能之间后门的多功能 PCI 设备,或者允许在不到达 IOMMU 的情况下重定向的非 PCI-ACS(访问控制服务)能力桥接器。拓扑结构也可能影响隐藏设备的因素。PCIe 到 PCI 桥接器会隐藏其后面的设备,使得事务看起来好像来自桥接器本身。显然,IOMMU 的设计也起着重要作用。

因此,虽然在大多数情况下,IOMMU 可能具有设备级别的粒度,但任何系统都可能受到粒度降低的影响。因此,IOMMU API 支持 IOMMU 组的概念。组是一组可以与系统中的所有其他设备隔离的设备。因此,组是 VFIO 使用的所有权单位。

虽然组是必须使用的最小粒度以确保安全的用户访问,但这并不一定是首选的粒度。在使用页表的 IOMMU 中,可能可以在不同的组之间共享一组页表,从而减少平台的开销(减少 TLB 抖动,减少重复的页表),以及用户的开销(只需编程一组翻译)。因此,VFIO 使用一个容器类,该类可以容纳一个或多个组。容器是通过简单地打开 /dev/vfio/vfio 字符设备来创建的。

单独使用容器提供的功能很少,除了一些版本和扩展查询接口被锁定。用户需要将一个组添加到容器中以获得下一级的功能。为此,用户首先需要识别与所需设备相关联的组。可以使用下面描述的 sysfs 链接来完成这一点。通过将设备从主机驱动程序解绑定并将其绑定到 VFIO 驱动程序,将为组创建一个新的 VFIO 组,其路径为 /dev/vfio/$GROUP,其中 $GROUP 是设备所属的 IOMMU 组号。如果 IOMMU 组包含多个设备,则需要在对 VFIO 组进行操作之前将每个设备绑定到 VFIO 驱动程序(如果 VFIO 驱动程序不可用,仅将设备从主机驱动程序解绑定也足够使组可用,但不包括特定设备)。待定 - 禁用驱动程序探测/锁定设备的接口。

一旦组准备就绪,可以通过打开 VFIO 组字符设备(/dev/vfio/$GROUP)并使用 VFIO_GROUP_SET_CONTAINER ioctl,传递先前打开的容器文件的文件描述符,将其添加到容器中。如果需要并且 IOMMU 驱动程序支持在组之间共享 IOMMU 上下文,可以将多个组设置为同一个容器。如果组无法设置到具有现有组的容器中,则需要使用一个新的空容器。

将组(或组)附加到容器后,剩余的 ioctls 就变得可用,从而可以访问 VFIO IOMMU 接口。此外,现在可以通过 VFIO 组文件描述符上的 ioctl 获取组内每个设备的文件描述符。

VFIO 设备 API 包括用于描述设备、设备描述符上的 I/O 区域及其读/写/mmap 偏移的 ioctls,以及用于描述和注册中断通知的机制。

VFIO 使用示例

假设用户想要访问 PCI 设备 0000:06:0d.0:

$ readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group
../../../../kernel/iommu_groups/26

因此,该设备位于 IOMMU 组 26 中。该设备位于 PCI 总线上,因此用户将使用 vfio-pci 来管理该组:

# modprobe vfio-pci

将该设备绑定到 vfio-pci 驱动程序将为该组创建 VFIO 组字符设备:

$ 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 释放该组:

$ ls -l /sys/bus/pci/devices/0000:06:0d.0/iommu_group/devices
total 0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:00:1e.0 ->
        ../../../../devices/pci0000:00/0000:00:1e.0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:06:0d.0 ->
        ../../../../devices/pci0000:00/0000:00:1e.0/0000:06:0d.0
lrwxrwxrwx. 1 root root 0 Apr 23 16:13 0000:06:0d.1 ->
        ../../../../devices/pci0000:00/0000:00:1e.0/0000:06:0d.1

该设备位于 PCIe 到 PCI 桥接器后面 4,因此我们还需要按照上述相同的步骤将设备 0000:06:0d.1 添加到该组中。设备 0000:00:1e.0 是一个当前没有主机驱动程序的桥接器,因此不需要将该设备绑定到 vfio-pci 驱动程序(vfio-pci 目前不支持 PCI 桥接器)。

最后一步是为用户提供对该组的访问权限,如果需要非特权操作(请注意,/dev/vfio/vfio 本身不提供任何功能,因此预期系统将其设置为 0666 模式):

# chown user:user /dev/vfio/26

现在,用户可以完全访问该组中的所有设备和 IOMMU,并可以按以下方式访问它们:

int container, group, device, i;
struct vfio_group_status group_status =
                                { .argsz = sizeof(group_status) };
struct vfio_iommu_type1_info iommu_info = { .argsz = sizeof(iommu_info) };
struct vfio_iommu_type1_dma_map dma_map = { .argsz = sizeof(dma_map) };
struct vfio_device_info device_info = { .argsz = sizeof(device_info) };

/* 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. */

/* 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);

/* 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);

/* 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);

IOMMUFD 和 vfio_iommu_type1

IOMMUFD 是用于从用户空间管理 I/O 页表的新用户 API。它旨在成为提供高级用户空间 DMA 功能(嵌套转换、PASID 等)的门户,同时为现有的 VFIO_TYPE1v2_IOMMU 使用案例提供向后兼容的接口。最终,vfio_iommu_type1 驱动程序以及传统的 vfio 容器和组模型都将被弃用。

IOMMUFD 的向后兼容接口可以通过两种方式启用。在第一种方法中,内核可以配置为使用 CONFIG_IOMMUFD_VFIO_CONTAINER,这样 IOMMUFD 子系统就会透明地为 VFIO 容器和 IOMMU 后端接口提供整个基础设施。兼容模式也可以通过 VFIO 容器接口访问,即 /dev/vfio/vfio 简单地链接到 /dev/iommu。需要注意的是,在撰写本文时,兼容模式相对于 VFIO_TYPE1v2_IOMMU(例如 DMA 映射 MMIO)并不完全具备功能,也不尝试提供对 VFIO_SPAPR_TCE_IOMMU 接口的兼容性。因此,目前不建议将原生的 VFIO 实现切换到 IOMMUFD 兼容接口。

从长远来看,VFIO 用户应该迁移到通过下面描述的 cdev 接口访问设备,并通过 IOMMUFD 提供的接口进行原生访问。

VFIO 设备 cdev

传统上,用户通过 VFIO_GROUP_GET_DEVICE_FD 在 VFIO 组中获取设备 fd。

使用 CONFIG_VFIO_DEVICE_CDEV=y,用户现在可以通过直接打开字符设备 /dev/vfio/devices/vfioX 来获取设备 fd,其中 "X" 是由 VFIO 为注册设备分配的唯一编号。cdev 接口不支持 noiommu 设备,因此如果需要 noiommu,则用户应该使用传统的组接口。

cdev 仅适用于 IOMMUFD。VFIO 驱动程序和应用程序都必须适应新的 cdev 安全模型,该模型要求在实际使用设备之前使用 VFIO_DEVICE_BIND_IOMMUFD 来声明 DMA 所有权。一旦绑定成功,用户就可以完全访问 VFIO 设备。

VFIO 设备 cdev 不依赖于 VFIO 组/容器/IOMMU 驱动程序。因此,在不存在传统的 VFIO 应用程序的环境中,这些模块可以完全编译出去。

到目前为止,SPAPR 尚不支持 IOMMUFD。因此,它也无法支持设备 cdev。

vfio 设备 cdev 访问仍受到 IOMMU 组语义的约束,即一个组只能有一个 DMA 所有者。属于同一组的设备不能绑定到多个 iommufd_ctx,也不能在原生内核和 vfio 总线驱动程序或其他支持 driver_managed_dma 标志的驱动程序之间共享。违反这一所有权要求将导致 VFIO_DEVICE_BIND_IOMMUFD ioctl 失败,从而限制了对设备的完全访问。

设备 cdev 示例

假设用户想要访问 PCI 设备 0000:6a:01.0:

$ ls /sys/bus/pci/devices/0000:6a:01.0/vfio-dev/
vfio0

因此,该设备表示为 vfio0。用户可以验证其存在:

$ ls -l /dev/vfio/devices/vfio0
crw------- 1 root root 511, 0 Feb 16 01:22 /dev/vfio/devices/vfio0
$ cat /sys/bus/pci/devices/0000:6a:01.0/vfio-dev/vfio0/dev
511:0
$ ls -l /dev/char/511\:0
lrwxrwxrwx 1 root root 21 Feb 16 01:22 /dev/char/511:0 -> ../vfio/devices/vfio0

然后,如果需要非特权操作,用户可以为设备提供访问权限:

$ chown user:user /dev/vfio/devices/vfio0

最后,用户可以通过以下方式获取 cdev fd:

cdev_fd = open("/dev/vfio/devices/vfio0", O_RDWR);

打开的 cdev_fd 并不赋予用户访问设备的任何权限,除非将 cdev_fd 绑定到一个 iommufd。在那之后,设备就可以完全访问,包括将其附加到 IOMMUFD IOAS/HWPT 以启用用户空间 DMA:

struct vfio_device_bind_iommufd bind = {
        .argsz = sizeof(bind),
        .flags = 0,
};
struct iommu_ioas_alloc alloc_data  = {
        .size = sizeof(alloc_data),
        .flags = 0,
};
struct vfio_device_attach_iommufd_pt attach_data = {
        .argsz = sizeof(attach_data),
        .flags = 0,
};
struct iommu_ioas_map map = {
        .size = sizeof(map),
        .flags = IOMMU_IOAS_MAP_READABLE |
                 IOMMU_IOAS_MAP_WRITEABLE |
                 IOMMU_IOAS_MAP_FIXED_IOVA,
        .__reserved = 0,
};

iommufd = open("/dev/iommu", O_RDWR);

bind.iommufd = iommufd;
ioctl(cdev_fd, VFIO_DEVICE_BIND_IOMMUFD, &bind);

ioctl(iommufd, IOMMU_IOAS_ALLOC, &alloc_data);
attach_data.pt_id = alloc_data.out_ioas_id;
ioctl(cdev_fd, VFIO_DEVICE_ATTACH_IOMMUFD_PT, &attach_data);

/* 分配一些空间并设置 DMA 映射 */
map.user_va = (int64_t)mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
                            MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
map.iova = 0; /* 从设备视图的 0x0 开始的 1MB */
map.length = 1024 * 1024;
map.ioas_id = alloc_data.out_ioas_id;;

ioctl(iommufd, IOMMU_IOAS_MAP, &map);

/* 其他设备操作如 "VFIO 使用示例" 中所述 */

VFIO用户API

请参阅include/uapi/linux/vfio.h以获取完整的API文档。

VFIO总线驱动API

VFIO总线驱动程序(例如vfio-pci)仅使用VFIO核心的少数接口。当设备绑定和解绑到驱动程序时,以下接口在设备绑定和解绑时被调用:

int vfio_register_group_dev(struct vfio_device *device);
int vfio_register_emulated_iommu_dev(struct vfio_device *device);
void vfio_unregister_group_dev(struct vfio_device *device);

驱动程序应将vfio_device嵌入到自己的结构中,并使用vfio_alloc_device()来分配结构,并可以注册@init/@release回调来管理包装vfio_device的任何私有状态:

vfio_alloc_device(dev_struct, member, dev, ops);
void vfio_put_device(struct vfio_device *device);

vfio_register_group_dev()指示核心开始跟踪指定dev的iommu_group,并将dev注册为VFIO总线驱动程序拥有的设备。一旦vfio_register_group_dev()返回,用户空间就可以开始访问驱动程序,因此驱动程序在调用之前应确保它完全准备好。驱动程序提供了一个类似于文件操作结构的ops结构体用于回调:

struct vfio_device_ops {
        char    *name;
        int     (*init)(struct vfio_device *vdev);
        void    (*release)(struct vfio_device *vdev);
        int     (*bind_iommufd)(struct vfio_device *vdev,
                                struct iommufd_ctx *ictx, u32 *out_device_id);
        void    (*unbind_iommufd)(struct vfio_device *vdev);
        int     (*attach_ioas)(struct vfio_device *vdev, u32 *pt_id);
        void    (*detach_ioas)(struct vfio_device *vdev);
        int     (*open_device)(struct vfio_device *vdev);
        void    (*close_device)(struct vfio_device *vdev);
        ssize_t (*read)(struct vfio_device *vdev, char __user *buf,
                        size_t count, loff_t *ppos);
        ssize_t (*write)(struct vfio_device *vdev, const char __user *buf,
                 size_t count, loff_t *size);
        long    (*ioctl)(struct vfio_device *vdev, unsigned int cmd,
                         unsigned long arg);
        int     (*mmap)(struct vfio_device *vdev, struct vm_area_struct *vma);
        void    (*request)(struct vfio_device *vdev, unsigned int count);
        int     (*match)(struct vfio_device *vdev, char *buf);
        void    (*dma_unmap)(struct vfio_device *vdev, u64 iova, u64 length);
        int     (*device_feature)(struct vfio_device *device, u32 flags,
                                  void __user *arg, size_t argsz);
};

每个函数都传递了最初在vfio_register_group_dev()或vfio_register_emulated_iommu_dev()调用中注册的vdev。这允许总线驱动程序使用container_of()获取其私有数据。

  • init/release回调在vfio_device初始化和释放时发出。

  • open/close device回调在为设备创建文件描述符的第一个实例(例如通过VFIO_GROUP_GET_DEVICE_FD)时发出,用于用户会话。

  • ioctl回调为一些VFIO_DEVICE_* ioctl提供了直接的传递。

  • [un]bind_iommufd回调在设备绑定到和解绑从iommufd时发出。

  • [de]attach_ioas回调在设备附加到和从绑定的iommufd管理的IOAS中分别发出。但是,当设备从iommufd解绑时,附加的IOAS也可以自动分离。

  • read/write/mmap回调实现了设备区域访问,该访问由设备自己的VFIO_DEVICE_GET_REGION_INFO ioctl定义。

  • request回调在设备将要注销时发出,例如尝试从vfio总线驱动程序解绑设备时。

  • dma_unmap回调在容器或设备附加的IOAS中取消映射一系列iova时发出。使用vfio页面固定接口的驱动程序必须实现此回调以在dma_unmap范围内取消固定页面。驱动程序必须容忍在调用open_device()之前甚至调用此回调。

PPC64 sPAPR实现的一些具体细节:

  1. 在旧系统(具有P5IOC2/IODA1的POWER7)上,每个容器只支持一个IOMMU组,因为在引导时分配了一个IOMMU表,每个IOMMU组一个表,而每个IOMMU组是一个可分区端点(PE)(PE通常是一个PCI域,但并非总是)。

    较新的系统(具有IODA2的POWER8)具有改进的硬件设计,可以消除这种限制,并且每个VFIO容器可以有多个IOMMU组。

  2. 硬件支持所谓的DMA窗口 - PCI地址范围内允许DMA传输,在窗口之外的地址空间的任何访问都会导致整个PE被隔离。

  3. PPC64客户端是半虚拟化的,但不是完全模拟的。有一个API用于映射/取消映射DMA页面,通常每次映射1到32个页面,目前没有办法减少调用次数。为了加快速度,映射/取消映射处理已经在实模式下实现,提供了出色的性能,但也有一些限制,比如无法实时进行锁定页面的计数。

  4. 根据sPAPR规范,可分区端点(PE)是可以视为一个单元进行分区和错误恢复的I/O子树。PE可以是单个或多功能IOA(IO适配器),多功能IOA的功能,或多个IOA(可能包括多个IOA上面的交换和桥接结构)。PPC64客户端通过EEH RTAS服务检测PCI错误并从中恢复,这是基于额外的ioctl命令的。

    因此,添加了4个额外的ioctl:

    • VFIO_IOMMU_SPAPR_TCE_GET_INFO:返回PCI总线上DMA窗口的大小和起始位置。
    • VFIO_IOMMU_ENABLE:启用容器。在这一点上进行锁定页面计数。这使用户首先了解DMA窗口是什么,并在执行任何真正的工作之前调整rlimit。
    • VFIO_IOMMU_DISABLE:禁用容器。
    • VFIO_EEH_PE_OP:提供EEH设置、错误检测和恢复的API。

    代码流程应该略有改变:

    struct vfio_eeh_pe_op pe_op = { .argsz = sizeof(pe_op), .flags = 0 };
    
    .....
    /* 将组添加到容器中 */
    ioctl(group, VFIO_GROUP_SET_CONTAINER, &container);
    
    /* 启用我们想要的IOMMU模型 */
    ioctl(container, VFIO_SET_IOMMU, VFIO_SPAPR_TCE_IOMMU)
    
    /* 获取额外的sPAPR IOMMU信息 */
    vfio_iommu_spapr_tce_info spapr_iommu_info;
    ioctl(container, VFIO_IOMMU_SPAPR_TCE_GET_INFO, &spapr_iommu_info);
    
    if (ioctl(container, VFIO_IOMMU_ENABLE))
            /* 无法启用容器,可能是rlimit太低 */
    
    /* 分配一些空间并设置DMA映射 */
    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; /* 从设备视图的0x0开始的1MB */
    dma_map.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE;
    
    /* 在这里检查.iova/.size是否在spapr_iommu_info的DMA窗口内 */
    ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
    
    /* 获取设备的文件描述符 */
    device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0");
    
    ....
    
    /* 无谓的设备重置然后继续... */
    ioctl(device, VFIO_DEVICE_RESET);
    
    /* 确保EEH受支持 */
    ioctl(container, VFIO_CHECK_EXTENSION, VFIO_EEH);
    
    /* 在设备上启用EEH功能 */
    pe_op.op = VFIO_EEH_PE_ENABLE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* 建议创建额外的数据结构来表示PE,并将属于同一IOMMU组的子设备放入PE实例以供以后引用。 */
    
    /* 检查PE的状态并确保它处于功能状态 */
    pe_op.op = VFIO_EEH_PE_GET_STATE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* 使用pci_save_state()保存设备状态。
     * EEH应该在指定设备上启用。
     */
    
    ....
    
    /* 注入EEH错误,预计是由32位配置加载引起的。 */
    pe_op.op = VFIO_EEH_PE_INJECT_ERR;
    pe_op.err.type = EEH_ERR_TYPE_32;
    pe_op.err.func = EEH_ERR_FUNC_LD_CFG_ADDR;
    pe_op.err.addr = 0ul;
    pe_op.err.mask = 0ul;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    ....
    
    /* 当从读取PCI配置空间或PCI设备的IO BARs返回0xFF时。
     * 检查PE的状态,看看是否已经被冻结。
     */
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* 等待待处理的PCI事务完成,并且不要产生更多的PCI流量从/到受影响的PE,直到恢复完成。 */
    
    /* 为受影响的PE启用IO并收集日志。通常,PCI配置空间的标准部分,AER寄存器被转储为进一步分析的日志。 */
    pe_op.op = VFIO_EEH_PE_UNFREEZE_IO;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /*
     * 发出PE重置:热重置或基本重置。通常,热重置就足够了。但是,一些PCI适配器的固件可能需要基本重置。
     */
    pe_op.op = VFIO_EEH_PE_RESET_HOT;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    pe_op.op = VFIO_EEH_PE_RESET_DEACTIVATE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* 配置受影响PE的PCI桥 */
    pe_op.op = VFIO_EEH_PE_CONFIGURE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* 恢复在初始化时保存的状态。pci_restore_state()作为示例已足够。 */
    
    /* 希望错误已成功恢复。现在,您可以恢复开始PCI流量到/从受影响的PE。 */
    
    ....
    
  5. 这里还有SPAPR TCE IOMMU的v2版本。它废弃了VFIO_IOMMU_ENABLE/VFIO_IOMMU_DISABLE,并实现了2个新的ioctl:VFIO_IOMMU_SPAPR_REGISTER_MEMORY和VFIO_IOMMU_SPAPR_UNREGISTER_MEMORY(在v1 IOMMU中不支持)。

    PPC64半虚拟化客户端生成大量的映射/取消映射请求,处理这些请求包括固定/解除固定页面和更新mm::locked_vm计数,以确保不超过rlimit。v2 IOMMU将计数和固定分为单独的操作:

    • VFIO_IOMMU_SPAPR_REGISTER_MEMORY/VFIO_IOMMU_SPAPR_UNREGISTER_MEMORY ioctl接收用户空间地址和要固定的块的大小。不支持二分法,预计VFIO_IOMMU_UNREGISTER_MEMORY将使用与注册内存块相同的确切地址和大小进行调用。不希望用户空间经常调用这些操作。这些范围存储在VFIO容器中的链表中。

    • VFIO_IOMMU_MAP_DMA/VFIO_IOMMU_UNMAP_DMA ioctl仅更新实际的IOMMU表,不进行固定;而是检查用户空间地址是否来自预先注册的范围。

    这种分离有助于优化客户端的DMA。

  6. sPAPR规范允许客户端在PCI总线上具有额外的DMA窗口,并且具有可变的页面大小。添加了两个ioctl来支持这一点:VFIO_IOMMU_SPAPR_TCE_CREATE和VFIO_IOMMU_SPAPR_TCE_REMOVE。平台必须支持该功能,否则将向用户空间返回错误。现有硬件支持最多2个DMA窗口,一个长度为2GB,使用4K页面,称为“默认32位窗口”;另一个可以达到整个RAM的大小,使用不同的页面大小,这是可选的 - 如果客户端驱动程序支持64位DMA,则客户端在运行时创建这些窗口。

    VFIO_IOMMU_SPAPR_TCE_CREATE接收一个页面偏移、DMA窗口大小和TCE表级数(如果TCE表足够大,内核可能无法分配足够的物理连续内存)。它在可用槽中创建一个新窗口,并返回新窗口开始的总线地址。由于硬件限制,用户空间无法选择DMA窗口的位置。

    VFIO_IOMMU_SPAPR_TCE_REMOVE接收窗口的总线起始地址并将其移除。

最后,还提到了一些相关的背景信息,包括VFIO的起源、设备行为的安全性、虚拟机设备分配的权衡以及IOMMU的一些特性。


  1. VFIO最初是由Tom Lyon在Cisco时的初始实现中的缩写,代表"Virtual Function I/O"。后来我们已经超出了这个缩写的范畴,但它很引人注目。

  2. "安全"也取决于设备是否"行为良好"。多功能设备之间可能存在后门,甚至单功能设备可能通过MMIO寄存器有替代访问PCI配置空间的途径。为了防范前者,我们可以在IOMMU驱动中增加额外的预防措施,将多功能PCI设备分组在一起(iommu=group_mf)。后者我们无法阻止,但IOMMU仍应提供隔离。对于PCI来说,SR-IOV虚拟功能是"行为良好"的最佳指标,因为它们专为虚拟化使用模型而设计。

  3. 虚拟机设备分配存在一些超出VFIO范畴的权衡。预计未来的IOMMU技术将减少一些,但可能不是全部这些权衡。

  4. 在这种情况下,设备位于PCI桥下,因此来自设备任一功能的事务对于IOMMU来说是无法区分的:

    -[0000:00]-+-1e.0-[06]--+-0d.0
                            \-0d.1

    00:1e.0 PCI桥: Intel Corporation 82801 PCI Bridge (rev 90)
  1. 嵌套翻译是IOMMU的一项功能,支持两级地址翻译。这提高了IOMMU虚拟化中的地址翻译效率。

  2. PASID代表Process Address Space ID,由PCI Express引入。它是共享虚拟地址(SVA)和可扩展I/O虚拟化(Scalable IOV)的先决条件。