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。EVENTFD
flag表示IRQ Space支持eventfd方式报告中断,MASKABLE
flag表示可以对其中的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_LOCKED
flag,却增加了locked_vm
的计数,究竟起到什么作用尚不清楚。
这段修改mm->locked_vm
的代码在可追溯的最早版本,即Tom Lyon的初版PATCH就已经出现,并且当时也是使用的get_user_pages_fast
来pin住内存,故其最初的用意已不可考。