Linux上的Virtio¶
简介¶
Virtio 是一个开放标准,它定义了一个协议,用于在不同类型的驱动程序和设备之间进行通信,请参阅 virtio 规范的第 5 章(“设备类型”)([1])。 它最初是作为虚拟机监控程序实现的半虚拟化设备的标准而开发的,但它可以用于将任何兼容设备(真实的或模拟的)与驱动程序连接。
为了便于说明,本文档将重点介绍在虚拟机中运行的 Linux 内核的常见情况,该内核使用虚拟机监控程序提供的半虚拟化设备,虚拟机监控程序通过 PCI 等标准机制将它们公开为 virtio 设备。
设备-驱动程序通信:virtqueues¶
虽然 virtio 设备实际上是虚拟机监控程序中的一个抽象层,但它们向客户机公开,就好像它们是使用特定传输方法(PCI、MMIO 或 CCW)的物理设备,这与设备本身是正交的。 virtio 规范详细定义了这些传输方法,包括设备发现、功能和中断处理。
客户机操作系统中的驱动程序和虚拟机监控程序中的设备之间的通信是通过共享内存完成的(这就是使 virtio 设备如此高效的原因),使用称为 virtqueues 的专用数据结构,实际上是类似于网络设备中使用的缓冲区描述符的环形缓冲区 [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 章(“Virtqueues”)([1]),有关主机设备和客户机驱动程序如何通信的图解概述,请参阅 “Virtqueues and virtio ring: How the data travels” 博客文章 ([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
设备发现和探测¶
在内核中,virtio 核心包含 virtio 总线驱动程序和特定于传输方式的驱动程序,如 virtio-pci 和 virtio-mmio。 然后,有针对特定设备类型的各个 virtio 驱动程序注册到 virtio 总线驱动程序。
内核如何查找和配置 virtio 设备取决于虚拟机监控程序如何定义它。 以 QEMU virtio-console 设备为例。 当使用 PCI 作为传输方法时,该设备将以供应商 0x1af4(Red Hat, Inc.)和设备 ID 0x1003(virtio 控制台)的形式出现在 PCI 总线上,如规范中所定义,因此内核将像检测任何其他 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] Virtqueues and virtio ring: How the data travels https://#/en/blog/virtqueues-and-virtio-ring-how-data-travels
脚注