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

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

我们为什么要这样做?虚拟机在配置为尽可能高的 I/O 性能时,通常会利用直接设备访问(“设备分配”)。从设备和主机的角度来看,这只是将 VM 变成一个用户空间驱动程序,具有显著减少延迟、更高带宽和直接使用裸机设备驱动程序[3]的好处。

一些应用程序,尤其是在高性能计算领域,也受益于用户空间中低开销的直接设备访问。示例包括网络适配器(通常不是基于 TCP/IP 的)和计算加速器。在 VFIO 之前,这些驱动程序要么必须经历完整的开发周期才能成为合适的上游驱动程序,要么在树外维护,要么使用 UIO 框架,该框架没有 IOMMU 保护的概念,中断支持有限,并且需要 root 权限才能访问诸如 PCI 配置空间之类的内容。

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 驱动程序不可用,也只需将设备从主机驱动程序取消绑定;这将使组可用,但不是特定的设备)。TBD - 用于禁用驱动程序探测/锁定设备的接口。

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

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

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

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 是新的用户 API,用于管理来自用户空间的 I/O 页表。它旨在成为交付高级用户空间 DMA 功能(嵌套转换[5]、PASID[6] 等)的门户,同时还为现有的 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 组中的 VFIO_GROUP_GET_DEVICE_FD 获取设备 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 所有权。一旦 BIND 成功,用户就可以完全访问 VFIO 设备。

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

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

vfio 设备 cdev 访问仍然受 IOMMU 组语义的约束,即一个组只能有一个 DMA 所有者。属于同一组的设备不能绑定到多个 iommufd_ctx,也不能在原生内核和 vfio 总线驱动程序或其他支持 driver_managed_dma 标志的驱动程序之间共享。违反此所有权要求将在 VFIO_DEVICE_BIND_IOMMUFD ioctl 中失败,该 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);

/* Allocate some space and setup a DMA mapping */
map.user_va = (int64_t)mmap(0, 1024 * 1024, PROT_READ | PROT_WRITE,
                            MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
map.iova = 0; /* 1MB starting at 0x0 from device view */
map.length = 1024 * 1024;
map.ioas_id = alloc_data.out_ioas_id;

ioctl(iommufd, IOMMU_IOAS_MAP, &map);

/* Other device operations as stated in "VFIO Usage Example" */

VFIO 用户 API

有关完整的 API 文档,请参阅 include/uapi/linux/vfio.h。

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() 获取其私有数据。

- The init/release callbacks are issued when vfio_device is initialized
  and released.

- The open/close device callbacks are issued when the first
  instance of a file descriptor for the device is created (eg.
  via VFIO_GROUP_GET_DEVICE_FD) for a user session.

- The ioctl callback provides a direct pass through for some VFIO_DEVICE_*
  ioctls.

- The [un]bind_iommufd callbacks are issued when the device is bound to
  and unbound from iommufd.

- The [de]attach_ioas callback is issued when the device is attached to
  and detached from an IOAS managed by the bound iommufd. However, the
  attached IOAS can also be automatically detached when the device is
  unbound from iommufd.

- The read/write/mmap callbacks implement the device region access defined
  by the device's own VFIO_DEVICE_GET_REGION_INFO ioctl.

- The request callback is issued when device is going to be unregistered,
  such as when trying to unbind the device from the vfio bus driver.

- The dma_unmap callback is issued when a range of iovas are unmapped
  in the container or IOAS attached by the device. Drivers which make
  use of the vfio page pinning interface must implement this callback in
  order to unpin pages within the dma_unmap range. Drivers must tolerate
  this callback even before calls to open_device().

PPC64 sPAPR 实现说明

此实现有一些特殊之处

  1. 在较旧的系统(带有 P5IOC2/IODA1 的 POWER7)上,每个容器仅支持一个 IOMMU 组,因为 IOMMU 表是在启动时分配的,每个 IOMMU 组一个表,该组是一个可分区端点 (PE)(PE 通常是 PCI 域,但并非总是如此)。

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

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

  3. PPC64 访客是半虚拟化的,但不是完全模拟的。有一个 API 用于映射/取消映射 DMA 的页面,它通常每次调用映射 1..32 个页面,并且目前没有办法减少调用次数。为了加快速度,映射/取消映射处理已在实模式下实现,该模式提供了卓越的性能,但有一些限制,例如无法实时进行锁定页面记帐。

  4. 根据 sPAPR 规范,可分区端点 (PE) 是一个 I/O 子树,在分区和错误恢复时可以将其视为一个单元。PE 可以是单功能或多功能 IOA(I/O 适配器)、多功能 IOA 的功能或多个 IOA(可能包括多个 IOA 上方的交换机和桥接结构)。PPC64 访客检测 PCI 错误并通过 EEH RTAS 服务从中恢复,该服务基于附加的 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 };
    
    .....
    /* 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_SPAPR_TCE_IOMMU)
    
    /* Get addition sPAPR IOMMU info */
    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))
            /* Cannot enable container, may be low rlimit */
    
    /* 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;
    
    /* Check here is .iova/.size are within DMA window from spapr_iommu_info */
    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");
    
    ....
    
    /* Gratuitous device reset and go... */
    ioctl(device, VFIO_DEVICE_RESET);
    
    /* Make sure EEH is supported */
    ioctl(container, VFIO_CHECK_EXTENSION, VFIO_EEH);
    
    /* Enable the EEH functionality on the device */
    pe_op.op = VFIO_EEH_PE_ENABLE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* You're suggested to create additional data struct to represent
     * PE, and put child devices belonging to same IOMMU group to the
     * PE instance for later reference.
     */
    
    /* Check the PE's state and make sure it's in functional state */
    pe_op.op = VFIO_EEH_PE_GET_STATE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* Save device state using pci_save_state().
     * EEH should be enabled on the specified device.
     */
    
    ....
    
    /* Inject EEH error, which is expected to be caused by 32-bits
     * config load.
     */
    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);
    
    ....
    
    /* When 0xFF's returned from reading PCI config space or IO BARs
     * of the PCI device. Check the PE's state to see if that has been
     * frozen.
     */
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* Waiting for pending PCI transactions to be completed and don't
     * produce any more PCI traffic from/to the affected PE until
     * recovery is finished.
     */
    
    /* Enable IO for the affected PE and collect logs. Usually, the
     * standard part of PCI config space, AER registers are dumped
     * as logs for further analysis.
     */
    pe_op.op = VFIO_EEH_PE_UNFREEZE_IO;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /*
     * Issue PE reset: hot or fundamental reset. Usually, hot reset
     * is enough. However, the firmware of some PCI adapters would
     * require fundamental reset.
     */
    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);
    
    /* Configure the PCI bridges for the affected PE */
    pe_op.op = VFIO_EEH_PE_CONFIGURE;
    ioctl(container, VFIO_EEH_PE_OP, &pe_op);
    
    /* Restored state we saved at initialization time. pci_restore_state()
     * is good enough as an example.
     */
    
    /* Hopefully, error is recovered successfully. Now, you can resume to
     * start PCI traffic to/from the affected 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 接收窗口的总线起始地址并将其删除。