1. What is VFIO?
-
VFIO是一个可以安全的把设备I/O、中断、DMA等暴露到用户空间(userspace),从而可以在用户空间完成设备驱动的框架。
-
得益于vfio低开销的用户空间直接设备访问,虚拟机设备分配(device assignment)、高性能应用等可以获得更高的I/O性能。
2. IOMMU
实现用户空间设备驱动,最困难的在于如何将DMA以安全可控的方式暴露到用户空间:
-
提供DMA的设备通常可以写内存的任意页,因此使用户空间拥有创建DMA的能力就等同于用户空间拥有了root权限,恶意的设备可能利用此发动DMA攻击。
-
I/O memory management unit(IOMMU)的引入对设备进行了限制,设备I/O地址需要经过IOMMU重映射为内存物理地址,如图2-1。恶意的或存在错误的设备不能读写没有被明确映射过的内存,运行在cpu上的操作系统以互斥的方式管理MMU与IOMMU,物理设备不能绕行或污染可配置的内存管理表项。
图 2-1 Comparison IOMMU and MMU
IOMMU其他好处:
-
IOMMU可以将连续的虚拟地址映射到不连续的多个物理内存片段,从而支持vectored I/O(scatter-gather list);
-
对于不能寻址全部物理地址空间的设备,通过IOMMU的重映射,从而避免了将数据从设备可访问的外围地址空间拷入拷出设备无法访址的物理地址空间的额外开销(避免了bounce buffer)。
3. Device,group, container
由于device本身的特性、互连(interconnect)及IOMMU的拓扑等,IOMMU提供device 隔离(ioslation)的最小粒度是group,而不是device。如一个pci device可能包括多个function,而这些function之间数据传递可以通过专用通道(backdoor),而不经过IOMMU等等,所以device并不适合做隔离的最小单元。
container可以包含多个group,这些group共享页表信息。
4. vfio use example
详看[linux-rootdir]/Documentation/vfio.txt
与链接:vfio_device_test.c
-
Linux kernel实现
5.1. 相关内核组件
5.1.1 内核组件概图
图5-1是vfio内核组件概图:
图5-1 vfio内核组件概图
vfio interface:vfio通过设备文件向userspace提供统一访问接口,包括container、group、device。
vfio_iommu_driver:为vfio提供了IOMMU重映射驱动,即向用户空间暴露DMA操作,如container的ioctl选项VFIO_IOMMU_MAP_DMA即由vfio containter设备文件对应的 file_operations 的ioctl转发到vfio_iommu_driver的ioctl实现,已实现的vfio_iommu_driver包括vfio_iommu_type1、vfio_spapr_eeh等,这里重点分析vfio_iommu_type1。
vfio-pci:vfio支持pci设备pass-through,vfio-pci作为pci driver挂载到pci总线,提供将pci设备io、interrupt暴露到用户空间实现。
5.1.2. 各组件之间如何关联
图3是vfio关键数据结构概图:
图5-2 vfio关键数据结构概图
注:M表示一对多
-
设备文件入口
a) Container
userspace通过open设备文件/dev/vfio/vfio获得container对应的文件描述符:
/* Create a new container */ int container = open("/dev/vfio/vfio", O_RDWR); |
文件标识符container关联struct file_operations vfio_fops,这是在vfio/vfio.c中通过注册miscdevice实现的:
1793 static struct miscdevice vfio_dev = { 1794 .minor = VFIO_MINOR, 1795 .name = "vfio", 1796 .fops = &vfio_fops, 1797 .nodename = "vfio/vfio", 1798 .mode = S_IRUGO | S_IWUGO, 1799 }; |
b) Group
userspace通过open设备文件/dev/vfio/{group-num}获得group对应的文件描述符,假设
group-num为26:
/* Open the group */ group = open("/dev/vfio/26", O_RDWR); |
其通过注册字符设备关联struct file_operations vfio_group_fops实现:
1567 static const struct file_operations vfio_group_fops = { 1568 .owner = THIS_MODULE, 1569 .unlocked_ioctl = vfio_group_fops_unl_ioctl, 1570 #ifdef CONFIG_COMPAT 1571 .compat_ioctl = vfio_group_fops_compat_ioctl, 1572 #endif 1573 .open = vfio_group_fops_open, 1574 .release = vfio_group_fops_release, 1575 }; |
接下来,userspace通过group fd关联的ioctl选项 VFIO_GROUP_SET_CONTAINER,
将group加入container:
/* Add the group to the container */ ioctl(group, VFIO_GROUP_SET_CONTAINER, &container); |
c) Device
与container和group不同,device的设备文件并不暴露在userspace,userspace通过group fd关联的ioctl选项VFIO_GROUP_GET_DEVICE_FD,得到device对应文件描述符,如:
/* Get a file descriptor for the device */ device = ioctl(group, VFIO_GROUP_GET_DEVICE_FD, "0000:06:0d.0"); |
在kernel中,通过group对应的ioctl==》vfio_group_get_device_fd:
- 遍历group得到对应vfio_device后,为device分配fd及struct file,并使两者关联,其中struct file关联的是匿名的inode。
Device fd关联的struct file_operations为vfio_device_fops:
1646 static const struct file_operations vfio_device_fops = { 1647 .owner = THIS_MODULE, 1648 .release = vfio_device_fops_release, 1649 .read = vfio_device_fops_read, 1650 .write = vfio_device_fops_write, 1651 .unlocked_ioctl = vfio_device_fops_unl_ioctl, 1652 #ifdef CONFIG_COMPAT 1653 .compat_ioctl = vfio_device_fops_compat_ioctl, 1654 #endif 1655 .mmap = vfio_device_fops_mmap, 1656 }; |
另外,vfio_device_fops方法实现主要是封装了vfio_device对应的struct_device_ops,
该方法在创建vfio_device时赋值。
2) vfio-pci如何与vfio interface关联?
vfio-pci实际上被注册为pci driver,在文件vfio/pci/vfio_pci.c:
1206 static struct pci_driver vfio_pci_driver = { 1207 .name = "vfio-pci", 1208 .id_table = NULL, /* only dynamic ids */ 1209 .probe = vfio_pci_probe, 1210 .remove = vfio_pci_remove, 1211 .err_handler = &vfio_err_handlers, 1212 };
|
1361 /* Register and scan for devices */ 1362 ret = pci_register_driver(&vfio_pci_driver); |
插入新的pci设备或驱动,并完成pci_bus_match之后,会调用pci driver对应的probe方法,在函数vfio_pci_probe中,会创建vfio_group、vfio_device并把vfio_device加入vfio_group链表,
In function vfio_pci_probe:==>vfio_add_group_dev:==>vfio_create_group分配vfio_group数据结构并完成初始化,并在初始化过程中使vfio_group指针关联idr整数(idr是内核提供的一种整数关联指针的机制,idr类似于身份证,唯一关联对应指针):
In file vfio/vfio.c:
267 * Group minor allocation/free - both called with vfio.group_lock held 268 */ 269 static int vfio_alloc_group_minor(struct vfio_group *group) 270 { 271 return idr_alloc(&vfio.group_idr, group, 0, MINORMASK + 1, GFP_KERNEL); 272 }
|
接下来是vfio interface与vfio-pci关联的关键一步:
在打开group设备文件,调用对应file_operations的open方法时,In function vfio_group_fops_open中通过idr得到在vfio-pci初始化时创建的vfio_group:
467 static struct vfio_group *vfio_group_get_from_minor(int minor) 468 { 469 struct vfio_group *group; 470 471 mutex_lock(&vfio.group_lock); 472 group = idr_find(&vfio.group_idr, minor); 473 if (!group) { 474 mutex_unlock(&vfio.group_lock); 475 return NULL; 476 } 477 vfio_group_get(group); 478 mutex_unlock(&vfio.group_lock); 479 480 return group; 481 } |
综上,vfio-pci被实现为pci driver,在初始化时创建vfio_group、vfio_device,并使vfio_group关联idr,而在打开group设备文件时,再通过idr获得已分配的vfio_group,从而将vfio interface与vfio-pci关联起来。
3) vfio_iommu_driver如何与vfio interface关联?
Userspace通过显式的ioctl调用为container关联对应的vfio_iommu_driver,如:
/* Enable the IOMMU model we want */ ioctl(container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU); |
在内核中会遍历已注册的vfio_iommu_driver,并赋给vfio_container对应成员。
5.2. 将DMA暴露到userspace
实现vfio,DMA是最棘手的部分,IOMMU的引入解决了将DMA暴露到用户空间的安全性问题。
Userspace设置IOMMU的重映射的方式如下:
/* 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); |
以上将container包含的所有设备的设备地址空间[0,1MB)映射到物理内存。
注:iova:I/O虚拟地址,用作配置DMA的地址
vaddr:用户空间虚拟地址
对于ioctl(container, VFIO_IOMMU_MAP_DMA, &dma_map),查看container设备文件对应的ioctl,该选项被转发到container关联的vfio_iommu_driver对应的ioctl,查看vfio_iommu_type1对应的ioctl实现:
In function vfio_iommu_type1_ioctl:==>vfio_dma_do_map:
- 如图5-2,在数据结构vfio_iommu中包含一个红黑树(struct rb_root dma_list),用于记录(iova, vaddr)的映射;在vfio_dma_do_map中首先检查dma_list,确保已映射的dma区域不与传入的、请求映射到iova的区域重叠;
- 接着转换得到用户空间虚拟地址vaddr对应的物理地址,并pin对应的内存区域;
- 最后,将(iova, pfn)传给iommu组件完成映射。
图5-2包含vfio_iommu_type1实现关键的数据结构:vfio_iommu, vfio_domain, vfio_group, vfio_dma等:
- iommu_domain, iommu_group是更底层iommu驱动组件抽象的数据结构;
- domain是一组资源的集合,包含了物理内存及可访问的设备, 同一个domain中的设备共享页表区域,container与domain的区别在于:
- container是vfio interface使用的概念,domain是更底层iommu_driver使用的概念;
- container与domain并不是对等的,一个container内的设备可能划分到多个domain之中,由于处在同一个container中的设备共享页表,每个(iova,pfn)的映射必须同时传递到所有的domain中。
vfio_iommu_map是将(iova, pfn)传递给iommu的关键函数,其遍历所有已划分的
domain,建立(iova,pfn)映射表:
In file vfio_iommu_type1.c
534 static int vfio_iommu_map(struct vfio_iommu *iommu, dma_addr_t iova, 535 unsigned long pfn, long npage, int prot) 536 { 537 struct vfio_domain *d; 538 int ret; 539 540 list_for_each_entry(d, &iommu->domain_list, next) { 541 ret = iommu_map(d->domain, iova, (phys_addr_t)pfn << PAGE_SHIFT, 542 npage << PAGE_SHIFT, prot | d->prot); 543 if (ret) { 544 if (ret != -EBUSY || 545 map_try_harder(d, iova, pfn, npage, prot)) 546 goto unwind; 547 } 548 549 cond_resched(); 550 } 551 552 return 0; ... 559 } |
5.3. 将I/O暴露到userspace
I/O暴露到userspace比较简单,只是把I/O物理地址remap到userspace,对于pci设备包括pci config space、bar等。
在userspace可按照如下方式访问I/O区域:
/* 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 */ } |
首先分析ioctl(device, VFIO_DEVICE_GET_REGION_INFO, ®),经device fd对应的struct file_operations vfio_device_fops{.ioctl} ==> struct vfio_device{vfio_device_ops{.ioctl}} :
1086 static const struct vfio_device_ops vfio_pci_ops = { 1087 .name = "vfio-pci", 1088 .open = vfio_pci_open, 1089 .release = vfio_pci_release, 1090 .ioctl = vfio_pci_ioctl, 1091 .read = vfio_pci_read, 1092 .write = vfio_pci_write, 1093 .mmap = vfio_pci_mmap, 1094 .request = vfio_pci_request, 1095 }; |
这里分析pci bar,其他的io region重映射方式类似;
581 case VFIO_PCI_BAR0_REGION_INDEX ... VFIO_PCI_BAR5_REGION_INDEX: 582 info.offset = VFIO_PCI_INDEX_TO_OFFSET(info.index); 583 info.size = pci_resource_len(pdev, info.index); 584 if (!info.size) { 585 info.flags = 0; 586 break; 587 } 588 589 info.flags = VFIO_REGION_INFO_FLAG_READ | 590 VFIO_REGION_INFO_FLAG_WRITE; 591 if (IS_ENABLED(CONFIG_VFIO_PCI_MMAP) && 592 pci_resource_flags(pdev, info.index) & 593 IORESOURCE_MEM && info.size >= PAGE_SIZE) { 594 info.flags |= VFIO_REGION_INFO_FLAG_MMAP; 595 if (info.index == vdev->msix_bar) { 596 ret = msix_sparse_mmap_cap(vdev, &caps); 597 if (ret) 598 return ret; 599 } 600 } 601 602 break; |
==>VFIO_PCI_INDEX_TO_OFFSET
22 #define VFIO_PCI_OFFSET_SHIFT 40 23 24 #define VFIO_PCI_OFFSET_TO_INDEX(off) (off >> VFIO_PCI_OFFSET_SHIFT) 25 #define VFIO_PCI_INDEX_TO_OFFSET(index) ((u64)(index) << VFIO_PCI_OFFSET_SHIFT) 26 #define VFIO_PCI_OFFSET_MASK (((u64)(1) << VFIO_PCI_OFFSET_SHIFT) - 1) |
pci bar对应的offset只是index <<40,而当userspace通过mmap、read/write等访问对应区域时,对于传入的参数ppos,ppos低40位存储了实际的偏移量,ppos >> 40即可得到pci bar对应的index,有了这个index,再通过pci_resource_start、pci_resoucre_end、pci_resource_len等就可得到pci bar io region对应的开始地址、结束地址、长度等信息,查看mmap实现:
==>vfio_pci_mmap
1001 static int vfio_pci_mmap(void *device_data, struct vm_area_struct *vma) 1002 { ... 1009 index = vma->vm_pgoff >> (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT); 1010 ... 1020 phys_len = pci_resource_len(pdev, index); 1021 req_len = vma->vm_end - vma->vm_start; 1022 pgoff = vma->vm_pgoff & 1023 ((1U << (VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)) - 1); 1024 req_start = pgoff << PAGE_SHIFT; ... 1058 vma->vm_private_data = vdev; 1059 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); 1060 vma->vm_pgoff = (pci_resource_start(pdev, index) >> PAGE_SHIFT) + pgoff; 1061 1062 return remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, 1063 req_len, vma->vm_page_prot); 1064 }
|
Line 1009之所以是(VFIO_PCI_OFFSET_SHIFT - PAGE_SHIFT)而不是(VFIO_PCI_OFFSET_SHIFT)是因为vma->vm_pgoff表示的是页粒度的偏移量(offset of the area in the file, in pages)。
Line 1060得到实际要访问的io区域,最后remap_pfn_range负责建立该物理区域的页表。
5.4. 将interrupt暴露到userspace
将interrupt暴露到userspace,利用了linux提供的系统调用eventfd。
int eventfd(unsigned int initval, int flags); |
eventfd创建了一个文件描述符用于事件通知:
- 创建了一个’eventfd object’,可以作为userspace应用程序间事件wait/notify机制;
- 也可以用于内核向userspace事件通知;
- 内核维护一个64- bit的integer counter,可由’initval’赋初值;在userspace,当read时:
- 若未置EFD_SEMAPHORE位,counter为非零值,则返回8bytes的counter值,
并将该counter reset为零;
- 如counter为0, 若置EFD_NONBLOCK,则返回error EAGAIN,否则阻塞。
- 在kernel space, 维护了struct eventfd_ctx数据结构, 可以通过调用eventfd_signal()函数增加eventfd counter, 并通知用户空间;
- 在userspace, eventfd返回的文件描述符支持select/poll/epoll,从而实现kernel到userspace的异步事件通知;
- eventfd在userspace的用法可参照http://linux.die.net/man/2/eventfd
- eventfd在kernel的使用参照linux kernel源代码[linux-kernel-root]/fs/eventfd.c.
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, &info);
/* Setup IRQs... eventfds, VFIO_DEVICE_SET_IRQ */ int irqfd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); .... irq_set->data = irqfd; ioctl(device, VFIO_DEVICE_SET_IRQ, irq_set);
/* for example , create a pthread for waiting irqfd */ pthread_create(&irq_thread[i], NULL, irq_handler_func, NULL); }
static irq_handler_func(void *arg) { ..... /* select/poll/epoll to wait */ ... read(irqfd,...); } |
在userspace可以按照以下方式设置与处理中断:
- 首先通过ioctl(VFIO_DEVICE_GET_IRQ_INFO)得到中断信息,
- 接着调用eventfd得到文件描述符irqfd,然后通过ioctl(VFIO_DEVICE_SET_IRQ)设置中断,使该文件描述符与中断关联。
- 接下来, userspace可以创建一个新的线程(可选的实现),在该线程中将irqfd加入select/poll/epoll的等待队列,
- 当接收到kernel space事件通知时,调用read(irqfd,...)减少eventfd counter,接着转发到Userspace设备驱动完成中断处理
在kernel space, 可以看一下ioctl(VFIO_DEVICE_SET_IRQ)实现,这里以pci msix中断为例。
vfio_pci_ioctl()==>vfio_pci_set_irqs_ioctl() ==> msix:vfio_pci_set_msi_trigger()
==>vfio_msi_set_block==>vfio_set_block_vector_signal():
308 static int vfio_msi_set_vector_signal(struct vfio_pci_device *vdev, 309 int vector, int fd, bool msix) 310 { 311 struct pci_dev *pdev = vdev->pdev; 312 struct eventfd_ctx *trigger; 313 int irq, ret; 314 315 if (vector < 0 || vector >= vdev->num_ctx) 316 return -EINVAL; 317 318 irq = msix ? vdev->msix[vector].vector : pdev->irq + vector; ... 337 trigger = eventfd_ctx_fdget(fd); 338 if (IS_ERR(trigger)) { 339 kfree(vdev->ctx[vector].name); 340 return PTR_ERR(trigger); 341 } ... 357 ret = request_irq(irq, vfio_msihandler, 0, 358 vdev->ctx[vector].name, trigger); 359 if (ret) { 360 kfree(vdev->ctx[vector].name); 361 eventfd_ctx_put(trigger); 362 return ret; 363 } ... |
Line 337调用eventfd_ctx_fdget()由fd得到该文件描述符对应的struct eventfd_ctx, line 357注册中断,并将该eventfd_ctx作为参数传给中断处理函数vfio_msihandler():
239 /* 240 * MSI/MSI-X 241 */ 242 static irqreturn_t vfio_msihandler(int irq, void *arg) 243 { 244 struct eventfd_ctx *trigger = arg; 245 246 eventfd_signal(trigger, 1); 247 return IRQ_HANDLED; 248 } |
在中断发生时,在中断处理函数中会调用eventfd_signal(), userspace接到通知,等待的select/poll/epoll会返回,接着利用read()递减eventfd关联的counter,并调用userspace中的设备driver完成实际的中断处理任务。
6. 总结
这篇文档着重介绍了vfio和iommu的概念,并对vfio使用及linux kernel实现进行的分析。
在分析linux kernel实现时,除介绍vfio各模块组织架构及如何关联外,着重从如何将DMA暴露到userspace、如何将I/O暴露到userspace、如何将中断暴露到userspace等三个方面分析了vfio实现。
对于vfio如何使用,可以参考第4部分vfio use example,及[qemu-root]/hw/vfio/pci.c。