VFIO Part I. VFIO Core
文中代码基于Linux 5.1 rc6版本
Overview
VFIO提供了两个字符设备文件作为提供给用户程序的入口点,分别是/dev/vfio/vfio和/dev/vfio/$GROUP,此外还在sysfs中添加了一些文件。
首先看/dev/vfio/vfio,它是一个misc device,在vfio模块的初始化函数vfio_init中注册:
1 | static struct miscdevice vfio_dev = { |
每次打开/dev/vfio/vfio文件,都会创建一个对应的Container即struct vfio_container:
1 | struct vfio_container { |
我们可以将VFIO Group加入到Container中,Container维护了一个VFIO Group(struct vfio_group)的链表group_list。Container的作用就是通过其iommu_driver为Group提供IOMMU的服务:
1 | struct vfio_iommu_driver { |
noiommu用于表示该Container是否用于存放no-iommu的Group(一个Container不能同时存放no-iommu Group和普通Group)。no-iommu Group即背后没有IOMMU但仍然强行建立的VFIO Group,这个高级特性(CONFIG_VFIO_NOIOMMU)通常不建议开启,我们忽略相关的代码即可。
/dev/vfio/$GROUP文件显然对应着VFIO Group,它的由来要更复杂一些,我们看vfio_init的一段代码来理解:
1 | /* /dev/vfio/$GROUP */ |
其中vfio_devnode函数的定义如下:
1 | /** |
这里为VFIO Group字符设备动态分配了一整个Major(即包含该Major下的所有Minor)的设备号并注册了cdev,一旦创建一个带devt的Device,并挂在VFIO Class(/sys/class/vfio)下,就会创建一个/dev/vfio/$GROUP字符设备文件。
VFIO分为VFIO核心模块和VFIO驱动模块,VFIO Group是由VFIO驱动模块创建的,最常用的是vfio-pci驱动。VFIO驱动是以设备驱动的形式实现,它们会注册一个Driver,并在其probe函数中调用vfio_add_group_dev,并最终会调用device_create为VFIO Group创建一个Device(从而也创建了/dev/vfio/$GROUP设备文件):
1 | /* vfio_add_group_dev --> vfio_create_group */ |
至于上面说的sysfs文件,也是由VFIO驱动创建的,因为它本身就是一个(虚拟)设备驱动,自然可以创建sysfs目录与属性。
VFIO Group
以下均以vfio-pci为例进行分析,对于其他VFIO驱动也有参考价值
Creation
我们先从VFIO Group的创建开始,对于vfio-pci,这是在vfio_pci_probe中完成的:
1 | static int vfio_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) |
这里创建了一个vfio_pci_device对象vdev,并使用VFIO Core提供的vfio_add_group_dev创建了一个VFIO Group。下面详细分析vfio_add_group_dev创建的数据结构。
首先,VFIO Core有一个全局变量vfio:
1 | static struct vfio { |
其中group_list是所有VFIO Group构成的链表,group_idr是由VFIO Group的Minor号构成的一棵Radix Tree。
再来看VFIO Group,每个VFIO Group都是和一个IOMMU Group相对应的:
1 | struct vfio_group { |
一个IOMMU Group代表一组设备,在硬件上无法区分它们的ID(例如它们都在PCIe-PCI Bridge后面),因此只能共用一张IOMMU页表。
VFIO Group的dev会指向/dev/vfio/$GROUP对应的Device,和vfio_add_group_dev传入的Device无关。由于VFIO Group和IOMMU Group是一一对应关系,一个Group下可以有多个VFIO Device,VFIO Group通过device_list链表引用这些VFIO Device。VFIO Device的定义如下:
1 | struct vfio_device { |
我们向vfio_add_group_dev传入的pdev->dev被放入了vfio_device->dev,vfio_pci_ops被放入了vfio_device->ops,vdev则放入了vfio_device->device_data。
下面分析vfio_add_group_dev(dev, ops, device_data)函数,该函数的目的实际上是创建一个VFIO Device,并加入相应的VFIO Group:
第一步,通过dev(即VFIO Device背后的设备)获得IOMMU Group
1 | iommu_group = iommu_group_get(dev); |
第二步,在全局变量vfio的VFIO Group链表中寻找匹配的Group,若找不到则创建一个新的,并令其iommu_group指向上面获得的IOMMU Group。创建VFIO Group在vfio_create_group中完成,其中这段代码值得注意:
1 | group->nb.notifier_call = vfio_iommu_group_notifier; |
这里向内核的IOMMU层注册了回调,当IOMMU Group上发生一些事件时,会通知VFIO层执行vfio_iommu_group_notifier。
最后一步,创建VFIO Device。我们首先调用vfio_group_get_device(group, dev),如果发现VFIO Group下已有对应的VFIO Device则返回-EBUSY。然后调用vfio_group_create_device(group, dev, ops, device_data):
1 | static |
Group Level API
我们首先来看/dev/vfio/$GROUP提供的API,该文件只支持ioctl操作:
1 | static const struct file_operations vfio_group_fops = { |
在open时,会利用Minor号从vfio.group_idr中找到对应的VIFO Group,然后将文件的private_data设置为该VFIO Group:
1 | group = vfio_group_get_from_minor(iminor(inode)); |
VFIO Group只有4个ioctl,分别是:
VFIO_GROUP_GET_STATUS, &status:获取一个struct vfio_group_status表示VFIO Group的状态VFIO_GROUP_SET_CONTAINER, fd:传入一个fd表示VFIO Container,将VFIO Group加入该ContainerVFIO_GROUP_UNSET_CONTAINER:将VFIO Group移出ContainerVFIO_GROUP_GET_DEVICE_FD, str:传入一个字符串表示VFIO Group下的Device,获取该Device对应的fd
实际上,vfio_group_status只包含一个flag,为其定义了两个位VFIO_GROUP_FLAGS_VIABLE和VFIO_GROUP_FLAGS_CONTAINER_SET,后者显然表示VFIO Group是否绑定到了某个Container,Viable的含义可参考vfio_dev_viable函数的注释:
1 | /* |
VFIO_GROUP_SET_CONTAINER调用了Container的IOMMU Driver的attach_group方法,来将Group加入Container:
1 | driver = container->iommu_driver; |
类似地,VFIO_GROUP_UNSET_CONTAINER调用了IOMMU Driver的detach_group方法:
1 | driver = container->iommu_driver; |
VFIO_GROUP_GET_DEVICE_FD首先调用了VFIO Device的open方法:
1 | ret = device->ops->open(device->device_data); |
对于vfio-pci就是vfio_pci_open,该函数主要对传入的vfio_pci_device对象作了初始化,初始化的过程依据了vdev背后的pdev的Configuration Space。
随后,为VFIO Device创建了一个Anonymous Inode,即不存在于任何目录下的游离于文件系统之外的孤儿Inode,并返回了其fd:
1 | /* |
Device Level API
上一节中的vfio_device_fops实际上只是VFIO Device的ops的一个Wrapper,它将对VFIO Device fd的read、write、mmap和ioctl代理给device->ops中的回调。
对于不同的VFIO驱动,read、write、mmap的含义各有不同,不过总的来说是将VFIO设备文件分为若干个Region,例如PIO Region、MMIO Region、PCI Configuration Space等,每个Region位于VFIO设备文件的不同offset并分别可以读写和映射。另外,每个VFIO设备还可以有一个或多个IRQ Space,用于提供中断的模拟。下面看一下相关的ioctl:
VFIO_DEVICE_GET_INFO, &info,获取一个struct vfio_device_info,表明VFIO Device的信息:
1 | struct vfio_device_info { |
提供的信息包括VFIO Device由哪种驱动提供(vfio-mdev设备则模拟其中一种),有几个Region,有几个IRQ Space。
VFIO_DEVICE_GET_REGION_INFO, &info,用于进一步查询Region的信息,传入并返回一个struct vfio_region_info(用户只填写index):
1 | struct vfio_region_info { |
VFIO_DEVICE_GET_IRQ_INFO, &info,用于查询IRQ Space的信息,传入并返回一个struct vfio_irq_info(用户只填写index):
1 | struct vfio_irq_info { |
count表示这个IRQ Space中的IRQ数量,例如某个IRQ Space代表MSI-X中断,那么它最多可以有2048个IRQ。EVENTFDflag表示IRQ Space支持eventfd方式报告中断,MASKABLEflag表示可以对其中的IRQ进行mask和unmask操作,AUTOMASKED表示当IRQ上触发一次中断后,IRQ会自动被mask。
VFIO_DEVICE_SET_IRQS, &irq_set,传入一个struct vfio_irq_set用于配置中断:
1 | struct vfio_irq_set { |
其中index表示选择第几个IRQ Space,start和count用于表示subindex的范围。关于flags中DATA和ACTION的组合,如下所示:
ACTION_MASK和ACTION_UNMASK分别表示屏蔽和启用选中的IRQDATA_NONE表示[start, start + count - 1]范围内的IRQ全部选中DATA_BOOL表示data[]为一个bool数组,其成员依次代表start到start + count - 1是否选中
ACTION_TRIGGER- 首先需使用
DATA_EVENTFD,通过data[]传入一个eventfd数组,其成员注册为相应的IRQ的Trigger(-1代表相应的IRQ不设置Trigger),即当VFIO Device上产生一个中断时,内核通过注册的eventfd通知用户程序。 - 一旦注册过了eventfd,就可以用
DATA_NONE或DATA_BOOL手动为选中的IRQ触发一个虚拟中断
- 首先需使用
VFIO_DEVICE_RESET,重置VFIO Device。
VFIO Container
Container Level API
VFIO Container和VFIO Group不同。VFIO Group和/dev/vfio/$GROUP设备文件绑定,每个设备文件唯一对应一个VFIO Group,且只能打开一次,试图第二次打开会返回-EBUSY。而VFIO Container只有一个入口点即/dev/vfio/vfio,每次打开该设备文件,都将获得一个新的VFIO Container实例。
VFIO Container本身具备的功能微乎其微,只有三个ioctl:
VFIO_GET_API_VERSION,返回VFIO_API_VERSION(目前版本号为0)VFIO_CHECK_EXTENSION, ext,返回1表示支持该extension(ext),返回0表示不支持VFIO_SET_IOMMU, type,设置IOMMU Driver为type类型,在调用该ioctl前必须至少挂载一个VFIO Group- 本质上只有两种类型,即Type1 IOMMU和sPAPR IOMMU,前者代表x86、ARM等架构上的IOMMU,后者代表POWER架构上的IOMMU
- 我们只关心Type1 IOMMU,它又细分为
VFIO_TYPE1_IOMMU、VFIO_TYPE1v2_IOMMU和VFIO_TYPE1_NESTING_IOMMU,一般来说用VFIO_TYPE1v2_IOMMU即可 - 所有的
type都可以作为VFIO_CHECK_EXTENSION的参数,检查内核是否支持该类型,用户应该先检查是否支持该类型再设置IOMMU Driver
回顾VFIO Container的定义,除了IOMMU Driver以外,还有一个iommu_data:
1 | struct vfio_container { |
在VFIO_SET_IOMMU的实现vfio_ioctl_set_iommu中,通过调用IOMMU Driver的open方法获得了IOMMU Data:
1 | data = driver->ops->open(arg); |
在Type1 IOMMU Driver中,返回的IOMMU Data是一个struct vfio_iommu(详下)。
这一步完成后,接着会对Container上已经挂载的VFIO Group调用IOMMU Driver的attach_group方法:
1 | list_for_each_entry(group, &container->group_list, container_next) { |
IOMMU Driver (Type 1)
External Interface
VFIO Container上的其余操作都会代理给其IOMMU Driver执行,包括read、write、mmap和上述三个ioctl以外的ioctl:
1 | /* vfio_fops_read */ |
另外,VFIO_CHECK_EXTENSION实际上也是代理给IOMMU Driver执行的,当Container尚未指定Driver时,是遍历系统中的IOMMU Driver依次调用VFIO_CHECK_EXTENSION,至少有一个返回1则最终返回1,否则返回0,当Container指定了Driver时,则对该Driver调用VFIO_CHECK_EXTENSION。
对于我们关心的Type 1 IOMMU Driver,其提供的重要的ioctl实际上只有VFIO_IOMMU_MAP_DMA和VFIO_IOMMU_UNMAP_DMA:
VFIO_IOMMU_MAP_DMA,传入一个struct vfio_iommu_type1_dma_map:
1 | struct vfio_iommu_type1_dma_map { |
VFIO_IOMMU_UNMAP_DMA,传入一个struct vfio_iommu_type1_dma_unmap,成功unmap的内存的size会在size中返回(可能比传入的size小):
1 | struct vfio_iommu_type1_dma_unmap { |
这里设置的DMA Remapping是针对整个Container,即针对其中的所有Group的,下面我们将详细讨论这一点。
Internal Interface
IOMMU Driver实际上只是一个接口,用于提供若干回调,与具体的实现解耦:
1 | struct vfio_iommu_driver { |
目前IOMMU Driver均未实现read、write、mmap回调,因此对VFIO Container实际上不能进行read、write或mmap操作,尽管不排除将来支持这些操作的可能。
在Type 1 IOMMU Driver中,实现了以下接口:
1 | static const struct vfio_iommu_driver_ops vfio_iommu_driver_ops_type1 = { |
Data Structures
在vfio_iommu_type1_open中,创建了一个struct vfio_iommu,存放在Container的iommu_data成员中:
1 | struct vfio_iommu { |
其中domain_list是struct vfio_domain构成的链表:
1 | struct vfio_domain { |
其中group_list又是struct vfio_group构成的链表(此VFIO Group非彼VFIO Group,前者定义在drivers/vfio/vfio_iommu_type1.c,后者定义在drivers/vfio/vfio.c):
1 | struct vfio_group { |
这里,一个struct vfio_group和一个VFIO Group相对应,同时也对应于一个IOMMU Group。不同的IOMMU Group可以共享同一张IOMMU页表,我们说这些IOMMU Group属于同一个IOMMU Domain,在这里struct vfio_domain就对应着IOMMU Domain。最后,一个Container中可以容纳若干IOMMU Domain,即可以同时管理多个IOMMU页表。external_domain是由VFIO驱动管理的外部IOMMU Domain,可以暂时忽略,分析vfio-mdev时会详细解释。
这里忽略同一个IOMMU Group在不同进程中可以对应不同IOMMU页表的情况(例如VT-d以及SMMU都可以根据PASID选取不同页表),这种场景在Linux 5.1 rc6尚未支持。Patchwork上可以找到尚未upstream的patch。
dma_list则是由struct vfio_dma构成的一棵红黑树,其索引是[iova, iova + size]区间(IOMMU Driver保证这些区间不重叠):
1 | struct vfio_dma { |
每个vfio_dma都代表一小段内存映射,而这些映射是作用于Container下的所有IOMMU Domain、所有IOMMU Group的,也就是说Container下不同IOMMU Domain的页表内容是相同的。不过这仍是有意义的,因为可能加入Container的不同VFIO Group,分别被不同的IOMMU管辖,因此必须使用不同的IOMMU Domain。
Operations
以下均不考虑vfio-mdev驱动的VFIO Group对IOMMU Driver造成的影响,对于vfio-mdev会在专门的文章讨论
我们首先考察vfio_iommu_type1_attach_group(vfio_iommu, iommu_group):
- 第一步,检查
vfio_iommu下是否已经有IOMMU Group了,若已存在则立即返回-EINVAL。 - 第二步,从IOMMU Group可以得到其下面的Device(
struct device),若它们所属的Bus不同则立即返回-EINVAL,否则记录下它们共同的Bus(记作bus)。 - 第三步,调用
iommu_domain_alloc(bus)创建一个IOMMU Domain,然后调用iommu_attach_group(iommu_domain, iommu_group)将IOMMU Group加入该Domain。 - 第四步,遍历
vfio_iommu的domain_list链表,查找可以容纳IOMMU Group的Domain,若找到则将IOMMU Group从上一步的Domain中去除,加入到这一步的Domain中,并直接返回:
1 | /* |
- 否则,要在新建的IOMMU Domain上设置DMA Mapping,即调用
vfio_iommu_replay(iommu, domain)重放所有DMA Mapping请求,最后将新Domain加入vfio_iommu的domain_list中。
我们再来考察vfio_iommu_type1_register_notifier和vfio_iommu_type1_unregister_notifier,它们的实现很简单:
1 | static int vfio_iommu_type1_register_notifier(void *iommu_data, |
那么iommu->notifier什么时候会被调用呢,答案是仅在用户调用VFIO_IOMMU_UNMAP_DMA时:
1 | /* vfio_iommu_type1_ioctl --> vfio_dma_do_unmap */ |
因此这里注册的notifier起的作用仅仅是在DMA Unmap的时候调用一个回调。
我们继续追溯vfio_iommu_type1_register_notifier的调用者,发现时vfio_register_notifier,该函数还可以用来注册Group Notifier(struct vfio_group (in "vfio.c")中的notifer):
1 | switch (type) { |
无独有偶,Group Notifier实际上也只会在一个时刻被触发,即VFIO Group和KVM绑定时:
1 | void vfio_group_set_kvm(struct vfio_group *group, struct kvm *kvm) |
接下来看vfio_iommu_type1_ioctl,实际上我们只关心其中VFIO_IOMMU_MAP_DMA和VFIO_IOMMU_UNMAP_DMA的实现,即vfio_dma_do_map和vfio_dma_do_unmap。
在vfio_dma_do_map中,首先是检查了DMA Mapping Request的IOVA是否和已有的vfio_dma重叠,若重叠则直接返回-EEXIST。随后,就是创建新的vfio_dma对象,加入vfio_iommu的红黑树,最后对其调用vfio_pin_map_dma建立DMA Remapping。
用户请求的IOVA Region和对应的HVA Region虽然都是连续的,但HVA对应的HPA不一定是连续的,可能要进一步分成若干HPA Region。
vfio_pin_map_dma由一个循环构成,每次先调用vfio_pin_pages_remote,pin住一段连续的物理内存,然后再调用vfio_iommu_map创建IOVA到HPA的DMA Remapping映射:
1 | while (size) { |
vfio_iommu_map的实现很简单,对Container下的所有IOMMU Domain依次调用iommu_map设置映射即可:
1 | list_for_each_entry(d, &iommu->domain_list, next) { |
vfio_pin_pages_remote的实现则要复杂一些:
总的来说,其逻辑是每次调用vaddr_get_pfn,就从一个vaddr(HVA)获得其对应的物理页的页框号(PFN),在一个for循环内不断获取PFN直到PFN不连续为止,将最后获得的不连续的PFN排除,剩下的就是一段连续的物理地址,可以交给vfio_iommu_map进行映射。
vaddr_get_pfn内部通过get_user_pages实现从HVA得到并pin住page(struct page),然后从page就可以获得PFN:
1 | down_read(&mm->mmap_sem); |
get_user_pages_*内部是通过try_get_page(page)将struct page的_refcount加一,来实现所谓的「pin住内存」的效果的。这样做的实际效果是:
- 该物理页仍可以被换出
- 该页不会被迁移,即虚拟地址和物理地址的对应关系被锁定
另一方面,mlock系统调用的「锁住内存」,其含义则是:
- 内存不会被换出
- 内存可以被迁移,即虚拟地址不变,物理地址改变
另一方面,vfio_pin_pages_remote还会统计pin住的页的总数,不过已经通过pin_pages回调pin住的(也就是重复被pin的)页不算在内:
1 | if (!rsvd && !vfio_find_vpfn(dma, iova)) { |
pin住的总页数统计在lock_acct中,函数的结尾会调用vfio_lock_acct(dma, lock_acct, false),为DMA Map的调用者的mm->locked_vm增加lock_acct(mm->locked_vm += lock_acct)。
被重复pin的情况只有在Container先挂载了vfio-mdev驱动的VFIO Group,并被调用了pin_pages方法pin住了部分页,然后再挂载普通VFIO Group时,才会发生。在挂载普通VFIO Group时,如前文所述,会对新创建的IOMMU Domain调用vfio_iommu_replay,它也会调用到vfio_pin_pages_remote,此时不会将已经被vfio-mdev pin住的页计入统计。
理论上mm->locked_vm是用来统计地址空间中有多少被mlock锁住的页的,此处并未调用mlock或为vma设置VM_LOCKEDflag,却增加了locked_vm的计数,究竟起到什么作用尚不清楚。
这段修改mm->locked_vm的代码在可追溯的最早版本,即Tom Lyon的初版PATCH就已经出现,并且当时也是使用的get_user_pages_fast来pin住内存,故其最初的用意已不可考。