Packed virtqueue: How to reduce overhead with virtio

This is the final post of a three-post series, the previous posts are “Virtio devices and drivers overview: The headjack and the phone,” and “Virtqueues and virtio ring: How the data travels.”

这是三篇系列文章的最后一篇,之前的文章是”Virtio设备和驱动概述:头戴式耳机和手机“,以及”Virtqueues和virtio环:数据如何传输“。

Split virtqueue issues: Too much spinning around

While the split virtqueue shines because of the simplicity of its design, it has a fundamental problem: The avail-used buffer cycle needs to use memory in a very sparse way. This puts pressure on the CPU cache utilization, and in the case of hardware means several PCI transactions for each descriptor.

虽然split virtqueue因其设计的简单性而大放异彩,但它有一个基本问题:可用的缓冲区环需要以一种非常稀疏的方式使用内存。这给CPU的缓存利用率带来了压力,在硬件的情况下,意味着每个描述符都要有几个PCI事务。

Packed virtqueue amends it by merging the three rings in just one location in virtual environment guest memory. While this may seem complicated at first glance, it’s a natural step after the split version if we realize that the device can discard and overwrite the data it already has read from the driver, and the same happens the other way around.

Packed virtqueue对其进行了修正,将三个环合并在虚拟环境guest内存的一个位置。虽然这乍看起来很复杂,但如果我们意识到设备可以丢弃和覆盖它已经从驱动中读取的数据,那么这就是分裂版本之后的一个自然步骤,反之亦然。

Supplying descriptors to the device: How to fill device todo-list

After initialization in the same process as described in Virtio device initialization: feature bits, and after the agreement on RING_PACKED feature flag, the driver and the device starts with a shared blank canvas of descriptors with an agreed length (up to 215 entries) in a agreed guest’s memory location. The layout of these descriptors is:

1
2
3
4
5
6
struct virtq_desc { 
le64 addr;
le32 len;
le16 id;
le16 flags;
};

Listing: Memory layout of a packed virtqueue descriptor

在Virtio设备初始化:特征位中描述的相同过程中进行初始化后,在就RING_PACKED特征标志达成一致后,驱动程序和设备开始在商定的客体内存位置上共享一个空白的描述符,其长度是商定的(最多215条)。这些描述符的布局是:。

This time, the id field is not an index for the device to look for the buffer: it is an opaque value for it, only has meaning for the driver.

The driver also maintains an internal single-bit ring wrap counter initialized to 1. The driver will flip its value every time it makes available the last descriptor in the ring.

As with split descriptors, the first step is to write the different fields: address, length, id and flags. However, packed descriptors take into account two new flags: AVAIL(0x7) and USED(0x15). To mark a descriptor as available, the driver makes the AVAIL(0x7) flag the same as its internal wrap counter, and the used flag the inverse. While just a binary flag avail/used would be easier to implement, it would prevent useful optimizations we will describe later.

这一次,id字段不是设备寻找缓冲区的索引:它是一个不透明的值,只对驱动有意义。

驱动程序还维护一个内部的单比特环形缠绕计数器,初始化为1,每次提供环形的最后一个描述符时,驱动程序都会翻转其值。

与分割描述符一样,第一步是写入不同的字段:地址、长度、ID和标志。然而,打包描述符考虑到了两个新的标志。AVAIL(0x7)和USED(0x15)。为了将一个描述符标记为可用,驱动程序使AVAIL(0x7)标志与它的内部包装计数器相同,而使用的标志则是相反的。虽然只有一个二进制标志AVA/USED会更容易实现,但它会妨碍我们后面要描述的有用的优化。

As an example, if the driver allocates a write buffer with 0x1000 bytes on position 0x80000000 in the step 1 in the diagram, and makes it the first available descriptor setting AVAIL(0x7) flag the same as internal wrap counter (set) in step 2. The descriptor table would look like this:

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 0 W|A

Figure: Descriptor table after add the first buffer

举个例子,如果驱动程序在图中的第1步中在0x80000000位置分配了一个0x1000字节的写缓冲区,并使其成为第一个可用的描述符,在第2步中设置AVAIL(0x7)标志与内部包络计数相同(设置)。描述符表将看起来像这样。

Note that the avail and used idx columns are in the table just for guidance, they don’t exist in the descriptor table: Each side should have its internal counter to know which position needs to poll or write next, and also the device must track the driver’s wrap counter. Lastly, as with used virtqueue, the driver notifies the device if the latter has notifications enabled (step 3 in the diagram).

注意,表中的avail和used idx列只是为了指导,它们在描述符表中并不存在。每一方都应该有自己的内部计数器,以知道下一步需要轮询或写入哪个位置,同时设备也必须跟踪驱动的wrap计数器。最后,和使用的virtqueue一样,如果设备启用了通知功能,驱动程序就会通知设备(图中第3步)。

And the usual diagram of the updates. Note the lack of the avail and used ring, as only the descriptor table is needed now.

还有通常的更新图。请注意,由于现在只需要描述符表,所以缺少可用和已用环。

Diagram: Driver makes available a descriptor using a packed queue

Returning used descriptors: How the device fills the “done” list

As the driver, the device maintains an internal single-bit ring wrap counter initialized to 1, and knows that the driver also has its internal ring wrap counter set. When the latter first searches for the first descriptor the driver has made available, it polls the first entry of the ring, looking for the avail flag equal to the driver internal wrap flag (set in this case).

作为驱动程序,设备维护着一个初始化为1的内部单比特环形缠绕计数器,并且知道驱动程序也设置了其内部环形缠绕计数器。当后者第一次搜索驱动器提供的第一个描述符时,它就会轮询环的第一个条目,寻找等于驱动器内部包络标志的可用标志(在这种情况下是设置的)。

As with a used ring, the length of the written data is returned in the “length” entry (if any), and the id of the used descriptor. At last, the device will make the avail (A) and used (U) flag the same as the device’s internal wrap counter.

Following the example, the device will let the descriptor table as figure 6. The device will know that the buffer has been returned because the used flag matches the available flag, and with the device internal wrap counter at the moment it wrote the descriptor. The returned address is not important: only the ID.

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 0 W|A|U

Figure: Descriptor table after add the first buffer

与已用环一样,写入数据的长度会在 “length “条目中返回(如果有的话),以及已用描述符的id。最后,设备将使可用(A)和已用(U)标志与设备的内部缠绕计数器相同。

按照这个例子,设备将让描述符表如图6所示。设备将知道缓冲区已经被返回,因为使用的标志与可用的标志相匹配,并且在写描述符的时候与设备内部的wrap计数器相匹配。返回的地址并不重要:只有ID。

Diagram: Device marks a descriptor as used using a packed queue

Wrapping the descriptor ring: How the lanes keep separated?

When the driver fills the complete descriptor table, it wraps and changes its internal Driver Ring Wrap. So, in the second round, the available descriptions will have the avail and used flags clear, so the device will have to poll looking for this condition once it wraps reading descriptors. Let’s see a full example of the different situations.

