Linux 上的 Virtio

简介

Virtio 是一种开放标准,它定义了不同类型的驱动程序和设备之间通信的协议,请参阅 virtio 规范的第 5 章(“设备类型”)([1])。它最初是作为一种用于虚拟机监控程序实现的准虚拟化设备的标准而开发的,它可以用来将任何兼容设备(真实的或模拟的)与驱动程序进行接口。

为了便于说明,本文档将重点介绍在虚拟机中运行 Linux 内核,并使用虚拟机监控程序提供的准虚拟化设备的常见情况,这些设备通过标准机制(例如 PCI)将其作为 virtio 设备公开。

设备 - 驱动程序通信:virtqueue

尽管 virtio 设备实际上是虚拟机监控程序中的一个抽象层,但它们会使用特定的传输方法(PCI、MMIO 或 CCW)暴露给客户机,就好像它们是物理设备一样,而该传输方法与设备本身无关。virtio 规范详细定义了这些传输方法,包括设备发现、功能和中断处理。

客户机操作系统中的驱动程序与虚拟机监控程序中的设备之间的通信是通过共享内存(这使得 virtio 设备非常高效)使用称为 virtqueue 的专用数据结构完成的,virtqueue 实际上是类似于网络设备中使用的缓冲区描述符的环形缓冲区 [1]

struct vring_desc

Virtio 环形描述符,长度为 16 字节。这些可以通过 next 链接在一起。

定义:

struct vring_desc {
    __virtio64 addr;
    __virtio32 len;
    __virtio16 flags;
    __virtio16 next;
};

成员

addr

缓冲区地址(客户机物理地址)

len

缓冲区长度

flags

描述符标志

next

如果设置了 VRING_DESC_F_NEXT 标志,则链中下一个描述符的索引。我们也通过此链来链接未使用的描述符。

描述符指向的所有缓冲区都由客户机分配,并且由主机用于读取或写入,但不能同时用于两者。

有关 virtqueue 的参考定义,请参阅 virtio 规范的第 2.5 章(“Virtqueue”)([1]),有关主机设备和客户机驱动程序如何通信的图解概述,请参阅“Virtqueue 和 virtio 环:数据如何传输”博客文章 ([2])。

vring_virtqueue 结构对 virtqueue 进行建模,包括环形缓冲区和管理数据。此结构中嵌入了 virtqueue 结构,该结构是 virtio 驱动程序最终使用的数据结构

struct virtqueue

用于注册要发送或接收的缓冲区的队列。

定义:

struct virtqueue {
    struct list_head list;
    void (*callback)(struct virtqueue *vq);
    const char *name;
    struct virtio_device *vdev;
    unsigned int index;
    unsigned int num_free;
    unsigned int num_max;
    bool reset;
    void *priv;
};

成员

list

此设备的 virtqueue 链

callback

缓冲区被使用时要调用的函数(可以为 NULL)。

name

此 virtqueue 的名称(主要用于调试)

vdev

为此队列创建的 virtio 设备。

index

此队列的从零开始的序号。

num_free

我们期望能够容纳的元素数量。

num_max

设备支持的最大元素数量。

reset

vq 是否处于重置状态。

priv

供 virtqueue 实现使用的指针。

描述

关于 num_free 的说明:使用间接缓冲区时,每个缓冲区在队列中需要一个元素,否则每个缓冲区在 sg 元素中需要一个元素。

当设备使用了驱动程序提供的缓冲区时,将触发此结构指向的回调函数。更具体地说,触发器将是虚拟机监控程序发出的中断(请参阅 vring_interrupt())。中断请求处理程序在 virtqueue 设置过程(特定于传输)期间注册到 virtqueue。

irqreturn_t vring_interrupt(int irq, void *_vq)

在中断时通知 virtqueue

参数

int irq

IRQ 号(已忽略)

void *_vq

要通知的 struct virtqueue

描述

调用 _vq 的回调函数来处理 virtqueue 通知。

设备发现和探测

在内核中,virtio 核心包含 virtio 总线驱动程序和特定于传输的驱动程序(如 virtio-pcivirtio-mmio)。然后,为注册到 virtio 总线驱动程序的特定设备类型提供单独的 virtio 驱动程序。

内核如何查找和配置 virtio 设备取决于虚拟机监控程序如何定义它。以 QEMU virtio-console 设备为例。当使用 PCI 作为传输方法时,该设备将在 PCI 总线上显示自身,供应商 ID 为 0x1af4 (Red Hat, Inc.),设备 ID 为 0x1003 (virtio 控制台),如规范中所定义,因此内核将像对待任何其他 PCI 设备一样检测到它。

在 PCI 枚举过程中,如果发现某个设备与 virtio-pci 驱动程序匹配(根据 virtio-pci 设备表,任何供应商 ID 为 0x1af4 的 PCI 设备)

/* Qumranet donated their vendor ID for devices 0x1000 thru 0x10FF. */
static const struct pci_device_id virtio_pci_id_table[] = {
        { PCI_DEVICE(PCI_VENDOR_ID_REDHAT_QUMRANET, PCI_ANY_ID) },
        { 0 }
};

则会探测 virtio-pci 驱动程序,如果探测顺利,则会将该设备注册到 virtio 总线

static int virtio_pci_probe(struct pci_dev *pci_dev,
                            const struct pci_device_id *id)
{
        ...

        if (force_legacy) {
                rc = virtio_pci_legacy_probe(vp_dev);
                /* Also try modern mode if we can't map BAR0 (no IO space). */
                if (rc == -ENODEV || rc == -ENOMEM)
                        rc = virtio_pci_modern_probe(vp_dev);
                if (rc)
                        goto err_probe;
        } else {
                rc = virtio_pci_modern_probe(vp_dev);
                if (rc == -ENODEV)
                        rc = virtio_pci_legacy_probe(vp_dev);
                if (rc)
                        goto err_probe;
        }

        ...

        rc = register_virtio_device(&vp_dev->vdev);

当设备注册到 virtio 总线后,内核将在总线中查找可以处理该设备的驱动程序,并调用该驱动程序的 probe 方法。

此时,将通过调用适当的 virtio_find 辅助函数(如 virtio_find_single_vq() 或 virtio_find_vqs())来分配和配置 virtqueue,最终将调用特定于传输的 find_vqs 方法。

参考资料

[1] Virtio 规范 v1.2: https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html

[2] Virtqueue 和 virtio 环:数据如何传输 https://www.redhat.com/en/blog/virtqueues-and-virtio-ring-how-data-travels

脚注