VFIO Part II. VFIO-mdev

Overview

查看drivers/vfio/mdev/Makefile可以发现,mdev(Mediated Device)实际上是由两个而不是一个模块构成:

1
2
3
4
mdev-y := mdev_core.o mdev_sysfs.o mdev_driver.o

obj-$(CONFIG_VFIO_MDEV) += mdev.o
obj-$(CONFIG_VFIO_MDEV_DEVICE) += vfio_mdev.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
2
3
4
5
6
struct bus_type mdev_bus_type = {
.name = "mdev",
.probe = mdev_probe,
.remove = mdev_remove,
};
EXPORT_SYMBOL_GPL(mdev_bus_type);

为这个mdev bus包装的Device和Driver数据结构分别是struct mdev_devicestruct mdev_driver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct mdev_device {
struct device dev;
struct mdev_parent *parent;
guid_t uuid;
void *driver_data;
struct kref ref;
struct list_head next;
struct kobject *type_kobj;
bool active;
};

struct mdev_driver {
const char *name;
int (*probe)(struct device *dev);
void (*remove)(struct device *dev);
struct device_driver driver;
};

这里mdev_device代表从一个Parent Device中模拟出来的Mediated Device,mdev_parent则代表其Parent Device,下面会详细展开。

mdev_probe的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int mdev_probe(struct device *dev)
{
struct mdev_driver *drv = to_mdev_driver(dev->driver);
struct mdev_device *mdev = to_mdev_device(dev);
int ret;

ret = mdev_attach_iommu(mdev);
if (ret)
return ret;

if (drv && drv->probe) {
ret = drv->probe(dev);
if (ret)
mdev_detach_iommu(mdev);
}

return ret;
}

我们调用了Driver的probe回调,并且将dev加入了一个dummy IOMMU Group:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int mdev_attach_iommu(struct mdev_device *mdev)
{
int ret;
struct iommu_group *group;

group = iommu_group_alloc();
if (IS_ERR(group))
return PTR_ERR(group);

ret = iommu_group_add_device(group, &mdev->dev);
if (!ret)
dev_info(&mdev->dev, "MDEV: group_id = %d\n",
iommu_group_id(group));

iommu_group_put(group);
return ret;
}

之所以说是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
2
3
4
5
6
7
8
struct mdev_parent {
struct device *dev;
const struct mdev_parent_ops *ops;
struct kref ref;
struct list_head next;
struct kset *mdev_types_kset;
struct list_head type_list;
};

mdev core提供了注册Parent Device的接口mdev_register_device,用户需要传入一个mdev_parent_ops,用来提供sysfs中的属性以及一些回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/*
* mdev_register_device : Register a device
* @dev: device structure representing parent device.
* @ops: Parent device operation structure to be registered.
*
* Add device to list of registered parent devices.
* Returns a negative value on error, otherwise 0.
*/
int mdev_register_device(struct device *dev, const struct mdev_parent_ops *ops)

struct mdev_parent_ops {
struct module *owner;
const struct attribute_group **dev_attr_groups;
const struct attribute_group **mdev_attr_groups;
struct attribute_group **supported_type_groups;

int (*create)(struct kobject *kobj, struct mdev_device *mdev);
int (*remove)(struct mdev_device *mdev);
int (*open)(struct mdev_device *mdev);
void (*release)(struct mdev_device *mdev);
ssize_t (*read)(struct mdev_device *mdev, char __user *buf,
size_t count, loff_t *ppos);
ssize_t (*write)(struct mdev_device *mdev, const char __user *buf,
size_t count, loff_t *ppos);
long (*ioctl)(struct mdev_device *mdev, unsigned int cmd,
unsigned long arg);
int (*mmap)(struct mdev_device *mdev, struct vm_area_struct *vma);
};

mdev_register_device(dev, ops)做了三件事:

  • 根据devops创建一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int parent_create_sysfs_files(struct mdev_parent *parent)
{
int ret;

parent->mdev_types_kset = kset_create_and_add("mdev_supported_types",
NULL, &parent->dev->kobj);

if (!parent->mdev_types_kset)
return -ENOMEM;

INIT_LIST_HEAD(&parent->type_list);

ret = sysfs_create_groups(&parent->dev->kobj,
parent->ops->dev_attr_groups);
if (ret)
goto create_err;

ret = add_mdev_supported_type_groups(parent);
if (ret)
sysfs_remove_groups(&parent->dev->kobj,
parent->ops->dev_attr_groups);
else
return ret;

create_err:
kset_unregister(parent->mdev_types_kset);
return ret;
}

一方面根据ops->dev_attr_groups注册了Device的属性,另一方面创建了/sys/devices/<device path>/mdev_supported_types目录。这个mdev_supported_types目录下会有若干个子目录,每个子目录对应一种mdev type,用struct mdev_type表示:

1
2
3
4
5
6
7
struct mdev_type {
struct kobject kobj;
struct kobject *devices_kobj;
struct mdev_parent *parent;
struct list_head next;
struct attribute_group *group;
};

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->groupsupported_type_groups数组中的一个attribute_group,用于提供/<device path>/mdev_supported_types/<type id>/下的属性
  • 此外还会为mdev_type->kobj创建一个create属性

