为 QEMU 编写 VirtIO 后端

本文概述开发者编写 QEMU 设备模拟所需的知识,重点是 VirtIO。 前端指客户机内运行的驱动,后端指实现 VirtIO 设备模拟所需的全部工作。 后端可完全在 QEMU 中实现,也可由 QEMU 和内核分担(vhost), 或由 QEMU 配置的独立进程处理(vhost-user)。

VirtIO 传输

VirtIO 支持多种传输方式。设备配置和操作细节通常相同, 但 QEMU 根据传输方式将其表示为不同设备。 例如 -device virtio-foo 使用 MMIO,-device virtio-foo-pci 则是同类设备使用 PCI 传输。

使用 QEMU 对象模型(QOM)

QEMU 设备通常位于 TYPE_DEVICE 类层级中。 VirtIO 设备应基于派生自该基类的 TYPE_VIRTIO_DEVICE,例如:

static const TypeInfo virtio_blk_info = {
    .name = TYPE_VIRTIO_BLK,
    .parent = TYPE_VIRTIO_DEVICE,
    .instance_size = sizeof(VirtIOBlock),
    .instance_init = virtio_blk_instance_init,
    .class_init = virtio_blk_class_init,
};

也可以建立更丰富的类层级,支持多个设备类型。例如 VirtIO GPU:

static const TypeInfo virtio_gpu_base_info = {
    .name = TYPE_VIRTIO_GPU_BASE,
    .parent = TYPE_VIRTIO_DEVICE,
    .instance_size = sizeof(VirtIOGPUBase),
    .class_size = sizeof(VirtIOGPUBaseClass),
    .class_init = virtio_gpu_base_class_init,
    .abstract = true
};

static const TypeInfo vhost_user_gpu_info = {
    .name = TYPE_VHOST_USER_GPU,
    .parent = TYPE_VIRTIO_GPU_BASE,
    .instance_size = sizeof(VhostUserGPU),
    .instance_init = vhost_user_gpu_instance_init,
    .instance_finalize = vhost_user_gpu_instance_finalize,
    .class_init = vhost_user_gpu_class_init,
};

static const TypeInfo virtio_gpu_info = {
    .name = TYPE_VIRTIO_GPU,
    .parent = TYPE_VIRTIO_GPU_BASE,
    .instance_size = sizeof(VirtIOGPU),
    .class_size = sizeof(VirtIOGPUClass),
    .class_init = virtio_gpu_class_init,
};

先定义 VirtIO GPU 基类,再派生内部实现和 vhost-user 两个版本。

VirtIOPCIProxy

[AJB:以下是推测,欢迎更了解情况的人补充。]

可能出于 QOM 出现前的历史原因,PCI VirtIO 设备不遵循普通层级。 独立对象基于 VirtIOPCIProxy,而具体 VirtIO 实例由手动创建:

/*
 * virtio-blk-pci: This extends VirtioPCIProxy.
 */
#define TYPE_VIRTIO_BLK_PCI "virtio-blk-pci-base"
DECLARE_INSTANCE_CHECKER(VirtIOBlkPCI, VIRTIO_BLK_PCI,
                         TYPE_VIRTIO_BLK_PCI)

struct VirtIOBlkPCI {
    VirtIOPCIProxy parent_obj;
    VirtIOBlock vdev;
};

static const Property virtio_blk_pci_properties[] = {
    DEFINE_PROP_UINT32("class", VirtIOPCIProxy, class_code, 0),
    DEFINE_PROP_BIT("ioeventfd", VirtIOPCIProxy, flags,
                    VIRTIO_PCI_FLAG_USE_IOEVENTFD_BIT, true),
    DEFINE_PROP_UINT32("vectors", VirtIOPCIProxy, nvectors,
                       DEV_NVECTORS_UNSPECIFIED),
};

static void virtio_blk_pci_realize(VirtIOPCIProxy *vpci_dev, Error **errp)
{
    VirtIOBlkPCI *dev = VIRTIO_BLK_PCI(vpci_dev);
    DeviceState *vdev = DEVICE(&dev->vdev);

    ...

    qdev_realize(vdev, BUS(&vpci_dev->bus), errp);
}

static void virtio_blk_pci_class_init(ObjectClass *klass, const void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    VirtioPCIClass *k = VIRTIO_PCI_CLASS(klass);
    PCIDeviceClass *pcidev_k = PCI_DEVICE_CLASS(klass);

    set_bit(DEVICE_CATEGORY_STORAGE, dc->categories);
    device_class_set_props(dc, virtio_blk_pci_properties);
    k->realize = virtio_blk_pci_realize;
    pcidev_k->vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET;
    pcidev_k->device_id = PCI_DEVICE_ID_VIRTIO_BLOCK;
    pcidev_k->revision = VIRTIO_PCI_ABI_VERSION;
    pcidev_k->class_id = PCI_CLASS_STORAGE_SCSI;
}

static void virtio_blk_pci_instance_init(Object *obj)
{
    VirtIOBlkPCI *dev = VIRTIO_BLK_PCI(obj);

    virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev),
                                TYPE_VIRTIO_BLK);
    object_property_add_alias(obj, "bootindex", OBJECT(&dev->vdev),
                              "bootindex");
}

static const VirtioPCIDeviceTypeInfo virtio_blk_pci_info = {
    .base_name              = TYPE_VIRTIO_BLK_PCI,
    .generic_name           = "virtio-blk-pci",
    .transitional_name      = "virtio-blk-pci-transitional",
    .non_transitional_name  = "virtio-blk-pci-non-transitional",
    .instance_size = sizeof(VirtIOBlkPCI),
    .instance_init = virtio_blk_pci_instance_init,
    .class_init    = virtio_blk_pci_class_init,
};

可以看到 instance_init 必须手动创建底层 TYPE_VIRTIO_BLOCK 对象, 并把其中一个属性以别名连接到 PCI 设备。

后端实现

后端可以在以下位置实现:

  • QEMU 自身;

  • 主机内核,即 vhost;

  • 独立进程,即 vhost-user。

vhost_ops 与 TYPE_VHOST_USER_BACKEND

vhost 代码有两种实现方式。 需要同时兼容 vhost 和 vhost-user 的代码,多数通过 vhost_dev_init() 创建相应后端, 因此需在主对象结构中包含 struct vhost_dev。

vhost-user 设备还需跟踪 chardev 的初始化, 该设备承载 QEMU 与外部 vhost-user 进程之间的控制套接字。

如果只需要 vhost-user 后端,也可以使用其 QOM 化实现。

static void
vhost_user_gpu_instance_init(Object *obj)
{
    VhostUserGPU *g = VHOST_USER_GPU(obj);

    g->vhost = VHOST_USER_BACKEND(object_new(TYPE_VHOST_USER_BACKEND));
    object_property_add_alias(obj, "chardev",
                              OBJECT(g->vhost), "chardev");
}

static const TypeInfo vhost_user_gpu_info = {
    .name = TYPE_VHOST_USER_GPU,
    .parent = TYPE_VIRTIO_GPU_BASE,
    .instance_size = sizeof(VhostUserGPU),
    .instance_init = vhost_user_gpu_instance_init,
    .instance_finalize = vhost_user_gpu_instance_finalize,
    .class_init = vhost_user_gpu_class_init,
};

这种方式需要在核心对象结构中添加 struct VhostUserBackend,并手动创建后端。 该子结构同时跟踪连接所需的 vhost_dev 和 CharDev。 应调用 vhost_user_backend_dev_init,而不是 vhost_dev_init, 由它代为完成所需工作。