VFIO - “虚拟函数 I/O”[1]

许多现代系统现在提供 DMA 和中断重映射机制,以帮助确保 I/O 设备在其分配的边界内运行。 这包括具有 AMD-Vi 和 Intel VT-d 的 x86 硬件、具有可分区端点 (PE) 的 POWER 系统以及嵌入式 PowerPC 系统,例如 Freescale PAMU。 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 包括用于描述设备的 ioctl,I/O 区域及其在设备描述符上的读/写/mmap 偏移量,以及用于描述和注册中断通知的机制。

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 组一张表,该 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(IO 适配器)、多功能 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. 有 v2 版本的 SPAPR TCE IOMMU。 它弃用了 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 接收窗口的总线起始地址并将其删除。