引言
Virtio 是一个开放标准,定义了不同类型驱动程序和设备之间的通信协议,参见 virtio 规范的第 5 章(“设备类型”)11。最初作为由 hypervisor 实现的准虚拟化设备的标凈而开发,它可以用于将任何兼容的设备(真实或模拟的)与驱动程序进行接口。
为了说明目的,本文档将重点关注 Linux 内核在虚拟机中运行并使用 hypervisor 提供的准虚拟化设备,这些设备通过标准机制如 PCI 暴露为 virtio 设备的常见情况。
设备 - 驱动程序通信:virtqueues
尽管 virtio 设备实际上是 hypervisor 中的一个抽象层,但它们被暴露给客户机,就好像它们是使用特定传输方法(PCI、MMIO 或 CCW)的物理设备一样,这与设备本身正交。virtio 规范详细定义了这些传输方法,包括设备发现、功能和中断处理。
客户机操作系统中的驱动程序与 hypervisor 中的设备之间的通信是通过共享内存(这就是 virtio 设备如此高效的原因)使用称为 virtqueues 的特殊数据结构完成的,它们实际上是类似于网络设备中使用的缓冲区描述符的环形缓冲区11:
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 标志,则为链中下一个描述符的索引。我们也通过这个链接未使用的描述符。
所有描述符指向的缓冲区都由客户机分配,并由主机用于读取或写入,但不同时用于两者。
有关 virtqueues 的参考定义,请参阅 virtio 规范的第 2.5 章(“Virtqueues”)11,以及“Virtqueues 和 virtio 环形:数据如何传输”博客文章22,了解主机设备和客户机驱动程序如何通信的图解概述。
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
此设备的 virtqueues 链
callback
当缓冲区被设备消耗时调用的函数(可以为 NULL)。
name
此 virtqueue 的名称(主要用于调试)
vdev
为此队列创建的 virtio 设备。
index
此队列的基于零的序数。
num_free
我们期望能够适应的元素数量。
num_max
设备支持的最大元素数量。
reset
vq 是否处于重置状态。
priv
virtqueue 实现使用的指针。
描述
关于 num_free 的说明:使用间接缓冲区时,每个缓冲区需要队列中的一个元素,否则缓冲区将需要每个 sg 元素一个元素。
当设备消耗了驱动程序提供的缓冲区时,此结构指向的回调函数将被触发。更具体地说,触发将是由 hypervisor 发出的中断(见 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-pci 和 virtio-mmio。然后是针对特定设备类型的个别 virtio 驱动程序,它们注册到 virtio 总线驱动程序。
virtio 设备如何被内核发现和配置取决于 hypervisor 如何定义它。以 QEMU virtio 控制台设备为例。当使用 PCI 作为传输方法时,设备将在 PCI 总线上以供应商 0x1af4(Red Hat, Inc.)和设备 ID 0x1003(virtio 控制台)的形式出现,如规范中所定义,因此内核将像处理任何其他 PCI 设备一样检测到它。
在 PCI 枚举过程中,如果发现一个设备与 virtio-pci 驱动程序匹配(根据 virtio-pci 设备表,任何供应商 ID = 0x1af4 的 PCI 设备):
/* Qumranet 捐赠了他们的供应商 ID,用于设备 0x1000 至 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);
/* 如果我们不能映射 BAR0(没有 IO 空间),也尝试现代模式。 */
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())来分配和配置 virtqueues,最终将调用特定于传输的 find_vqs
方法。
参考文献
11 Virtio 规范 v1.2: Virtual I/O Device (VIRTIO) Version 1.2
22 Virtqueues 和 virtio 环形:数据如何传输 Virtqueues and virtio ring: How the data travels
脚注
Virtio协议具体是如何工作的?
Virtio 协议是一种用于虚拟环境中的虚拟化设备和宿主机操作系统之间的通信标准。它允许虚拟机(客户机)上的驱动程序与虚拟化硬件(由 hypervisor 提供)进行高效通信。以下是 Virtio 协议工作方式的详细解释:
1. 设备和驱动程序抽象
Virtio 定义了一套标准,使得不同类型的虚拟化设备(如网络卡、硬盘等)可以通过统一的接口与驱动程序交互。这意味着,无论底层硬件的具体实现如何,驱动程序都可以以相同的方式与它们通信。
2. virtqueues(虚拟队列)
Virtio 通信的核心是 virtqueues,这是一种特殊的数据结构,用于在客户机操作系统的驱动程序和 hypervisor 之间的设备之间传递数据。virtqueues 实际上是由缓冲区描述符组成的环形缓冲区(ring buffer),这些描述符指向实际的数据缓冲区。
- vring_desc 结构:这是 virtio 环形描述符,用于描述数据缓冲区。每个描述符包含数据缓冲区的地址、长度、标志和指向下一个描述符的索引。
3. 数据传输
数据在 virtio 设备和驱动程序之间通过共享内存传输,这是通过 virtqueues 实现的。当驱动程序需要发送数据时,它会将数据放入一个缓冲区,并将该缓冲区的描述符放入 virtqueue 中。然后,它通知 hypervisor 数据已准备好。
- virtio 驱动程序:负责管理 virtqueues,包括分配缓冲区、填充数据、设置描述符和通知 hypervisor。
- Hypervisor:负责处理来自驱动程序的通知,并在适当的时候将数据从缓冲区传输到虚拟设备。
4. 中断和回调
当 hypervisor 完成数据的处理后,它会通过生成一个中断来通知驱动程序。驱动程序可以注册一个回调函数,当 virtqueue 中的缓冲区被消耗时,这个回调函数会被调用。
- vring_interrupt 函数:这是一个中断处理函数,当 virtqueue 上发生中断时被调用,它会触发回调函数来处理 virtqueue 通知。
5. 设备发现和探测
在 Linux 内核中,Virtio 核心包含总线驱动程序和特定于传输的驱动程序(如 virtio-pci 和 virtio-mmio)。当 hypervisor 定义了一个新设备时,内核会通过 PCI 枚举或其他机制发现这个设备,并尝试使用相应的 virtio 驱动程序进行探测和配置。
6. 设备注册和驱动程序加载
一旦设备被探测到并注册到 Virtio 总线上,内核会寻找一个合适的驱动程序来处理这个设备,并调用该驱动程序的 probe
方法。驱动程序会配置 virtqueues 并准备与设备通信。
Virtio 协议的设计允许它在不同的虚拟化环境中高效地工作,同时保持了驱动程序和设备之间的一致性和兼容性。通过这种方式,Virtio 成为了现代虚拟化技术中不可或缺的一部分。
虚拟队列(virtqueues)的工作原理是怎样的?
虚拟队列(virtqueues)是 Virtio 协议中用于在虚拟机(客户机)上的驱动程序和宿主机(hypervisor)之间的设备之间传递数据的核心机制。以下是 virtqueues 的工作原理:
1. 缓冲区描述符(vring_desc)
virtqueues 由一系列 vring_desc
结构组成,这些结构称为缓冲区描述符。每个描述符指向一个数据缓冲区,并包含以下信息:
- addr:指向数据缓冲区的物理地址。
- len:缓冲区的长度。
- flags:描述符的标志,例如是否是链中的最后一个描述符。
- next:指向同一个链中的下一个描述符的索引。
2. 描述符链
描述符可以链接在一起形成链表,这允许一个 virtqueue 项可以引用多个物理上不连续的缓冲区,这在处理分散/聚合(scatter/gather)I/O 操作时非常有用。
3. 环形缓冲区
virtqueues 是环形缓冲区,意味着它们有一个头指针(head)和一个尾指针(tail),这两个指针定义了 virtqueue 中的空闲和已用描述符。当驱动程序添加新的描述符到 virtqueue 时,它会增加尾指针。当 hypervisor 处理这些描述符时,它会增加头指针。
4. 数据传输
- 发送数据:驱动程序准备好要发送的数据,并构建描述符链。然后,它将描述符添加到 virtqueue 的尾部,并通知 hypervisor(通常通过写入一个特定的寄存器或触发一个中断)。hypervisor 然后读取描述符,并将数据从虚拟机的内存传输到虚拟设备。
- 接收数据:对于接收操作,hypervisor 将数据写入由驱动程序提供的缓冲区,并更新描述符。然后,它通过增加头指针来通知驱动程序数据已准备好。驱动程序随后可以从缓冲区读取数据。
5. 通知机制
- 通知:当驱动程序准备好数据缓冲区并将其添加到 virtqueue 时,它必须通知 hypervisor。这可以通过多种机制完成,例如写入一个共享的寄存器或触发一个中断。
- 中断:当 hypervisor 完成对缓冲区的处理时,它会通过中断来通知驱动程序。驱动程序可以注册一个回调函数,当 virtqueue 中的缓冲区被消耗时,这个回调函数会被调用。
6. 回调函数
驱动程序可以为 virtqueue 设置一个回调函数,当 hypervisor 处理完 virtqueue 中的缓冲区时,这个回调函数会被触发。这允许驱动程序知道何时可以重新使用这些缓冲区,或者何时有新的数据可用。
7. 性能优化
virtqueues 的设计允许高效的数据传输,因为它们减少了需要在客户机和宿主机之间复制的数据量。驱动程序和 hypervisor 只需要共享对描述符的引用,而不是实际的数据。
通过这种方式,virtqueues 提供了一个高效、灵活且可扩展的机制,用于在虚拟化环境中的设备和驱动程序之间传递数据。