参考文章:VFIO Introduction、kernel-doc/vfio.rst、intel vt-d、kernel-doc/intel-iommu.txt、笔记intel vt-d
VFIO - "Virtual Function I/O"
许多硬件平台提供DMA和中断重映射功能,以确保I/O设备只在分配给它们的域内访问内存,一般这样的硬件单元称为IOMMU。VFIO驱动程序是一个与IOMMU具体硬件无关的框架,可以支持x86架构的VT-D、AMD-Vi,POWER PE,ARM等各种IOMMU硬件架构。使用VFIO可以实现安全的,非特权的,用户空间驱动程序。相较于VFIO,用UIO框架实现的用户空间驱动,将不受IOMMU的保护,且中断支持有限,并且需要root权限运行。
Groups, Devices, and IOMMUs
VFIO层有Group,Device及IOMMU的概念,以下分别对他们进行介绍。Device是所有IO驱动的主体。通常对它创建IO访问、中断和DMA的编程接口。如果对设备的DMA可以访问的内存空间不加限制;这将非常危险。IOMMU硬件可以将设备的可访问物理内存空间进行彼此的隔离,当在IOMMU硬件中创建好IO虚拟地址到物理地址的映射后,设备可使用IO虚拟地址访问物理内存。
有时候,相关的一组设备可能会使用同一块内存空间。因此IOMMU给内存创建隔离区的最小粒度不是Device,而是group。因此,group也是VFIO使用的最小粒度。虽然group是确保用户访问所必须使用的最小粒度,但它不一定是首选粒度。在使用页表的IOMMU中,可以在不同group之间共享一组页表,从而减少平台(减少TLB抖动、减少重复页表)和用户(仅编程一组地址转换)的开销。因此,VFIO使用container 类,该类可以包含一个或多个group。只需打开/dev/vfio/vfio字符设备即可创建container。
容器本身提供的功能很少,除了一对版本和扩展查询接口外,其他所有接口都被锁定。用户需要在容器中添加一个group以获得下一级功能,即IOMMU,不同架构的IOMMU可能提供的接口不同,因此container并未提供统一接口访问IOMMU。为此,用户首先需要标识(这个标识即后文的/dev/vfio/{group})与所需设备关联的group。这可以使用下面示例中描述的sysfs链接来查找。通过绑定设备到VFIO驱动程序,为该组添加一个新的VFIO组/dev/VFIO/{group}字符设备文件接口,其中{group}是设备所属的IOMMU组号。
一旦组准备好,就可以通过打开VFIO组字符设备(/dev/VFIO/$group)并使用VFIO_GROUP_SET_CONTAINER ioctl,传递先前打开的容器文件的文件描述符,将其添加到容器中。如果需要,并且IOMMU驱动程序支持在组之间共享IOMMU上下文,则可以将多个组设置为同一容器。如果组不能设置为具有现有组的容器,则需要使用新的空容器。将一个组(或多个组)附加到容器后,剩余的ioctl将变为可用,从而可以访问VFIO IOMMU接口。此外,现在可以使用VFIO组文件描述符上的ioctl为组中的每个设备获取文件描述符。
VFIO操作device的API包括用于设备的描述(PCI配置空间)、IO区域(BAR空间)及其在设备描述符上的读/写/mmap偏移量的ioctl,以及用于描述和注册中断通知的机制。
VFIO使用示例
以Intel平台下PCI 设备号0000:06:0.0举例。
某个设备所属的iommu组号由内核指定(?不会改变?)
使用如下命令获取设备的组id(该设备为26)
$ readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group
../../../../kernel/iommu_groups/26
对于PCI设备,需要加载内核模块vfio-pci,并将设备绑定到该驱动上,方法如下
$ modprobe vfio-pci
$ lspci -n -s 0000:06:0d.0 (查看设备的PCI vendor id和device id号)
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
注意: iommu_group 26下的所有设备必须都绑定到vfio或者部分设备不绑定任何驱动。
最后修改字符设备/dev/vfio/26的owner。并确保/dev/vfio/vfio的权限是0666;保证非root用户有读写权限。之前的步骤都需要在root下执行,在此之后可以在非root用户下执行了
$ chown user:user /dev/vfio/26
在此之后,user用户就可以访问属于iommu group 26的所有设备了。
通常,用户态程序使用如下步骤:
- 创建新的container,并检查版本(保证兼容性),以及支持的iommu驱动类型
int 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. */
- 打开iommu group 26并添加到刚才创建的container中
int group;
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);
- 因为不同硬件的iommu实现不同,因此内核支持多种iommu的类型。在使用iommu之前需要设置iommu的类型。也可以查询该iommu的硬件信息(例如iommu的page size)
struct vfio_iommu_type1_info iommu_info = { .argsz = sizeof(iommu_info) };
/* 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);
- 现在可以使用vfio的iommu做映射了。通常首先使用mmap的匿名映射方式在内核中分配内存,并完成映射。注意:匿名内存映射方式创建的页表项位于内核的全局页表中,在iommu映射的时候,使用iommu返回给进程的虚拟地址(vaddr)来查找物理页框号(pfn)。
/* 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;
/* intel平台在内核中使用函数vfio_iommu_type1_ioctl处理,
* 其调用函数vfio_dma_do_map完成具体工作,过程分两步:
* 1. 使用vaddr在全局页表中找到其第一个页的页框号pfn
* 2. 使用vfio_iommu_map建立iommu的映射,映射iova<-->页框号的对应关系
* 映射完成后,进程就可以通过vaddr,设备可以通过iova来访问pfn中的内存了。
*/
ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map);
- 在第四步中申请和映射了iommu的DMA内存。这些内存必须要给设备使用才有意义。因此首先获取VFIO的设备文件描述符;并通过设备的文件描述符获取设备的PCI BAR信息和IRQ信息。当然也可以对设备做复位操作
int device, i;
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);