当驱动程序填满了完整的描述符表,它就会包裹并改变其内部的驱动程序环形包裹。所以,在第二轮中,可用的描述符将有avail和used标志被清除,所以设备一旦包裹读取描述符,就必须轮询寻找这个条件。让我们来看看不同情况的完整例子。

If we have a descriptor table with only two entries, the Driver Ring Wrap Counter is set, and it fills the descriptor table making available two buffers at the beginning of the operation, driver will reverse its internal wrap counter, so it will be clear (0). We have the next table:

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 0 W|A
0x81000000 0x1000 1 W|A

Figure: Full two-entries descriptor table

如果我们有一个只有两个条目的描述符表,驱动环形缠绕计数器被设置,它填满描述符表,在操作开始时腾出两个缓冲区,驱动将扭转其内部缠绕计数器,所以它将是clear(0)。我们有下一个表。

After that, the device realizes that has both descriptors with id #0 and #1 available: it knows that the driver had its wrap counter set when it wrote them, the avail flag is set on them, and the used one is clear on both. If device uses the descriptor with id #1, we have the Figure 8 descriptor table. The buffer #0 still belongs to the device!

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 1 W|A|U
0x81000000 0x1000 1 W|A

Figure: Using first buffer out of order

之后,设备意识到有两个ID为#0和#1的描述符是可用的:它知道驱动程序在写它们的时候设置了wrap计数器,它们的avail标志被设置,而且这两个描述符的used标志都是清零的。如果设备使用id为#1的描述符,我们就有了图8的描述符表。缓冲区#0仍然属于设备!

Now the driver realize the buffer #1 has been used, since avail and used flags are the same (set) and match the device’s internal wrap counter at the moment it wrote it. If device now uses the buffer id #0, it will make the table look like this:

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 1 W|A|U
0x81000000 0x1000 0 W|A|U

Figure: Using second buffer out of order

现在驱动程序意识到1号缓冲区已经被使用了,因为avail和used标志是一样的(设置),并且与设备的内部wrap计数器在写的时候是一致的。如果设备现在使用缓冲区ID #0,它将使表看起来像这样。

But there is a more interesting case: Starting from the “first buffer out of order” situation, the driver makes available the buffer #1 again. In that case, the descriptor table goes directly from the “first buffer” to the next figure, “Full two-entries descriptor table.”

Avail idx Address Length ID Flags Used idx
0x81000000 0x1000 1 W|(!A)|U
0x81000000 0x1000 1 W|A

Figure: Full two-entries descriptor table

但还有一种更有趣的情况。从 “第一个缓冲区失灵 “的情况开始,驱动程序再次提供了1号缓冲区。在这种情况下,描述符表直接从 “第一个缓冲区 “进入下一个图,”完整的两行描述符表”。

Chained descriptors: No more jumps

Chained descriptors work likewise: no need for the next field in the head (or subsequent) descriptor in the chain to search subsequent ones, since the latter always occupies the next position. However, while in the split used ring you only need to return as used the id of the head of the chain, in packed you only need to return the tail id.

链式描述符的工作原理也是如此:不需要在链中的头部(或后续)描述符的下一个字段来搜索后续的描述符,因为后者总是占据着下一个位置。然而,在分割使用的环中,你只需要返回链头的id作为使用,而在打包中你只需要返回尾部的id。

Back to the used ring, every time we use chained descriptors, we make the used idx lag regarding the avail idx. More than one descriptor mark as available to the device, but we only send one as used to the driver. While this is not a problem in the split ring, this would cause descriptor entry exhaustion in the packed version.

回到已用环,每次我们使用链式描述符时,都会使已用idx滞后于可用idx。一个以上的描述符被标记为设备可用,但我们只把一个描述符作为已使用的描述符发送给驱动。虽然这在分割环中不是一个问题,但在打包版本中会导致描述符条目耗尽。

The straightforward solution is to make the device mark as used every descriptor in the chain. However, this can be expensive, since we are modifying a shared area of memory, and could cause cache bounces.

However, the driver already knows the chain, so it can skip all the chain with only the last id. This is why we need to compare the used/avail pair with the driver/device Wrap Counter: after a jump, we wouldn’t know if the next descriptor has been made available in this driver’s round or in the next if we only have a binary available/used flag.

直接的解决方案是让设备将链上的每个描述符都标记为已使用。然而,这可能是昂贵的,因为我们正在修改内存的共享区域,并可能导致缓存跳出。

然而,驱动程序已经知道了链,所以它可以跳过所有的链,只保留最后一个ID。这就是为什么我们需要将已用/可用对与驱动/设备的Wrap Counter进行比较:在跳转之后,如果我们只有一个二进制的可用/已用标志,我们就不知道下一个描述符是在这个驱动的回合中还是在下一个回合中被提供的。

For example, in a four entries ring, the driver makes available the chain of three descriptors:

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 0 W|A
0x81000000 0x1000 1 W|A
0x82000000 0x1000 2 W|A
0

Figure: Three chained descriptors available

例如,在一个四项环中,驱动器提供了三个描述符的链。

After that, the device discovers the chain (polling position 0) and marks it as used, overwriting only the position 0. It skips completely the positions 1 and 2. When the driver polls for used, it will skip them too, knowing that the chain was 3 descriptors long:

Avail idx Address Length ID Flags Used idx
0x80000000 0x1000 2 W|A|U
0x81000000 0x1000 1 W|A
0x82000000 0x1000 2 W|A
0

Figure: Using the descriptor chain

之后,设备会发现这个链(轮询位置0),并将其标记为已用,只覆盖位置0,完全跳过位置1和2。当驱动轮询已使用时,它也会跳过这些位置,因为它知道该链有3个描述符长。

Now the driver produces another two descriptor long chain, and it has to take into account the wrapping:

Avail idx Address Length ID Flags Used idx
0x81000000 0x1000 1 W|(!A)|U
0x81000000 0x1000 1 W|A
0x82000000 0x1000 2 W|A
0x80000000 0x1000 0 W|A

Figure: Make available another descriptor chain

现在,驱动程序又产生了一个两根描述符的长链,它必须考虑到包装的问题。

And the device marks it as used, so only the first descriptor in the chain (4th in the table) needs to be updated.

Avail idx Address Length ID Flags Used idx
0x81000000 0x1000 1 W|(!A)|U
0x81000000 0x1000 1 W|A
0x82000000 0x1000 2 W|A
0x80000000 0x1000 0 W|A|U

Figure: Using another descriptor chain

Although the next descriptor (2nd) seems like available, since the avail flag is different from the used one, the device knows that it is not because of knowing the internal Driver Wrap Counter: The right flag combination is avail clear, used set.

而设备将其标记为已使用,所以只有链中的第一个描述符(表中的第四个)需要更新。

尽管下一个描述符(第2个)看起来是可用的,但由于avail标志与used标志不同,设备知道它不是,因为知道内部的Driver Wrap Counter。正确的标志组合是avail clear,used set。

Indirect descriptors: When chains are not enough

