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->kobj
mdev_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_DMA
ioctl,若当前只有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_pages
pin住的内存。在进行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请求在硬件上执行。