第一篇------Virtual I/O Device (VIRTIO) Version 1.1

1 介绍

本文档描述了“virtio”设备系列的规格。这些设备通常出现在虚拟环境中,但按设计,它们在虚拟机内部看起来像物理设备,而本文档将其视为这样的设备。这种相似性允许虚拟机内的客户端使用标准驱动程序和发现机制。
virtio及其规格的目的在于,虚拟环境和客户端应该具有一种简单、高效、标准和可扩展的虚拟设备机制,而不是为每个环境或操作系统定制的机制。

简单:Virtio设备使用标准的中断和DMA总线机制,这对于任何设备驱动程序作者都应该很熟悉。没有奇特的页面翻转或COW(写时复制)机制:它只是一个普通的设备。

高效:Virtio设备包括输入和输出的描述符环,这些环被精心设计,以避免驱动程序和设备同时写入相同的缓存行而产生的缓存效应。

标准:Virtio不对其运行环境做任何假设,除了支持设备连接的总线。在本规格中,virtio设备是通过MMIO、Channel I/O和PCI总线传输来实现的2,在此之前的草案已经在其他不包括在此处的总线上实现。

可扩展:Virtio设备包含功能位,客户操作系统在设备设置期间会承认这些功能位。这允许前向和后向兼容性:设备提供其了解的所有功能,驱动程序承认它了解并希望使用的功能。

2 Virtio设备的基本功能

Virtio设备是通过总线特定的方法进行发现和识别的(请参阅总线特定部分:4.1 Virtio在PCI总线上、4.2 Virtio在MMIO上和4.3 Virtio在Channel I/O上)。每个设备包括以下部分:
• 设备状态字段
• 特性位
• 通知
• 设备配置空间
• 一个或多个virtqueue(虚拟队列)

2.1 设备状态字段

在驱动程序进行设备初始化时,驱动程序按照3.1中指定的步骤序列进行操作。
设备状态字段提供了对此序列中已完成步骤的简单低级指示。最好将其想象成连接到控制台上的交通信号灯,指示每个设备的状态。以下位被定义(按照通常设置的顺序列出):
ACKNOWLEDGE(1)表示客户操作系统已找到该设备并将其识别为有效的virtio设备。
DRIVER(2)表示客户操作系统知道如何驱动该设备。
注意:在设置此位之前可能会有显著的(或无限的)延迟。例如,在Linux下,驱动程序可以是可加载模块。
FAILED(128)表示在客户操作系统中发生了错误,并且已放弃该设备。这可能是内部错误,或者驱动程序出于某种原因不喜欢该设备,甚至在设备操作期间发生了致命错误。
FEATURES_OK(8)表示驱动程序已确认了其理解的所有特性,特性协商已完成。
DRIVER_OK(4)表示驱动程序已设置并准备好驱动该设备。
DEVICE_NEEDS_RESET(64)表示设备遇到了无法恢复的错误。

2.1.1 驱动程序要求:设备状态字段

驱动程序必须更新设备状态,将位设置为指示在3.1中指定的驱动程序初始化序列的已完成步骤。驱动程序不得清除设备状态位。如果驱动程序设置了FAILED位,则驱动程序必须稍后在尝试重新初始化之前重置设备。
如果设置了DEVICE_NEEDS_RESET,驱动程序不应依赖于设备操作的完成。
注意:例如,如果设置了DEVICE_NEEDS_RESET,驱动程序不能假定正在进行的请求将在完成,也不能假定它们尚未完成。一个良好的实现将尝试通过发出重置来恢复。

2.1.2 设备要求:设备状态字段

设备在重置时必须将设备状态初始化为0。
在DRIVER_OK之前,设备不得消耗缓冲区或向驱动程序发送任何已使用的缓冲区通知。
设备应在进入需要重置的错误状态时设置DEVICE_NEEDS_RESET。如果设置了DRIVER_OK,并在设置了DEVICE_NEEDS_RESET之后,设备必须向驱动程序发送设备配置更改通知。

2.2 特性位

每个virtio设备都提供其了解的所有特性。在设备初始化期间,驱动程序会读取这些特性并告知设备它所接受的子集。重新协商的唯一方法是重置设备。
这允许前向和后向兼容性:如果设备增加了一个新的特性位,旧版驱动程序将不会将该特性位写回设备。同样,如果驱动程序增加了设备不支持的特性,它将看到新特性未提供。
特性位分配如下
0 到 23 特定设备类型的特性位
24 到 37 保留用于队列和特性协商机制的扩展的特性位
38 及以上 保留用于未来扩展的特性位。
注意:例如,用于网络设备的特性位0(即设备ID为1)表示该设备支持数据包的校验和。
特别是,通过提供新的特性位来指示设备配置空间中的新字段。

2.2.1 驱动程序要求:特性位

驱动程序不得接受设备未提供的特性,并不得接受需要另一个未被接受的特性的特性。
如果设备不提供驱动程序了解的特性,驱动程序应该进入向后兼容模式,否则必须设置FAILED设备状态位并停止初始化。

2.2.2 设备要求:特性位

设备不得提供需要另一个未提供的特性。设备应该接受驱动程序接受的特性的任何有效子集,否则当驱动程序写入它时,设备必须无法设置FEATURES_OK设备状态位。
如果设备已成功地协商了至少一组特性(通过在设备初始化期间接受FEATURES_OK设备状态位),那么在设备或系统重置后,它不应该失败重新协商相同的特性集。否则将干扰从挂起状态恢复和错误恢复。

2.2.3 传统接口:关于特性位的注意事项

过渡驱动程序必须通过检测未提供特性位VIRTIO_F_VERSION_1来检测传统设备。传统设备必须通过检测驱动程序未确认VIRTIO_F_VERSION_1来检测传统驱动程序。
在这种情况下,设备通过传统接口使用。传统接口支持是可选的。因此,符合本规范的传统和非传统设备和驱动程序。
与传统设备和驱动程序有关的要求包含在类似于此的名为“LegacyInterface”的部分中。
当设备通过传统接口使用时,过渡设备和过渡驱动程序必须根据这些传统接口部分中记录的要求运行。这些部分中的规范文本通常不适用于非过渡设备。

2.3 通知

在本规格中,发送通知(从驱动程序到设备或从设备到驱动程序)的概念起着重要作用。通知的操作方式取决于具体的传输方式。
有三种类型的通知:
• 配置更改通知
• 可用缓冲区通知
• 已使用缓冲区通知。
配置更改通知和已使用缓冲区通知由设备发送,接收者是驱动程序。配置更改通知表示设备配置空间已更改;已使用缓冲区通知表示在通知所指定的virtqueue上可能已使用了缓冲区。
可用缓冲区通知由驱动程序发送,接收者是设备。这种类型的通知表示在通知所指定的virtqueue上可能已提供了缓冲区。
不同通知的语义、传输特定的实现以及其他重要方面在以下章节中详细指定。
大多数传输方式使用设备发送给驱动程序的中断来实现通知。因此,在本规格的早期版本中,这些通知通常被称为中断。本规格中定义的一些名称仍然保留了这个中断术语。偶尔,术语“事件”用来指代通知或通知的接收。

2.4 设备配置空间

设备配置空间通常用于很少更改或初始化时的参数。对于可选的配置字段,它们的存在由特性位来指示:本规格的未来版本可能会通过在尾部添加额外字段来扩展设备配置空间。
注意:设备配置空间使用小端格式表示多字节字段。
每种传输方式还为设备配置空间提供了一个生成计数,每当有可能两次访问设备配置空间可能看到该空间的不同版本时,该计数将发生变化。

2.4.1 驱动程序要求:设备配置空间

驱动程序不得假设从大于32位宽度的字段的读取是原子的,也不得假设从多个字段的读取是原子的:驱动程序应该以如下方式读取设备配置空间字段:

u32 before, after;
do {
	before = get_config_generation(device);
	// read config entry/entries.
	after = get_config_generation(device);
} while (after != before);

对于可选的配置空间字段,在访问配置空间的那部分之前,驱动程序必须检查相应的特性是否已提供。
注意:有关特性协商的详细信息,请参阅第3.1节。
驱动程序不得限制结构大小和设备配置空间大小。相反,驱动程序应该仅检查设备配置空间是否足够大,以包含设备操作所需的字段。
注意:例如,如果规格说明设备配置空间“包括一个8位字段”,驱动程序应该理解这意味着设备配置空间可能还包括任意数量的尾部填充,并接受任何等于或大于指定的8位大小的设备配置空间大小。

2.4.2 设备要求:设备配置空间

在驱动程序设置FEATURES_OK之前,设备必须允许读取任何特定于设备的配置字段。这包括受特性位条件限制的字段,只要这些特性位由设备提供。

2.4.3 传统接口:有关设备配置空间的字节序的注意事项

请注意,对于传统接口,设备配置空间通常是虚拟机的本机字节序,而不是PCI的小端字节序。每个设备都记录了正确的字节序。

2.4.4 传统接口:设备配置空间

传统设备没有配置生成字段,因此在更新配置时容易受到竞态条件的影响。这会影响块容量(请参阅5.2.4)和网络MAC地址(请参阅5.1.4)字段;在使用传统接口时,驱动程序应该多次读取这些字段,直到两次读取生成一致的结果。

2.5 Virtqueues

在virtio设备上进行批量数据传输的机制被称为virtqueue。每个设备可以拥有零个或多个virtqueue。

驱动程序通过将可用的缓冲区添加到队列中(即,将描述请求的缓冲区添加到virtqueue中),并且可选择地触发驱动程序事件(即,向设备发送可用的缓冲区通知)来使请求对设备可用。

设备执行请求,当完成时,将已使用的缓冲区添加到队列中,即通过将缓冲区标记为已使用来通知驱动程序。然后,设备可以触发设备事件,即向驱动程序发送已使用的缓冲区通知。

设备报告它已经写入内存的每个缓冲区的字节数。这称为“已使用长度”。

通常情况下,设备不需要按照驱动程序提供的顺序使用这些缓冲区。某些设备总是按照驱动程序提供的顺序使用描述符。这些设备可以提供VIRTIO_F_IN_ORDER特性。如果经过协商,这个知识可能允许优化或简化驱动程序和/或设备代码。

每个virtqueue可以包含最多3部分:

  • 描述符区域 - 用于描述缓冲区
  • 驱动程序区域 - 驱动程序提供给设备的额外数据
  • 设备区域 - 设备提供给驱动程序的额外数据

注意: 请注意,本规范的早期版本对这些部分使用不同的名称(遵循2.6):

  • 描述符表 - 用于描述符区域
  • 可用环 - 用于驱动程序区域
  • 已使用环 - 用于设备区域

支持两种格式:Split Virtqueues(参见2.6 Split Virtqueues)和Packed Virtqueues(参见2.7 Packed Virtqueues)。

每个驱动程序和设备都支持Packed或Split Virtqueue格式之一,或两者都支持。

2.6 Split Virtqueues

分离的virtqueue格式是标准的1.0版本(以及更早版本)唯一支持的格式。

分离的virtqueue格式将virtqueue分成几个部分,其中每个部分可以由驱动程序或设备中的一个写入,但不能同时由两者写入。在将缓冲区可用和标记为已使用时,需要更新多个部分和/或部分内的位置。

每个队列都有一个16位的队列大小参数,该参数设置了条目的数量并暗示了队列的总大小。

每个virtqueue由三个部分组成:

  • 描述符表 - 占用描述符区域
  • 可用环 - 占用驱动程序区域
  • 已使用环 - 占用设备区域

其中,每个部分在虚拟机内存中是物理连续的,并具有不同的对齐要求。

下表总结了virtqueue的每个部分的内存对齐和大小要求,以字节为单位:

Virtqueue部分 对齐要求 大小
Descriptor Table 16 16∗(队列大小)
Available Ring 2 6 + 2∗(队列大小)
Used Ring 4 6 + 8∗(队列大小)

对齐要求列给出了virtqueue的每个部分的最小对齐要求。

大小列给出了virtqueue的每个部分的总字节数。

队列大小对应于virtqueue中的最大缓冲区数量 2 。队列大小的值始终是2的幂。最大队列大小值为32768。这个值以特定于总线的方式指定。

当驱动程序想要向设备发送一个缓冲区时,它填写描述符表中的一个槽(或将多个槽链接在一起),并将描述符索引写入可用环中。然后它通知设备。当设备完成一个缓冲区时,它将描述符索引写入已使用环中,并发送已使用的缓冲区通知。

2.6.1 驱动程序要求:Virtqueues

驱动程序必须确保每个virtqueue部分的第一个字节的物理地址是上表中指定的对齐值的倍数。

2.6.2 旧版接口:关于Virtqueue布局的说明

对于旧版接口,virtqueue的布局受到一些额外的限制:
每个virtqueue占用两个或更多物理连续页面(通常定义为4096字节,但取决于传输方式;以下简称为队列对齐),并由三个部分组成:

  • 描述符表
  • 可用环(…填充…)
  • 已使用环
    Descriptor Table Available Ring (…padding…) Used Ring
    总线特定的队列大小字段控制virtqueue的总字节数。当使用旧版接口时,过渡性驱动程序必须从设备中检索队列大小字段,并必须根据以下公式(qalign中的队列对齐和qsz中的队列大小)分配virtqueue的总字节数:
#define ALIGN(x) (((x) + qalign) & ~qalign)
static inline unsigned virtq_size(unsigned int qsz)
{
	return ALIGN(sizeof(struct virtq_desc)*qsz + sizeof(u16)*(3 + qsz))
	+ ALIGN(sizeof(u16)*3 + sizeof(struct virtq_used_elem)*qsz);
}

2.6.3 用于旧版接口的Virtqueue布局

这会在填充方面浪费一些空间。在使用旧版接口时,过渡性设备和驱动程序都必须使用以下virtqueue布局结构来定位virtqueue的元素:

struct virtq {
	// The actual descriptors (16 bytes each)
	struct virtq_desc desc[ Queue Size ];
	// A ring of available descriptor heads with free-running index.
	struct virtq_avail avail;
	// Padding to the next Queue Align boundary.
	u8 pad[ Padding ];
	// A ring of used descriptor heads with free-running index.
	struct virtq_used used;
};

2.6.3 旧版接口:关于Virtqueue字节顺序的说明

请注意,在使用旧版接口时,过渡性设备和驱动程序必须使用虚拟机的本机字节顺序来处理字段和virtqueue,而不是像本标准规定的非旧版接口一样使用小端字节顺序。这假定主机已经知道虚拟机的字节顺序。

2.6.4 消息帧

使用描述符的消息帧与缓冲区的内容无关。例如,网络传输缓冲区由12字节的标头后跟网络数据包组成。这可以简单地放在描述符表中,作为一个12字节的输出描述符,后跟一个1514字节的输出描述符,但在标头和数据包相邻的情况下,它也可以由一个1526字节的输出描述符组成,甚至可以由三个或更多描述符组成(在这种情况下可能会降低效率)。

请注意,一些设备实现对总描述符大小有合理但较大的限制(例如,基于主机操作系统中的IOV_MAX)。这在实际中并没有成为问题:不会对创建不合理大小的描述符的驱动程序(例如,将网络数据包分成1500个单字节描述符)提供多少同情!