Indirect descriptors work like in the split case. First, the driver allocates a table of indirect descriptors each with the same layout as the regular packed descriptors anywhere in memory. After that, it sets each descriptor in this indirect table to the buffer it wants to make available for the driver (steps 1-2), and inserts a descriptor in the virtqueue with the flag VIRTQ_DESC_F_INDIRECT (0x4) set (step 3). The descriptor’s address and length correspond to the indirect table’s ones.

间接描述符的工作方式与分割情况类似。首先,驱动程序分配一个间接描述符表,每个描述符的布局与内存中任何地方的常规打包描述符相同。之后,它将这个间接表中的每个描述符设置为它想为驱动提供的缓冲区(步骤1-2),并在virtqueue中插入一个设置了标志VIRTQ_DESC_F_INDIRECT(0x4)的描述符(步骤3)。该描述符的地址和长度对应于间接表的那些。

In packed layout buffers must come in order in the indirect table, and the ID field is completely ignored. Also, the only valid flag for them is VIRTQ_DESC_F_WRITE, others are reserved and ignored by the device. As usual, the driver will notify the device if the conditions for the notification are met (step 4).

在打包布局中,缓冲区必须按顺序出现在间接表中,ID字段完全被忽略。另外,它们唯一有效的标志是VIRTQ_DESC_F_WRITE,其他的是保留的,被设备忽略。像往常一样,如果通知的条件得到满足,驱动程序将通知设备(步骤4)。

Diagram: Driver makes available a descriptor using a packed queue

For example, the driver would need to allocate this 48 bytes table for a 3 descriptors indirect table:

Address Length ID Flags
0x80000000 0x1000 W
0x81000000 0x1000 W
0x82000000 0x1000 W

Figure: Three descriptor long indirect packed table

And if it introduces the indirect table the first in the descriptor table, assuming it is allocated in 0x83000000 address:

Avail idx Address Length ID Flags Used idx
0x80000000 48 0 A|I

Figure: Drivers makes an indirect table available

After indirect buffer consumption, the device needs to return the indirect buffer id (0 in the example) in its used descriptor. The table looks like the return of the first buffer, except for the indirect (I) flag set:

Avail idx Address Length ID Flags Used idx
0x80000000 48 0 A|U|I

Figure: Device makes an indirect table used

After that, the device cannot access the memory table anymore unless the driver makes it available again, so the latter can free or reuse it.

Notifications: how to manage interruptions?

Like in the used queue, each side of the communication maintains two identical structures used for controlling notifications between the device and the driver. The driver’s one is read-only by the device, and the device’s one is read-only by the driver.

The struct layout is:

1
2
3
4
struct pvirtq_event_suppress { 
le16 desc;
le16 flags;
};

Listing: Event suppression struct notification

就像在用过的队列中,通信的每一方都维护着两个相同的结构,用于控制设备和驱动之间的通知。驱动程序的那个结构是设备只读的,而设备的那个结构是驱动程序只读的。

The member flags can take the values:

  • 0: Notifications are enabled
  • 1: Notifications are disabled
  • 2: Notifications are enabled for a specific descriptor, specified from the desc member.

If flags value is 2, the other side will notify until the wrap counter matches the most significant bit of desc and the descriptor placed in the position desc discarding that bit is made used/available. For this mode to work, VIRTIO_F_RING_EVENT_IDX flag needs to be negotiated in Virtio device initialization: feature bits.

None of these mechanisms are 100% reliable, since the other side could have sent the notification already when we set the values, so expect it even when disable.

Note that, since the descriptor ring size is not being forced to be a power of two (comparing with the split version), the notification structure can fit in the same page as the descriptor table. This can be advantageous for some implementations.

成员标志可以采取以下值。

  • 0: 通知被启用
  • 1: 通知被禁用
  • 2: 对一个特定的描述符启用通知,由desc成员指定。

如果标志值为2,另一方将进行通知,直到wrap计数器与desc的最重要的位相匹配,并且放置在desc位置的描述符放弃该位而被使用/可用。为了使这种模式工作,VIRTIO_F_RING_EVENT_IDX标志需要在Virtio设备初始化中协商:特征位。

这些机制都不是100%可靠的,因为当我们设置这些值时,对方可能已经发送了通知,所以即使在禁用的情况下也要期待它。

请注意,由于描述符环的大小没有被强制为2的幂(与分裂版本相比),通知结构可以与描述符表放在同一页面中。这对某些实现来说是有利的。

Summary

In this series we have taken you through the different virtio data plane layouts and its virtqueues implementations. They are the means for virtio devices and virtio drivers to exchange information.

We start by covering the simpler and less optimized split virtqueue layout. This layout is relatively easy to implement and to debug thus it’s a good entry point for learning the virtio dataplane basics.

We then moved on to the packed virtqueue layout specified in virtio 1.1 which allows requests exchange using a more compact descriptor representation. This avoids all the overhead of scattering the data through memory, avoiding cache contention and reducing the PCI transactions in case of actual hardware.

在这个系列中,我们已经带你了解了不同的virtio数据平面布局及其virtqueues的实现。它们是virtio设备和virtio驱动交换信息的手段。

我们首先介绍了更简单、更不优化的分离式virtqueue布局。这种布局相对容易实现和调试,因此它是学习virtio数据平面基础知识的一个很好的切入点。

然后,我们转向virtio 1.1中规定的打包式virtqueue布局,它允许使用更紧凑的描述符来交换请求。这避免了在内存中分散数据的所有开销,避免了缓存争用,并在实际硬件的情况下减少了PCI事务。

We also covered a number of optimizations on top of both ring layouts which depends on the communication/device type or how each part is implemented. Mainly, they are oriented to reduce the communication overhead, both in notifications and in memory transactions. Virtio offers a simple protocol to communicate what features and optimizations support each side, so they can agree on how the data is going to be exchanged and is highly future-proof.

我们还在这两个环状布局的基础上进行了一些优化,这取决于通信/设备类型或每个部分的实现方式。主要的是,它们的方向是减少通信开销,包括通知和内存事务。Virtio提供了一个简单的协议来沟通每一方支持哪些功能和优化,所以他们可以就数据的交换方式达成一致,并且是高度面向未来的。

This series covered the essence of the virtio data plane and provided you with the tool to analyze and develop your own virtio device and drivers. It should be noted that this series summarizes the relevant sections from the virtio spec thus you should refer to the spec for additional information and see it as the source of truth.

In the next posts we will return to vDPA including the kernel framework, hands on blogs and vDPA in Kubernetes.

这个系列涵盖了virtio数据平面的本质,并为你提供了分析和开发自己的virtio设备和驱动的工具。应该注意的是,这个系列总结了virtio规范中的相关部分,因此你应该参考规范以获得更多信息,并将其视为真理的来源。

在接下来的文章中,我们将回到vDPA,包括内核框架、实践博客和Kubernetes中的vDPA。

Virtqueues and virtio ring: How the data travels

This post continues where the “Virtio devices and drivers overview“ leaves off. After we have explained the scenario in the previous post, we are reaching the main point: how does the data travel from the virtio-device to the driver and back?

