VFIO Part II. VFIO-mdev
Overview
查看drivers/vfio/mdev/Makefile可以发现,mdev(Mediated Device)实际上是由两个而不是一个模块构成:
1 | mdev-y := mdev_core.o mdev_sysfs.o mdev_driver.o |
其中mdev.ko是mdev core模块,包括了mdev的绝大多数核心功能。该模块在Device Model中定义了一种新的Bus即mdev总线,而vfio_mdev.ko则是定义了mdev总线上的一种Driver,用来实现和VFIO的对接,换句话说就是起到和vfio-pci驱动相同的作用。
这样设计的好处在于,mdev.ko模块提供的是通用的创建和销毁Mediated Device,以及将Mediated Device加入或移出IOMMU Group的功能,它不一定要和VFIO绑定。vfio_mdev.ko提供了Mediated Device和VFIO的绑定,将来也可以创建新的模块(即新的mdev驱动),提供其他形式的接口。
Mdev Core
Mdev Bus
mdev.ko定义了一个mdev bus:
1 | struct bus_type mdev_bus_type = { |
为这个mdev bus包装的Device和Driver数据结构分别是struct mdev_device和struct mdev_driver:
1 | struct mdev_device { |
这里mdev_device代表从一个Parent Device中模拟出来的Mediated Device,mdev_parent则代表其Parent Device,下面会详细展开。
mdev_probe的定义如下:
1 | static int mdev_probe(struct device *dev) |
我们调用了Driver的probe回调,并且将dev加入了一个dummy IOMMU Group:
1 | static int mdev_attach_iommu(struct mdev_device *mdev) |
之所以说是dummy IOMMU Group,是因为这里创建的IOMMU Group只是一个Device的容器,它既不对应于硬件上的IOMMU Group,也不会被attach到任何一个IOMMU Domain上(即不会为其设置IOMMU页表)。有了这个dummy IOMMU Group,我们才可以将Mediated Device置于一个VFIO Group中。
Parent Device
前面提到,mdev总线上的设备都属于Mediated Device,它们创建自Parent Device,用struct mdev_parent表示:
1 | struct mdev_parent { |
mdev core提供了注册Parent Device的接口mdev_register_device,用户需要传入一个mdev_parent_ops,用来提供sysfs中的属性以及一些回调函数:
1 | /* |
mdev_register_device(dev, ops)做了三件事:
- 根据
dev和ops创建一个mdev_parent对象,并加入全局的链表parent_list中 - 在Compatibility Class mdev_bus(
/sys/class/mdev_bus/)下,创建了一个指向dev这个Device的目录的link - 调用
parent_create_sysfs_files(parent),创建sysfs目录和文件
整个函数的关键实际上就在于parent_create_sysfs_files,我们来看它的实现:
1 | int parent_create_sysfs_files(struct mdev_parent *parent) |
一方面根据ops->dev_attr_groups注册了Device的属性,另一方面创建了/sys/devices/<device path>/mdev_supported_types目录。这个mdev_supported_types目录下会有若干个子目录,每个子目录对应一种mdev type,用struct mdev_type表示:
1 | struct mdev_type { |
add_mdev_supported_type_groups就是根据ops->supported_type_groups数组,为数组中的每个attribute_group创建了一个mdev_type,并加入到了Parent Device的type_list链表中,此外还会创建sysfs文件:
mdev_type->kobj就是mdev_supported_types下的子目录,不妨记作/<device path>/mdev_supported_types/<type id>mdev_type->devices_kobj目录位于/<device path>/mdev_supported_types/<type id>/devices,即它的parent为mdev_type->kobjmdev_type->group即supported_type_groups数组中的一个attribute_group,用于提供/<device path>/mdev_supported_types/<type id>/下的属性- 此外还会为
mdev_type->kobj创建一个create属性
总结一下,sysfs目录的结构如下:
1 | |- <parent device path> |
根据内核文档的建议,Supported type attributes应当包括以下几个属性:
device_api,表示Mediated Device采用何种Device API,例如"vfio-pci"表示虚拟PCI设备available_instances,表示能创建的<type id>类型的Mediated Device的数量name(可选),一个<type id>的更可读的名字description(可选),显示该类型的简短介绍,可以包括支持哪些特性
Mediated Device
我们通过向/sysfs/devices/<device path>/mdev_supported_types/<type id>/create输入一个UUID (aka. GUID),就可以创建一个<type id>类型的Mediated Device,具体实现位于mdev_device_create,它进行了如下操作:
- 创建一个
mdev_device对象mdev,将其加入全局的mdev_list链表- 其
parent成员即Mediated Device的Parent Device - 其
type_kobj成员即Mediated Device的mdev type的kobj对象,对应着/.../<type id>/目录
- 其
- 注册
mdev->dev:
1 | mdev->dev.parent = dev; |
- 调用
parent->ops->create(type_kobj, mdev),这通常会创建一个数据结构来代表Mediated Device,它会被存放在mdev->driver_data - 创建sysfs目录和文件,包括
- 在
/sys/devices/<mdev path>/下创建parent->ops->mdev_attr_groups属性组 - 在
/sys/devices/<type id path>/devices/下创建一个link$UUID,指向/sys/devices/<mdev path> - 在
/sys/devices/<mdev path>/下创建一个linkmdev_type,指向/sys/devices/<type id path> - 在
/sys/device/<mdev path>/下创建属性remove
- 在
向remove写入1即可将Medaited Device销毁,整个过程就是注销上述过程申请的资源和注册的内容,在此过程中会调用parent->ops->remove(mdev)销毁Parent Device驱动申请的资源和注册的内容。
VFIO-mdev
现在再来看vfio-mdev.ko模块,它通过mdev_register_driver注册了一个mdev bus的驱动vfio_mdev:
1 | int mdev_register_driver(struct mdev_driver *drv, struct module *owner) |
像其他VFIO驱动一样,vfio_mdev在probe回调中调用vfio_add_group_dev为每个配对的Mediated Device创建了一个新的VFIO Group(因为每个Mediated Device都有自己的dummy IOMMU Group,每次都必须创建新的VFIO Group):
1 | static int vfio_mdev_probe(struct device *dev) |
用户最终取得的VFIO Device fd的回调由vfio_mdev_dev_ops定义:
1 | static const struct vfio_device_ops vfio_mdev_dev_ops = { |
回顾一下mdev_parent_ops的定义,恰好也有open, release, read, write, mmap和ioctl这些回调,上面定义的这些vfio_mdev_*函数实际上就是直接调用mdev->parent->ops中的回调,将这些文件系统操作代理给了Parent Ops。
至此,只需要Parent Device实现VFIO Device API,就可以实现用户态对Port IO、MMIO以及中断的访问、控制。Parent Device这一层要进行的工作和vfio-pci驱动的工作是类似的,只不过对于vfio-mdev,每种Parent Device都要自己实现一遍相关功能。
VFIO-mdev DMA (for Type 1 IOMMU Driver)
回顾struct vfio_iommu的定义:
1 | struct vfio_iommu { |
在attach_group时,会根据传入的IOMMU Group创建struct vfio_group。通常来说,vfio_group会被挂载到一个新创建的vfio_domain或domain_list中已存在的某个vfio_domain中,而vfio_domain对应于一个IOMMU Domain。最终,会调用iommu_attach_group(iommu_domain, iommu_group),为传入的IOMMU Group指定IOMMU Domain。
然而,若传入的IOMMU Group是Mediated Device的dummy IOMMU Group,则不能这么做,因为mdev bus根本就没有定义iommu_ops成员,也就是说不支持IOMMU,调用iommu_attach_group只会返回-ENODEV。对于Mediated Device,我们的操作如下:
1 | if (mdev_bus) { |
也就是将创建的vfio_group挂到external_domain下,external_domain只是一个dummy Domain,没有对应的IOMMU Domain。
对于VFIO_IOMMU_MAP_DMAioctl,若当前只有external_domain,即Container中尚未加入过普通的VFIO Group,则我们只是创建一个struct vfio_dma并加入红黑树dma_list,不再进行pin内存以及建立IOMMU Mapping的操作:
1 | /* Don't pin and map if container doesn't contain IOMMU capable domain*/ |
那么在这种情况下,怎么保证Mediated Device的DMA操作正确执行呢?显然,答案就是不采用Passthrough的方式进行DMA,而是让Parent Device Driver参与DMA的过程。
在这个过程中,Parent Device会使用vfio_iommu_type1_pin_pages和vfio_iommu_type1_unpin_pages来pin住DMA要操作的页。向vfio_iommu_type1_pin_pages传入的user_pfn参数是一个IOVA空间的PFN数组,而作为输出的phys_pfn则是HPA空间的PFN数组:
1 | static int vfio_iommu_type1_pin_pages(void *iommu_data, |
如果是虚拟机配合VFIO进行Passthrough,则这里的user_pfn代表GFN,如果是DPDK这类用户态驱动,则user_pfn代表用户态驱动任意选取的IOVA地址
我们用struct vfio_pfn表示一个被pin住的物理页:
1 | /* |
每个struct vfio_dma中都有一棵由struct vfio_pfn构成的红黑树pfn_list,它表示被Parent Device通过vfio_iommu_type1_pin_pagespin住的内存。在进行pin_pages操作时,我们每次从user_pfn[i]中取出一个IOFN,进行如下操作:
- 在Container中寻找IOFN所属的
vfio_dma,若找不到则调用失败 - 在
vfio_dma中寻找IOFN对应的vfio_pfn,若找到了则直接取出其PFN,填入phys_pfn[i],然后可继续操作下一个IOFN - 否则,调用
vfio_pin_page_external从HVA得到PFN,并pin住内存,将得到的PFN填入phys_pfn[i],最后创建一个vfio_pfn对象,加入第一步找到的vfio_dma中的红黑树
其中vfio_pin_page_external一方面调用vaddr_get_pfn --> get_user_pages_*获取PFN并pin住内存,另一方面还调用vfio_lock_acct增加了mm->locked_vm的计数,确保了对已经pin住的内存页数的统计没有遗漏。
当Parent Device利用pin_pages操作pin住物理内存后,它就会使用Linux内核提供的dma_map_*函数来分配pIOVA,这个pIOVA是给物理设备(Parent Device)使用的IOVA,而Mediated Device作为虚拟设备使用的则可以称为vIOVA。Parent Device随后就可以将用户态的DMA请求,翻译为对pIOVA的DMA请求在硬件上执行。