2.6.4.1 设备要求:消息帧

设备不得对描述符的特定排列方式做出假设。设备可以对它允许的链式描述符数量设定合理的限制。

2.6.4.2 驱动程序要求:消息帧

驱动程序必须将任何可由设备写入的描述符元素放在任何可由设备读取的描述符元素之后。

驱动程序不应使用过多的描述符来描述一个缓冲区。

2.6.4.3 旧版接口:消息帧

不幸的是,最初的驱动程序实现使用了简单的布局,尽管有这个规范的措辞,设备仍然依赖于它。此外,virtio_blk SCSI命令的规范要求从帧边界推断字段长度(请参见5.2.6.3 旧版接口:设备操作)

因此,当使用旧版接口时,VIRTIO_F_ANY_LAYOUT功能指示设备和驱动程序都不会对帧做出任何假设。当未协商此功能时,过渡性驱动程序的要求包含在每个设备部分中。

2.6.5 Virtqueue描述符表

描述符表是指驱动程序为设备使用的缓冲区。addr是物理地址,缓冲区可以通过next链接。每个描述符描述了一个对设备是只读的缓冲区(“设备可读”)或对设备是只写的缓冲区(“设备可写”),但是一系列描述符可以包含既适用于设备可读又适用于设备可写的缓冲区。

提供给设备的内存的实际内容取决于设备类型。最常见的方式是以一个头部(包含小端字段)开头,供设备读取,然后以状态尾部结尾,供设备写入。

struct virtq_desc {
    /* 地址(宿主物理地址)。 */
    le64 addr;
    /* 长度。 */
    le32 len;
    /* 通过下一个字段标记一个缓冲区的继续性。 */
    #define VIRTQ_DESC_F_NEXT 1
    /* 通过此标志将缓冲区标记为设备仅写入(否则为设备只读)。 */
    #define VIRTQ_DESC_F_WRITE 2
    /* 这表示缓冲区包含一系列缓冲区描述符。 */
    #define VIRTQ_DESC_F_INDIRECT 4
    /* 如上所示的标志。 */
    le16 flags;
    /* 如果标志 & NEXT,则为下一个字段。 */
    le16 next;
};

描述符表中的描述符数量由此virtqueue的队列大小定义:这是最大可能的描述符链长度。

如果已经协商了VIRTIO_F_IN_ORDER功能,则驱动程序将按照环绕顺序使用描述符:从表中的偏移量0开始,并在表的末尾进行环绕。

2.6.5.1 设备要求:Virtqueue描述符表
  • 设备不得写入设备可读缓冲区,设备不应读取设备可写缓冲区(但可以出于调试或诊断目的这样做)。
2.6.5.2 驱动程序要求:Virtqueue描述符表
  • 驱动程序不得添加超过2^32字节总长度的描述符链,这意味着描述符链中不允许出现循环!
  • 如果已经协商了VIRTIO_F_IN_ORDER功能,并在向设备提供的表中的偏移量x处设置了flags中的VRING_DESC_F_NEXT,驱动程序必须在表中的最后一个描述符(其中x = queue_size − 1)的next设置为0,并对其余描述符设置为x + 1
2.6.5.3 间接描述符
  • 某些设备通过同时分派大量大型请求而受益。VIRTIO_F_INDIRECT_DESC功能允许这样做(参见Avirtio_queue.h)。
  • 为了增加环的容量,驱动程序可以在内存的任何位置存储一个间接描述符表,并在主virtqueue中插入一个引用包含此间接描述符表的内存缓冲区的描述符(带有&VIRTQ_DESC_F_INDIRECT标志);addrlen分别指的是间接表的地址和长度(以字节为单位)。

间接表的布局结构如下(len是引用此表的描述符的长度,这是一个变量,因此此代码无法编译):

struct indirect_descriptor_table {
    /* 实际的描述符(每个16字节) */
    struct virtq_desc desc[len / 16];
};

第一个间接描述符位于间接描述符表的开头(索引0),附加的间接描述符通过next字段进行链接。没有有效next字段(flags&VIRTQ_DESC_F_NEXT关闭)的间接描述符表示描述符的结束。单个间接描述符表可以包含设备可读和设备可写的描述符。

如果已经协商了VIRTIO_F_IN_ORDER功能,间接描述符使用连续的索引,按顺序排列:从索引0开始,然后是索引1,然后是索引2,依此类推。

2.6.5.3.1 驱动程序要求:间接描述符
  • 驱动程序必须在已协商的情况下才能设置VIRTQ_DESC_F_INDIRECT标志。
  • 驱动程序不能在间接描述符内设置VIRTQ_DESC_F_INDIRECT标志(即一个描述符只能引用一个表)。
  • 驱动程序不得创建比设备队列大小更长的描述符链。
  • 驱动程序不能同时在标志中设置VIRTQ_DESC_F_INDIRECTVIRTQ_DESC_F_NEXT
  • 如果已经协商了VIRTIO_F_IN_ORDER功能,则间接描述符必须按顺序出现,第一个描述符的next值为1,第二个为2,以此类推。
2.6.5.3.2 设备要求:间接描述符
  • 设备必须忽略引用间接表的描述符中的写入标志(flags&VIRTQ_DESC_F_WRITE)。
  • 设备必须处理零个或多个正常链式描述符后跟一个具有flags&VIRTQ_DESC_F_INDIRECT标志的单个描述符的情况。注意:尽管不寻常(大多数实现要么完全使用非间接描述符创建链式结构,要么只使用单个间接元素),但这样的布局是有效的。

2.6.6 可用环(Virtqueue Available Ring)

可用环具有以下布局结构:

struct virtq_avail {
    #define VIRTQ_AVAIL_F_NO_INTERRUPT 1
    le16 flags;             // 标志字段,用于指示一些特性
    le16 idx;               // 队列中下一个可用描述符的索引
    le16 ring[ /* Queue Size */ ]; // 可用环,是一个数组,其大小为队列的大小
    le16 used_event;        // 仅在 VIRTIO_F_EVENT_IDX 标志设置时存在
};

驱动程序使用可用环来向设备提供缓冲区:每个环条目都指向一个描述符链的头部。它只由驱动程序写入并由设备读取。
idx字段指示驱动程序将在环中放置下一个描述符条目的位置(对队列大小取模)。它从0开始递增。
注意:旧版的[Virtio PCI Draft]将此结构称为vring_avail,并将常量称为VRING_AVAIL_F_NO_INTERRUPT,但布局和值是相同的。

2.6.6.1 驱动程序要求:可用环

驱动程序不得在虚拟队列上减少可用idx(即没有“取消公开”缓冲区的方法)。

2.6.7 用于缓冲区使用通知的抑制

如果未协商VIRTIO_F_EVENT_IDX特性位,则可用环中的flags字段提供了一个粗糙的机制,供驱动程序通知设备不需要在缓冲区使用时进行通知。否则,used_event是一个性能更好的替代方法,其中驱动程序指定设备可以在需要通知之前进行多远的进度。

这两种通知抑制方法都不是可靠的,因为它们与设备未同步,但它们作为有用的优化方式。

2.6.7.1 驱动程序要求:抑制已使用缓冲区通知

如果未协商VIRTIO_F_EVENT_IDX特性位:

  • 驱动程序必须将flags设置为0或1。
  • 驱动程序可以将flags设置为1,以通知设备不需要通知。

否则,如果已协商VIRTIO_F_EVENT_IDX特性位:

  • 驱动程序必须将flags设置为0。
  • 驱动程序可以使用used_event通知设备,直到设备写入带有used_event指定的索引的条目到已使用环(等效地,直到已使用环中的idx达到used_event + 1的值)为止,不需要通知。
    驱动程序必须处理来自设备的虚假通知。

2.6.7.2 设备要求:已使用缓冲区通知抑制
如果未协商VIRTIO_F_EVENT_IDX特性位:

  • 设备必须忽略used_event值。
  • 在设备将描述符索引写入已使用环后:
    • 如果flags为1,则设备不应发送通知。
    • 如果flags为0,则设备必须发送通知。

否则,如果已协商VIRTIO_F_EVENT_IDX特性位:

  • 设备必须忽略flags的较低位。
  • 在设备将描述符索引写入已使用环后:
    • 如果已使用环中的idx字段(确定描述符索引放置位置的字段)等于used_event,则设备必须发送通知。
    • 否则,设备不应发送通知。

注意:例如,如果used_event为0,则使用VIRTIO_F_EVENT_IDX的设备将在第一个缓冲区使用后发送已使用缓冲区通知给驱动程序(以及在第65536个缓冲区之后再次发送通知,依此类推)。

2.6.8 已使用环的布局结构
已使用环具有以下布局结构:

struct virtq_used {
    #define VIRTQ_USED_F_NO_NOTIFY 1
    le16 flags;                     // 标志字段,用于指示一些特性
    le16 idx;                       // 队列中下一个已使用描述符的索引
    struct virtq_used_elem ring[ /* Queue Size */ ]; // 已使用描述符的环形缓冲区,其大小为队列的大小
    le16 avail_event;               // 仅在 VIRTIO_F_EVENT_IDX 标志设置时存在,表示用于通知可用描述符的事件
};

/* le32 is used here for ids for padding reasons. */
struct virtq_used_elem {
    /* 描述符链的起始索引。 */
    le32 id;
    /* 已使用的描述符链的总长度(写入的长度)。 */
    le32 len;
};

已使用环是设备在使用完缓冲区后返回它们的地方:它只由设备写入,由驱动程序读取。
环中的每个条目都是一对:id 表示描述缓冲区的描述符链的头条目(这与之前由客户机放置在可用环中的条目匹配),len 表示写入缓冲区的总字节数。

注意:对于使用不受信任的缓冲区的驱动程序,len 非常有用:如果驱动程序不知道设备写入了多少数据,那么驱动程序将不得不提前将缓冲区清零,以确保不发生数据泄漏。

例如,网络驱动程序可能会将接收到的缓冲区直接交给不受信任的用户空间应用程序。如果网络设备没有覆盖掉该缓冲区中的字节,这可能会将其他进程的释放内存内容泄漏到应用程序中。
idx 字段指示设备将在环中的下一个描述符条目(模除队列大小)放置在哪里。这从 0 开始递增。

注意:传统的 [Virtio PCI Draft] 将这些结构称为 vring_used 和 vring_used_elem,将常量称为 VRING_USED_F_NO_NOTIFY,但布局和值是相同的。

2.6.8.1 Legacy Interface: The Virtqueue Used Ring

历史接口:Virtqueue 已使用环

从历史上看,许多驱动程序忽略了 len 值,因此许多设备错误地设置了 len。因此,在使用传统接口时,如果可能的话,通常最好忽略已使用环中条目中的 len 值。具体已知的问题会根据设备类型列出。

2.6.8.2 Device Requirements: The Virtqueue Used Ring

设备要求:Virtqueue 已使用环

设备在更新 used idx 之前必须设置 len。
设备在更新 used idx 之前必须向描述符的第一个可写设备缓冲区写入至少 len 个字节。
设备可以向描述符写入多于 len 个字节的数据。
注意:可能存在设备不知道哪些缓冲区的哪些部分已经被写入的潜在错误情况。这就是为什么允许 len 低估的原因:这比驱动程序认为未初始化的内存已被覆盖要好。

2.6.8.3 Driver Requirements: The Virtqueue Used Ring

驱动程序要求:Virtqueue 已使用环

驱动程序不能对超出第一个 len 字节的可写设备缓冲区中的数据做出假设,并且应该忽略这些数据。

2.6.9 In-order use of descriptors

描述符的顺序使用

一些设备始终按照它们被提供的顺序使用描述符。这些设备可以提供 VIRTIO_F_IN_ORDER 特性。如果协商成功,这种特性允许设备通过仅写出一个 used 环条目,该条目的 id 对应于描述最后一个缓冲区的描述符链的头条目,来通知驱动程序使用一批缓冲区。

然后,设备根据批处理的大小向前跳转在环中。因此,它将批处理的大小增加到 used idx。

驱动程序需要查找 used id 并计算批处理大小,以能够前进到设备将写入的下一个 used 环条目的位置。

这将导致 used 环条目位于与批处理中的第一个可用环条目匹配的偏移量处,下一个批处理的 used 环条目位于与下一个批处理的第一个可用环条目匹配的偏移量处,依此类推。

被跳过的缓冲区(未写入 used 环条目的缓冲区)被假定已完全被设备使用(读取或写入)。

2.6.10 Available Buffer Notification Suppression

可用缓冲区通知抑制

设备可以以类似于驱动程序在第2.6.7节中详细描述的方式抑制可用缓冲区通知。设备以与驱动程序在可用环中操作 flags 或 avail_event

2.6.10.1 Driver Requirements: Available Buffer Notification Suppression

驱动程序要求:可用缓冲区通知抑制

驱动程序在分配可用环时必须将 used 环中的 flags 初始化为 0。
如果未协商 VIRTIO_F_EVENT_IDX 特性位:

  • 驱动程序必须忽略 avail_event 值。
  • 在驱动程序将描述符索引写入可用环后:
    • 如果 flags 为 1,则驱动程序不应发送通知。
    • 如果 flags 为 0,则驱动程序必须发送通知。
      否则,如果已协商 VIRTIO_F_EVENT_IDX 特性位:
  • 驱动程序必须忽略 flags 的较低位。
  • 在驱动程序将描述符索引写入可用环后:
    • 如果可用环中的 idx 字段(确定描述符索引的放置位置)等于 avail_event,则驱动程序必须发送通知。
    • 否则,驱动程序不应发送通知。
2.6.10.2 Device Requirements: Available Buffer Notification Suppression

设备要求:可用缓冲区通知抑制

如果未协商 VIRTIO_F_EVENT_IDX 特性位:

  • 设备必须将 flags 设置为 0 或 1。
  • 设备可以将 flags 设置为 1 以建议驱动程序不需要通知。
    否则,如果已协商 VIRTIO_F_EVENT_IDX 特性位:
  • 设备必须将 flags 设置为 0。
  • 设备可以使用 avail_event 通知驱动程序,直到驱动程序将索引指定为 avail_event 写入可用环(等效于直到可用环中的 idx 达到 avail_event + 1 的值)为止,通知是不必要的。
    设备必须处理来自驱动程序的意外通知。

2.6.11 Helpers for Operating Virtqueues

操作 Virtqueue 的辅助工具

Linux 内核源代码包含上述定义和更易用形式的辅助例程,位于 include/uapi/linux/virtio_ring.h 中。IBM 和 Red Hat 明确根据(3-Clause)BSD 许可证授权了此代码,以便所有其他项目都可以自由使用,并且在 A virtio_queue.h 中(稍微有所变化)重新复制了此代码。

2.6.12 Virtqueue Operation

Virtqueue 操作有两个部分:向设备提供新的可用缓冲区和处理设备返回的已使用缓冲区。