这篇文章继续 “Virtio设备和驱动概述 “的内容。在上一篇文章中,我们已经解释了这个场景,我们即将到达重点:数据如何从virtio设备到驱动,然后再返回?

Buffers and notifications: The work routine

As stated earlier, a virtqueue is just a queue of guest’s buffers that the host consumes, either reading them or writing to them. A buffer can be read-only or write-only from the device point of view, but never both.

如前所述,virtqueue只是一个guest的缓冲区队列,主机消耗它们,要么读取它们,要么写入它们。从设备的角度来看,一个缓冲区可以是只读的,也可以是只写的,但绝不是两者都是。

The descriptors can be chained, and the framing of the message can be spread whatever way is more convenient. For example, to spread a 2000 byte message in one single buffer or to use two 1000 byte buffers should be the same.

描述符可以是链状的,消息的构架可以以任何更方便的方式传播。例如,将2000字节的信息分散在一个单一的缓冲区中,或使用两个1000字节的缓冲区,应该是一样的。

Also, it provides driver to device notifications (doorbell) method, to signal that one or more buffers have been added to the queue, and vice-versa, devices can interrupt the driver to signal used buffers. It is up to the underlying driver to provide the right method to dispatch the actual notification, for example using PCI interruptions or memory writing: The virtqueue only standardizes the semantics of it.

另外,它还提供了驱动程序到设备的通知(门铃)方法,以信号显示一个或多个缓冲区已被添加到队列中,反之亦然,设备可以中断驱动程序以信号显示已使用的缓冲区。这取决于底层驱动程序提供正确的方法来调度实际的通知,例如使用PCI中断或内存写入。virtqueue只是对它的语义进行了标准化。

As stated before, the driver and the device can advise the other to not to emit notifications to reduce its dispatching overhead. Since this operation is asynchronous we will describe how to do so in further sections.

如前所述,驱动和设备可以建议对方不要发出通知,以减少其调度开销。由于这个操作是异步的,我们将在后续章节中描述如何做到这一点。

Split virtqueue: the beauty of simplicity

The split virtqueue format separates the virtqueue into three areas, where each area is writable by either the driver or the device, but not both:

  • Descriptor Area: used for describing buffers.
  • Driver Area: data supplied by driver to the device. Also called avail virtqueue.
  • Device Area: data supplied by device to driver. Also called used virtqueue.

split virtqueue格式将virtqueue分成三个区域,每个区域都可以被驱动或设备写入,但不能同时写入。

  • 描述符区:用于描述缓冲区。
  • 驱动区:由驱动提供给设备的数据。也称为利用虚拟队列。
  • 设备区:由设备提供给驱动的数据。也称为used virtqueue。

They need to be allocated in the driver’s memory for it to be able to access them in a straightforward way. Buffer addresses are stored from the driver’s point of view, and the device needs to perform an address translation. There are many ways for the device to access it depending on the latter nature:

  • For an emulated device in the hypervisor (like qemu), the guest’s address is in its own process memory.
  • For other emulated devices like vhost-net or vhost-user, a memory mapping needs to be done, like POSIX shared memory. A file descriptor to that memory is shared through vhost protocol.
  • For a real device a hardware-level translation needs to be done, usually via IOMMU.

它们需要被分配到驱动程序的内存中,以便它能够直接访问它们。缓冲区地址从驱动程序的角度存储,设备需要进行地址转换。根据后者的性质,设备有很多方法可以访问它。

  • 对于管理程序中的仿真设备(如qemu),客户的地址在它自己的进程内存中。
  • 对于其他仿真设备,如vhost-net或vhost-user,需要做一个内存映射,像POSIX共享内存一样。该内存的文件描述符是通过vhost协议共享的。
  • 对于一个真实的设备,需要做一个硬件级的转换,通常是通过IOMMU。

Shared memory with split ring elements

Descriptor ring: Where is my data?

The descriptor area (or descriptor ring) is the first one that needs to be understood. It contains an array of a number of guest addressed buffers and its length. Each descriptor also contains a set of flags indicating more information about it. For example, the buffer continues in another descriptor buffer if the 0x1 bit is set, and the buffer is write-only for the device if the bit 0x2 is set, and is read-only if it is clear.

描述符区(或描述符环)是第一个需要被理解的。它包含一个由若干客体寻址的缓冲区和其长度组成的数组。每个描述符还包含一组标志,表示关于它的更多信息。例如,如果0x1位被设置,缓冲区在另一个描述符缓冲区中继续,如果0x2位被设置,缓冲区对设备来说是只写的,如果它被清除,则是只读的。

This is the layout of a single descriptor. We will call leN for N bits in little endian format.

1
2
3
4
5
6
struct virtq_desc { 
le64 addr;
le32 len;
le16 flags;
le16 next; // Will explain this one later in the section "Chained descriptors"
};

Listing: Split Virtqueue descriptor layout

这是一个单一描述符的布局。我们将调用leN来表示little endian格式的N位。

Avail ring: Supplying data to the device

The next interesting structure is the driver area, or avail ring. Is the room where the driver places the descriptor (indexes) the device is going to consume. Note that placing a buffer here doesn’t mean that the device needs to consume immediately: virtio-net, for example, provides a bunch of descriptors for packet receiving that are only used by the device when a packet arrives, and are “ready to consume” until that moment.

下一个有趣的结构是驱动区,或者说Avail环。是驱动程序放置设备要消耗的描述符(索引)的空间。注意,在这里放置缓冲区并不意味着设备需要立即消费:例如,virtio-net为数据包接收提供了一堆描述符,这些描述符只有在数据包到达时才会被设备使用,直到那一刻才会 “准备消费”。

The avail ring has two important fields that only the driver can write and the device only can read them: idx and flags. The idx field indicates where the driver would put the next descriptor entry in the avail ring (modulo the queue size). On the other hand, the least significant bit of flags indicates if the driver wants to be notified or not (called VIRTQ_AVAIL_F_NO_INTERRUPT).

avail环有两个重要的字段,只有驱动程序可以写入,设备只能读取它们:idx和flags。idx字段指出了驱动程序将把下一个描述符条目放在avail ring中的位置(modulo the queue size)。另一方面,flags的最小有效位表示驱动是否要被通知(称为VIRTQ_AVAIL_F_NO_INTERRUPT)。

After these two fields, an array of integers of the same length as the descriptors ring. So the avail virtqueue layout is:

1
2
3
4
5
struct virtq_avail {
le16 flags;
le16 idx;
le16 ring[ /* Queue Size */ ];
};

Listing: Avail virtqueue layout

在这两个字段之后,是一个与描述符环相同长度的整数阵列。因此,avail virtqueue layout:

Figure 1 shows a descriptor table with a 2000 bytes long buffer that starts in position 0x8000, and an avail ring that still does not have any entry. After all the steps, a components diagram highlighting the descriptor area update. The first step for the driver is to allocate the buffer with the memory and fill it (this is the step 1 in the “Process to make a buffer available” diagram), and to make available on the descriptor area after that (step 2).

