0. VFIO Mdev Introduction

VFIO框架解决了设备直通的问题,使得用户态可以直接使用设备的DMA能力。 VFIO使用IOMMU页表来对设备访问进行保护,将用户态对设备的访问限定在一个安全的域内。 利用这个框架,我们可以将GPU、网卡适配器、一些计算加速器(例如:AI芯片,FPGA加速器)直接呈现给虚拟机。

VFIO直通的缺点是单个PCIe设备只能直通给一台虚拟机,这样设备的利用率较低. 为了能够提升设备资源的利用率就出现了SRIOV(Single Root I/O Virtualization)技术。 SRIOV技术就像孙悟空一样,从本体是上拔出一根猴毛一吹,生成了多个分身,每个分身都有独自的战斗力。 SRIOV技术将设备的本体称为PF(Physical Function),可以从PF上生成多个VF(Virtual Function), 多个VF之间使用PCIe ACS(Access Control Services)进行隔离,确保互相之间不会访问到对方的数据以确保安全。

那么对于一些不具备SR_IOV能力的设备,如何来做到同时向多个用户态进程提供服务呢? Nvidia提出了基于VFIO的VFIO Mdev框架来解决这个问题。 在mdev模型的核心在于mdev会对硬件设备的状态进行抽象,将硬件设备的“状态”保存在mdev device数据结构中, 设备驱动层面要求实现一个调度器,将多个mdev设备在硬件设备上进行调度(分时复用), 从而实现把一个物理硬件设备分享给多个虚拟机实例进行使用。

1. VFIO mdev框架Overview

下图是vfio mdev框架的示意图,这个框架对外提供了一组管理接口,其中包括:

  • 创建和销毁一个mdev
  • 从mdev bus driver上添加和删除一个mdev设备
  • 从IOMMU group上增加和删除一个mdev设备

mdev_arch

从框架来看Top Level是mdev bus driver,是为mdev虚拟的总线类型驱动, 这里我们可以绑定到vfio上,于是就有了vfio mdev,不过目前好像就只设计了vfio mdev。 可以通过调用mdev_register_driver向mdev bus添加vfio_device_ops的驱动。 下面的一层是和物理设备相关的接口,可以调用mdev_register_device来注册某一个物理设备到mdev上, mdev_register_device传入了mdev_parent_ops参数, mdev_parent_ops是跟物理设备相关的驱动集合,例如它可以是nvidia的GPU设备驱动(nvidia.ko)的callback, 可以是Intel集成显卡(i915.ko)的callback,还可以是s390设备驱动(ccw_device.ko)的callback。

mdev在vfio的基础上通过增加一个mdev bus,然后在这个bus下挂载mdev设备, 通过mdev的抽象,将和物理设备相关的驱动实现细节做了剥离,不同的厂商利用mdev框架去 实现自家设备的vendor driver hook(可以是开源的,可以是闭源的)。 Nvidia就是利用这个mdev框架去设计了Nvidia vGPU设备驱动,将自己的驱动实现细节做了闭源,这是Nvidia常用的套路。

2. VFIO mdev框架代码分析

从代码框架来看,vfio mdev首先提供了2个关键的注册接口,mdev_register_drivermdev_register_device

先看mdev bus driver,它提供了一个mdev_register_driver接口api来注册一个新的mdev driver, 在vfio_mdev_init的时候注册了mdev_driver类型的vfio_mdev_driver

struct bus_type mdev_bus_type = {
        .name           = "mdev",
        .probe          = mdev_probe,     //设备加载
        .remove         = mdev_remove,    //设备卸载
};
EXPORT_SYMBOL_GPL(mdev_bus_type);

(mdev.ko) mdev_init -> mdev_bus_register -> bus_register(&mdev_bus_type);  //将mdev_bus注册到系统上