注意:以最简单的 virtio 网络设备为例,它有两个 virtqueue:传输 virtqueue 和接收 virtqueue。驱动程序将传出(设备可读)的数据包添加到传输 virtqueue 中,然后在使用后释放它们。类似地,接收 virtqueue 中添加了传入(设备可写)的缓冲区,在使用后进行处理。

接下来,我们将更详细地了解在使用分离 virtqueue 格式时,这两个部分的要求。

2.6.13 Supplying Buffers to The Device

向设备提供缓冲区

驱动程序向设备的 virtqueue 之一提供缓冲区的步骤如下:

  1. 驱动程序将缓冲区放入描述符表中的空闲描述符中,必要时进行链接(参见 2.6.5 节 “Virtqueue 描述符表”)。
  2. 驱动程序将描述符链的头索引放入可用环的下一个环条目中。
  3. 如果可以批量处理,可以重复执行步骤 1 和 2。
  4. 驱动程序执行适当的内存屏障,以确保设备在执行下一步之前看到更新后的描述符表和可用环。
  5. 可用 idx 值增加等于添加到可用环的描述符链头数。
  6. 驱动程序执行适当的内存屏障,以确保在检查是否抑制通知之前更新 idx 字段。
  7. 如果未抑制此类通知,则驱动程序向设备发送可用缓冲区通知。

请注意,上述代码不采取措施防止可用环缓冲区的环绕:这是不可能的,因为环缓冲区的大小与描述符表相同,因此步骤(1)将防止此类情况发生。
此外,最大队列大小为 32768(适合于 16 位的最高 2 的幂),因此 16 位 idx 值始终可以区分满缓冲区和空缓冲区。

2.6.13.1 将缓冲区放入描述符表中

一个缓冲区由零个或多个设备可读的物理连续元素和零个或多个物理连续的设备可写元素(每个至少有一个元素)组成。此算法将其映射到描述符表中,以形成描述符链:
对于每个缓冲区元素 b:

  1. 获取下一个空闲的描述符表条目 d。
  2. 将 d.addr 设置为 b 起始位置的物理地址。
  3. 将 d.len 设置为 b 的长度。
  4. 如果 b 是设备可写的,则将 d.flags 设置为 VIRTQ_DESC_F_WRITE,否则设置为 0。
  5. 如果此后还有缓冲区元素:
    (a) 将 d.next 设置为下一个空闲描述符元素的索引。
    (b) 设置 d.flags 中的 VIRTQ_DESC_F_NEXT 位。

实际上,d.next 通常用于链接空闲描述符,并保持分离计数以在开始映射之前检查是否有足够的空闲描述符。

2.6.13.2 更新可用环

描述符链头是上述算法中的第一个 d,即指向缓冲区第一部分的描述符表条目的索引。一个简单的驱动程序实现可以执行以下操作(假定已经适当地转换为和从小端序):

avail->ring[avail->idx % qsz] = head;

然而,通常情况下,驱动程序可以在更新 idx(此时它们对设备可见)之前添加许多描述符链,因此通常需要保持一个计数器来跟踪驱动程序已添加了多少描述符链:

avail->ring[(avail->idx + added++) % qsz] = head;
2.6.13.3 更新 idx

idx 始终递增,并在自然情况下回绕到 65536:

avail->idx += added;

一旦可用的 idx 被驱动程序更新,这将暴露描述符及其内容。设备可以立即访问驱动程序创建的描述符链和它们引用的内存。

2.6.13.3.1 驱动程序要求:更新 idx

驱动程序在更新 idx 之前必须执行适当的内存屏障,以确保设备看到最新的副本。

2.6.13.4 通知设备

设备通知的实际方法是特定于总线的,但通常可能会很昂贵。因此,如果设备不需要通知,它可以抑制这些通知,详见第2.6.10节。
驱动程序必须小心,在检查是否抑制通知之前公开新的 idx 值。

2.6.13.4.1 驱动程序要求:通知设备

在读取 flags 或 avail_event 之前,驱动程序必须执行适当的内存屏障,以避免错过通知。

2.6.14 从设备接收已使用的缓冲区

一旦设备使用了由描述符引用的缓冲区(根据虚拟队列和设备的性质,可以从中读取或写入它们,或者两者都有),它将按照第2.6.7节的详细信息向驱动程序发送已使用的缓冲区通知。
注意:为了实现最佳性能,驱动程序可以在处理已使用的环之间禁用已使用的缓冲区通知,但要注意在清空环并重新启用通知之间可能会丢失通知的问题。通常在重新启用通知后重新检查是否有更多的已使用的缓冲区来处理这个问题:

// 禁用已使用缓冲区的通知
virtq_disable_used_buffer_notifications(vq);

for (;;) {
    // 检查上一次观察到的已使用描述符索引是否与虚拟队列的 used.idx 相等
    if (vq->last_seen_used != le16_to_cpu(virtq->used.idx)) {
        // 如果不相等,表示有新的已使用描述符,需要进行处理

        // 启用已使用缓冲区的通知,通知虚拟机或驱动程序有新的数据可用
        virtq_enable_used_buffer_notifications(vq);

        // 执行内存屏障操作,确保之前的操作被完全刷新到内存
        mb();

        // 再次检查已使用描述符索引是否已经被更新,以确保没有在内存屏障之后的新的已使用描述符
        if (vq->last_seen_used != le16_to_cpu(virtq->used.idx)) {
            // 如果有新的已使用描述符被检测到但在内存屏障之后未更新

            // 再次禁用已使用缓冲区的通知
            virtq_disable_used_buffer_notifications(vq);
            // 退出循环
            break;
        }
    }

    // 获取当前已使用描述符环中索引为 last_seen_used 的元素
    struct virtq_used_elem *e = virtq.used->ring[vq->last_seen_used % vsz];

    // 处理获取到的已使用描述符元素
    process_buffer(e);

    // 增加 last_seen_used 的值,以指向下一个待处理的已使用描述符
    vq->last_seen_used++;
}

2.7 Packed Virtqueues

紧凑型虚拟队列是一种替代的紧凑型虚拟队列布局,使用读写内存,即主机和客户机都可以读取和写入的内存。
使用紧凑型虚拟队列需要通过 VIRTIO_F_RING_PACKED 功能位来进行协商。
紧凑型虚拟队列每个支持最多 2^15 个条目。
在当前的传输方式下,虚拟队列位于由驱动程序分配的客户机内存中。每个紧凑型虚拟队列由三个部分组成:

描述符环 - 占据描述符区域
驱动程序事件抑制 - 占据驱动程序区域
设备事件抑制 - 占据设备区域
其中,描述符环又包括描述符,每个描述符可以包含以下部分:

缓冲区 ID
元素地址
元素长度
标志
一个缓冲区由零个或多个设备可读的物理连续元素组成,后面跟随零个或多个物理连续的设备可写元素(每个缓冲区至少包含一个元素)。
当驱动程序希望将这样一个缓冲区发送给设备时,它将至少一个可用描述符描述缓冲区的元素写入描述符环中。描述符通过描述符内部存储的缓冲区 ID 与缓冲区关联。

然后,驱动程序通知设备。当设备完成处理缓冲区时,它会将包含缓冲区 ID 的已使用设备描述符写入描述符环中(覆盖以前提供的驱动程序描述符),并发送已使用事件通知。

描述符环是以循环方式使用的:驱动程序按顺序将描述符写入环中。在达到环的末尾后,下一个描述符被放置在环的开头。一旦环充满了驱动程序描述符,驱动程序就会停止发送新的请求,并等待设备开始处理描述符并在提供新的驱动程序描述符之前写入一些已使用的描述符。

类似地,设备按顺序从环中读取描述符,并检测是否已提供了驱动程序描述符。随着描述符的处理完成,设备会将已使用的描述符写回环中。

注意:在按顺序读取驱动程序描述符并开始按顺序处理它们后,设备可能会无序地完成其处理。已使用的设备描述符按照其处理完成的顺序写入。

设备事件抑制数据结构是设备的只写数据结构。它包括有关减少设备事件数量的信息,即向设备发送更少的可用缓冲区通知。

驱动程序事件抑制数据结构是设备的只读数据结构。它包括有关减少驱动程序事件数量的信息,即向驱动程序发送更少的已使用缓冲区通知。

2.7.1 驱动程序和设备环绕计数器

每个驱动程序和设备都应在内部维护一个单比特的环绕计数器,初始值为1。这个由驱动程序维护的计数器称为驱动程序环绕计数器。每当驱动程序使环中的最后一个描述符可用时(在使最后一个描述符可用后),它都会更改此计数器的值。

同样,设备也应该在内部维护一个单比特的环绕计数器,称为设备环绕计数器。每当设备使用环中的最后一个描述符时(在标记最后一个描述符已使用后),它都会更改此计数器的值。

可以很容易地看出,当驱动程序和设备处理相同的描述符或当所有可用描述符都已使用完时,驱动程序中的驱动程序环绕计数器将与设备中的设备环绕计数器匹配。

为了标记描述符为可用和已使用,驱动程序和设备都使用以下两个标志:

  • VIRTQ_DESC_F_AVAIL:表示描述符可用。
  • VIRTQ_DESC_F_USED:表示描述符已被使用。

这些标志通过环中的描述符的状态来交替,以便双方可以了解描述符的状态,而不会发生冲突。这些环绕计数器和标志是确保通信的一致性和可靠性的关键机制。

#define VIRTQ_DESC_F_AVAIL (1 << 7)
#define VIRTQ_DESC_F_USED (1 << 15)

2.7.1 驱动程序和设备环形计数器

驱动程序和设备都应在内部维护一个单位位环形计数器,初始值为1。驱动程序维护的计数器称为驱动程序环形计数器。每当驱动程序使环形中的最后一个描述符可用(在使最后一个描述符可用之后),驱动程序都会更改此计数器的值。

设备维护的计数器称为设备环形计数器。设备在使用环形中的最后一个描述符后(在标记最后一个描述符已使用之后)更改此计数器的值。

很容易看出,驱动程序中的驱动程序环形计数器与设备中的设备环形计数器在处理相同的描述符或已使用所有可用描述符时是匹配的。

要将描述符标记为可用和已使用,驱动程序和设备都使用以下两个标志:

  • 要将描述符标记为可用,驱动程序将Flags中的VIRTQ_DESC_F_AVAIL位设置为与内部驱动程序环形计数器匹配。它还设置了VIRTQ_DESC_F_USED位以匹配相反的值(即不匹配内部驱动程序环形计数器)。

  • 要将描述符标记为已使用,设备将Flags中的VIRTQ_DESC_F_USED位设置为与内部设备环形计数器匹配。它还设置了VIRTQ_DESC_F_AVAIL位以匹配相同的值。

因此,对于可用描述符,VIRTQ_DESC_F_AVAILVIRTQ_DESC_F_USED位是不同的,而对于已使用的描述符,它们是相同的。

请注意,此观察通常用于检查合理性,因为这些是必要但不充分的条件-例如,所有描述符都会初始化为零。要检测已使用和可用描述符,驱动程序和设备可以跟踪VIRTQ_DESC_F_USED/VIRTQ_DESC_F_AVAIL的最后观察值。还可以使用其他技术来检测VIRTQ_DESC_F_AVAIL/VIRTQ_DESC_F_USED位的更改可能性。

2.7.2 可用和已使用描述符的轮询

设备和驱动程序描述符的写入通常可以重新排序,但是每一方(驱动程序和设备)仅需要在内存中轮询(或测试)单个位置:它们在之前处理的描述符之后的下一个设备描述符,按循环顺序。

有时,设备只需要在处理多个可用描述符的批次后写出一个已使用描述符。如下所述,当使用描述符链接或按顺序使用描述符时,可以发生这种情况。在这种情况下,设备将已使用描述符的缓冲区ID写入描述符环形中的最后一个描述符。在处理已使用的描述符后,设备和驱动程序都将环形中的剩余描述符组的数量跳到下一个已使用的描述符的处理位置(对于驱动程序是读取,对于设备是写入)。

2.7.3 写入标志

在可用描述符中,Flags中的VIRTQ_DESC_F_WRITE位用于标记描述符是否对应于缓冲区的只写或只读元素。

/* 这将一个描述符标记为设备仅写入(否则为设备只读)。 */
#define VIRTQ_DESC_F_WRITE 2

2.7.4 写标志

  • 在可用描述符中,Flags 中的 VIRTQ_DESC_F_WRITE 位用于标记描述符是否对应于缓冲区的只写或只读元素。

2.7.5 元素地址和长度

  • 在可用描述符中,元素地址对应于缓冲区元素的物理地址。元素的长度被存储在 Element Length 中,假定该元素是物理连续的。
  • 在已使用的描述符中,Element Address 未使用。Element Length 指定了设备已初始化(写入)的缓冲区的长度。对于未设置 VIRTQ_DESC_F_WRITE 标志的已使用描述符,Element Length 保留,驱动程序会忽略它。

2.7.6 分散-聚集支持

  • 一些驱动程序需要能够使用请求提供多个缓冲区元素的列表(也称为分散/聚集列表)。有两种功能支持这一点:描述符链接和间接描述符。
  • 如果驱动程序不使用这两个功能中的任何一个,那么每个缓冲区都是物理连续的,要么只读要么只写,并且由单个描述符完全描述。
  • 尽管不常见(大多数实现要么只使用非间接描述符创建所有列表,要么始终使用单个间接元素),但如果已经协商了这两个功能,则在环中混合使用间接和非间接描述符是有效的,只要每个列表只包含给定类型的描述符。
  • 分散/聚集列表仅适用于可用描述符。单个已使用的描述符对应于整个列表。
  • 设备通过传输特定和/或设备特定的值来限制列表中描述符的数量。如果没有限制,列表中描述符的最大数量是 virtqueue 的大小。

2.7.6 下一个标志:描述符链接

  • 压缩环格式允许驱动程序通过使用多个描述符并在除了最后一个可用描述符以外的所有描述符中设置 Flags 中的 VIRTQ_DESC_F_NEXT 位来向设备提供分散/聚集列表。
/* 这将一个缓冲区标记为继续的。 */
#define VIRTQ_DESC_F_NEXT 1

缓冲区 ID 包含在列表的最后一个描述符中。

在将列表的其余部分写入环形队列之后,驱动程序总是使列表中的第一个描述符可用。这可以确保设备永远不会在环形队列中观察到部分分散/聚合列表。

注意: 所有标志,包括 VIRTQ_DESC_F_AVAILVIRTQ_DESC_F_USEDVIRTQ_DESC_F_WRITE,必须在列表中的所有描述符中正确设置/清除,而不仅仅是第一个描述符。

设备仅为整个列表写出一个已使用描述符。然后,它根据列表中的描述符数量向前跳跃。驱动程序需要跟踪与每个缓冲区 ID 对应的列表大小,以便能够跳到设备写入的下一个已使用描述符的位置。

例如,如果描述符按照它们可用的顺序使用,这将导致已使用的描述符覆盖列表中的第一个可用描述符,下一个列表的已使用描述符覆盖下一个列表中的第一个可用描述符,依此类推。

VIRTQ_DESC_F_NEXT 在已使用的描述符中保留,驱动程序应忽略它。

2.7.7 间接标志:分散/聚合支持

