序言
虚拟化场景下,有时为了提升虚拟机io性能通常会选择把设备直通给vm。设备直通要么使用设备的SRIOV或者SIOV能力,要么把整个设备都通给虚拟机。有些场景下,为了提升设备利用率我们希望能把后端的硬件设备切分成更小的实例提供给更多的vm来使用,但是这个设备又没有SRIOV或者SIOV的能力(比如gpu,nvme)那该怎么办呢?别着急,vfio mdev框架就是为了解决这样的问题而生的。
vfio mdev框架
从字面上看就知道这个框架涉及到两个核心组件vfio 和mdev。那么下面我们就来看一下这两个组件是如何在一起来工作的。想必大家都应该比较熟悉linux当中的bus,driver,device的框架,简单说来某个device它必须挂在某个具体的bus上,然后device由跟这个bus绑定的driver来驱动。那么在vfio_mdev这个架构当中,linux内核定义了一种叫mdev_bus
的 bus_type。相关定义具体如下
struct bus_type mdev_bus_type = {
.name = "mdev",
.probe = mdev_probe,
.remove = mdev_remove,
};
EXPORT_SYMBOL_GPL(mdev_bus_type);
这里的mdev_probe
和mdev_remove
分别会在device probe和remove的时候调用到,通常情况下在bus probe和remove函数里面会再调用跟这个bus bind的 device driver的probe和remove函数。
接着mdev core driver在加载的时候会将 mdev_bus 注册到系统当中
mdev_bus_register->bus_register(&mdev_bus_type)
bus_register
这个函数主要是初始化subsys_private 以及 klist_devices、klist_drivers这两个klist,然后在sysfs下创建相关目录
/sys/bus/mdev/
├── devices
├── drivers
├── drivers_autoprobe
├── drivers_probe
└── uevent
然后我们再来讲vfio侧,其为mdev bus专门开发了一个vfio_mdev driver
static struct mdev_driver vfio_mdev_driver = {
.name = "vfio_mdev",
.probe = vfio_mdev_probe,
.remove = vfio_mdev_remove,
}
紧接着在vfio mdev driver load的时候会将自己跟mdev bus绑定起来,具体通过mdev_register_driver
来实现
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; // bus 设置为mdev
drv->driver.owner = owner;
/* register with core */
return driver_register(&drv->driver); //注册这个 driver
}
driver_register->bus_add_driver
bus_add_driver
这个函数里面主要实现为:初始化driver_private,将driver放到上其所属bus的 klist_drivers上,然后通过driver_attach
去probe 该 bus klist_devices上的设备,最后在sysfs下面创建相关的目录
/sys/bus/mdev/
├── devices
├── drivers
│ └── vfio_mdev
│ ├── bind
│ ├── module -> ../../../../module/vfio_mdev
│ ├── uevent
│ └── unbind
├── drivers_autoprobe
├── drivers_probe
└── uevent
可以看到 mdev bus下面driver目录下已经有内容了,而此时devices目录下还是空的。
设备在mdev框架下的使能
通常我们会为这个设备重新写一个具有mdev功能的driver,这个driver与设备本身的driver所做的事情是有所不同的。mdev driver具体要做哪些事情呢?回答这个问题之前我们先来想想mdev需要帮我解决哪些问题。
- config space、bar space、pcie capbility
因为我们需要给vm呈现一个完整的设备,而backend 物理设备是没有sriov功能的,所以config space、bar以及相关的pci cap是需要在这个mdev driver里面进行模拟。
-
mmio 访问
回想一下,在sriov场景下设备的mmio是passthrough给vm的(除msix table相关mmio),vm侧对mmio的访问都是不需要hypervisor参与的。而在mdev场景下,由于一个设备要share给多个vm的,所以这个些mmio的访问操作都是需要在mdev driver侧来进行处理的。
-
dma
在sriov场景下dma的转换是由iommu硬件侧自动完成的,而在mdev场景下由于同一个设备要在多个vm下共享无法再使用iommu功能(物理设备侧只一个bdf号,多个vm之间无法进行区分)。因此,mdev场景下dma的相关转换和相关操作也是需要在驱动里面实现。
-
中断
sriov的场景下,中断采用的是dma remapping和posted interrupt,而之所以能这么干主要是因为其后端对应的是一个独立的VF;而在mdev场景下,多个vm share 一个后端硬件设备只能通过per vm的irqfd这种方式。
设备的 mdev driver除了需要实现的相关功能之外,在驱动init的时候还需要将自己注册为虚拟设备的parent,具体是通过mdev_register_device
来实现。这个函数的主要逻辑就是创建mdev_parent,然后给parent dev和ops分别赋值为当前的物理设备以及该设备mde_driver当中定义的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);
};
最后,在sysfs下给parent创建相应的目录(以vfio_mdev sample mtty 驱动为例子)
/sys/devices/virtual/mtty/
└── mtty
├── mdev_supported_types
│ ├── mtty-1
│ │ ├── available_instances
│ │ ├── create
│ │ ├── device_api
│ │ ├── devices
│ │ └── name
│ └── mtty-2
│ ├── available_instances
│ ├── create
│ ├── device_api
│ ├── devices
│ └── name
├── mtty_dev
│ └── sample_mtty_dev
├── power
│ ├── async
│ ├── autosuspend_delay_ms
│ ├── control
│ ├── runtime_active_kids
│ ├── runtime_active_time
│ ├── runtime_enabled
│ ├── runtime_status
│ ├── runtime_suspended_time
│ └── runtime_usage
├── subsystem -> ../../../../class/mtty
└── uevent
下面我们来看一下如何创建可以通给vm的mdev instance。原来在sriov场景下需要把设备从原生驱动unbind,然后再将其bind到vfio-pci的驱动上;而mdev设备也有类似这样的流程,下面还是以mtty这个sample为例子来看一下相关流程
# echo "83b8f4f2-509f-382f-3c1e-e6bfe0fa1001" > \
/sys/devices/virtual/mtty/mtty/mdev_supported_types/mtty-1/create
-----------
直通给vm的时候,qemu侧参数配置
-device vfio-pci,\
sysfsdev=/sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa1001
当往create去写uuid的时候,会触发 mdev_device_create
函数,这个函数主要实现逻辑如下:
- 新建mdev虚拟设备,并进行相关的初始化
mdev->parent = parent; //这个parent指的是后端对应的物理设备
..... //skip
mdev->dev.parent = dev; //新创建mdev->dev实例,并将其父指向当前的dev即mtty-1
mdev->dev.bus = &mdev_bus_type;
mdev->dev.release = mdev_device_release;
dev_set_name(&mdev->dev, "%pUl", uuid.b)
- 调用
device_register
对mdev->dev
进行初始化
device_register->device_add->bus_probe_device->device_initial_probe->__device_attach_driver -> ...->bus->probe
最终一路会调到mdev_probe
,然后再调用vfio_mdev driver的probe函数vfio_mdev_probe
。先来看一下mdev_probe函数具体都干了啥。
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;
}
首先通过mdev_attach_iommu
函数,为该mdev设备创建iommu_group,然后将设备添加到这个iommu_group里面。注意,这个iommu_group并没有创建default的domain,所以不会创建domain_mapping。接着看一下vfio_mdev_probe
函数,在这个函数里面核心逻辑就是创建vfio_group并将其跟iommu_group对应起来,然后再创建vfio_device并将dev,vfio_group和定义的vfio_mdev_dev_ops绑定起来。最后,再调用parent dev的mdev_parent_ops当中的create函数。设备成功create之后视图如下:
/sys/bus/mdev/devices/83b8f4f2-509f-382f-3c1e-e6bfe0fa1001
├── driver -> ../../../../../bus/mdev/drivers/vfio_mdev
├── iommu_group -> ../../../../../kernel/iommu_groups/0
├── mdev_type -> ../mdev_supported_types/mtty-1
├── power
│ ├── async
│ ├── autosuspend_delay_ms
│ ├── control
│ ├── runtime_active_kids
│ ├── runtime_active_time
│ ├── runtime_enabled
│ ├── runtime_status
│ ├── runtime_suspended_time
│ └── runtime_usage
├── remove
├── subsystem -> ../../../../../bus/mdev
├── uevent
└── vendor
└── sample_mdev_dev
mdev 设备直通给vm的实现
如上面所示,mdev virtual dev直通给vm走的还是qemu当中vfio pci的方式。这里我们先回忆一下qemu vfio 都做了哪些事情。简单来说就是几个概念,一类是per vm的:container, vfio_iommu, vfio_domain;另一类 per device的:vfio_group。一个container下面包含多个vfio_group;一个vfio_iommu的domain_list里面当前只有一个vfio_domain。同时还有几个fd,具体如下:
-
group_fd = open("/dev/vfio/$group")
通过这个fd下发的ioctl主要做的事情有:判断这个group是否可用,获取这个device fd,将这个group绑定到container下面。
-
container_fd = open("/dev/vfio/vfio")
通过这个fd下发的ioctl主要做的事情有:获取iommu type(是type1还是spapr_tce),将每个vfio_iommu attach到vfio_domain上。其中,后面这个事情要注意一下因为在mdev的场景下在逻辑上有些区别,具体见
vfio_iommu_type1_attach_group
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);
}
从上面的逻辑可以得知,如果是mdev设备则会为这些设备单独创建一个external_domain表示其没有iommu能力 。
另外,vfio dma map也是通过container fd来做的,具体执行的是内核侧vfio_dma_do_map
,函数实现当中如果发现vm直通设备当中只有mdev设备的话,那它是不会去真正执行 pin and map的,具体见
/* 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);
-
device fd
设备的相关的操作比如config space,mmio的读写,bar region的map以及irq相关的操作都是通过这个fd来进行的,那么在mdev的场景 所有的操作比如read, write, ioctl都是先call到 vfio_mdev driver,然后再经过vfio_mdev call到parent ops注册的callback函数即底层硬件设备mdev driver ops 里面实现的 callback函数。
总结
这里我就不详细介绍 mdev driver的具体实现了,大家如果感兴趣可以去看一下intel vgpu,mdev nvme等这些 mdev driver的实现,看完之后可能你会对这篇文章有更多的理解。从上面来看mdev的方案在性能上不可能做的太高,siov方案当中虽然也使用了mdev,但是siov当中数据面重一些要路径访问仍然走的是passthrough的方式。但是不管怎么样,mdev给我们提供了这样一种能力,至于是否能用就得看你的场景呢。