struct mdev_driver {
        const char *name;
        int  (*probe)(struct device *dev);
        void (*remove)(struct device *dev);
        struct device_driver driver;          //定义结构体的时候,加上struct device_driver driver就表示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;    //driver的bus指定为mdev_bus
        drv->driver.owner = owner;

        /* register with core */
        return driver_register(&drv->driver); //注册设备驱动
}
EXPORT_SYMBOL(mdev_register_driver);

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

static int __init vfio_mdev_init(void)
{
        return mdev_register_driver(&vfio_mdev_driver, THIS_MODULE);
}

当mdev设备在加载的时候,mdev bus driver负责将设备绑定到指定的iommu group上,具体流程是:

mdev create -> mdev_probe -> mdev_attach_iommu, drv->probe(dev) -> vfio_mdev_probe -> vfio_add_group_dev

mdev_probe的时候会创建一个iommu group,然后将设备添加到这个iommu group, 再调用vfio_mdev_probe将设备添加到一个vfio group当中。

再看下mdev device driver,这里提供了mdev_register_device api来注册一个新的物理设备驱动(Physical Device Driver Interface),注意这儿有个概念叫做parent_device,其实指的就是一个特定的物理设备,而mdev device则是物理设备的一个“实例”。

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所做的事情是创建一个parent device并对其进行初始化 (很奇怪,取名为mdev_register_parent_device似乎更合适啊)。 mdev parent设备创建的时候,会在sysfs路径/sys/class/mdev_bus/下面创建一堆目录结构, 用户态通过sysfs接口可以完成mdev设备创建,删除,查询等操作。

/*
 * 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 *parent;
        // 创建 mdev_parent设备
        parent = kzalloc(sizeof(*parent), GFP_KERNEL);
        // 初始化mdev parent设备
        kref_init(&parent->ref);
        init_rwsem(&parent->unreg_sem);

        parent->dev = dev;
        parent->ops = ops;      // 给mdev_parent_ops赋值

        // 创建sysfs接口
        ret = parent_create_sysfs_files(parent);
} 

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_parent_ops定义了一些属性,这些属性给QEMU/Libvirt等管理接口提供了一些查询和配置单个mdev设备实例的接口, 例如:dev_attr_groups,mdev_attr_groupssupported_config等。

  • dev_attr_groups: attributes of the parent device
  • mdev_attr_groups: attributes of the mediated device
  • supported_config: attributes to define supported configuration

mdev

Nvidia vGPU方案中使用mdev框架对GPU进行分时复用,可以将一个GPU实例化成多个mdev vGPU设备, 然后将不同的vGPU直通给不同的虚拟机,提升了GPU的使用效率,降低了GPU实例的成本。 同时,mdev的作者为mdev框架提供了一些示例程序,阅读一个示例程序对我们理解mdev的工作原理有一定的帮助。

3. mdev based hardware virtio offloading

https://patchwork.freedesktop.org/series/66987/

这个是最近Jason Wang在搞的一个东东,目的是为了搞一个vDPA的技术方案, 这个方案还是有点意思。我们知道virtio半虚拟化通过前后端内存共享的方式,减少了IO过程中的数据拷贝流程, 提升了模拟设备的性能。vDPA的核心思想是Qemu模拟vDPA设备的控制面,将设备的数据面直接卸载到硬件上, 因为我们可以直接将virtio virtqueue里面的io请求地址提供给硬件,让硬件直接通过DMA从virtqueue里面做IO请求,是不是很赞? 这个vDPA方案要求硬件支持virtqueue作为数据面,目前只有少量新硬件支持。

4. Refs

  1. VGPU ON KVM VFIO BASED MEDIATED DEVICE FRAMEWORK
  2. Virtual Function I/O (VFIO) Mediated device
  3. Mediated Device For Ethernet
  4. 持续助力数据中心虚拟化:KVM里的虚拟GPU
  5. Add migration support for VFIO device

Share on: TwitterFacebookEmail


Published

Category

virt

Tags

Contact