一些设备通过同时分派大量大型请求而受益。VIRTIO_F_INDIRECT_DESC 特性允许这样做。为了增加环的容量,驱动程序可以在内存的任何位置存储一张(设备只读的)间接描述符表,并在主 virtqueue 中插入一个描述符(使用 Flags 中的 VIRTQ_DESC_F_INDIRECT 位),该描述符引用一个包含此间接描述符表的缓冲区元素;addrlen 分别指示间接表的地址和长度(以字节为单位)。

/* 这表示该元素包含一张描述符表(table of descriptors)。 */
#define VIRTQ_DESC_F_INDIRECT 4

间接表的布局结构如下所示(len 是指向此表的描述符的缓冲区长度,它是一个可变的):

struct pvirtq_indirect_descriptor_table {
    /* 实际的描述符结构体(每个是 struct pvirtq_desc) */
    struct pvirtq_desc desc[len / sizeof(struct pvirtq_desc)];
};

第一个描述符位于间接描述符表的开头,附加的间接描述符紧随其后。在间接表中,VIRTQ_DESC_F_WRITE 标志位是描述符仅有的有效标志位。其他标志位均为保留标志位,设备会忽略。Buffer ID 也是保留字段,设备同样会忽略。

在具有 VIRTQ_DESC_F_INDIRECT 设置的描述符中,VIRTQ_DESC_F_WRITE 被保留,并且同样会被设备忽略。

2.7.8 描述符的有序使用

某些设备始终按照它们可用的顺序使用描述符。这些设备可以提供 VIRTIO_F_IN_ORDER 功能。如果协商成功,这种特性允许设备通过仅写出一个带有与批处理中最后一个描述符对应的 Buffer ID 的已使用描述符来通知驱动程序使用一批缓冲区。

然后,设备根据批处理的大小向前跳过环。驱动程序需要查找已使用的 Buffer ID 并计算批处理大小,以便能够前进到设备将要写入的下一个已使用描述符的位置。

这将导致已使用描述符覆盖批处理中的第一个可用描述符,下一批的已使用描述符将覆盖下一批中的第一个可用描述符,依此类推。跳过的缓冲区(未写入已使用描述符的缓冲区)被假定已被设备完全使用(读取或写入)。

2.7.9 多缓冲区请求

某些设备在处理单个请求时会组合多个缓冲区。这些设备总是在请求的第一个缓冲区对应的描述符之后,将与请求的其余缓冲区对应的描述符标记为已使用,并写入环中。这确保了驱动程序永远不会在环中观察到不完整的请求。

2.7.10 驱动程序和设备事件抑制

在许多系统中,已使用和可用的缓冲区通知涉及显著的开销。为了减轻这种开销,每个 virtqueue 都包含两个用于控制设备和驱动程序之间通知的相同结构。

驱动程序事件抑制结构对设备是只读的,用于控制设备发送给驱动程序的已使用缓冲区通知。
设备事件抑制结构对驱动程序是只读的,用于控制驱动程序发送给设备的可用缓冲区通知。
每个这些事件抑制结构都包括以下字段:

描述符环改变事件标志,可以取以下值:

/* 启用事件 */
#define RING_EVENT_FLAGS_ENABLE 0x0
/* 禁用事件 */
#define RING_EVENT_FLAGS_DISABLE 0x1
/* 启用指定描述符的事件(由描述符环中的偏移量/包装计数指定)。
   仅在已协商 VIRTIO_F_RING_EVENT_IDX 时有效。 */
#define RING_EVENT_FLAGS_DESC 0x2
/* 保留值,值为0x3 */

Descriptor Ring Change Event Offset 如果事件标志设置为描述符特定事件:在环内的偏移量(以描述符大小的单位)。只有在该描述符相应地可用/已使用时才会触发事件。
Descriptor Ring Change Event Wrap Counter 如果事件标志设置为描述符特定事件:在环内的偏移量(以描述符大小的单位)。只有当环包装计数器与此值匹配并相应地设置了描述符时,事件才会触发。
在写出一些描述符后,设备和驱动程序都应查阅相关结构,以确定是否应发送已使用缓冲区通知或可用缓冲区通知。

2.7.10.1 结构大小和对齐

virtqueue 的每个部分在虚拟机内存中是物理连续的,并具有不同的对齐要求。
每个 virtqueue 部分的内存对齐和大小要求(以字节为单位)总结在以下表格中:

第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第1张图片
对齐列给出了virtqueue每个部分的最小对齐要求。
大小列给出了virtqueue每个部分的总字节数。
Queue Size 对应于virtqueue中描述符的最大数量。Queue Size值不必是2的幂。

2.7.11 驱动程序要求:Virtqueues

驱动程序必须确保每个virtqueue部分的第一个字节的物理地址是上述表中指定对齐值的倍数。

2.7.12 设备要求:Virtqueues

设备必须按照它们在环中出现的顺序开始处理驱动程序描述符。设备必须按照它们完成的顺序开始将设备描述符写入环中。设备在开始写入描述符后可以重新排序描述符的写入。

2.7.13 Virtqueue描述符格式

可用描述符是指驱动程序发送给设备的缓冲区。addr是物理地址,描述符使用id字段标识一个缓冲区。

struct pvirtq_desc {
    /* 缓冲区地址。 */
    le64 addr;
    /* 缓冲区长度。 */
    le32 len;
    /* 缓冲区ID。 */
    le16 id;
    /* 根据描述符类型而异的标志。 */
    le16 flags;
};

2.7.14 事件抑制结构格式

以下结构用于减少驱动程序和设备之间发送的通知数量。

struct pvirtq_event_suppress {
    le16 {
        desc_event_off : 15; /* 描述符环变更事件偏移量 */
        desc_event_wrap : 1; /* 描述符环变更事件包装计数 */
    } desc; /* 如果 desc_event_flags 设置为 RING_EVENT_FLAGS_DESC */

    le16 {
        desc_event_flags : 2, /* 描述符环变更事件标志 */
        reserved : 14; /* 保留,设置为 0 */
    } flags;
};

2.7.15 设备要求:Virtqueue描述符表

设备不得写入可供设备读取的缓冲区,设备不应读取可供设备写入的缓冲区。设备不得使用描述符,除非观察到其标志中的VIRTQ_DESC_F_AVAIL位已更改(例如,与初始零值相比)。设备不得在更改VIRTQ_DESC_F_USED位之后更改描述符。

2.7.16 驱动程序要求:Virtqueue描述符表

除非观察到其标志中的VIRTQ_DESC_F_USED位已更改,否则驱动程序不得更改描述符。驱动程序不得在更改VIRTQ_DESC_F_AVAIL位之后更改描述符。在通知设备时,驱动程序必须设置next_off和next_wrap以匹配尚未提供给设备的下一个描述符。驱动程序可以发送多个可用的缓冲区通知,而无需向设备提供任何新的描述符。

2.7.17 驱动程序要求:Scatter-Gather支持

驱动程序不得创建比设备允许的更长的描述符列表。驱动程序不得创建比队列大小更长的描述符列表。这意味着禁止描述符列表中的循环!驱动程序必须在任何设备可读的描述符元素之后放置任何设备可写的描述符元素。驱动程序不得依赖于设备使用更多的描述符来能够写出列表中的所有描述符。驱动程序在将列表中的第一个描述符提供给设备之前,必须确保环中有足够的空间容纳整个列表。驱动程序不得在提供列表中的所有后续描述符之前使列表中的第一个描述符可用。

2.7.18 设备要求:Scatter-Gather支持

设备必须按照驱动程序提供的顺序使用由VIRTQ_DESC_F_NEXT标志链接的列表中的描述符。

2.7.19 驱动程序要求:间接描述符

除非已经协商了VIRTIO_F_INDIRECT_DESC功能,否则驱动程序不得设置DESC_F_INDIRECT标志。在间接描述符内,驱动程序除了在DESC_F_WRITE中设置任何标志外,不得设置任何标志。驱动程序不得创建比设备允许的更长的描述符链。驱动程序不得在由VIRTQ_DESC_F_NEXT链接的散列/聚合列表中设置了DESC_F_INDIRECT的直接描述符。标志。

2.7.20 Virtqueue操作

virtqueue操作分为两部分:向设备提供新的可用缓冲区和处理来自设备的已使用缓冲区。以下是在使用紧凑型virtqueue格式时更详细地描述这两个部分的要求。

2.7.21 向设备提供缓冲区

驱动程序将缓冲区提供给设备的virtqueue如下所示:

  1. 驱动程序将缓冲区放入Descriptor Ring中的空闲描述符。
  2. 驱动程序执行适当的内存屏障,以确保在检查通知抑制之前更新了描述符。
  3. 如果未抑制通知,则驱动程序通知设备新的可用缓冲区。
    以下更详细描述了每个阶段的要求。
2.7.21.1 将可用缓冲区放入描述符环中

对于每个缓冲区元素b:

  1. 获取下一个描述符表条目d
  2. 获取下一个空闲缓冲区ID值
  3. 将d.addr设置为b的起始物理地址
  4. 将d.len设置为b的长度。
  5. 将d.id设置为缓冲区ID
  6. 根据以下计算标志:
    (a) 如果b是设备可写的,则将VIRTQ_DESC_F_WRITE位设置为1,否则设置为0
    (b) 将VIRTQ_DESC_F_AVAIL位设置为Driver Ring Wrap Counter的当前值
    © 将VIRTQ_DESC_F_USED位设置为反向值
  7. 执行内存屏障以确保已初始化描述符
  8. 将d.flags设置为计算得出的标志值
  9. 如果d是环中的最后一个描述符,则切换Driver Ring Wrap Counter
  10. 否则,将d递增为指向下一个描述符
    这使单个描述符缓冲区可用。但是,通常情况下,驱动程序可以在单个请求的一部分中使用一批描述符。在这种情况下,它会延迟更新第一个描述符的描述符标志(以及先前的内存屏障),直到其余描述符已初始化。驱动程序更新了描述符标志字段后,将公开描述符及其内容。设备可以立即访问驱动程序创建的描述符及其后续的任何描述符和它们引用的内存。
2.7.21.1.1 驱动程序要求:更新标志

驱动程序在更新标志之前必须执行适当的内存屏障,以确保设备看到最新的副本。

2.7.21.2 发送可用缓冲区通知

设备通知的实际方法是与总线相关的,但通常会很昂贵。因此,如果设备不需要这些通知,设备可以抑制此类通知,使用包括设备区域的事件抑制结构,如第2.7.14节所述。驱动程序必须小心,在检查是否抑制通知之前,公开新的标志值。

2.7.21.3 实现示例

以下是一个驱动程序代码示例。它不尝试减少可用缓冲区通知的数量,也不支持VIRTIO_F_RING_EVENT_IDX功能。

/* 注意:vq->avail_wrap_count 初始化为 1 */
/* 注意:vq->sgs 是与 ring 大小相同的数组 */
id = alloc_id(vq); // 为描述符分配一个唯一的ID
first = vq->next_avail; // 记录起始可用描述符的索引
sgs = 0; // sgs 用于跟踪当前描述符的数量

// 对每个缓冲区元素 b 进行循环处理
for (each buffer element b) {
    sgs++; // 增加描述符计数

    // 将当前描述符的 ID 设置为 -1,表示未使用
    vq->ids[vq->next_avail] = -1;

    // 设置描述符的地址和长度
    vq->desc[vq->next_avail].address = get_addr(b);
    vq->desc[vq->next_avail].len = get_len(b);

    // 根据描述符类型和状态设置标志
    avail = vq->avail_wrap_count ? VIRTQ_DESC_F_AVAIL : 0;
    used = !vq->avail_wrap_count ? VIRTQ_DESC_F_USED : 0;
    f = get_flags(b) | avail | used;

    // 如果当前描述符不是最后一个缓冲区元素,则标记为 VIRTQ_DESC_F_NEXT
    if (b is not the last buffer element) {
        f |= VIRTQ_DESC_F_NEXT;
    }

    // 只有当所有描述符都准备好后,才将第一个描述符标记为可用
    if (vq->next_avail == first) {
        flags = f;
    } else {
        vq->desc[vq->next_avail].flags = f;
    }
    last = vq->next_avail;
    vq->next_avail++;

    // 如果索引超出队列大小,则回绕到队列的开头,并切换 avail_wrap_count
    if (vq->next_avail >= vq->size) {
        vq->next_avail = 0;
        vq->avail_wrap_count ^= 1;
    }
}

// 记录当前描述符的数量到 sgs 数组
vq->sgs[id] = sgs;

// 将 ID 包含在列表中的最后一个描述符中
vq->desc[last].id = id;

// 执行内存写入屏障,确保之前的写操作被刷新到内存
write_memory_barrier();

// 设置第一个描述符的标志
vq->desc[first].flags = flags;

// 执行内存屏障,确保描述符的写入在通知设备之前完成
memory_barrier();

// 如果设备事件标志不等于 RING_EVENT_FLAGS_DISABLE,则通知设备
if (vq->device_event.flags != RING_EVENT_FLAGS_DISABLE) {
    notify_device(vq);
}

2.7.21.3.1 驱动程序要求:发送可用缓冲区通知

驱动程序在读取占用设备区域的事件抑制结构之前必须执行适当的内存屏障。不这样做可能导致未发送强制性可用缓冲区通知。

2.7.22 从设备接收已使用的缓冲区

一旦设备使用了由描述符引用的缓冲区(根据virtqueue的性质和设备的性质,可以从中读取或写入它们,或者两者的部分),它会向驱动程序发送已使用的缓冲区通知,详细信息请参见第2.7.14节。

注意:为了实现最佳性能,驱动程序可以在处理已使用的缓冲区时禁用已使用的缓冲区通知,但要注意在清空环并重新启用已使用的缓冲区通知之间可能会丢失通知的问题。通常在重新启用通知后会重新检查是否有更多的已使用缓冲区来处理:

/* 注意:vq->used_wrap_count 初始化为 1 */
vq->driver_event.flags = RING_EVENT_FLAGS_DISABLE;

for (;;) {
    struct pvirtq_desc *d = vq->desc[vq->next_used];

    /*
    * 检查以下条件:
    * 1. 描述符已经被标记为可用。这个检查是必要的,因为如果驱动程序正在并行地
    *    提供新的描述符,同时也在处理已使用的描述符(例如,来自另一个线程),
    *    需要确保这个描述符已经被标记为可用。
    *    注意:还有其他检查方法,例如,追踪可用描述符或缓冲区的未完成数量,
    *    并检查其是否为零。
    * 2. 描述符已经被设备使用。
    */
    flags = d->flags;
    bool avail = flags & VIRTQ_DESC_F_AVAIL;
    bool used = flags & VIRTQ_DESC_F_USED;
    
    if (avail != vq->used_wrap_count || used != vq->used_wrap_count) {
        vq->driver_event.flags = RING_EVENT_FLAGS_ENABLE;
        memory_barrier();
        
        /*
        * 再次检查,以防驱动程序在处理已使用描述符的同时并行提供了更多的描述符,
        * 或者在驱动程序启用事件之前,设备使用了更多的描述符。
        */
        flags = d->flags;
        bool avail = flags & VIRTQ_DESC_F_AVAIL;
        bool used = flags & VIRTQ_DESC_F_USED;
        
        if (avail != vq->used_wrap_count || used != vq->used_wrap_count) {
            break;
        }
        vq->driver_event.flags = RING_EVENT_FLAGS_DISABLE;
    }

    read_memory_barrier();
    
    /* 跳过描述符,直到下一个缓冲区 */
    id = d->id;
    assert(id < vq->size);
    sgs = vq->sgs[id];
    vq->next_used += sgs;

    if (vq->next_used >= vq->size) {
        vq->next_used -= vq->size;
        vq->used_wrap_count ^= 1;
    }
    
    free_id(vq, id); // 释放描述符的ID
    process_buffer(d); // 处理描述符中的数据
}