图1显示了一个具有2000字节长的缓冲区的描述符表,它从位置0x8000开始,而一个利用环仍然没有任何条目。在所有的步骤之后,一个组件图突出了描述符被更新的部分。驱动程序的第一步是分配缓冲区的内存并将其填满(这是 “使缓冲区可用的过程 “图中的第1步),然后在描述符区上使其可用(第2步)。

Figure 1: Driver writes a buffer in descriptor ring

After populating descriptor entry, driver advises of it using the avail ring: It writes the descriptor index #0 in the first entry of the avail ring, and updates idx entry accordly. The result of this is shown in Figure 2. In the case that supply chained buffers, only the descriptor head index should be added this way, and avail idx would increase only by 1. This is the step 3 in the diagram.

在填充完描述符条目后,驱动通知它使用空闲环。它将描述符的索引#0写在avail ring的第一个条目中,并相应地更新idx条目。其结果如图2所示。在提供链式缓冲区的情况下,只有描述符头部的索引应该这样添加,而avail idx只增加1。这就是图中的第三步。

Figure 2: Driver offers the buffer with avail ring

From now on, the driver should not modify the available descriptor or the exposed buffer at any moment: It is under the device’s control. Now the driver needs to notify the device if the latter has enabled notifications at that moment (more on how the device manages this later). This is the last step 4 in the diagram.

从现在开始,驱动程序不应该在任何时候修改可用的描述符或暴露的缓冲区。这是由设备控制的。现在,驱动程序需要通知设备,如果后者在当时启用了通知功能(后面会有更多关于设备如何管理的内容)。这就是图中的最后一步4。

Diagram: Process to make a buffer available

The avail ring must be able to hold the same number of descriptors as the descriptor area, and the descriptor area must have a size power of two, so idx wraps naturally at some point. For example, if the ring size is 256 entries, idx 1 references the same descriptor as idx 257, 513… And it will wrap at a 16 bit boundary. This way, neither side needs to worry about processing an invalid idx: They are all valid.

Avail环必须能够容纳与描述符区相同数量的描述符,描述符区的大小必须是2的幂,所以idx在某一点上自然会被包裹起来。例如,如果环的大小是256个条目,idx 1引用的描述符与idx 257、513…相同。而它将在16位边界处被包裹起来。这样一来,双方都不需要担心处理无效的idx。它们都是有效的。

Note that descriptors can be added in any order to the avail ring, one does not need to start from descriptor table entry 0 nor continue by the next descriptor.

请注意,描述符可以以任何顺序添加到利用环中,不需要从描述符表的第0条开始,也不需要从下一个描述符继续。

Chained descriptors: Supplying large data to the device

The driver can also chain more than one descriptor using its next member. If the NEXT (0x1) flag of a descriptor is set, the data continue in another buffer, making a chain of descriptors. Note that the descriptors in a chain do not share flags: Some descriptors can be read-only, and the others can be write-only. In this case, write-only descriptors must come after all write-only ones.

驱动程序也可以使用其下一个成员来连锁一个以上的描述符。如果一个描述符的NEXT(0x1)标志被设置,数据在另一个缓冲区中继续,形成一个描述符链。注意,一个链中的描述符不共享标志。有些描述符可以是只读的,而其他描述符可以是只写的。在这种情况下,只写的描述符必须排在所有只写的描述符之后。

For example, if the driver has sent us two buffers in a chain with descriptor table indexes 0 and 1 as first operation, the device would see the scenario in Figure 3, and it would be the step 2 again.

例如,如果驱动程序在描述符表索引为0和1的链中向我们发送了两个缓冲区,作为第一次操作,设备会看到图3中的情景,它将再次成为步骤2。

Figure 3: Device sees chained buffers

Used ring: When the device is done with the data

The device employs the used ring to return the used (read or written) buffers to the driver. As the avail ring, it has the flags and idx members. They have the same layout and serve the same purpose, although the notification flag is now called VIRTQ_USED_F_NO_NOTIFY.

设备使用使用过的环将使用过的(读或写)缓冲区返回给驱动。与avail环一样,它也有flags和idx成员。它们具有相同的布局和相同的目的,尽管通知标志现在被称为VIRTQ_USED_F_NO_NOTIFY

After them, it maintains an array of used descriptors. In this array, the device returns not only the descriptor index but also the used length in case of writing.

1
2
3
4
5
6
7
8
9
10
11
12
struct virtq_used {
le16 flags;
le16 idx;
struct virtq_used_elem ring[ /* Queue Size */];
};

struct virtq_used_elem {
/* Index of start of used descriptor chain. */
le32 id;
/* Total length of the descriptor chain which was used (written to) */
le32 len;
};

Listing: Used virtqueue layout

在它们之后,它维护一个已使用的描述符数组。在这个数组中,设备不仅返回描述符的索引,而且在写入的情况下返回已使用的长度。

In case of returning a chain of descriptors, only the id of the head of the chain is returned, and the total written length through all descriptors, not increasing it when data is read. The descriptor table is not touched at all, it is read-only for the device. This is step 5 in the “Process to make a buffer as used” diagram.

在返回描述符链的情况下,只返回链头的id,以及通过所有描述符的总写入长度,在读取数据时不增加它。描述符表完全不被触及,它对设备来说是只读的。这是 “制作使用的缓冲区的过程 “图中的第5步。

For example, if the device uses the chain of descriptors exposed in the Chained descriptors version:

例如,如果设备使用链式描述符版本中暴露的链式描述符:

Figure 4: Device returns buffer chain

Diagram: Process to mark a buffer as used

Lastly, the device will notify the driver if it sees that the driver wants to be notified, using the used queue flags to know it (step 6).

最后,如果设备看到驱动想被通知,它将通知驱动,使用使用的队列标志来知道它(步骤6)。

Indirect descriptors: supplying a lot of data to the device

Indirect descriptors are a way to dispatch a larger number of descriptors in a batch, increasing the ring capacity. The driver stores a table of indirect descriptors (the same layout as the regular descriptors) anywhere in memory, and inserts a descriptor in the virtqueue with the flag VIRTQ_DESC_F_INDIRECT (0x4) set. The descriptor’s address and length correspond to the indirect table’s ones.

间接描述符是一种在一个批次中调度更多描述符的方法,增加了环的容量。驱动程序在内存的任何地方存储一个间接描述符表(与普通描述符的布局相同),并在virtqueue中插入一个描述符,并设置标志VIRTQ_DESC_F_INDIRECT(0x4)。该描述符的地址和长度对应于间接表的长度。

If we want to add the chain described in section Chained descriptors to an indirect table, the driver first allocates the memory region of 2 entries (32 bytes) to hold the latter (step 2 in the diagram after allocate the buffers in the step 1):

Buffer Len Flags Next
0x8000 0x2000 W|N 1
0xD000 0x2000 W

Figure 4: Indirect table for indirect descriptors

如果我们想在一个间接表上添加链式描述符,驱动程序首先分配2个条目(32字节)的内存区域来容纳后者(图中的第2步,在第1步中分配了缓冲区之后)。