总结一下,sysfs目录的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|- <parent device path>
|--- Parent device attributes
|--- mdev_supported_types
|--- <type id 1>
|--- devices
|--- <device 1>
|--- <device 2>
|--- ...
|--- create
|--- Supported type attributes
|--- <type id 2>
|--- ...
|--- <type id 3>
|--- ...

根据内核文档的建议,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
2
3
4
5
6
7
8
9
10
mdev->dev.parent  = dev;
mdev->dev.bus = &mdev_bus_type;
mdev->dev.release = mdev_device_release;
dev_set_name(&mdev->dev, "%pUl", uuid);

ret = device_register(&mdev->dev);
if (ret) {
put_device(&mdev->dev);
goto mdev_fail;
}
  • 调用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int mdev_register_driver(struct mdev_driver *drv, struct module *owner)
{
/* initialize common driver fields */
drv->driver.name = drv->name;
drv->driver.bus = &mdev_bus_type;
drv->driver.owner = owner;

/* register with core */
return driver_register(&drv->driver);
}

static struct mdev_driver vfio_mdev_driver = {
.name = "vfio_mdev",
.probe = vfio_mdev_probe,
.remove = vfio_mdev_remove,
};

像其他VFIO驱动一样,vfio_mdev在probe回调中调用vfio_add_group_dev为每个配对的Mediated Device创建了一个新的VFIO Group(因为每个Mediated Device都有自己的dummy IOMMU Group,每次都必须创建新的VFIO Group):

1
2
3
4
5
6
static int vfio_mdev_probe(struct device *dev)
{
struct mdev_device *mdev = to_mdev_device(dev);

return vfio_add_group_dev(dev, &vfio_mdev_dev_ops, mdev);
}

用户最终取得的VFIO Device fd的回调由vfio_mdev_dev_ops定义:

1
2
3
4
5
6
7
8
9
static const struct vfio_device_ops vfio_mdev_dev_ops = {
.name = "vfio-mdev",
.open = vfio_mdev_open,
.release = vfio_mdev_release,
.ioctl = vfio_mdev_unlocked_ioctl,
.read = vfio_mdev_read,
.write = vfio_mdev_write,
.mmap = vfio_mdev_mmap,
};

回顾一下mdev_parent_ops的定义,恰好也有open, release, read, write, mmapioctl这些回调,上面定义的这些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
2
3
4
5
6
7
8
9
10
struct vfio_iommu {
struct list_head domain_list;
struct vfio_domain *external_domain; /* domain for external user */
struct mutex lock;
struct rb_root dma_list;
struct blocking_notifier_head notifier;
unsigned int dma_avail;
bool v2;
bool nesting;
};

attach_group时,会根据传入的IOMMU Group创建struct vfio_group。通常来说,vfio_group会被挂载到一个新创建的vfio_domaindomain_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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (mdev_bus) {
if ((bus == mdev_bus) && !iommu_present(bus)) {
symbol_put(mdev_bus_type);
if (!iommu->external_domain) {
INIT_LIST_HEAD(&domain->group_list);
iommu->external_domain = domain;
} else
kfree(domain);

list_add(&group->next,
&iommu->external_domain->group_list);
mutex_unlock(&iommu->lock);
return 0;
}
symbol_put(mdev_bus_type);
}

也就是将创建的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
2
3
4
5
/* Don't pin and map if container doesn't contain IOMMU capable domain*/
if (!IS_IOMMU_CAP_DOMAIN_IN_CONTAINER(iommu))
dma->size = size;
else
ret = vfio_pin_map_dma(iommu, dma, size);

那么在这种情况下,怎么保证Mediated Device的DMA操作正确执行呢?显然,答案就是不采用Passthrough的方式进行DMA,而是让Parent Device Driver参与DMA的过程。

在这个过程中,Parent Device会使用vfio_iommu_type1_pin_pagesvfio_iommu_type1_unpin_pages来pin住DMA要操作的页。向vfio_iommu_type1_pin_pages传入的user_pfn参数是一个IOVA空间的PFN数组,而作为输出的phys_pfn则是HPA空间的PFN数组:

1
2
3
4
static int vfio_iommu_type1_pin_pages(void *iommu_data,
unsigned long *user_pfn,
int npage, int prot,
unsigned long *phys_pfn)

如果是虚拟机配合VFIO进行Passthrough,则这里的user_pfn代表GFN,如果是DPDK这类用户态驱动,则user_pfn代表用户态驱动任意选取的IOVA地址

我们用struct vfio_pfn表示一个被pin住的物理页:

1
2
3
4
5
6
7
8
9
/*
* Guest RAM pinning working set or DMA target
*/
struct vfio_pfn {
struct rb_node node;
dma_addr_t iova; /* Device address */
unsigned long pfn; /* Host pfn */
atomic_t ref_count;
};

每个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请求在硬件上执行。