2.7.23 驱动程序通知

在某些情况下,驱动程序需要向设备发送可用缓冲区通知。
当未协商VIRTIO_F_NOTIFICATION_DATA时,此通知涉及向设备发送virtqueue号码(方法取决于传输方式)。
但是,某些设备受益于能够了解队列中可用数据的数量,而无需访问内存中的virtqueue:出于效率或作为调试辅助。
为了帮助实现这些优化,当协商了VIRTIO_F_NOTIFICATION_DATA时,驱动程序通知设备将包括以下信息:

  • vqn:要通知的VQ号码。
  • next_off:环中下一个可用环条目将被写入的偏移量。当未协商VIRTIO_F_RING_PACKED时,这是可用索引的15个最低有效位。
  • 当协商了VIRTIO_F_RING_PACKED时,这是在描述符环中下一个可用描述符将被写入的偏移量(以描述符条目为单位)。
  • next_wrap:环包装计数器。对于协商了VIRTIO_F_RING_PACKED的情况,这是下一个可用描述符的包装计数器。对于未协商VIRTIO_F_RING_PACKED的情况,这是可用索引的最高位(第15位)。
    请注意,驱动程序可以发送多个通知,即使没有提供更多的缓冲区。当协商了VIRTIO_F_NOTIFICATION_DATA时,这些通知将具有相同的next_off和next_wrap值。

3 通用初始化和设备操作

我们从设备初始化的概述开始,然后详细介绍设备以及每个步骤是如何执行的。最好阅读此部分以及描述如何与特定设备通信的特定总线部分。

3.1 设备初始化

3.1.1 驱动程序要求:设备初始化

驱动程序必须按照以下顺序初始化设备:

  1. 重置设备。
  2. 设置ACKNOWLEDGE状态位:客户操作系统已注意到设备。
  3. 设置DRIVER状态位:客户操作系统知道如何操作设备。
  4. 读取设备特性位,并将操作系统和驱动程序理解的特性位子集写入设备。在此步骤中,驱动程序可以读取(但不能写入)设备特定的配置字段,以检查是否可以支持设备,然后才接受它。
  5. 设置FEATURES_OK状态位。驱动程序不能在此步骤之后接受新的特性位。
  6. 重新读取设备状态,以确保FEATURES_OK位仍然设置:否则,设备不支持我们的特性子集,设备无法使用。
  7. 执行设备特定的设置,包括发现设备的virtqueues、可选的每总线设置、读取和可能写入设备的virtio配置空间,以及填充virtqueues。
  8. 设置DRIVER_OK状态位。此时设备是“活动的”。
    如果这些步骤中的任何一个出现不可恢复的错误,驱动程序应该设置FAILED状态位,表示已放弃设备(如果需要,可以稍后重置设备以重新启动)。在这种情况下,驱动程序不得继续初始化。
    驱动程序不得在设置DRIVER_OK之前向设备发送任何缓冲区可用通知。

3.1.2 旧接口:设备初始化

旧设备不支持FEATURES_OK状态位,因此没有一种优雅的方式可以让设备指示不支持的特性组合。它们也没有提供清晰的机制来结束特性协商,这意味着设备在首次使用时最终确定特性,不能引入在根本改变设备初始操作的情况下引入特性。
旧的驱动程序实现通常在设置DRIVER_OK位之前使用设备,有时甚至在将特性位写入设备之前。
结果,步骤5和6被省略,步骤4、7和8被合并。
因此,当使用旧接口时:

  • 过渡驱动程序必须按照3.1中描述的初始化顺序执行初始化序列,但省略步骤5和6。
  • 过渡设备必须支持在步骤4之前驱动程序写入设备配置字段。
  • 过渡设备必须支持在步骤8之前驱动程序使用设备。

3.2 设备操作

在操作设备时,设备配置空间中的每个字段都可以由驱动程序或设备更改。
每当设备触发此类配置更改时,都会通知驱动程序。这使得驱动程序能够缓存设备配置,避免除非通知,否则会进行昂贵的配置读取。

3.2.1 设备配置更改通知

对于设备,其中设备特定配置信息可以更改的设备,会在发生设备特定配置更改时发送配置更改通知。
此外,该通知是由设备设置DEVICE_NEEDS_RESET(请参阅2.1.2)时触发的。

3.3 设备清理

一旦驱动程序设置了DRIVER_OK状态位,设备的所有配置的virtqueue都被视为“活动的”。一旦设备被重置,设备的任何virtqueue都不再“活动”。

3.3.1 驱动程序要求:设备清理

驱动程序不得更改已暴露给设备的virtqueue条目,即已提供给设备(尚未被设备使用)的缓冲区。
因此,驱动程序必须确保virtqueue不处于活动状态(通过设备重置),然后再删除已暴露的缓冲区。

4 Virtio Transport Options

Virtio可以使用各种不同的总线,因此标准分为virtio通用部分和总线特定部分。

4.1 通过PCI总线的Virtio

Virtio设备通常作为PCI设备实现。
Virtio设备可以实现为任何类型的PCI设备:传统PCI设备或PCI Express设备。要确保设计符合最新的级别要求,请参见PCI-SIG主页http://www.pcisig.com以获取任何批准的更改。

4.1.1 设备要求:通过PCI总线的Virtio

使用通过PCI总线的Virtio的Virtio设备必须向客户端公开一个符合适当PCI规范的接口规范要求:[PCI]和[PCIe]分别。

4.1.2 PCI设备发现

PCI设备的PCI供应商ID为0x1AF4,PCI设备ID为0x1000到0x107F(包括0x107F)的任何PCI设备都是virtio设备。此范围内的实际值表示设备支持的virtio设备。PCI设备ID通过将0x1040添加到Virtio设备ID中计算,如第5节所示。另外,根据设备类型,设备可以使用过渡PCI设备ID范围0x1000到0x103F。

4.1.2.1 设备要求:PCI设备发现

设备必须具有PCI供应商ID 0x1AF4。设备必须具有通过将0x1040添加到Virtio设备ID中计算的PCI设备ID,如第5节所示,或者根据设备类型使用过渡PCI设备ID。
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第2张图片

例如,Virtio设备ID为1的网络卡设备的PCI设备ID为0x1041或过渡PCI设备ID为0x1000。
PCI子系统供应商ID和PCI子系统设备ID可以反映环境的PCI供应商和设备ID(由驱动程序用于信息目的)。非过渡设备应具有范围在0x1040到0x107F的PCI设备ID。非过渡设备应具有PCI修订ID为1或更高。非过渡设备应具有PCI子系统设备ID为0x40或更高。
这是为了减小旧版驱动程序尝试驱动设备的机会。

4.1.2.2 驱动程序要求:PCI设备发现

驱动程序必须匹配PCI供应商ID 0x1AF4和PCI设备ID在0x1040到0x107F的范围内,通过将0x1040添加到Virtio设备ID中计算,如第5节所示的PCI设备ID。第4.1.2节列出的设备类型的驱动程序必须与PCI供应商ID 0x1AF4和第4.1.2节中指定的过渡PCI设备ID匹配。
驱动程序必须匹配任何PCI修订ID值。驱动程序可以匹配任何PCI子系统供应商ID和任何PCI子系统设备ID值。

4.1.2.3 旧接口:关于PCI设备发现的说明

过渡设备必须具有PCI修订ID为0。过渡设备必须具有与Virtio设备ID匹配的PCI子系统设备ID,如第5节所示。过渡设备的过渡PCI设备ID范围必须在0x1000到0x103F之间。
这是为了与旧版驱动程序匹配。

4.1.3 PCI设备布局

该设备通过I/O和/或内存区域配置(尽管请参见4.1.4.7以通过PCI配置空间访问),根据Virtio结构PCI功能指定。
设备配置区域中存在不同大小的字段。所有64位、32位和16位字段都是小端字节序。64位字段应被视为两个32位字段,低32位部分后跟高32位部分。

4.1.3.1 驱动程序要求:PCI设备布局

对于设备配置访问,驱动程序必须对于8位宽字段使用8位宽访问,对于16位宽字段使用16位宽和对齐访问,对于32位和64位宽字段使用32位宽和对齐访问。对于64位字段,驱动程序可以独立访问字段的高32位和低32位部分。

4.1.3.2 设备要求:PCI设备布局

对于64位设备配置字段,设备必须允许对字段的高32位和低32位部分进行驱动程序独立访问。

4.1.4 Virtio结构PCI功能

virtio设备配置布局包括多个结构:

  • 通用配置
  • 通知
  • ISR状态
  • 设备特定配置(可选)
  • PCI配置访问
    每个结构可以由函数所属的基地址寄存器(BAR)映射,或通过PCI配置空间中的特殊VIRTIO_PCI_CAP_PCI_CFG字段访问。

每个结构的位置都使用位于设备PCI配置空间中的能力列表上的供应商特定的PCI功能来指定。这个virtio结构能力使用小端字节顺序;除非另有说明,所有字段对于驱动程序来说都是只读的:

struct virtio_pci_cap {
    u8 cap_vndr;    /* 通用 PCI 字段:PCI_CAP_ID_VNDR */
    u8 cap_next;    /* 通用 PCI 字段:下一个指针。 */
    u8 cap_len;     /* 通用 PCI 字段:能力长度 */
    u8 cfg_type;    /* 识别结构类型。 */
    u8 bar;         /* 在哪里找到它。 */
    u8 padding[3];  /* 填充到完整的双字。 */
    le32 offset;    /* 在 BAR 中的偏移量。 */
    le32 length;    /* 结构的长度,以字节为单位。 */
};

根据下面的文档,此结构可以后跟额外的数据,取决于cfg_type。
这些字段的解释如下:

  • cap_vndr:0x09;标识供应商特定的能力。
  • cap_next:链接到PCI配置空间中能力列表中的下一个能力。
  • cap_len:包括struct virtio_pci_cap的整个长度以及任何额外数据的此能力结构的长度。此长度可能包括填充或驱动程序未使用的字段。
  • cfg_type:根据以下表格标识结构:
/* 通用配置(Common configuration) */
#define VIRTIO_PCI_CAP_COMMON_CFG 1

/* 通知(Notifications) */
#define VIRTIO_PCI_CAP_NOTIFY_CFG 2

/* ISR 状态(Interrupt Status Register Status) */
#define VIRTIO_PCI_CAP_ISR_CFG 3

/* 设备特定配置(Device specific configuration) */
#define VIRTIO_PCI_CAP_DEVICE_CFG 4

/* PCI 配置访问(PCI configuration access) */
#define VIRTIO_PCI_CAP_PCI_CFG 5

任何其他值均保留供将来使用。
每个结构都将在下面单独详细说明。

设备可以提供多个相同类型的结构 - 这使设备可以向驱动程序公开多个接口。能力列表中的能力顺序指定了设备建议的首选顺序。

注意:例如,在某些虚拟化环境中,使用IO访问进行通知比内存访问更快。在这种情况下,设备将公开两个cfg_type设置为VIRTIO_PCI_CAP_NOTIFY_CFG的能力:第一个地址位于I/O BAR,第二个地址位于内存BAR。在此示例中,如果有I/O资源可用,驱动程序将使用I/O BAR,如果没有I/O资源可用,将回退到内存BAR。
bar值0x0到0x5指定了一个位于PCI配置空间中从10h开始的函数所属的基址寄存器(BAR),用于将结构映射到内存或I/O空间。BAR可以是32位或64位,可以映射到内存空间或I/O空间。
任何其他值均保留供将来使用。
offset表示结构相对于与BAR关联的基址的起始位置。offset的对齐要求在下面的每个结构特定部分中指示。
length表示结构的长度。
长度可以包括填充、驱动程序未使用的字段或未来的扩展。注意:例如,将来的设备可能会呈现数兆字节的大型结构大小。由于当前设备从不使用大于4K字节的结构,驱动程序可以将映射的结构大小限制为4K字节(因此忽略了第一个4K字节之后的结构部分),以实现与这种设备的前向兼容性,而不会损失功能并且不会浪费资源。

4.1.4.1 驱动程序要求:Virtio结构PCI功能
  • 驱动程序必须忽略具有保留的cfg_type值的任何供应商特定的功能结构。
  • 驱动程序应该使用它们可以支持的每个virtio结构类型的第一个实例。
  • 驱动程序必须接受比此处规定的cap_len值更大的值。
  • 驱动程序必须忽略任何具有保留的bar值的供应商特定功能结构。
  • 驱动程序应该只映射足够大以用于设备操作的配置结构的一部分。驱动程序必须处理意外地大的长度,但可以检查长度是否足够大以用于设备操作。
  • 驱动程序不能写入能力结构的任何字段,除了具有cap_type VIRTIO_PCI_CAP_PCI_CFG的字段,如4.1.4.7.2中所述。
4.1.4.2 设备要求:Virtio结构PCI功能
  • 设备必须在cap_len中包括任何额外的数据(从cap_vndr字段的开头到任何额外数据字段的结尾,如果有的话)。设备可以在任何结构之后追加额外的数据或填充。
  • 如果设备呈现了相同类型的多个结构,则应该将它们从最佳(第一个)到次佳(最后)进行排序。
4.1.4.3 通用配置结构布局

通用配置结构位于VIRTIO_PCI_CAP_COMMON_CFG功能内的bar和offset处;其布局如下。

struct virtio_pci_common_cfg {
    /* 关于整个设备的信息。 */
    le32 device_feature_select;  /* 读写 */
    le32 device_feature;         /* 仅驱动程序可读 */
    le32 driver_feature_select;  /* 读写 */
    le32 driver_feature;         /* 读写 */
    le16 msix_config;            /* 读写 */
    le16 num_queues;             /* 仅驱动程序可读 */
    u8 device_status;            /* 读写 */
    u8 config_generation;        /* 仅驱动程序可读 */

    /* 关于特定 virtqueue 的信息。 */
    le16 queue_select;           /* 读写 */
    le16 queue_size;             /* 读写 */
    le16 queue_msix_vector;      /* 读写 */
    le16 queue_enable;           /* 读写 */
    le16 queue_notify_off;       /* 仅驱动程序可读 */
    le64 queue_desc;             /* 读写 */
    le64 queue_driver;           /* 读写 */
    le64 queue_device;           /* 读写 */
};

device_feature_select 驱动程序使用此字段来选择要显示的设备功能位。值0x0选择功能位0到31,0x1选择功能位32到63,以此类推。