Let’s suppose it has been allocated on memory position 0x2000, and it is the first descriptor made available. As usual, the first step is to include it in the Descriptor area (step 3 in the diagram), so it would look like:

Descriptor Area
Buffer Len Flags Next
0x2000 32 I

Figure 5: Add indirect table to Descriptor area

让我们假设它被分配在内存位置0x2000,并且是第一个可用的描述符。像往常一样,第一步是把它纳入描述符区域(图中的第3步),所以它看起来像。

After that, the steps are the same as with regular descriptors: The driver adds the index of the descriptor marked with the flag in the descriptor area to the avail ring (#0 in this case, step 4 in the diagram), and notify the device as usual (step 5).

之后,步骤与普通描述符相同。驱动程序将描述符区域中标有标志的描述符的索引添加到利用环中(本例中为#0,图中第4步),并像往常一样通知设备(第5步)。

Diagram: Driver make available indirect descriptors

For the device to use its data, and would use the same memory addresses to return its 0x3000 bytes (all 0x8000-0x9FFF and 0xD000-0xDFFF) (Step 6 and 7, same as with regular descriptors). Once used by the device, the driver can release the indirect memory or do whatever it wants with it, as it could do with any regular buffer.

对于设备使用其数据,并将使用相同的内存地址来返回其0x3000字节(所有0x8000-0x9FFF和0xD000-0xDFFF)(步骤6和7,与常规描述符相同)。一旦被设备使用,驱动程序可以释放间接内存或对其做任何事情,就像它可以对任何常规缓冲区做的那样。

Diagram: Device mark the indirect descriptor as used

Descriptors with INDIRECT flag cannot have NEXT or WRITE flags set, so you cannot chain indirect descriptors in the descriptor table, and the indirect table can contain at maximum the same number of descriptors as the descriptor table.

带有INDIRECT标志的描述符不能设置NEXT或WRITE标志,所以不能在描述符表中连锁间接描述符,间接表最多可以包含与描述符表相同数量的描述符。

Notifications. Learning the “do not disturb” mode

In many systems used and available buffer notifications involve significant overhead. To mitigate it, each virtring maintains a flag to indicate when it wants to be notified. Remember that the driver’s one is read-only by the device, and the device’s one is read-only by the driver.

在许多系统中,使用的和可用的缓冲区通知涉及大量的开销。为了减轻它,每个virtring都维护着一个标志,以表明它什么时候想被通知。记住,驱动的那个是设备只读的,而设备的那个是驱动只读的。

We already know all of this, and its use is pretty straightforward. The only thing you need to take care of is the asynchronous nature of this method: The side of the communication that disables or enables it can’t be sure that the other end is going to know the change, so you can miss notifications or to have more than expected.

我们已经知道了这些,它的使用是非常直接的。你唯一需要注意的是这个方法的异步性。通信中禁用或启用它的一方不能确定另一端是否会知道这个变化,所以你可能会错过通知或要比预期的多。

A more effective way of notifications toggle is enabled if the VIRTIO_F_EVENT_IDX feature bit is negotiated by device and driver: Instead of disable them in a binary fashion, driver and device can specify how far the other can progress before a notification is required using an specific descriptor id. This id is advertised using a extra le16 member at the end of the structure, so they grow like this:

如果设备和驱动协商VIRTIO_F_EVENT_IDX特性位,就可以启用一种更有效的通知切换方式。而不是以二进制的方式禁用它们,驱动和设备可以使用一个特定的描述符id来指定对方在需要通知之前可以进展到什么程度。这个id在结构的末尾使用一个额外的le16成员进行宣传,所以它们的增长方式是这样的。

The struct layout is:

1
2
3
4
5
6
struct virtq_avail {              struct virtq_used {
le16 flags; le16 flags;
le16 idx; le16 idx;
le16 ring[ /* Queue Size */ ]; struct virtq_used_elem ring[Q. size];
le16 used_event; le16 avail_event;
}; };

Listing 3: Event suppression struct notification

This way, every time the driver wants to make available a buffer it needs to check the avail_event on the used ring: If driver’s idx field was equal to avail_event, it’s time to send a notification, ignoring the lower bit of used ring flags member (VIRTQ_USED_F_NO_NOTIFY).

这样一来,每次驱动程序想要提供一个缓冲区时,它需要检查已用环上的avail_event。如果驱动的idx字段等于avail_event,那么就是发送通知的时候了,忽略已用环标志成员的低位(VIRTQ_USED_F_NO_NOTIFY)。

Similarly, if VIRTIO_F_EVENT_IDX has been negotiated, the device will check used_event to know if it needs to send a notification or not. This can be very effective for maintaining a virtqueue of buffers for the device to write, like in the virtio-net device receive queue.

同样,如果VIRTIO_F_EVENT_IDX已经协商好了,设备将检查used_event以知道它是否需要发送通知。这对于维护一个供设备写入的缓冲区的虚拟队列非常有效,就像在virtio-net设备接收队列中一样。

In our next post, we’re going to wrap up and take a look at a number of optimizations on top of both ring layouts which depend on the communication/device type or how each part is implemented.

在我们的下一篇文章中,我们将总结并看看在这两个环形布局之上的一些优化,这些优化取决于通信/设备类型或每个部分的实现方式。

Virtio devices and drivers overview: The headjack and the phone

This three-part series will take you through the main virtio data plane layouts: the split virtqueue and the packed virtqueue. This is the basis for the communication between hosts and virtual environments like guests or containers.

这个由三部分组成的系列,将会带你了解virtio数据平面的布局,split virtuqueue 和 packed virtqueue。这是物理机与虚拟环境比如虚拟机或容器交流的基础。

One of the challenges when coming to explain these approaches is the lack of documentation and the many terms involved. This set of posts attempts to demystify the virtio data plane and provide you with a clear down to earth explanation of what is what.

在解释这些方法时,面临的挑战之一是缺乏文档和涉及的许多术语。这组文章试图揭开virtio数据平面的神秘面纱,并为你提供一个清晰的解释,说明什么是什么。

This is a technical deep dive and is relevant for those who are interested in the bits and bytes of things. It details the communication format between the different virtio parts and data plane protocols.

这是一个技术上的深入研究,与那些对事物的比特和字节感兴趣的人有关。它详细介绍了不同virtio部件和数据平面协议之间的通信格式。

While further extensions, optimizations and features are being added to both virtqueue versions, to improve performance and to simplify implementation, the core of the virtqueue operations remains the same. This is because it has been designed with extensibility in mind.

虽然两个版本的virtqueue都加入了进一步的扩展、优化和功能,以提高性能和简化实现,但virtqueue操作的核心仍然保持不变。这是因为它在设计时就考虑到了可扩展性。

Packed virtqueue, which complements the split virtqueue has been merged in the virtio 1.1 spec, and successfully implemented in both emulated devices (qemu, virtio_net, dpdk) and physical devices.

作为split virtqueue的补充,packed virtqueue已被合并到virtio 1.1规范中,并在模拟设备(qemu、virtio_net、dpdk)和物理设备中成功实现。

We’ll start with an overview of the virtio device, drivers and their data plane interaction. Then we’ll move on to explain the details of the split virtqueue ring layout. This is followed by an overview of the packed ring layout and the advantages it brings over the split virtqueue approach.

我们将首先概述virtio设备、驱动程序和它们的数据平面互动。然后,我们将继续解释split virtqueue ring layout的细节。随后,我们将概述packed ring layout以及它比split virtqueue方法带来的优势。

Virtio devices and drivers overview: who is who

This section provides a brief overview of the virtio devices, virtio drivers, examples of the different architectures you can use and the different components. If you’re already familiar with these topics or you have already followed the virtio networking series you can jump directly to the next section focusing on the virtio rings.

本节简要介绍了virtio设备、virtio驱动、你可以使用的不同架构的例子以及不同的组件。如果你已经熟悉了这些主题,或者你已经关注了virtio网络系列,你可以直接跳到下一节,重点介绍virtio rings。

Virtio devices: In and out the virtual world

A virtio device is a device that exposes a virtio interface for the software to manage and exchange information. It can be exposed to the emulated environment using PCI, Memory Mapping I/O (Just to expose the device in a region of memory) and S/390 Channel I/O. Part of the communication needs to be delegated to theses, like device discovery.

Virtio设备是一个暴露出virtio接口的设备,供软件管理和交换信息。它可以使用PCI、内存映射I/O(只是在内存的一个区域暴露设备)和S/390通道I/O暴露在仿真环境中。部分通信需要委托给这些设备,如设备发现。

Its main task is to convert the signal from the format they have outside of the virtual environment (the VM, the container, etc) to the format they need to be exchanged through the virtio dataplane and vice-versa. This signal could be real (for example the electricity or the light from a NIC) or already virtual (like the representation the host has from a network packet).

它的主要任务是将信号从它们在虚拟环境(虚拟机、容器等)之外的格式转换成它们需要通过virtio数据线交换的格式,反之亦然。这个信号可以是真实的(例如来自网卡的电或光),或者已经是虚拟的(如主机从网络包中得到的表示)。

The virtio interface consist of the following mandatory parts (virtio1.1 spec):

  • Device status field
  • Feature bits
  • Notifications
  • One or more virtqueues

Now we’ll provide additional details to each of these parts and how the device and driver starts communicating using these.

virtio接口由以下强制性部分组成(virtio1.1规范):

  • 设备状态字段
  • 特征位
  • 通知
  • 一个或多个虚拟队列

现在我们将提供这些部分的额外细节,以及设备和驱动如何使用这些部分开始通信。

Device status field: Is everything ok?

The device status field is a sequence of bits the device and the driver use to perform their initialization. We can imagine it as traffic lights on a console, each part set and clear each bit indicating their status.

设备状态字段是设备和驱动程序用来执行其初始化的一个比特序列。我们可以把它想象成控制台上的交通灯,每个部分设置和清除每个位,表示它们的状态。

The guest or the driver set the bit ACKNOWLEDGE (0x1) in the device status field to indicate that it acknowledges the device, and the bit DRIVER (0x2) to indicate an initialization in progress. After that, it starts a feature negotiation using the feature bits (more on this later), and sets bit DRIVER_OK (0x4) and FEATURES_OK (0x8) to acknowledge the features, so communication can start. If the device wants to indicate a fatal failure, it can set bit DEVICE_NEEDS_RESET (0x40), and the driver can do the same with bit FAILED (0x80).

访客或驱动程序在设备状态字段中设置位ACKNOWLEDGE(0x1)以表示它确认了设备,并设置位DRIVER(0x2)以表示初始化正在进行。之后,它开始使用功能位进行功能协商(后面会详细介绍),并设置位DRIVER_OK(0x4)和FEATURES_OK(0x8)来确认功能,这样就可以开始通信了。如果设备想指示一个致命的故障,它可以设置位DEVICE_NEEDS_RESET (0x40),而驱动程序可以用位FAILED (0x80)做同样的事情。

The device communicates the location of these bits using transport specific methods, like PCI scanning or knowing the address for MMIO.

设备使用传输的特定方法来传达这些位的位置,如PCI扫描或知道MMIO的地址。

Feature bits: Setting the communication agreement points

Device’s feature bits are used to communicate what features it supports, and to agree with the drivers about what of them will be used. These can be device-generic or device-specific. As an example of the first case, a bit can acknowledge if the device supports SR-IOV or what memory mode can be used. An example of the second case can be the different offloads it can perform, like checksumming or scatter-gather If the device is a network interface.

设备的功能位用于交流它支持哪些功能,并与驱动程序商定将使用其中哪些功能。这些位可以是设备通用的,也可以是设备特定的。作为第一种情况的一个例子,一个比特可以确认设备是否支持SR-IOV或者可以使用什么内存模式。第二种情况的一个例子是,如果设备是一个网络接口,它可以执行不同的卸载,如校验和或散点收集。

After the device initialization exposed in the previous section, the former reads the feature bits the device offers, and sends back the subset that it can handle. If they agree on them, the driver will allocate and inform about the virtqueues to the device, and all other configuration needed.

在上一节暴露的设备初始化之后,前者会读取设备提供的功能位,并发回它能处理的子集。如果他们达成一致,驱动程序将分配和通知设备的虚拟队列,以及所有其他需要的配置。

Notifications: You have work to do

Devices and drivers must notify that they have information to communicate using a notification. While the semantic of these is specified in the standard, the implementation of these are transport specific, like a PCI interruption or to write to a specific memory location. The device and the driver needs to expose at least one notification method. We will expand on this later in future sections.

设备和驱动程序必须通知他们有使用通知的信息进行通信。虽然这些的语义是在标准中规定的,但这些的实现是特定于传输的,比如PCI中断或写到一个特定的内存位置。设备和驱动程序需要公开至少一个通知方法。我们将在以后的章节中对此进行阐述。

One or more virtqueues: The communication vehicles

A virtqueue is just a queue of guest’s buffers that the host consumes, either reading them or writing to them, and returns to the guest. The current memory layout of a virtqueue implementation is a circular ring, so it is often called the virtring or vring.

They will be the main topic of the next section, Virtqueues and virtio ring, so at this moment is enough with that definition.

virtqueue只是一个guest缓冲区的队列,主机消耗这些缓冲区,要么读取它们,要么写入它们,然后返回给客体。目前virtqueue实现的内存布局是一个圆形的环,所以它通常被称为virtring或vring。

它们将是下一节的主要话题,即virtqueues和virtio ring,所以此刻有了这个定义就足够了。

Virtio drivers: The software avatar

The virtio driver is the software part in the virtual environment that talks with the virtio device using the relevant parts of the virtio spec.

Generally speaking, its virtio control plane tasks are:

  • Look for the device
  • To allocate shared memory in the guest for the communication

Start it using the protocol in Virtio devices.

virtio驱动是虚拟环境中的软件部分,它使用virtio规范的相关部分与virtio设备对话。

一般来说,其virtio控制面的任务是。

  • 寻找设备
  • 在客户中为通信分配共享内存

使用virtio设备中的协议启动它。

Devices and drivers interaction: The scenarios

In this section we are going to locate each virtio networking element (device, driver, and how the communication works) in three different architectures, to provide both a common frame to start explaining the virtio data plane and to show how adaptive it is. We have already presented these elements in past posts, so you can skip this section if you are a virtio-net series reader. On the other hand, if you have not read them, you can use them as a reference to understand this part better.

在这一节中,我们将把每个virtio网络元素(设备、驱动和通信如何工作)放在三个不同的架构中,以提供一个共同的框架来开始解释virtio数据平面,并展示它的适应性。我们已经在过去的文章中介绍了这些元素,所以如果你是virtio-net系列的读者,你可以跳过这一部分。另一方面,如果你没有读过这些文章,你可以把它们作为参考来更好地理解这一部分。

In Introduction to virtio-networking and vhost-net we showed the environment in which qemu created an emulated net device and offered it to the guest’s virtio-net driver. In this environment, the driver notifications are routed from whatever method is exposed to guests (usually, PCI) to KVM interruptions that stop the guest’s processor and return the control to the host (vmexit). Similarly, the device notifications are a special ioctl the host can send to the KVM device (vCPU IRQ). QEMU can access virtqueue information using the shared memory.

在介绍virtio-networking和vhost-net时,我们展示了qemu创建一个模拟的net设备并将其提供给客户的virtio-net驱动程序的环境。在这个环境中,驱动程序的通知从任何暴露给客体的方法(通常是PCI)被路由到KVM中断,停止客体的处理器并将控制权返回给主机(vmexit)。同样地,设备通知是主机可以向KVM设备发送的特殊ioctl(vCPU IRQ)。QEMU可以使用共享内存访问virtqueue信息。

Please note the implications of the virtio rings shared memory concept: The memory the driver and the device access is the same page in RAM, they are not two different regions that follow a protocol to synchronize.

请注意virtio环的共享内存概念的含义。驱动程序和设备访问的内存是RAM中的同一个页面,它们不是两个不同的区域,它们遵循一个协议来进行同步。

Figure 1: Qemu emulated device component diagram

Since the notification now needs to travel from the guest (KVM), to QEMU, and then to the kernel for the latter to forward the network frame, we can spawn a thread in the kernel with access to the guest’s shared memory mapping and then let it handle the virtio dataplane.

由于通知现在需要从guest(KVM)到QEMU,再到内核,以便后者转发网络帧,我们可以在内核中生成一个线程,访问客体的共享内存映射,然后让它处理virtio数据平面。

In that context, QEMU initiates the device using the virtio dataplane, and then forwards the virtio device status to vhost-net, delegating the data plane to it. In this scenario, KVM will use an event file descriptor (eventfd) to communicate the device interruptions, and expose another one to receive CPU interruptions. The guest does not need to be aware of this change, it will operate as the previous scenario.

在这种情况下,QEMU使用virtio数据平面启动设备,然后将virtio设备状态转发给vhost-net,将数据平面委托给它。在这种情况下,KVM将使用一个事件文件描述符(eventfd)来传达设备中断,并公开另一个文件描述符来接收CPU中断。guest不需要意识到这种变化,它将像之前的方案一样操作。

Also, in order to increase the performance, we created an in-kernel virtio-net device (called vhost-net) to offload the data plane directly to the kernel, where packet forwarding takes place:

另外,为了提高性能,我们创建了一个内核内的virtio-net设备(称为vhost-net),将数据平面直接卸载到内核,在那里进行数据包转发。

Figure 2: Virtio-net components diagram

Later on, we moved the virtio device from the kernel to an userspace process in the host (covered in the post “A journey to the vhost-users realm”) that can run a packet forwarding framework like DPDK. The protocol to set all this up is called virtio-user.

后来,我们把virtio设备从内核移到了主机的用户空间进程中(在 “通往vhost-users领域的旅程 “一文中有所涉及),该进程可以运行像DPDK这样的包转发框架。设置这一切的协议被称为virtio-user。

Figure 3: Virtio-user components diagram

It even allows guests to run virtio drivers in guest’s userland, instead of the kernel! In this case, virtio names driver the process that is managing the memory and the virtqueues, not the kernel code that runs in the guest.

它甚至允许客户在客户的用户区运行virtio驱动,而不是在内核中运行 在这种情况下,virtio将驱动程序命名为管理内存和virtqueues的进程,而不是在guest中运行的内核代码

Figure 4: Virtio-user with userland driver in guest

Lastly, we can directly do a virtio device passthrough with the proper hardware. If the NIC supports the virtio data plane, we can expose it directly to the guest with proper hardware (IOMMU device, able to translate between the guest’s and device’s memory addresses) and software (for example, VFIO linux driver, that enables the host to directly give the control of a PCI device to the guest). The device uses the typical hardware signals for notifications infrastructure, like PCI and CPU interruptions (IRQ).

最后,我们可以通过适当的硬件直接进行virtio设备透传。如果网卡支持virtio数据平面,我们可以通过适当的硬件(IOMMU设备,能够在guest和设备的内存地址之间进行转换)和软件(例如,VFIO linux驱动,使主机能够直接将PCI设备的控制权交给guest)将其直接暴露给guest。该设备使用典型的硬件信号来通知基础设施,如PCI和CPU中断(IRQ)。

If a hardware NIC wants to go this way, the easiest approach is to build its driver on top of vDPA, also explained in earlier posts of this series.

如果硬件网卡想走这条路,最简单的方法是在vDPA的基础上构建它的驱动程序,在本系列的早期文章中也有解释.

Figure 5: Virtio hardware passthrough components diagram

We will explain what happens inside of the dataplane communication in the rest of the posts.

我们将在接下来的文章中解释数据平面通信内部发生了什么。

Thanks to the deep investment in standardization, the virtio data plane is the same in whatever way we use across these scenarios, and whatever transport protocol we use. The format of the exchanged messages are the same, and different devices or drivers can negotiate different capabilities or features based on its characteristics using the feature bits, previously mentioned. This way, the virtqueues only act as a common thin layer of device-driver communication that allows to reduce the investment of development and deployment.

由于对标准化的深入投资,virtio数据平面在这些场景中,无论我们使用什么方式,无论我们使用什么传输协议,都是一样的。交换的消息的格式是相同的,不同的设备或驱动程序可以根据它的特点,使用前面提到的特征位,协商不同的能力或特征。这样一来,虚拟队列只是作为设备-驱动程序通信的一个普通薄层,可以减少开发和部署的投资。

As stated on previous blogs on this series, the interest of this standardization is to achieve a slim layer of communication with the virtual environment (instead of emulating a complete piece of hardware), that makes it easier to verify for correctness across different virtualization technologies or hardware.

正如本系列的前几篇博客所述,这种标准化的兴趣在于实现与虚拟环境的薄层通信(而不是模拟一个完整的硬件),这使得在不同的虚拟化技术或硬件之间验证正确性更加容易。