本文档描述了“virtio”设备系列的规格。这些设备通常出现在虚拟环境中,但按设计,它们在虚拟机内部看起来像物理设备,而本文档将其视为这样的设备。这种相似性允许虚拟机内的客户端使用标准驱动程序和发现机制。
virtio及其规格的目的在于,虚拟环境和客户端应该具有一种简单、高效、标准和可扩展的虚拟设备机制,而不是为每个环境或操作系统定制的机制。
简单:Virtio设备使用标准的中断和DMA总线机制,这对于任何设备驱动程序作者都应该很熟悉。没有奇特的页面翻转或COW(写时复制)机制:它只是一个普通的设备。
高效:Virtio设备包括输入和输出的描述符环,这些环被精心设计,以避免驱动程序和设备同时写入相同的缓存行而产生的缓存效应。
标准:Virtio不对其运行环境做任何假设,除了支持设备连接的总线。在本规格中,virtio设备是通过MMIO、Channel I/O和PCI总线传输来实现的2,在此之前的草案已经在其他不包括在此处的总线上实现。
可扩展:Virtio设备包含功能位,客户操作系统在设备设置期间会承认这些功能位。这允许前向和后向兼容性:设备提供其了解的所有功能,驱动程序承认它了解并希望使用的功能。
Virtio设备是通过总线特定的方法进行发现和识别的(请参阅总线特定部分:4.1 Virtio在PCI总线上、4.2 Virtio在MMIO上和4.3 Virtio在Channel I/O上)。每个设备包括以下部分:
• 设备状态字段
• 特性位
• 通知
• 设备配置空间
• 一个或多个virtqueue(虚拟队列)
在驱动程序进行设备初始化时,驱动程序按照3.1中指定的步骤序列进行操作。
设备状态字段提供了对此序列中已完成步骤的简单低级指示。最好将其想象成连接到控制台上的交通信号灯,指示每个设备的状态。以下位被定义(按照通常设置的顺序列出):
ACKNOWLEDGE(1)表示客户操作系统已找到该设备并将其识别为有效的virtio设备。
DRIVER(2)表示客户操作系统知道如何驱动该设备。
注意:在设置此位之前可能会有显著的(或无限的)延迟。例如,在Linux下,驱动程序可以是可加载模块。
FAILED(128)表示在客户操作系统中发生了错误,并且已放弃该设备。这可能是内部错误,或者驱动程序出于某种原因不喜欢该设备,甚至在设备操作期间发生了致命错误。
FEATURES_OK(8)表示驱动程序已确认了其理解的所有特性,特性协商已完成。
DRIVER_OK(4)表示驱动程序已设置并准备好驱动该设备。
DEVICE_NEEDS_RESET(64)表示设备遇到了无法恢复的错误。
驱动程序必须更新设备状态,将位设置为指示在3.1中指定的驱动程序初始化序列的已完成步骤。驱动程序不得清除设备状态位。如果驱动程序设置了FAILED位,则驱动程序必须稍后在尝试重新初始化之前重置设备。
如果设置了DEVICE_NEEDS_RESET,驱动程序不应依赖于设备操作的完成。
注意:例如,如果设置了DEVICE_NEEDS_RESET,驱动程序不能假定正在进行的请求将在完成,也不能假定它们尚未完成。一个良好的实现将尝试通过发出重置来恢复。
设备在重置时必须将设备状态初始化为0。
在DRIVER_OK之前,设备不得消耗缓冲区或向驱动程序发送任何已使用的缓冲区通知。
设备应在进入需要重置的错误状态时设置DEVICE_NEEDS_RESET。如果设置了DRIVER_OK,并在设置了DEVICE_NEEDS_RESET之后,设备必须向驱动程序发送设备配置更改通知。
每个virtio设备都提供其了解的所有特性。在设备初始化期间,驱动程序会读取这些特性并告知设备它所接受的子集。重新协商的唯一方法是重置设备。
这允许前向和后向兼容性:如果设备增加了一个新的特性位,旧版驱动程序将不会将该特性位写回设备。同样,如果驱动程序增加了设备不支持的特性,它将看到新特性未提供。
特性位分配如下:
0 到 23 特定设备类型的特性位
24 到 37 保留用于队列和特性协商机制的扩展的特性位
38 及以上 保留用于未来扩展的特性位。
注意:例如,用于网络设备的特性位0(即设备ID为1)表示该设备支持数据包的校验和。
特别是,通过提供新的特性位来指示设备配置空间中的新字段。
驱动程序不得接受设备未提供的特性,并不得接受需要另一个未被接受的特性的特性。
如果设备不提供驱动程序了解的特性,驱动程序应该进入向后兼容模式,否则必须设置FAILED设备状态位并停止初始化。
设备不得提供需要另一个未提供的特性。设备应该接受驱动程序接受的特性的任何有效子集,否则当驱动程序写入它时,设备必须无法设置FEATURES_OK设备状态位。
如果设备已成功地协商了至少一组特性(通过在设备初始化期间接受FEATURES_OK设备状态位),那么在设备或系统重置后,它不应该失败重新协商相同的特性集。否则将干扰从挂起状态恢复和错误恢复。
过渡驱动程序必须通过检测未提供特性位VIRTIO_F_VERSION_1来检测传统设备。传统设备必须通过检测驱动程序未确认VIRTIO_F_VERSION_1来检测传统驱动程序。
在这种情况下,设备通过传统接口使用。传统接口支持是可选的。因此,符合本规范的传统和非传统设备和驱动程序。
与传统设备和驱动程序有关的要求包含在类似于此的名为“LegacyInterface”的部分中。
当设备通过传统接口使用时,过渡设备和过渡驱动程序必须根据这些传统接口部分中记录的要求运行。这些部分中的规范文本通常不适用于非过渡设备。
在本规格中,发送通知(从驱动程序到设备或从设备到驱动程序)的概念起着重要作用。通知的操作方式取决于具体的传输方式。
有三种类型的通知:
• 配置更改通知
• 可用缓冲区通知
• 已使用缓冲区通知。
配置更改通知和已使用缓冲区通知由设备发送,接收者是驱动程序。配置更改通知表示设备配置空间已更改;已使用缓冲区通知表示在通知所指定的virtqueue上可能已使用了缓冲区。
可用缓冲区通知由驱动程序发送,接收者是设备。这种类型的通知表示在通知所指定的virtqueue上可能已提供了缓冲区。
不同通知的语义、传输特定的实现以及其他重要方面在以下章节中详细指定。
大多数传输方式使用设备发送给驱动程序的中断来实现通知。因此,在本规格的早期版本中,这些通知通常被称为中断。本规格中定义的一些名称仍然保留了这个中断术语。偶尔,术语“事件”用来指代通知或通知的接收。
设备配置空间通常用于很少更改或初始化时的参数。对于可选的配置字段,它们的存在由特性位来指示:本规格的未来版本可能会通过在尾部添加额外字段来扩展设备配置空间。
注意:设备配置空间使用小端格式表示多字节字段。
每种传输方式还为设备配置空间提供了一个生成计数,每当有可能两次访问设备配置空间可能看到该空间的不同版本时,该计数将发生变化。
驱动程序不得假设从大于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位大小的设备配置空间大小。
在驱动程序设置FEATURES_OK之前,设备必须允许读取任何特定于设备的配置字段。这包括受特性位条件限制的字段,只要这些特性位由设备提供。
请注意,对于传统接口,设备配置空间通常是虚拟机的本机字节序,而不是PCI的小端字节序。每个设备都记录了正确的字节序。
传统设备没有配置生成字段,因此在更新配置时容易受到竞态条件的影响。这会影响块容量(请参阅5.2.4)和网络MAC地址(请参阅5.1.4)字段;在使用传统接口时,驱动程序应该多次读取这些字段,直到两次读取生成一致的结果。
在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格式之一,或两者都支持。
分离的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。这个值以特定于总线的方式指定。
当驱动程序想要向设备发送一个缓冲区时,它填写描述符表中的一个槽(或将多个槽链接在一起),并将描述符索引写入可用环中。然后它通知设备。当设备完成一个缓冲区时,它将描述符索引写入已使用环中,并发送已使用的缓冲区通知。
驱动程序必须确保每个virtqueue部分的第一个字节的物理地址是上表中指定的对齐值的倍数。
对于旧版接口,virtqueue的布局受到一些额外的限制:
每个virtqueue占用两个或更多物理连续页面(通常定义为4096字节,但取决于传输方式;以下简称为队列对齐),并由三个部分组成:
#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);
}
这会在填充方面浪费一些空间。在使用旧版接口时,过渡性设备和驱动程序都必须使用以下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;
};
请注意,在使用旧版接口时,过渡性设备和驱动程序必须使用虚拟机的本机字节顺序来处理字段和virtqueue,而不是像本标准规定的非旧版接口一样使用小端字节顺序。这假定主机已经知道虚拟机的字节顺序。
使用描述符的消息帧与缓冲区的内容无关。例如,网络传输缓冲区由12字节的标头后跟网络数据包组成。这可以简单地放在描述符表中,作为一个12字节的输出描述符,后跟一个1514字节的输出描述符,但在标头和数据包相邻的情况下,它也可以由一个1526字节的输出描述符组成,甚至可以由三个或更多描述符组成(在这种情况下可能会降低效率)。
请注意,一些设备实现对总描述符大小有合理但较大的限制(例如,基于主机操作系统中的IOV_MAX)。这在实际中并没有成为问题:不会对创建不合理大小的描述符的驱动程序(例如,将网络数据包分成1500个单字节描述符)提供多少同情!
设备不得对描述符的特定排列方式做出假设。设备可以对它允许的链式描述符数量设定合理的限制。
驱动程序必须将任何可由设备写入的描述符元素放在任何可由设备读取的描述符元素之后。
驱动程序不应使用过多的描述符来描述一个缓冲区。
不幸的是,最初的驱动程序实现使用了简单的布局,尽管有这个规范的措辞,设备仍然依赖于它。此外,virtio_blk SCSI命令的规范要求从帧边界推断字段长度(请参见5.2.6.3 旧版接口:设备操作)
因此,当使用旧版接口时,VIRTIO_F_ANY_LAYOUT功能指示设备和驱动程序都不会对帧做出任何假设。当未协商此功能时,过渡性驱动程序的要求包含在每个设备部分中。
描述符表是指驱动程序为设备使用的缓冲区。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开始,并在表的末尾进行环绕。
VIRTIO_F_IN_ORDER
功能,并在向设备提供的表中的偏移量x处设置了flags
中的VRING_DESC_F_NEXT
,驱动程序必须在表中的最后一个描述符(其中x = queue_size − 1
)的next
设置为0,并对其余描述符设置为x + 1
。VIRTIO_F_INDIRECT_DESC
功能允许这样做(参见Avirtio_queue.h
)。&VIRTQ_DESC_F_INDIRECT
标志);addr
和len
分别指的是间接表的地址和长度(以字节为单位)。间接表的布局结构如下(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,依此类推。
可用环具有以下布局结构:
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,但布局和值是相同的。
驱动程序不得在虚拟队列上减少可用idx(即没有“取消公开”缓冲区的方法)。
如果未协商VIRTIO_F_EVENT_IDX特性位,则可用环中的flags字段提供了一个粗糙的机制,供驱动程序通知设备不需要在缓冲区使用时进行通知。否则,used_event是一个性能更好的替代方法,其中驱动程序指定设备可以在需要通知之前进行多远的进度。
这两种通知抑制方法都不是可靠的,因为它们与设备未同步,但它们作为有用的优化方式。
如果未协商VIRTIO_F_EVENT_IDX特性位:
否则,如果已协商VIRTIO_F_EVENT_IDX特性位:
2.6.7.2 设备要求:已使用缓冲区通知抑制
如果未协商VIRTIO_F_EVENT_IDX特性位:
否则,如果已协商VIRTIO_F_EVENT_IDX特性位:
注意:例如,如果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,但布局和值是相同的。
历史接口:Virtqueue 已使用环
从历史上看,许多驱动程序忽略了 len 值,因此许多设备错误地设置了 len。因此,在使用传统接口时,如果可能的话,通常最好忽略已使用环中条目中的 len 值。具体已知的问题会根据设备类型列出。
设备要求:Virtqueue 已使用环
设备在更新 used idx 之前必须设置 len。
设备在更新 used idx 之前必须向描述符的第一个可写设备缓冲区写入至少 len 个字节。
设备可以向描述符写入多于 len 个字节的数据。
注意:可能存在设备不知道哪些缓冲区的哪些部分已经被写入的潜在错误情况。这就是为什么允许 len 低估的原因:这比驱动程序认为未初始化的内存已被覆盖要好。
驱动程序要求:Virtqueue 已使用环
驱动程序不能对超出第一个 len 字节的可写设备缓冲区中的数据做出假设,并且应该忽略这些数据。
描述符的顺序使用
一些设备始终按照它们被提供的顺序使用描述符。这些设备可以提供 VIRTIO_F_IN_ORDER 特性。如果协商成功,这种特性允许设备通过仅写出一个 used 环条目,该条目的 id 对应于描述最后一个缓冲区的描述符链的头条目,来通知驱动程序使用一批缓冲区。
然后,设备根据批处理的大小向前跳转在环中。因此,它将批处理的大小增加到 used idx。
驱动程序需要查找 used id 并计算批处理大小,以能够前进到设备将写入的下一个 used 环条目的位置。
这将导致 used 环条目位于与批处理中的第一个可用环条目匹配的偏移量处,下一个批处理的 used 环条目位于与下一个批处理的第一个可用环条目匹配的偏移量处,依此类推。
被跳过的缓冲区(未写入 used 环条目的缓冲区)被假定已完全被设备使用(读取或写入)。
可用缓冲区通知抑制
设备可以以类似于驱动程序在第2.6.7节中详细描述的方式抑制可用缓冲区通知。设备以与驱动程序在可用环中操作 flags 或 avail_event
驱动程序要求:可用缓冲区通知抑制
驱动程序在分配可用环时必须将 used 环中的 flags 初始化为 0。
如果未协商 VIRTIO_F_EVENT_IDX 特性位:
设备要求:可用缓冲区通知抑制
如果未协商 VIRTIO_F_EVENT_IDX 特性位:
操作 Virtqueue 的辅助工具
Linux 内核源代码包含上述定义和更易用形式的辅助例程,位于 include/uapi/linux/virtio_ring.h 中。IBM 和 Red Hat 明确根据(3-Clause)BSD 许可证授权了此代码,以便所有其他项目都可以自由使用,并且在 A virtio_queue.h 中(稍微有所变化)重新复制了此代码。
Virtqueue 操作有两个部分:向设备提供新的可用缓冲区和处理设备返回的已使用缓冲区。
注意:以最简单的 virtio 网络设备为例,它有两个 virtqueue:传输 virtqueue 和接收 virtqueue。驱动程序将传出(设备可读)的数据包添加到传输 virtqueue 中,然后在使用后释放它们。类似地,接收 virtqueue 中添加了传入(设备可写)的缓冲区,在使用后进行处理。
接下来,我们将更详细地了解在使用分离 virtqueue 格式时,这两个部分的要求。
向设备提供缓冲区
驱动程序向设备的 virtqueue 之一提供缓冲区的步骤如下:
请注意,上述代码不采取措施防止可用环缓冲区的环绕:这是不可能的,因为环缓冲区的大小与描述符表相同,因此步骤(1)将防止此类情况发生。
此外,最大队列大小为 32768(适合于 16 位的最高 2 的幂),因此 16 位 idx 值始终可以区分满缓冲区和空缓冲区。
一个缓冲区由零个或多个设备可读的物理连续元素和零个或多个物理连续的设备可写元素(每个至少有一个元素)组成。此算法将其映射到描述符表中,以形成描述符链:
对于每个缓冲区元素 b:
实际上,d.next 通常用于链接空闲描述符,并保持分离计数以在开始映射之前检查是否有足够的空闲描述符。
描述符链头是上述算法中的第一个 d,即指向缓冲区第一部分的描述符表条目的索引。一个简单的驱动程序实现可以执行以下操作(假定已经适当地转换为和从小端序):
avail->ring[avail->idx % qsz] = head;
然而,通常情况下,驱动程序可以在更新 idx(此时它们对设备可见)之前添加许多描述符链,因此通常需要保持一个计数器来跟踪驱动程序已添加了多少描述符链:
avail->ring[(avail->idx + added++) % qsz] = head;
idx 始终递增,并在自然情况下回绕到 65536:
avail->idx += added;
一旦可用的 idx 被驱动程序更新,这将暴露描述符及其内容。设备可以立即访问驱动程序创建的描述符链和它们引用的内存。
驱动程序在更新 idx 之前必须执行适当的内存屏障,以确保设备看到最新的副本。
设备通知的实际方法是特定于总线的,但通常可能会很昂贵。因此,如果设备不需要通知,它可以抑制这些通知,详见第2.6.10节。
驱动程序必须小心,在检查是否抑制通知之前公开新的 idx 值。
在读取 flags 或 avail_event 之前,驱动程序必须执行适当的内存屏障,以避免错过通知。
一旦设备使用了由描述符引用的缓冲区(根据虚拟队列和设备的性质,可以从中读取或写入它们,或者两者都有),它将按照第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++;
}
紧凑型虚拟队列是一种替代的紧凑型虚拟队列布局,使用读写内存,即主机和客户机都可以读取和写入的内存。
使用紧凑型虚拟队列需要通过 VIRTIO_F_RING_PACKED 功能位来进行协商。
紧凑型虚拟队列每个支持最多 2^15 个条目。
在当前的传输方式下,虚拟队列位于由驱动程序分配的客户机内存中。每个紧凑型虚拟队列由三个部分组成:
描述符环 - 占据描述符区域
驱动程序事件抑制 - 占据驱动程序区域
设备事件抑制 - 占据设备区域
其中,描述符环又包括描述符,每个描述符可以包含以下部分:
缓冲区 ID
元素地址
元素长度
标志
一个缓冲区由零个或多个设备可读的物理连续元素组成,后面跟随零个或多个物理连续的设备可写元素(每个缓冲区至少包含一个元素)。
当驱动程序希望将这样一个缓冲区发送给设备时,它将至少一个可用描述符描述缓冲区的元素写入描述符环中。描述符通过描述符内部存储的缓冲区 ID 与缓冲区关联。
然后,驱动程序通知设备。当设备完成处理缓冲区时,它会将包含缓冲区 ID 的已使用设备描述符写入描述符环中(覆盖以前提供的驱动程序描述符),并发送已使用事件通知。
描述符环是以循环方式使用的:驱动程序按顺序将描述符写入环中。在达到环的末尾后,下一个描述符被放置在环的开头。一旦环充满了驱动程序描述符,驱动程序就会停止发送新的请求,并等待设备开始处理描述符并在提供新的驱动程序描述符之前写入一些已使用的描述符。
类似地,设备按顺序从环中读取描述符,并检测是否已提供了驱动程序描述符。随着描述符的处理完成,设备会将已使用的描述符写回环中。
注意:在按顺序读取驱动程序描述符并开始按顺序处理它们后,设备可能会无序地完成其处理。已使用的设备描述符按照其处理完成的顺序写入。
设备事件抑制数据结构是设备的只写数据结构。它包括有关减少设备事件数量的信息,即向设备发送更少的可用缓冲区通知。
驱动程序事件抑制数据结构是设备的只读数据结构。它包括有关减少驱动程序事件数量的信息,即向驱动程序发送更少的已使用缓冲区通知。
每个驱动程序和设备都应在内部维护一个单比特的环绕计数器,初始值为1。这个由驱动程序维护的计数器称为驱动程序环绕计数器。每当驱动程序使环中的最后一个描述符可用时(在使最后一个描述符可用后),它都会更改此计数器的值。
同样,设备也应该在内部维护一个单比特的环绕计数器,称为设备环绕计数器。每当设备使用环中的最后一个描述符时(在标记最后一个描述符已使用后),它都会更改此计数器的值。
可以很容易地看出,当驱动程序和设备处理相同的描述符或当所有可用描述符都已使用完时,驱动程序中的驱动程序环绕计数器将与设备中的设备环绕计数器匹配。
为了标记描述符为可用和已使用,驱动程序和设备都使用以下两个标志:
VIRTQ_DESC_F_AVAIL
:表示描述符可用。VIRTQ_DESC_F_USED
:表示描述符已被使用。这些标志通过环中的描述符的状态来交替,以便双方可以了解描述符的状态,而不会发生冲突。这些环绕计数器和标志是确保通信的一致性和可靠性的关键机制。
#define VIRTQ_DESC_F_AVAIL (1 << 7)
#define VIRTQ_DESC_F_USED (1 << 15)
驱动程序和设备都应在内部维护一个单位位环形计数器,初始值为1。驱动程序维护的计数器称为驱动程序环形计数器。每当驱动程序使环形中的最后一个描述符可用(在使最后一个描述符可用之后),驱动程序都会更改此计数器的值。
设备维护的计数器称为设备环形计数器。设备在使用环形中的最后一个描述符后(在标记最后一个描述符已使用之后)更改此计数器的值。
很容易看出,驱动程序中的驱动程序环形计数器与设备中的设备环形计数器在处理相同的描述符或已使用所有可用描述符时是匹配的。
要将描述符标记为可用和已使用,驱动程序和设备都使用以下两个标志:
要将描述符标记为可用,驱动程序将Flags中的VIRTQ_DESC_F_AVAIL
位设置为与内部驱动程序环形计数器匹配。它还设置了VIRTQ_DESC_F_USED
位以匹配相反的值(即不匹配内部驱动程序环形计数器)。
要将描述符标记为已使用,设备将Flags中的VIRTQ_DESC_F_USED
位设置为与内部设备环形计数器匹配。它还设置了VIRTQ_DESC_F_AVAIL
位以匹配相同的值。
因此,对于可用描述符,VIRTQ_DESC_F_AVAIL
和VIRTQ_DESC_F_USED
位是不同的,而对于已使用的描述符,它们是相同的。
请注意,此观察通常用于检查合理性,因为这些是必要但不充分的条件-例如,所有描述符都会初始化为零。要检测已使用和可用描述符,驱动程序和设备可以跟踪VIRTQ_DESC_F_USED
/VIRTQ_DESC_F_AVAIL
的最后观察值。还可以使用其他技术来检测VIRTQ_DESC_F_AVAIL
/VIRTQ_DESC_F_USED
位的更改可能性。
设备和驱动程序描述符的写入通常可以重新排序,但是每一方(驱动程序和设备)仅需要在内存中轮询(或测试)单个位置:它们在之前处理的描述符之后的下一个设备描述符,按循环顺序。
有时,设备只需要在处理多个可用描述符的批次后写出一个已使用描述符。如下所述,当使用描述符链接或按顺序使用描述符时,可以发生这种情况。在这种情况下,设备将已使用描述符的缓冲区ID写入描述符环形中的最后一个描述符。在处理已使用的描述符后,设备和驱动程序都将环形中的剩余描述符组的数量跳到下一个已使用的描述符的处理位置(对于驱动程序是读取,对于设备是写入)。
在可用描述符中,Flags
中的VIRTQ_DESC_F_WRITE
位用于标记描述符是否对应于缓冲区的只写或只读元素。
/* 这将一个描述符标记为设备仅写入(否则为设备只读)。 */
#define VIRTQ_DESC_F_WRITE 2
/* 这将一个缓冲区标记为继续的。 */
#define VIRTQ_DESC_F_NEXT 1
缓冲区 ID 包含在列表的最后一个描述符中。
在将列表的其余部分写入环形队列之后,驱动程序总是使列表中的第一个描述符可用。这可以确保设备永远不会在环形队列中观察到部分分散/聚合列表。
注意: 所有标志,包括 VIRTQ_DESC_F_AVAIL
、VIRTQ_DESC_F_USED
和 VIRTQ_DESC_F_WRITE
,必须在列表中的所有描述符中正确设置/清除,而不仅仅是第一个描述符。
设备仅为整个列表写出一个已使用描述符。然后,它根据列表中的描述符数量向前跳跃。驱动程序需要跟踪与每个缓冲区 ID 对应的列表大小,以便能够跳到设备写入的下一个已使用描述符的位置。
例如,如果描述符按照它们可用的顺序使用,这将导致已使用的描述符覆盖列表中的第一个可用描述符,下一个列表的已使用描述符覆盖下一个列表中的第一个可用描述符,依此类推。
VIRTQ_DESC_F_NEXT 在已使用的描述符中保留,驱动程序应忽略它。
一些设备通过同时分派大量大型请求而受益。VIRTIO_F_INDIRECT_DESC 特性允许这样做。为了增加环的容量,驱动程序可以在内存的任何位置存储一张(设备只读的)间接描述符表,并在主 virtqueue 中插入一个描述符(使用 Flags 中的 VIRTQ_DESC_F_INDIRECT 位),该描述符引用一个包含此间接描述符表的缓冲区元素;addr 和 len 分别指示间接表的地址和长度(以字节为单位)。
/* 这表示该元素包含一张描述符表(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 被保留,并且同样会被设备忽略。
某些设备始终按照它们可用的顺序使用描述符。这些设备可以提供 VIRTIO_F_IN_ORDER 功能。如果协商成功,这种特性允许设备通过仅写出一个带有与批处理中最后一个描述符对应的 Buffer ID 的已使用描述符来通知驱动程序使用一批缓冲区。
然后,设备根据批处理的大小向前跳过环。驱动程序需要查找已使用的 Buffer ID 并计算批处理大小,以便能够前进到设备将要写入的下一个已使用描述符的位置。
这将导致已使用描述符覆盖批处理中的第一个可用描述符,下一批的已使用描述符将覆盖下一批中的第一个可用描述符,依此类推。跳过的缓冲区(未写入已使用描述符的缓冲区)被假定已被设备完全使用(读取或写入)。
某些设备在处理单个请求时会组合多个缓冲区。这些设备总是在请求的第一个缓冲区对应的描述符之后,将与请求的其余缓冲区对应的描述符标记为已使用,并写入环中。这确保了驱动程序永远不会在环中观察到不完整的请求。
在许多系统中,已使用和可用的缓冲区通知涉及显著的开销。为了减轻这种开销,每个 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 如果事件标志设置为描述符特定事件:在环内的偏移量(以描述符大小的单位)。只有当环包装计数器与此值匹配并相应地设置了描述符时,事件才会触发。
在写出一些描述符后,设备和驱动程序都应查阅相关结构,以确定是否应发送已使用缓冲区通知或可用缓冲区通知。
virtqueue 的每个部分在虚拟机内存中是物理连续的,并具有不同的对齐要求。
每个 virtqueue 部分的内存对齐和大小要求(以字节为单位)总结在以下表格中:
对齐列给出了virtqueue每个部分的最小对齐要求。
大小列给出了virtqueue每个部分的总字节数。
Queue Size 对应于virtqueue中描述符的最大数量。Queue Size值不必是2的幂。
驱动程序必须确保每个virtqueue部分的第一个字节的物理地址是上述表中指定对齐值的倍数。
设备必须按照它们在环中出现的顺序开始处理驱动程序描述符。设备必须按照它们完成的顺序开始将设备描述符写入环中。设备在开始写入描述符后可以重新排序描述符的写入。
可用描述符是指驱动程序发送给设备的缓冲区。addr是物理地址,描述符使用id字段标识一个缓冲区。
struct pvirtq_desc {
/* 缓冲区地址。 */
le64 addr;
/* 缓冲区长度。 */
le32 len;
/* 缓冲区ID。 */
le16 id;
/* 根据描述符类型而异的标志。 */
le16 flags;
};
以下结构用于减少驱动程序和设备之间发送的通知数量。
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;
};
设备不得写入可供设备读取的缓冲区,设备不应读取可供设备写入的缓冲区。设备不得使用描述符,除非观察到其标志中的VIRTQ_DESC_F_AVAIL位已更改(例如,与初始零值相比)。设备不得在更改VIRTQ_DESC_F_USED位之后更改描述符。
除非观察到其标志中的VIRTQ_DESC_F_USED位已更改,否则驱动程序不得更改描述符。驱动程序不得在更改VIRTQ_DESC_F_AVAIL位之后更改描述符。在通知设备时,驱动程序必须设置next_off和next_wrap以匹配尚未提供给设备的下一个描述符。驱动程序可以发送多个可用的缓冲区通知,而无需向设备提供任何新的描述符。
驱动程序不得创建比设备允许的更长的描述符列表。驱动程序不得创建比队列大小更长的描述符列表。这意味着禁止描述符列表中的循环!驱动程序必须在任何设备可读的描述符元素之后放置任何设备可写的描述符元素。驱动程序不得依赖于设备使用更多的描述符来能够写出列表中的所有描述符。驱动程序在将列表中的第一个描述符提供给设备之前,必须确保环中有足够的空间容纳整个列表。驱动程序不得在提供列表中的所有后续描述符之前使列表中的第一个描述符可用。
设备必须按照驱动程序提供的顺序使用由VIRTQ_DESC_F_NEXT标志链接的列表中的描述符。
除非已经协商了VIRTIO_F_INDIRECT_DESC功能,否则驱动程序不得设置DESC_F_INDIRECT标志。在间接描述符内,驱动程序除了在DESC_F_WRITE中设置任何标志外,不得设置任何标志。驱动程序不得创建比设备允许的更长的描述符链。驱动程序不得在由VIRTQ_DESC_F_NEXT链接的散列/聚合列表中设置了DESC_F_INDIRECT的直接描述符。标志。
virtqueue操作分为两部分:向设备提供新的可用缓冲区和处理来自设备的已使用缓冲区。以下是在使用紧凑型virtqueue格式时更详细地描述这两个部分的要求。
驱动程序将缓冲区提供给设备的virtqueue如下所示:
对于每个缓冲区元素b:
驱动程序在更新标志之前必须执行适当的内存屏障,以确保设备看到最新的副本。
设备通知的实际方法是与总线相关的,但通常会很昂贵。因此,如果设备不需要这些通知,设备可以抑制此类通知,使用包括设备区域的事件抑制结构,如第2.7.14节所述。驱动程序必须小心,在检查是否抑制通知之前,公开新的标志值。
以下是一个驱动程序代码示例。它不尝试减少可用缓冲区通知的数量,也不支持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);
}
驱动程序在读取占用设备区域的事件抑制结构之前必须执行适当的内存屏障。不这样做可能导致未发送强制性可用缓冲区通知。
一旦设备使用了由描述符引用的缓冲区(根据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); // 处理描述符中的数据
}
在某些情况下,驱动程序需要向设备发送可用缓冲区通知。
当未协商VIRTIO_F_NOTIFICATION_DATA时,此通知涉及向设备发送virtqueue号码(方法取决于传输方式)。
但是,某些设备受益于能够了解队列中可用数据的数量,而无需访问内存中的virtqueue:出于效率或作为调试辅助。
为了帮助实现这些优化,当协商了VIRTIO_F_NOTIFICATION_DATA时,驱动程序通知设备将包括以下信息:
我们从设备初始化的概述开始,然后详细介绍设备以及每个步骤是如何执行的。最好阅读此部分以及描述如何与特定设备通信的特定总线部分。
驱动程序必须按照以下顺序初始化设备:
旧设备不支持FEATURES_OK状态位,因此没有一种优雅的方式可以让设备指示不支持的特性组合。它们也没有提供清晰的机制来结束特性协商,这意味着设备在首次使用时最终确定特性,不能引入在根本改变设备初始操作的情况下引入特性。
旧的驱动程序实现通常在设置DRIVER_OK位之前使用设备,有时甚至在将特性位写入设备之前。
结果,步骤5和6被省略,步骤4、7和8被合并。
因此,当使用旧接口时:
在操作设备时,设备配置空间中的每个字段都可以由驱动程序或设备更改。
每当设备触发此类配置更改时,都会通知驱动程序。这使得驱动程序能够缓存设备配置,避免除非通知,否则会进行昂贵的配置读取。
对于设备,其中设备特定配置信息可以更改的设备,会在发生设备特定配置更改时发送配置更改通知。
此外,该通知是由设备设置DEVICE_NEEDS_RESET(请参阅2.1.2)时触发的。
一旦驱动程序设置了DRIVER_OK状态位,设备的所有配置的virtqueue都被视为“活动的”。一旦设备被重置,设备的任何virtqueue都不再“活动”。
驱动程序不得更改已暴露给设备的virtqueue条目,即已提供给设备(尚未被设备使用)的缓冲区。
因此,驱动程序必须确保virtqueue不处于活动状态(通过设备重置),然后再删除已暴露的缓冲区。
Virtio可以使用各种不同的总线,因此标准分为virtio通用部分和总线特定部分。
Virtio设备通常作为PCI设备实现。
Virtio设备可以实现为任何类型的PCI设备:传统PCI设备或PCI Express设备。要确保设计符合最新的级别要求,请参见PCI-SIG主页http://www.pcisig.com以获取任何批准的更改。
使用通过PCI总线的Virtio的Virtio设备必须向客户端公开一个符合适当PCI规范的接口规范要求:[PCI]和[PCIe]分别。
PCI设备的PCI供应商ID为0x1AF4,PCI设备ID为0x1000到0x107F(包括0x107F)的任何PCI设备都是virtio设备。此范围内的实际值表示设备支持的virtio设备。PCI设备ID通过将0x1040添加到Virtio设备ID中计算,如第5节所示。另外,根据设备类型,设备可以使用过渡PCI设备ID范围0x1000到0x103F。
设备必须具有PCI供应商ID 0x1AF4。设备必须具有通过将0x1040添加到Virtio设备ID中计算的PCI设备ID,如第5节所示,或者根据设备类型使用过渡PCI设备ID。
例如,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或更高。
这是为了减小旧版驱动程序尝试驱动设备的机会。
驱动程序必须匹配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值。
过渡设备必须具有PCI修订ID为0。过渡设备必须具有与Virtio设备ID匹配的PCI子系统设备ID,如第5节所示。过渡设备的过渡PCI设备ID范围必须在0x1000到0x103F之间。
这是为了与旧版驱动程序匹配。
该设备通过I/O和/或内存区域配置(尽管请参见4.1.4.7以通过PCI配置空间访问),根据Virtio结构PCI功能指定。
设备配置区域中存在不同大小的字段。所有64位、32位和16位字段都是小端字节序。64位字段应被视为两个32位字段,低32位部分后跟高32位部分。
对于设备配置访问,驱动程序必须对于8位宽字段使用8位宽访问,对于16位宽字段使用16位宽和对齐访问,对于32位和64位宽字段使用32位宽和对齐访问。对于64位字段,驱动程序可以独立访问字段的高32位和低32位部分。
对于64位设备配置字段,设备必须允许对字段的高32位和低32位部分进行驱动程序独立访问。
virtio设备配置布局包括多个结构:
每个结构的位置都使用位于设备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。
这些字段的解释如下:
/* 通用配置(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字节之后的结构部分),以实现与这种设备的前向兼容性,而不会损失功能并且不会浪费资源。
通用配置结构位于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节。
通知位置是通过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.offset
和notify_off_multiplier
来自上面的通知能力结构,而queue_notify_off
来自通用配置结构。
请注意,例如,如果notify_off_multiplier
为0,则设备对所有队列使用相同的队列通知地址。
cap.length >= queue_notify_off * notify_off_multiplier + 2
对于提供 VIRTIO_F_NOTIFICATION_DATA 的设备:
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节
struct virtio_pci_cfg_cap {
struct virtio_pci_cap cap; /* PCI 能力描述 */
u8 pci_cfg_data[4]; /* 用于 BAR 访问的数据 */
};
已知的所有旧驱动程序都会检查PCI版本或设备和供应商ID,因此不会尝试驱动非过渡设备。
有缺陷的旧驱动程序可能会错误地尝试驱动非过渡设备。如果需要支持这样的驱动程序(而不是修复错误),则以下是检测和处理它们的推荐方式。
注意:目前未知是否在生产中使用此类有缺陷的驱动程序。
在已知以前存在具有相同ID(包括PCI版本、设备和供应商ID)的旧设备的平台上,非过渡设备应该采取以下步骤,以使旧驱动程序在尝试驱动它们时能够优雅地失败:
本部分记录了设备初始化期间执行的特定于PCI的步骤。
在设备初始化之前,驱动程序会扫描PCI能力列表,以检测Virtio设备配置布局,具体步骤请参见4.1.4。
####### 4.1.5.1.1.1 遗留接口:有关设备布局检测的说明
遗留驱动程序跳过了设备布局检测步骤,无条件地假设在I/O空间的BAR0中存在遗留设备配置空间。
遗留设备的能力列表中没有Virtio PCI能力。
因此:
如果设备启用并存在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
请注意,将事件映射到向量可能需要设备分配内部设备资源,因此可能失败。
具有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值来报告映射失败。
驱动程序必须支持具有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或报告设备故障。
由于设备可以具有用于大容量数据传输的零个或多个虚拟队列,因此驱动程序需要在设备特定配置的一部分来配置它们。
驱动程序通常会按照以下步骤为设备的每个虚拟队列执行此操作:
在使用遗留接口时,队列布局遵循2.6.2中的Legacy Interfaces:关于虚拟队列布局的说明,对齐为4096。驱动程序将物理地址除以4096并写入Queue Address字段2。没有协商队列大小的机制。
当未经协商使用VIRTIO_F_NOTIFICATION_DATA时,驱动程序通过将此虚拟队列的16位索引写入Queue Notify地址来向设备发送可用缓冲区通知。
当协商了VIRTIO_F_NOTIFICATION_DATA时,驱动程序通过将以下32位值写入Queue Notify地址来向设备发送可用缓冲区通知:
le32 {
vqn : 16;
next_off : 15;
next_wrap : 1;
};
如果需要用于虚拟队列的已使用缓冲区通知,设备通常会执行以下操作:
• 如果禁用了MSI-X能力:
如果对于虚拟队列启用了MSI-X能力,并且queue_msix_vector为NO_VECTOR,则设备不得传递该虚拟队列的中断。
某些virtio PCI设备可以更改设备配置状态,反映在设备的特定于设备的配置区域中。在这种情况下:
• 如果禁用了MSI-X能力:
如果对于虚拟队列启用了MSI-X能力,并且config_msix_vector为NO_VECTOR,则设备不得传递有关设备配置空间更改的中断。
驱动程序必须处理同一个中断用于指示设备配置空间更改和一个或多个虚拟队列已被使用的情况。
驱动程序中断处理程序通常会执行以下操作:
• 如果禁用了MSI-X能力:
没有PCI支持的虚拟环境(在嵌入式设备模型中很常见)可能会使用简单的内存映射设备(“virtio-mmio”)而不是PCI设备。
内存映射的virtio设备行为基于PCI设备规范。因此,包括设备初始化、队列配置和缓冲区传输在内的大多数操作几乎相同。现有的差异在以下各节中描述。
与PCI不同,MMIO没有通用的设备发现机制。对于每个设备,客户操作系统需要知道寄存器和使用的中断位置。使用平坦设备树的系统建议绑定如下所示的示例:
// 示例:virtio_block 设备,占用 512 字节的内存地址为 0x1e000,使用中断号 42。
virtio_block@1e000 {
compatible = "virtio,mmio"; // 设备兼容性标识,指定这是一个 Virtio 设备,并使用 MMIO 接口。
reg = <0x1e000 0x200>; // 设备在内存中的地址范围,从 0x1e000 开始,占用 0x200 字节的空间。
interrupts = <42>; // 中断号,这个设备使用中断号 42。
}
MMIO virtio设备提供了一组内存映射控制寄存器,后面跟着一个设备特定的配置空间,如表4.1所描述。
le32 {
vqn : 16;
next_off : 15;
next_wrap : 1;
};
内存映射 virtio 设备使用一个单独的、专用的中断信号,当至少有一个在 InterruptStatus 描述中的位被设置时,此信号会被激活。这是设备向驱动发送已使用缓冲区通知或配置更改通知的方式。
在接收到中断后,驱动程序必须读取 InterruptStatus,以检查引起中断的原因(请查看寄存器描述)。已使用缓冲区通知位的设置应被解释为对每个活动 virtqueue 的已使用缓冲区通知。在处理完中断后,驱动程序必须通过将与已处理事件对应的位掩码写入 InterruptACK 寄存器来确认中断。
传统的 MMIO 传输使用基于页面的寻址,从而导致了略微不同的控制寄存器布局、设备初始化和虚拟队列配置过程。表格 4.2 呈现了控制寄存器布局,省略了未改变其功能或行为的寄存器的描述。
虚拟队列页面大小由来宾写入的GuestPageSize定义。驱动程序在配置虚拟队列之前执行此操作。
虚拟队列布局遵循第2.6.2节"Legacy Interfaces: A Note on Virtqueue Layout"中定义的布局,并且遵循QueueAlign中定义的对齐方式。
虚拟队列的配置如下:
通知机制没有更改。
基于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类型明确表示。
作为代理设备,virtio-ccw使用具有特殊控制单元类型(0x3832)的通道附加I/O控制单元,以及与附加virtio设备的子系统设备ID相对应的控制单元模型,通过虚拟I/O子通道和类型为0x32的虚拟通道路径访问。这个代理设备可以通过正常的通道子系统设备发现(通常是STORE SUBCHANNEL循环)进行发现,并且会回应基本的通道命令:
对于virtio-ccw代理设备,感知ID将返回以下信息:
对于 virtio-ccw 代理设备,SENSE ID 命令返回以下信息:
virtio-ccw 代理设备具有几个重要功能:
除了基本的通道命令外,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.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节中有描述。
virtio-ccw 设备的行为类似于普通通道设备,如[S390 PoP]和[S390 Common I/O]中所规定。具体而言:
用于 virtio-ccw 设备的驱动程序必须检查控制单元类型是否为0x3832,并且必须忽略设备类型和型号。
即使驱动程序抑制了该命令的长度检查,它也应该尝试在通道命令中提供正确的长度。
virtio-ccw 使用多个通道命令来设置设备。
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 包含数据部分的长度以及修订版本相关的其他所需选项。
支持以下数值:
请注意,virtio 标准的更改不一定对应于 virtio-ccw 的修订版本更改。
设备必须对不支持的任何修订版本发布具有命令拒绝的单位检查。对于修订版本、长度和数据的任何无效组合,它也必须发布具有命令拒绝的单位检查。非过渡设备必须拒绝修订版本 ID 0。设备必须对不包含在驱动程序选择的修订版本中的任何 virtio-ccw 特定通道命令回答命令拒绝。
设备必须对在成功选择修订版本后尝试选择不同修订版本的任何尝试都回答命令拒绝。
设备必须将修订版本视为从相关子通道已启用的时间起到驱动程序成功设置之时为止。这意味着修订版本在禁用和启用相关子通道之间不是持久的。
驱动程序应该从尝试设置其支持的最高修订版本开始,并在收到命令拒绝时继续尝试较低的修订版本。
驱动程序在设置修订版本之前不得发布任何其他 virtio-ccw 特定通道命令。
在驱动程序成功选择修订版本后,它不得尝试选择不同的修订版本。
传统设备不支持 CCW_CMD_SET_VIRTIO_REV 并回答命令拒绝。
非过渡驱动程序必须停止尝试在这种情况下操作该设备。过渡驱动程序必须操作该设备,就好像它能够设置修订版本 0 一样。
传统驱动程序不会在发布其他 virtio-ccw 特定通道命令之前发布 CCW_CMD_SET_VIRTIO_REV。因此,非过渡设备必须回答任何此类尝试,具有命令拒绝。在这种情况下,过渡设备必须假定驱动程序是传统驱动程序,继续进行,就好像驱动程序选择了修订版本 0 一样。这意味着设备必须拒绝任何对修订版本 0 无效的命令,包括随后的 CCW_CMD_SET_VIRTIO_REV。
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 中传输。
res0 保留字段,设备必须忽略它。
对于遗留驱动程序或选择了修订版 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 布局的说明。
驱动程序通过 CCW_CMD_WRITE_STATUS 命令更改设备的状态,该命令传输一个 8 位状态值。
如 2.2.2 中所述,设备有时无法设置设备状态字段:例如,在设备初始化期间,它可能无法接受 FEATURES_OK 状态位。
使用修订版 2,定义了 CCW_CMD_READ_STATUS:它从设备读取一个 8 位状态值,并充当 CCW_CMD_WRITE_STATUS 的反向操作。
如果设备在响应 CCW_CMD_WRITE_STATUS 命令时发布了带有命令拒绝的单元检查,驱动程序必须假定设备未能设置状态,并且设备状态字段保留了其先前的值。
如果至少已经协商了修订版 2,则在检测到配置更改后,驱动程序应该使用 CCW_CMD_READ_STATUS 命令检索设备状态字段。
如果未至少协商了修订版 2,则驱动程序不得尝试发出 CCW_CMD_READ_STATUS 命令。
如果设备无法将设备状态字段设置为驱动程序写入的值,则设备必须确保设备状态字段保持不变,并且必须发布带有命令拒绝的单元检查。
如果至少已经协商了修订版 2,则如果发出 CCW_CMD_READ_STATUS 命令,则设备必须返回当前的设备状态字段。
特性位按照 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 组合。
设备的配置空间位于主机内存中。
为了从配置空间获取信息,驱动程序使用 CCW_CMD_READ_CONF,指定设备要写入的客户端内存。
要更改配置信息,驱动程序使用 CCW_CMD_WRITE_CONF,指定设备要读取的客户端内存。
在这两种情况下,都传输完整的配置空间。这允许驱动程序将新配置空间与旧版本进行比较,并在其更改时在内部保留一代计数。
为了设置主机->客户端通知的指示位,驱动程序根据是否希望使用绑定到子通道的传统 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 命令发布带有命令拒绝的单元检查。
每个 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 字节。
如果驱动程序已经通过 CCW_CMD_SET_IND 命令设置了经典队列指示器,则设备必须对任何后续的 CCW_CMD_SET_IND_ADAPTER 命令发布带有命令拒绝的单元检查。
在某些情况下,传统设备只支持经典队列指示器;在这种情况下,它们将拒绝 CCW_CMD_SET_IND_ADAPTER,因为它们不知道该命令。但某些传统设备支持两级队列指示器,驱动程序可以成功使用 CCW_CMD_SET_IND_ADAPTER 来设置它们。
关于主机->客户端通知有两种操作模式,分别是经典 I/O 中断和适配器 I/O 中断。由驱动程序使用 CCW_CMD_SET_IND 或 CCW_CMD_SET_IND_ADAPTER 来设置队列指示器来确定要使用的模式。
对于配置更改,驱动程序始终使用经典 I/O 中断。
如果驱动程序使用 CCW_CMD_SET_IND 命令来设置队列指示器,设备将使用经典 I/O 中断来通知有关 virtqueue 活动的主机->客户端通知。
要通知驱动程序有关 virtqueue 缓冲区的情况,设备会设置客户端提供的指示器中的相应位。如果子通道中没有未决的中断,设备会生成非预期的 I/O 中断。如果设备要通知驱动程序有关配置更改,则它会设置配置指示器中的位 0,并在需要时生成非预期的 I/O 中断。如果用于队列通知使用了适配器 I/O 中断,也适用此规则。
如果驱动程序使用 CCW_CMD_SET_IND_ADAPTER 命令来设置队列指示器,设备将使用适配器 I/O 中断来通知有关 virtqueue 活动的主机->客户端通知。
要通知驱动程序有关 virtqueue 缓冲区的情况,设备会设置客户端提供的指示器区域中的相应偏移位。客户端提供的摘要指示器设置为 0x01。将生成与相应中断子类相对应的适配器 I/O 中断。
驱动程序处理适配器 I/O 中断的推荐方式如下:
• 处理与摘要指示器相关的所有队列指示器位。
• 清除摘要指示器,并在之后执行同步(内存屏障)。
• 再次处理与摘要指示器相关的所有队列指示器位。
只有在通知之前未设置摘要指示器的情况下,设备应该生成适配器 I/O 中断。
在接收到适配器 I/O 中断后,驱动程序必须在处理队列指示器之前清除摘要指示器。
由于传统设备和驱动程序只支持经典队列指示器,因此主机->客户端通知始终通过经典 I/O 中断进行。
为了通知设备有关 virtqueue 缓冲区的情况,驱动程序不幸无法使用通道命令(通道 I/O 的异步特性与主机块 I/O 后端产生冲突)。相反,它使用了带有子代码 3 的诊断 0x500 调用来指定队列,如下:
当未协商 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,则表示下一个位置在当前位置之后。
};