device_feature 设备使用此字段来报告其提供给驱动程序的功能位:驱动程序将写入device_feature_select以选择要显示的功能位。

driver_feature_select 驱动程序使用此字段来选择要显示的驱动程序功能位。值0x0选择功能位0到31,0x1选择功能位32到63,以此类推。

driver_feature 驱动程序将此字段写入以接受设备提供的功能位。由driver_feature_select选择的驱动程序功能位。

config_msix_vector 驱动程序设置用于MSI-X的配置向量。

num_queues 设备在此处指定支持的最大virtqueue数量。

device_status 驱动程序在此处写入设备状态(参见2.1)。将0写入此字段会重置设备。

config_generation 配置的原子值。设备每当配置显着更改时都会更改此值。

queue_select 队列选择。驱动程序选择下面字段所指的virtqueue。

queue_size 队列大小。在重置时,指定设备支持的最大队列大小。驱动程序可以修改此值以减小内存要求。值为0表示队列不可用。

queue_msix_vector 驱动程序使用此字段指定用于MSI-X的队列向量。

queue_enable 驱动程序使用此字段有选择地阻止设备执行来自此virtqueue的请求。1 - 启用;0 - 禁用。

queue_notify_off 驱动程序读取此字段以计算位于通知结构开头的偏移量,表示此virtqueue的位置。
注意:这不是字节偏移量。请参阅下面的4.1.4.4。

queue_desc 驱动程序在此处写入描述符区域的物理地址。请参阅第2.5节。

queue_driver 驱动程序在此处写入驱动程序区域的物理地址。请参阅第2.5节。

queue_device 驱动程序在此处写入设备区域的物理地址。请参阅第2.5节。

4.1.4.3.1 设备要求:通用配置结构布局
  • 偏移必须是4字节对齐的。
  • 设备必须至少提供一个通用配置能力。
  • 设备必须在device_feature中以device_feature_select写入的任何值的device_feature_select位开始提供其提供的功能位。这意味着对于驱动程序写入的任何device_feature_select,除0或1外,它将提供0,因为这里没有定义的功能超过63。
  • 设备必须在driver_feature中提供由驱动程序写入的任何有效功能位,这些功能位从由驱动程序写入的driver_feature_select位开始的driver_feature_select位。有效功能位是设备功能位的子集。设备可以提供驱动程序编写的无效位。
  • 设备必须在驱动程序读取自设备特定配置后更改config_generation,该配置自上次读取设备特定配置的任何部分以来已更改。
  • 当将0写入device_status时,设备必须进行复位,并在完成后在device_status中呈现0。
  • 设备必须在重置时在queue_enable中呈现0。如果与当前queue_select对应的virtqueue不可用,则设备必须在queue_size中呈现0。
  • 如果没有协商VIRTIO_F_RING_PACKED,设备必须在queue_size中呈现值0或2的幂。
4.1.4.3.2 驱动程序要求:通用配置结构布局
  • 驱动程序不得写入device_feature、num_queues、config_generation或queue_notify_off。
  • 如果已经协商了VIRTIO_F_RING_PACKED,则驱动程序不得将值0写入queue_size。
  • 如果没有协商VIRTIO_F_RING_PACKED,则驱动程序不得写入不是2的幂的值到queue_size。
  • 驱动程序必须在启用queue_enable之前配置其他virtqueue字段。
  • 在将0写入device_status后,驱动程序必须等待读取device_status返回0,然后才能重新初始化设备。
  • 驱动程序不得将0写入queue_enable。
4.1.4.4 通知结构布局

通知位置是通过VIRTIO_PCI_CAP_NOTIFY_CFG能力找到的。该能力后面紧跟一个额外的字段,如下所示:

struct virtio_pci_notify_cap {
    struct virtio_pci_cap cap;          /* PCI 能力描述 */
    le32 notify_off_multiplier;         /* queue_notify_off 的倍数 */
};

notify_off_multiplier与queue_notify_off结合在一起,用于计算在一个BAR内的队列通知地址。具体计算方式为:

cap.offset + queue_notify_off * notify_off_multiplier

cap.offsetnotify_off_multiplier来自上面的通知能力结构,而queue_notify_off来自通用配置结构。
请注意,例如,如果notify_off_multiplier为0,则设备对所有队列使用相同的队列通知地址。

4.1.4.4.1 设备要求:通知能力
  • 设备必须提供至少一个通知能力。
  • 对于不提供 VIRTIO_F_NOTIFICATION_DATA 的设备:
    • cap.offset 必须是2字节对齐的。
    • 设备必须要么提供 notify_off_multiplier 作为一个偶数的2次方,要么将 notify_off_multiplier 设置为0。
    • 设备呈现的 cap.length 值必须至少为2,并且必须足够大,以支持所有可能配置中所有支持的队列的通知偏移量。
  • 对于所有队列,设备呈现的 cap.length 值必须满足:
cap.length >= queue_notify_off * notify_off_multiplier + 2

对于提供 VIRTIO_F_NOTIFICATION_DATA 的设备:

  • 设备必须要么提供 notify_off_multiplier 作为2的幂次方且是4的倍数的数字,要么将 notify_off_multiplier 设置为0。
  • cap.offset 必须是4字节对齐的。
  • 设备呈现的 cap.length 值必须至少为4,并且必须足够大,以支持所有可能配置中所有支持的队列的通知偏移量。
  • 对于所有队列,设备呈现的 cap.length 值必须满足:
cap.length >= queue_notify_off * notify_off_multiplier + 4

ISR 状态能力
VIRTIO_PCI_CAP_ISR_CFG 能力至少指的是一个字节,其中包含用于 INT#x 中断处理的8位 ISR 状态字段。
ISR 状态的偏移量没有对齐要求。
ISR 位允许设备区分设备特定配置更改中断和正常的 virtqueue 中断:

在这里插入图片描述
为了避免额外的访问,简单地读取此寄存器将其重置为0并导致设备取消断言中断。
这种方式,驱动程序读取 ISR 状态会导致设备取消断言中断。
有关此如何使用的详细信息,请参阅第4.1.5.3和4.1.5.4节

4.1.4.5 ISR 状态能力
  • 设备必须至少提供一个 VIRTIO_PCI_CAP_ISR_CFG 能力。
  • 在向驱动程序发送设备配置更改通知之前,设备必须在 ISR 状态中设置 Device Configuration Interrupt 位。
  • 如果 MSI-X 能力被禁用,设备必须在向驱动程序发送 virtqueue 通知之前,在 ISR 状态中设置 Queue Interrupt 位。
  • 如果 MSI-X 能力被禁用,设备必须将 ISR 状态的所有位的逻辑 OR 设置为设备的 PCI 配置头的 PCI Status 寄存器中的 Interrupt Status 位。然后,设备根据标准 PCI 规则 [PCI] 断言/取消断言 INT#x 中断,除非按照标准 PCI 规则被屏蔽。
  • 设备必须在驱动程序读取时将 ISR 状态重置为 0。
4.1.4.5.2 驱动程序要求:ISR 状态能力
  • 如果启用了 MSI-X 能力,则在检测到队列中断时,驱动程序不应访问 ISR 状态。
4.1.4.6 设备特定配置
  • 对于具有设备特定配置的任何设备类型,设备必须至少提供一个 VIRTIO_PCI_CAP_DEVICE_CFG 能力。
4.1.4.6.1 设备要求:设备特定配置
  • 设备特定配置的偏移必须是 4 字节对齐的。
4.1.4.7 PCI 配置访问能力
  • VIRTIO_PCI_CAP_PCI_CFG 能力创建了一个替代的(可能是次优的)访问方法,用于常规配置、通知、ISR 和设备特定配置区域。
  • 能力紧接着后面是一个附加字段,如下所示:
struct virtio_pci_cfg_cap {
    struct virtio_pci_cap cap;  /* PCI 能力描述 */
    u8 pci_cfg_data[4];         /* 用于 BAR 访问的数据 */
};

  • 对于驱动程序,cap.bar、cap.length、cap.offset 和 pci_cfg_data 字段是读写(RW)的。
  • 为了访问设备区域,驱动程序按照以下方式写入能力结构(即在 PCI 配置空间内):
    • 驱动程序通过写入 cap.bar 来设置要访问的 BAR。
    • 驱动程序通过写入 1、2 或 4 来设置访问的大小到 cap.length。
    • 驱动程序通过写入 cap.offset 来设置在 BAR 内的偏移量。
  • 在这一点上,pci_cfg_data 将提供一个大小为 cap.length 的窗口,位于给定的 cap.bar 内,偏移为 cap.offset。
4.1.4.7.1 设备要求:PCI 配置访问能力
  • 设备必须至少提供一个 VIRTIO_PCI_CAP_PCI_CFG 能力。
  • 在检测到驱动程序对 pci_cfg_data 的写入访问时,设备必须使用来自 pci_cfg_data 的前 cap.length 字节,在 cap.bar 选择的偏移 cap.offset 处执行写访问。
  • 在检测到驱动程序对 pci_cfg_data 的读取访问时,设备必须在 cap.bar 选择的偏移 cap.offset 处执行长度为 cap.length 的读取访问,并将前 cap.length 字节存储在 pci_cfg_data 中。
4.1.4.7.2 驱动程序要求:PCI 配置访问能力
  • 驱动程序不能写入不是 cap.length 的倍数的 cap.offset(即,所有访问必须对齐)。
  • 驱动程序不能读取或写入 pci_cfg_data,除非 cap.bar、cap.length 和 cap.offset 地址是由类型为 VIRTIO_PCI_CAP_PCI_CFG 的其他 Virtio 结构 PCI 能力指定的某个 BAR 范围内的 cap.length 字节。
4.1.4.8 传统接口:关于 PCI 设备布局的说明
  • 过渡设备必须在 PCI 设备的第一个 I/O 区域的 BAR0 中呈现部分配置寄存器,如下所述。在使用传统接口时,过渡驱动程序必须使用在 PCI 设备的第一个 I/O 区域的 BAR0 中的传统配置结构,如下所述。
  • 当使用传统接口时,驱动程序可以使用任何宽度的访问方式来访问设备特定的配置区域,并且在访问时,过渡设备必须为驱动程序提供与使用“自然”访问方法(例如,32 位字段的 32 位访问等)时相同的结果。请注意,这是可能的,因为虚拟共享配置结构是 PCI(即,小端)字节序,而在使用传统接口时,设备特定的配置区域是以客户机的本机字节序编码的(在适用的情况下存在此区别)。
  • 在通过传统接口使用时,虚拟共享配置结构如下所示:
    第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第3张图片
    如果设备启用了 MSI-X,那么在这个头部之后会立即跟随两个附加字段:
    在这里插入图片描述
    注意:启用 MSI-X 功能时,设备特定配置从 virtio 通用配置结构的字节偏移 24 开始。当未启用 MSI-X 功能时,设备特定配置从 virtio 标头的字节偏移 20 开始。也就是说,一旦你在设备上启用了 MSI-X,其他字段就会移动。如果再次关闭它,它们将恢复原状!任何设备特定的配置空间都紧随在这些通用标头之后:
    在这里插入图片描述
    在使用传统接口访问设备特定配置空间时,过渡驱动程序必须在通用标头之后立即访问设备特定配置空间的偏移量。
    在使用传统接口时,过渡设备必须在通用标头之后立即提供任何设备特定的配置空间。
    请注意,只有特性位 0 到 31 可以通过传统接口访问。在通过传统接口使用时,过渡设备必须假定特性位 32 到 63 不会被驱动程序承认。
    由于传统设备没有config_generation字段,请参阅2.4.4 传统接口:设备配置空间中的解决方法。
4.1.4.9 PCI特定的初始化和设备操作
4.1.4.9.0.1 非过渡设备与旧驱动程序:有关PCI设备布局的说明

已知的所有旧驱动程序都会检查PCI版本或设备和供应商ID,因此不会尝试驱动非过渡设备。

有缺陷的旧驱动程序可能会错误地尝试驱动非过渡设备。如果需要支持这样的驱动程序(而不是修复错误),则以下是检测和处理它们的推荐方式。

注意:目前未知是否在生产中使用此类有缺陷的驱动程序。

4.1.4.9.0.1 设备要求:非过渡设备与旧驱动程序

在已知以前存在具有相同ID(包括PCI版本、设备和供应商ID)的旧设备的平台上,非过渡设备应该采取以下步骤,以使旧驱动程序在尝试驱动它们时能够优雅地失败:

  1. 提供一个I/O BAR(基地址寄存器)。
  2. 响应对于在BAR0(基地址寄存器0)的偏移量18处执行的单字节零写入操作,即设备状态寄存器(legacy布局中的寄存器),通过在所有BAR上提供零并忽略写入操作。

4.1.5 PCI特定的初始化和设备操作

4.1.5.1 设备初始化

本部分记录了设备初始化期间执行的特定于PCI的步骤。

4.1.5.1.1 检测Virtio设备配置布局

在设备初始化之前,驱动程序会扫描PCI能力列表,以检测Virtio设备配置布局,具体步骤请参见4.1.4。
####### 4.1.5.1.1.1 遗留接口:有关设备布局检测的说明
遗留驱动程序跳过了设备布局检测步骤,无条件地假设在I/O空间的BAR0中存在遗留设备配置空间。
遗留设备的能力列表中没有Virtio PCI能力。
因此:

  • 过渡设备必须在I/O空间的BAR0中公开遗留接口。
  • 过渡驱动程序必须查找能力列表上的Virtio PCI能力。如果这些能力不存在,则驱动程序必须假设为遗留设备,并通过遗留接口使用。
  • 非过渡驱动程序必须查找能力列表上的Virtio PCI能力。如果这些能力不存在,则驱动程序必须假设为遗留设备,并进行优雅的错误处理。
4.1.5.1.2 MSI-X向量配置

如果设备启用并存在MSI-X能力(通过标准PCI配置空间),则使用config_msix_vector和queue_msix_vector将配置更改和队列中断映射到MSI-X向量。在这种情况下,ISR状态未使用。
通过向config_msix_vector/queue_msix_vector写入有效的MSI-X表条目编号,从0到0x7FF,可以将由配置更改/所选队列事件触发的中断分别映射到相应的MSI-X向量。要禁用事件类型的中断,驱动程序可以通过写入特殊的NO_VECTOR值来取消映射此事件:

/* 用于禁用队列的 MSI(Message Signaled Interrupts) 的向量值 */
#define VIRTIO_MSI_NO_VECTOR 0xffff

请注意,将事件映射到向量可能需要设备分配内部设备资源,因此可能失败。

4.1.5.1.2.1 设备要求:MSI-X向量配置

具有MSI-X功能的设备应至少支持2个,最多支持0x800个MSI-X向量。设备必须在MSI-X功能中的Table Size字段中报告支持的向量数,如[PCI]所规定。设备应该将报告的MSI-X Table Size字段限制为可能有益于系统性能的值。
注意:例如,不希望以高速率发送中断的设备可能只指定2个MSI-X向量。
设备必须支持将任何事件类型映射到任何有效的向量,从0到MSI-X Table Size。设备必须支持取消映射任何事件类型。
设备在读取config_msix_vector/queue_msix_vector时必须返回与给定事件映射的向量(如果未映射,则返回NO_VECTOR)。设备在复位时必须取消映射所有队列和配置更改事件。
设备不应导致事件映射到向量失败,除非设备无法满足映射请求。设备必须在读取相关的config_msix_vector/queue_msix_vector字段时通过返回NO_VECTOR值来报告映射失败。

4.1.5.1.2.2 驱动程序要求:MSI-X向量配置

驱动程序必须支持具有0到0x7FF的任何MSI-X Table Size的设备。对于仅支持一个MSI-X向量(MSI-X Table Size = 0)的设备,驱动程序可以回退使用INT#x中断。
驱动程序可以将Table Size视为设备建议使用的MSI-X向量数量的提示。
驱动程序不能尝试将事件映射到设备支持的MSI-X Table Size之外的向量,如MSI-X能力中的Table Size所报告的。
在将事件映射到向量之后,驱动程序必须通过读取Vector字段值来验证成功:成功时,返回先前写入的值,失败时返回NO_VECTOR。如果检测到映射失败,驱动程序可以尝试减少向量映射,禁用MSI-X或报告设备故障。

4.1.5.1.3 Virtqueue配置

由于设备可以具有用于大容量数据传输的零个或多个虚拟队列,因此驱动程序需要在设备特定配置的一部分来配置它们。
驱动程序通常会按照以下步骤为设备的每个虚拟队列执行此操作:

  1. 将虚拟队列索引(第一个队列为0)写入queue_select。
  2. 从queue_size中读取虚拟队列的大小。这控制虚拟队列的大小(请参阅2.5虚拟队列)。如果此字段为0,则虚拟队列不存在。
  3. 可选地,选择较小的虚拟队列大小,并将其写入queue_size。
  4. 在连续的物理内存中为虚拟队列分配并清零描述符表、可用和已使用环。
  5. 可选地,如果设备启用并存在MSI-X功能,则选择要用于请求由虚拟队列事件触发的中断的向量。将与此向量对应的MSI-X表条目编号写入queue_msix_vector。读取queue_msix_vector:成功时,返回先前写入的值;失败时,返回NO_VECTOR值。
4.1.5.1.3.1 遗留接口:有关虚拟队列配置的说明

在使用遗留接口时,队列布局遵循2.6.2中的Legacy Interfaces:关于虚拟队列布局的说明,对齐为4096。驱动程序将物理地址除以4096并写入Queue Address字段2。没有协商队列大小的机制。

4.1.5.2 可用缓冲区通知

当未经协商使用VIRTIO_F_NOTIFICATION_DATA时,驱动程序通过将此虚拟队列的16位索引写入Queue Notify地址来向设备发送可用缓冲区通知。
当协商了VIRTIO_F_NOTIFICATION_DATA时,驱动程序通过将以下32位值写入Queue Notify地址来向设备发送可用缓冲区通知:

le32 {
vqn : 16;
next_off : 15;
next_wrap : 1;
};
4.1.5.3 已使用缓冲区通知

如果需要用于虚拟队列的已使用缓冲区通知,设备通常会执行以下操作:
• 如果禁用了MSI-X能力:

  1. 设置设备的ISR状态字段的最低位。
  2. 发送设备的适当PCI中断。
    • 如果启用了MSI-X能力:
  3. 如果queue_msix_vector不是NO_VECTOR,则请求设备的适当MSI-X中断消息,queue_msix_vector设置MSI-X表项号。
4.1.5.3.1 设备要求:已使用缓冲区通知

如果对于虚拟队列启用了MSI-X能力,并且queue_msix_vector为NO_VECTOR,则设备不得传递该虚拟队列的中断。

4.1.5.4 设备配置更改的通知

某些virtio PCI设备可以更改设备配置状态,反映在设备的特定于设备的配置区域中。在这种情况下:
• 如果禁用了MSI-X能力:

  1. 设置设备的ISR状态字段的第二个较低位。
  2. 发送设备的适当PCI中断。
    • 如果启用了MSI-X能力:
  3. 如果config_msix_vector不是NO_VECTOR,则请求设备的适当MSI-X中断消息,config_msix_vector设置MSI-X表项号。
    一个中断可以表示一个或多个虚拟队列已被使用以及配置空间已更改。
4.1.5.4.1 设备要求:设备配置更改的通知

如果对于虚拟队列启用了MSI-X能力,并且config_msix_vector为NO_VECTOR,则设备不得传递有关设备配置空间更改的中断。

4.1.5.4.2 驱动程序要求:设备配置更改的通知

驱动程序必须处理同一个中断用于指示设备配置空间更改和一个或多个虚拟队列已被使用的情况。

4.1.5.5 驱动程序处理中断

驱动程序中断处理程序通常会执行以下操作:
• 如果禁用了MSI-X能力:

  • 读取ISR状态字段,该操作将其重置为零。
  • 如果设置了最低位:查看设备的所有虚拟队列,以查看是否需要服务设备所做的任何进展。
  • 如果设置了第二个较低位:重新检查配置空间,以查看发生了什么变化。
    • 如果启用了MSI-X能力:
  • 查看映射到设备的该MSI-X向量的所有虚拟队列,以查看是否需要服务设备所做的任何进展。
  • 如果MSI-X向量等于config_msix_vector,则重新检查配置空间,以查看发生了什么变化。

4.2 基于MMIO的Virtio

没有PCI支持的虚拟环境(在嵌入式设备模型中很常见)可能会使用简单的内存映射设备(“virtio-mmio”)而不是PCI设备。
内存映射的virtio设备行为基于PCI设备规范。因此,包括设备初始化、队列配置和缓冲区传输在内的大多数操作几乎相同。现有的差异在以下各节中描述。

4.2.1 MMIO设备发现

与PCI不同,MMIO没有通用的设备发现机制。对于每个设备,客户操作系统需要知道寄存器和使用的中断位置。使用平坦设备树的系统建议绑定如下所示的示例:

// 示例:virtio_block 设备,占用 512 字节的内存地址为 0x1e000,使用中断号 42。
virtio_block@1e000 {
    compatible = "virtio,mmio";  // 设备兼容性标识,指定这是一个 Virtio 设备,并使用 MMIO 接口。
    reg = <0x1e000 0x200>;       // 设备在内存中的地址范围,从 0x1e000 开始,占用 0x200 字节的空间。
    interrupts = <42>;           // 中断号,这个设备使用中断号 42。
}

4.2.2 MMIO设备寄存器布局

MMIO virtio设备提供了一组内存映射控制寄存器,后面跟着一个设备特定的配置空间,如表4.1所描述。

所有寄存器值都以小端序(Little Endian)组织。
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第4张图片
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第5张图片
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第6张图片

4.2.2.1 设备要求: MMIO 设备寄存器布局
  • 设备必须在 MagicValue 中返回 0x74726976。
  • 设备必须在 Version 中返回值 0x2。
  • 设备必须通过在事件发生时设置 InterruptStatus 中的相应位来呈现每个事件,直到驱动程序通过向 InterruptACK 寄存器写入相应的位掩码来确认中断。不表示已发生事件的位必须为零。
  • 在重置时,设备必须清除所有队列中的 InterruptStatus 中的所有位以及 QueueReady 寄存器中的准备位。
  • 设备必须在存在驱动程序看到不一致的配置状态的风险时更改 ConfigGeneration 中返回的值。
  • 当 QueueReady 为零(0x0)时,设备不得访问虚拟队列内容。
4.2.2.2 驱动程序要求: MMIO 设备寄存器布局
  • 驱动程序不得访问未在表4.1中描述的内存位置(或在配置空间的情况下,在设备规范中描述),不得写入只读寄存器(方向R)并且不得从只写寄存器(方向W)中读取。
  • 驱动程序只能使用32位宽度和对齐的读取和写入来访问表4.1中描述的控制寄存器。对于设备特定的配置空间,驱动程序必须对于8位宽度字段使用8位宽度的访问,对于16位宽度字段使用16位宽度和对齐的访问,对于32位和64位宽度字段使用32位宽度和对齐的访问。
  • 驱动程序必须忽略MagicValue不是0x74726976的设备,尽管它可以报告错误。
  • 驱动程序必须忽略Version不是0x2的设备,尽管它可以报告错误。
  • 驱动程序必须忽略DeviceID为0x0的设备,但不得报告任何错误。
  • 在从DeviceFeatures读取之前,驱动程序必须向DeviceFeaturesSel写入一个值。
  • 在写入DriverFeatures寄存器之前,驱动程序必须向DriverFeaturesSel寄存器写入一个值。
  • 驱动程序必须写入一个小于或等于设备在QueueNumMax中呈现的值的QueueNum值。
  • 当QueueReady不为零时,驱动程序不得访问QueueNum、QueueDescLow、QueueDescHigh、QueueAvailLow、QueueAvailHigh、QueueUsedLow、QueueUsedHigh。
  • 为了停止使用队列,驱动程序必须将零(0x0)写入QueueReady,并且必须读取该值以确保同步。
  • 驱动程序必须忽略InterruptStatus中的未定义位。
  • 当驱动程序完成中断处理并且不得设置值中的任何未定义位时,驱动程序必须写入具有描述其处理的事件的位掩码的值到InterruptACK。
le32 {
vqn : 16;
next_off : 15;
next_wrap : 1;
};
4.2.3.4 Notifications From The Device

内存映射 virtio 设备使用一个单独的、专用的中断信号,当至少有一个在 InterruptStatus 描述中的位被设置时,此信号会被激活。这是设备向驱动发送已使用缓冲区通知或配置更改通知的方式。

4.2.3.4.1 驱动程序要求:来自设备的通知

在接收到中断后,驱动程序必须读取 InterruptStatus,以检查引起中断的原因(请查看寄存器描述)。已使用缓冲区通知位的设置被解释为对每个活动 virtqueue 的已使用缓冲区通知。在处理完中断后,驱动程序必须通过将与已处理事件对应的位掩码写入 InterruptACK 寄存器来确认中断。

4.2.4 传统接口

传统的 MMIO 传输使用基于页面的寻址,从而导致了略微不同的控制寄存器布局、设备初始化和虚拟队列配置过程。表格 4.2 呈现了控制寄存器布局,省略了未改变其功能或行为的寄存器的描述。
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第7张图片
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第8张图片
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第9张图片
虚拟队列页面大小由来宾写入的GuestPageSize定义。驱动程序在配置虚拟队列之前执行此操作。

虚拟队列布局遵循第2.6.2节"Legacy Interfaces: A Note on Virtqueue Layout"中定义的布局,并且遵循QueueAlign中定义的对齐方式。

虚拟队列的配置如下:

  1. 选择队列,将其索引(第一个队列为0)写入QueueSel。
  2. 检查队列是否已被使用:读取QueuePFN,期望返回值为零(0x0)。
  3. 从QueueNumMax读取最大队列大小(元素数量)。如果返回值为零(0x0),则表示队列不可用。
  4. 在连续的虚拟内存中分配并清零队列页面,将已使用的环(通常是页面大小)对齐。驱动程序应选择小于或等于QueueNumMax的队列大小。
  5. 通过将大小写入QueueNum来通知设备有关队列大小的信息。
  6. 通过将其以字节为单位的值写入QueueAlign来通知设备已使用的对齐方式。
  7. 将队列的第一页的物理编号写入QueuePFN寄存器。

通知机制没有更改。

4.3 基于通道I/O的Virtio

基于S/390的虚拟机不支持PCI也不支持MMIO,因此需要使用不同的传输方式。virtio-ccw使用了基于标准通道I/O的机制,这是S/390上大多数设备使用的方式。一台具有特殊控制单元类型(0x3832)的通道附加I/O控制单元充当virtio设备的代理(类似于virtio-pci使用PCI设备的方式),并且virtio设备的配置和操作通过通道命令(主要)完成。这意味着virtio设备可以通过标准操作系统算法进行发现,并且添加virtio支持主要是支持新的控制单元类型的问题。

由于S/390是大端字节序的机器,通过通道命令传输的数据结构是大端字节序的:这通过使用be16、be32和be64类型明确表示。

4.3.1 基本概念

作为代理设备,virtio-ccw使用具有特殊控制单元类型(0x3832)的通道附加I/O控制单元,以及与附加virtio设备的子系统设备ID相对应的控制单元模型,通过虚拟I/O子通道和类型为0x32的虚拟通道路径访问。这个代理设备可以通过正常的通道子系统设备发现(通常是STORE SUBCHANNEL循环)进行发现,并且会回应基本的通道命令:

  • 无操作(0x03)
  • 基本感知(0x04)
  • 传输到通道(0x08)
  • 感知ID(0xe4)

对于virtio-ccw代理设备,感知ID将返回以下信息:
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第10张图片
对于 virtio-ccw 代理设备,SENSE ID 命令返回以下信息:

virtio-ccw 代理设备具有几个重要功能:

  • 发现和连接 virtio 设备(如上所述)。
  • 初始化 virtqueue 和特定于传输的功能(使用 virtio 特定的通道命令完成)。
  • 处理通知(通过超级调用、I/O 中断和指示位的组合实现)。
4.3.1.1 Virtio 的通道命令

除了基本的通道命令外,virtio-ccw 还引入了一组专门用于配置和操作 virtio 的通道命令:

#define CCW_CMD_SET_VQ 0x13               // 设置虚拟队列 (Set Virtual Queue)
#define CCW_CMD_VDEV_RESET 0x33           // 重置虚拟设备 (Virtual Device Reset)
#define CCW_CMD_SET_IND 0x43              // 设置间接访问模式 (Set Indirect)
#define CCW_CMD_SET_CONF_IND 0x53         // 设置配置和间接访问模式 (Set Configuration and Indirect)
#define CCW_CMD_SET_IND_ADAPTER 0x73      // 设置间接访问适配器 (Set Indirect Adapter)
#define CCW_CMD_READ_FEAT 0x12            // 读取特性 (Read Feature)
#define CCW_CMD_WRITE_FEAT 0x11           // 写入特性 (Write Feature)
#define CCW_CMD_READ_CONF 0x22            // 读取配置 (Read Configuration)
#define CCW_CMD_WRITE_CONF 0x21           // 写入配置 (Write Configuration)
#define CCW_CMD_WRITE_STATUS 0x31         // 写入状态 (Write Status)
#define CCW_CMD_READ_VQ_CONF 0x32         // 读取虚拟队列配置 (Read Virtual Queue Configuration)
#define CCW_CMD_SET_VIRTIO_REV 0x83       // 设置 Virtio 协议版本 (Set Virtio Revision)
#define CCW_CMD_READ_STATUS 0x72          // 读取状态 (Read Status)

4.3.1.2 通知

可用缓冲区通知通过超级调用来实现,驱动程序不需要进行任何额外的设置。可用缓冲区通知的操作在第4.3.3.2节中有描述。
已用缓冲区通知根据传输级别的协商,可以实现为所谓的经典方式或适配器I/O中断。初始化分别在第4.3.2.6.1和第4.3.2.6.3节中描述。每种方式的操作分别在第4.3.3.1.1和第4.3.3.1.2节中有描述。
配置更改通知使用所谓的经典I/O中断来完成。初始化在第4.3.2.6.2节中描述,操作在第4.3.3.1.1节中有描述。

4.3.1.3 设备要求:基本概念

virtio-ccw 设备的行为类似于普通通道设备,如[S390 PoP]和[S390 Common I/O]中所规定。具体而言:

  • 设备必须为不支持的任何命令发出命令拒绝的单元检查。
  • 如果驱动程序没有抑制通道命令的长度检查,则当实际长度与预期长度不匹配时,设备必须按照体系结构中的详细信息提供子通道状态。
  • 如果驱动程序抑制了通道命令的长度检查,则如果传输的数据不包含足够的数据来处理命令,设备必须提供检查条件。如果驱动程序提交了一个太长的缓冲区,设备应该接受该命令。
4.3.1.4 驱动程序要求:基本概念

用于 virtio-ccw 设备的驱动程序必须检查控制单元类型是否为0x3832,并且必须忽略设备类型和型号。
即使驱动程序抑制了该命令的长度检查,它也应该尝试在通道命令中提供正确的长度。

4.3.2 设备初始化

virtio-ccw 使用多个通道命令来设置设备。

4.3.2.1 设置 Virtio 版本

CCW_CMD_SET_VIRTIO_REV由驱动程序发出,以设置它打算使用的 virtio-ccw 传输的版本。它使用以下通信结构:

struct virtio_rev_info {
    be16 revision;   // 协议版本号(Big-endian 16位整数)
    be16 length;     // 数据长度(Big-endian 16位整数)
    u8 data[];       // 版本数据(可变长度字节数组)
};

revision 包含所期望的修订版本 ID,length 包含数据部分的长度以及修订版本相关的其他所需选项。
支持以下数值:
第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第11张图片
请注意,virtio 标准的更改不一定对应于 virtio-ccw 的修订版本更改。

4.3.2.1.1 设备要求:设置 Virtio 修订版本

设备必须对不支持的任何修订版本发布具有命令拒绝的单位检查。对于修订版本、长度和数据的任何无效组合,它也必须发布具有命令拒绝的单位检查。非过渡设备必须拒绝修订版本 ID 0。设备必须对不包含在驱动程序选择的修订版本中的任何 virtio-ccw 特定通道命令回答命令拒绝。
设备必须对在成功选择修订版本后尝试选择不同修订版本的任何尝试都回答命令拒绝。
设备必须将修订版本视为从相关子通道已启用的时间起到驱动程序成功设置之时为止。这意味着修订版本在禁用和启用相关子通道之间不是持久的。

4.3.2.1.2 驱动程序要求:设置 Virtio 修订版本

驱动程序应该从尝试设置其支持的最高修订版本开始,并在收到命令拒绝时继续尝试较低的修订版本。
驱动程序在设置修订版本之前不得发布任何其他 virtio-ccw 特定通道命令。
在驱动程序成功选择修订版本后,它不得尝试选择不同的修订版本。

4.3.2.1.3 传统接口:关于设置 Virtio 修订版本的注意

传统设备不支持 CCW_CMD_SET_VIRTIO_REV 并回答命令拒绝。
非过渡驱动程序必须停止尝试在这种情况下操作该设备。过渡驱动程序必须操作该设备,就好像它能够设置修订版本 0 一样。
传统驱动程序不会在发布其他 virtio-ccw 特定通道命令之前发布 CCW_CMD_SET_VIRTIO_REV。因此,非过渡设备必须回答任何此类尝试,具有命令拒绝。在这种情况下,过渡设备必须假定驱动程序是传统驱动程序,继续进行,就好像驱动程序选择了修订版本 0 一样。这意味着设备必须拒绝任何对修订版本 0 无效的命令,包括随后的 CCW_CMD_SET_VIRTIO_REV。

4.3.2.2 配置 Virtqueue

CCW_CMD_READ_VQ_CONF 由驱动程序发布,以获取有关队列的信息。它使用以下结构进行通信:

struct vq_config_block {
    be16 index;     // 队列索引(Big-endian 16位整数)
    be16 max_num;   // 最大队列数量(Big-endian 16位整数)
};

队列索引的请求的缓冲区数量在 max_num 中返回。
随后,驱动程序发布 CCW_CMD_SET_VQ,通知设备关于用于其队列的位置。传输的结构如下:

struct vq_info_block {
    be64 desc;      // 描述符表的物理地址(Big-endian 64位整数)
    be32 res0;      // 保留字段(Big-endian 32位整数)
    be16 index;     // 队列索引(Big-endian 16位整数)
    be16 num;       // 队列中可用的描述符数量(Big-endian 16位整数)
    be64 driver;    // 驱动程序的物理地址(Big-endian 64位整数)
    be64 device;    // 设备的物理地址(Big-endian 64位整数)
};

desc、driver 和 device 分别包含了队列索引的描述符区域、可用区域和已使用区域的客户端地址。实际的 virtqueue 大小(已分配缓冲区的数量)在 num 中传输。

4.3.2.2.1 设备要求:配置 Virtqueue

res0 保留字段,设备必须忽略它。

4.3.2.2.2 遗留接口:关于配置 Virtqueue 的说明

对于遗留驱动程序或选择了修订版 0 的驱动程序,CCW_CMD_SET_VQ 使用以下通信块:

struct vq_info_block_legacy {
    be64 queue;     // 队列的物理地址(Big-endian 64位整数)
    be32 align;     // 队列的对齐方式(Big-endian 32位整数)
    be16 index;     // 队列索引(Big-endian 16位整数)
    be16 num;       // 队列中可用的描述符数量(Big-endian 16位整数)
};

queue 包含队列索引的客户端地址,num 包含缓冲区数量,align 包含对齐方式。队列布局遵循 2.6.2 遗留接口:有关 Virtqueue 布局的说明。

4.3.2.3 传输状态信息

驱动程序通过 CCW_CMD_WRITE_STATUS 命令更改设备的状态,该命令传输一个 8 位状态值。
如 2.2.2 中所述,设备有时无法设置设备状态字段:例如,在设备初始化期间,它可能无法接受 FEATURES_OK 状态位。
使用修订版 2,定义了 CCW_CMD_READ_STATUS:它从设备读取一个 8 位状态值,并充当 CCW_CMD_WRITE_STATUS 的反向操作。

4.3.2.3.1 驱动程序要求:传输状态信息

如果设备在响应 CCW_CMD_WRITE_STATUS 命令时发布了带有命令拒绝的单元检查,驱动程序必须假定设备未能设置状态,并且设备状态字段保留了其先前的值。
如果至少已经协商了修订版 2,则在检测到配置更改后,驱动程序应该使用 CCW_CMD_READ_STATUS 命令检索设备状态字段。
如果未至少协商了修订版 2,则驱动程序不得尝试发出 CCW_CMD_READ_STATUS 命令。

4.3.2.3.2 设备要求:传输状态信息

如果设备无法将设备状态字段设置为驱动程序写入的值,则设备必须确保设备状态字段保持不变,并且必须发布带有命令拒绝的单元检查。
如果至少已经协商了修订版 2,则如果发出 CCW_CMD_READ_STATUS 命令,则设备必须返回当前的设备状态字段。

4.3.2.4 处理设备特性

特性位按照 32 位值的数组排列,总共有 8192 个特性位。特性位按照小端字节顺序排列。
处理特性的 CCW 命令使用以下通信块:

struct virtio_feature_desc {
    le32 features;  // 特性标志(Little-endian 32位整数)
    u8 index;       // 特性描述的索引号(8位无符号整数)
};

特性是当前访问的特性的 32 位位域,而索引描述要访问的特性位的哪个值。结构末尾不添加填充,长度恰好为 5 字节。
客户端通过 CCW_CMD_READ_FEAT 命令获取设备的设备特性集。设备将特性存储在 index 处的 features 中。
为了向设备传达其支持的特性,驱动程序使用 CCW_CMD_WRITE_FEAT 命令,指示一个特性/index 组合。

4.3.2.5 设备配置

设备的配置空间位于主机内存中。
为了从配置空间获取信息,驱动程序使用 CCW_CMD_READ_CONF,指定设备要写入的客户端内存。
要更改配置信息,驱动程序使用 CCW_CMD_WRITE_CONF,指定设备要读取的客户端内存。
在这两种情况下,都传输完整的配置空间。这允许驱动程序将新配置空间与旧版本进行比较,并在其更改时在内部保留一代计数。

4.3.2.6 设置指示器

为了设置主机->客户端通知的指示位,驱动程序根据是否希望使用绑定到子通道的传统 I/O 中断或用于 virtqueue 通知的适配器 I/O 中断使用不同的通道命令。对于任何给定的设备,这两种机制是互斥的。
对于配置更改指示器,只提供使用传统 I/O 中断的机制,无论 virtqueue 通知使用传统 I/O 中断还是适配器 I/O 中断。

4.3.2.6.1 设置经典队列指示器
通过经典 I/O 中断进行通知的指示器包含每个 virtio-ccw 代理设备的 64 位值。
为了传达主机->客户端通知的指示器位的位置,驱动程序使用 CCW_CMD_SET_IND 命令,指向一个包含指示器的客户端地址的 64 位值的位置。
如果驱动程序已经通过 CCW_CMD_SET_IND_ADAPTER 命令设置了两级队列指示器,则设备必须对任何后续的 CCW_CMD_SET_IND 命令发布带有命令拒绝的单元检查。

4.3.2.6.2 设置配置更改指示器

每个 virtio-ccw 代理设备的配置更改主机->客户端通知的指示器包含一个 64 位值。
为了传达用于配置更改主机->客户端通知的指示器位的位置,驱动程序发出 CCW_CMD_SET_CONF_IND 命令,指向一个包含指示器的客户端地址的位置。

4.3.2.6.3 设置两级队列指示器
适用于适配器 I/O 中断通知的指示器包括两个阶段:
• 概要指示器字节,涵盖一个或多个 virtio-ccw 代理设备的 virtqueue
• 一组连续的 virtio-ccw 代理设备 virtqueue 的指示位
为了传达摘要和队列指示位的位置,驱动程序使用以下有效载荷发出 CCW_CMD_SET_IND_ADAPTER 命令:

struct virtio_thinint_area {
    be64 summary_indicator; // 摘要指示器(Big-endian 64位整数)
    be64 indicator;         // 指示器(Big-endian 64位整数)
    be64 bit_nr;            // 位号(Big-endian 64位整数)
    u8 isc;                 // 中断服务线程编号(8位无符号整数)
} __attribute__ ((packed));

summary_indicator 包含 8 位摘要指示器的客户端地址。indicator 包含一个区域的客户端地址,其中包含设备的指示器,从 bit_nr 开始,每个设备的 virtqueue 一个位。位号从左边开始,即第一个字节中最高有效位分配为位号 0。isc 包含要用于适配器 I/O 中断的 I/O 中断子类。它可以与代理 virtio-ccw 设备子通道使用的 isc 不同。结构末尾不添加填充,长度恰好为 25 字节。

4.3.2.6.3.1 设备要求:设置两级队列指示器

如果驱动程序已经通过 CCW_CMD_SET_IND 命令设置了经典队列指示器,则设备必须对任何后续的 CCW_CMD_SET_IND_ADAPTER 命令发布带有命令拒绝的单元检查。

4.3.2.6.4 Legacy 接口:关于设置指示器的说明

在某些情况下,传统设备只支持经典队列指示器;在这种情况下,它们将拒绝 CCW_CMD_SET_IND_ADAPTER,因为它们不知道该命令。但某些传统设备支持两级队列指示器,驱动程序可以成功使用 CCW_CMD_SET_IND_ADAPTER 来设置它们。

4.3.3 设备操作

4.3.3.1 主机->客户端通知

关于主机->客户端通知有两种操作模式,分别是经典 I/O 中断和适配器 I/O 中断。由驱动程序使用 CCW_CMD_SET_IND 或 CCW_CMD_SET_IND_ADAPTER 来设置队列指示器来确定要使用的模式。
对于配置更改,驱动程序始终使用经典 I/O 中断。

4.3.3.1.1 经典 I/O 中断通知

如果驱动程序使用 CCW_CMD_SET_IND 命令来设置队列指示器,设备将使用经典 I/O 中断来通知有关 virtqueue 活动的主机->客户端通知。
要通知驱动程序有关 virtqueue 缓冲区的情况,设备会设置客户端提供的指示器中的相应位。如果子通道中没有未决的中断,设备会生成非预期的 I/O 中断。如果设备要通知驱动程序有关配置更改,则它会设置配置指示器中的位 0,并在需要时生成非预期的 I/O 中断。如果用于队列通知使用了适配器 I/O 中断,也适用此规则。

4.3.3.1.2 适配器 I/O 中断通知

如果驱动程序使用 CCW_CMD_SET_IND_ADAPTER 命令来设置队列指示器,设备将使用适配器 I/O 中断来通知有关 virtqueue 活动的主机->客户端通知。
要通知驱动程序有关 virtqueue 缓冲区的情况,设备会设置客户端提供的指示器区域中的相应偏移位。客户端提供的摘要指示器设置为 0x01。将生成与相应中断子类相对应的适配器 I/O 中断。
驱动程序处理适配器 I/O 中断的推荐方式如下:
• 处理与摘要指示器相关的所有队列指示器位。
• 清除摘要指示器,并在之后执行同步(内存屏障)。
• 再次处理与摘要指示器相关的所有队列指示器位。

4.3.3.1.2.1 设备要求:适配器 I/O 中断通知

只有在通知之前未设置摘要指示器的情况下,设备应该生成适配器 I/O 中断。

4.3.3.1.2.2 驱动程序要求:适配器 I/O 中断通知

在接收到适配器 I/O 中断后,驱动程序必须在处理队列指示器之前清除摘要指示器。

4.3.3.1.3 Legacy 接口:关于主机->客户端通知

由于传统设备和驱动程序只支持经典队列指示器,因此主机->客户端通知始终通过经典 I/O 中断进行。

4.3.3.2 客户端->主机通知

为了通知设备有关 virtqueue 缓冲区的情况,驱动程序不幸无法使用通道命令(通道 I/O 的异步特性与主机块 I/O 后端产生冲突)。相反,它使用了带有子代码 3 的诊断 0x500 调用来指定队列,如下:

第一篇------Virtual I/O Device (VIRTIO) Version 1.1_第12张图片
当未协商 VIRTIO_F_NOTIFICATION_DATA 时,Notificationdata 包含 Virtqueue 编号。
当已经协商了 VIRTIO_F_NOTIFICATION_DATA 时,该值具有以下格式:

be32 {
    vqn : 16;      // 占用 16 位,表示虚拟队列编号(Virtual Queue Number)。
    next_off : 15; // 占用 15 位,表示下一个偏移量(Next Offset)。
    next_wrap : 1; // 占用 1 位,表示下一个位置是否在环形结构中的下一个位置。如果为 1,则表示下一个位置是环形结构的开始,如果为 0,则表示下一个位置在当前位置之后。
};

你可能感兴趣的:(网络虚拟化,信息与通信)