快捷搜索:  汽车  科技

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)描述符区域(或描述符ring)是需要了解的第一个区域。它包含一个由多个guest寻址的缓冲区及其长度的数组。每个描述符还包含一组标志,用于指示有关该标志的更多信息。例如,如果将0x1位置1,则该缓冲区在另一个描述符缓冲区中继续;如果将0x2位置1,则该缓冲区对于设备仅写;如果清零,则该缓冲区为只读。带有分割环元素的共享内存如前所述,驱动程序和设备可以建议对方不要发出通知以减少其调度开销。由于此操作是异步的,因此我们将在后续部分中介绍如何执行此操作。拆分式virtqueue格式将virtqueue分为三个区域,其中每个区域可由驱动程序或设备写入,但不能同时由两个区域写入:需要在驱动程序的内存中分配它们,以便它可以直接访问它们。从驱动程序的角度存储缓冲区地址,并且设备需要执行地址转换。设备有多种访问方式,具体取决于后者的性质:

这篇文章继续到“virtio设备和驱动概述”的结尾处。在解释了上一篇文章中的场景之后,数据如何从virtio设备传递到驱动程序并返回?

缓冲区和通知:

如前所述,virtqueue只是host使用的guest缓冲队列(读取或写入它们)。从设备的角度来看,缓冲区可以是只读的,也可以是仅写的,但不能两者兼有。

描述符可以链接起来,并且可以通过任何更方便的方式来扩展消息的框架。例如,在一个缓冲区中传播2000字节的消息或使用两个1000字节的缓冲区应该是相同的。

此外,它还提供了驱动程序到设备的通知(门铃)方法,以表示已将一个或多个缓冲区添加到队列中,反之亦然,设备可以中断驱动程序以通知已使用的缓冲区。由底层驱动程序决定提供正确的方法来分发实际的通知,例如使用PCI中断或内存写入:虚拟队列仅使它的语义标准化。

如前所述,驱动程序和设备可以建议对方不要发出通知以减少其调度开销。由于此操作是异步的,因此我们将在后续部分中介绍如何执行此操作。

拆分式virtqueue:简约之美

拆分式virtqueue格式将virtqueue分为三个区域,其中每个区域可由驱动程序或设备写入,但不能同时由两个区域写入:

  • 描述符区域:用于描述缓冲区。
  • 驱动程序区域:驱动程序提供给设备的数据。也被称为avail virtqueue。
  • 设备区域:设备提供给驱动程序的数据。也称为used virtqueue。

需要在驱动程序的内存中分配它们,以便它可以直接访问它们。从驱动程序的角度存储缓冲区地址,并且设备需要执行地址转换。设备有多种访问方式,具体取决于后者的性质:

  • 对于虚拟机管理程序中的仿真设备(例如qemu),guest的地址位于其自己的进程内存中。
  • 对于其他仿真设备(例如vhost-net或vhost-user),需要完成内存映射,例如POSIX共享内存。通过vhost协议共享该内存的文件描述符。
  • 对于真实设备,通常需要通过IOMMU完成硬件级别的转换。

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(1)

带有分割环元素的共享内存

描述符ring:数据在哪里?

描述符区域(或描述符ring)是需要了解的第一个区域。它包含一个由多个guest寻址的缓冲区及其长度的数组。每个描述符还包含一组标志,用于指示有关该标志的更多信息。例如,如果将0x1位置1,则该缓冲区在另一个描述符缓冲区中继续;如果将0x2位置1,则该缓冲区对于设备仅写;如果清零,则该缓冲区为只读。

这是单个描述符的布局。我们将以小端格式N位称为leN。

struct virtq_desc { le64 addr; le32 len; le16 flags; le16 next; //稍后将在“链接的描述符”部分中说明这一点 };可用环:向设备提供数据

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

可用环具有两个重要字段,只有驱动程序可以写入,而设备只能读取它们: idx和flags。idx字段指示驱动程序将下一个描述符条目放在可用环中的位置(以队列大小为模)。另一方面,标志的最低有效位指示是否希望通知驱动(称为VIRTQ_AVAIL_F_NO_INTERRUPT)。

在这两个字段之后,是一个长度与描述符相同的整数数组。因此,可用的virtqueue布局为:

struct virtq_avail { le16 flags; le16 idx; le16 ring [/ *Queue Size* /]; };

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

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(2)

图1:驱动程序在描述符环中写入一个缓冲区

填充描述符条目后,驱动程序使用avail环建议它:它将描述符索引#0写入到avail环的第一个条目中,并一致地更新idx条目,其结果如图2所示。在供应链缓冲区的情况下,仅应以这种方式添加描述符头索引,并且可用idx仅增加1。这是图中的步骤3。

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(3)

图2:驱动程序提供带可用环的缓冲区

从现在开始,驱动程序不应在任何时候修改可用的描述符或公开的缓冲区:它在设备的控制之下。现在,驱动程序需要通知设备该设备是否已在此时启用通知(有关设备稍后如何管理此消息的更多信息)。这是该图中的最后步骤4。

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(4)

图3:使缓冲区可用的过程

可用环必须能够容纳与描述符区域相同数量的描述符,并且描述符区域的大小必须为2的幂,因此idx在某些时候自然地换行。例如,如果环大小为256个条目,则idx 1引用与idx 257、513相同的描述符...并且它将环绕16位边界。这样,双方都无需担心处理无效的idx:它们都是有效的。

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

链接描述符

驱动程序还可以使用其下一个成员链接多个描述符。如果设置了描述符的NEXT(0x1)标志,则数据在另一个缓冲区中继续,从而形成了描述符链。请注意,链中的描述符不共享标志:一些描述符可以是只读的,而其他描述符可以是只写的。在这种情况下,只写描述符必须位于所有只写描述符之后。

例如,如果驱动程序已将具有描述符表索引0和1的两个缓冲区发送给我们作为第一个操作,则设备将看到图4中的场景,这将再次成为步骤2。

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(5)

图4:设备看到链接的缓冲区

used ring:设备处理完数据后

设备使用used ring将已使用(读取或写入)的缓冲区返回给驱动程序。作为有效环,它具有flags和idx成员。它们具有相同的布局,并且具有相同的用途,尽管现在将通知标记称为VIRTQ_USED_F_NO_NOTIFY。

在它们之后,它维护一个使用过的描述符数组。在此数组中,设备不仅返回描述符索引,还返回写入时使用的长度。

struct virtq_used { le16 flags; le16 idx; struct virtq_used_elem ring [/ *Queue Size* /]; }; struct virtq_used_elem { / *used descriptor链的开始索引。* / le32 id; / *used(写入)的描述符链的总长度* / le32 len; };

在返回描述符链的情况下,仅返回链头的id,并返回所有描述符的总写入长度,而不是在读取数据时增加它的长度。对于设备是只读的,描述符表根本没有被触及。这是“处理used缓冲区的过程”图6中的第5步。

例如,如果设备使用在链接描述符版本中公开的描述符链:

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(6)

图5:设备返回缓冲链

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(7)

图6:将缓冲区标记为已使用的过程

最后,设备将使用used队列标志来通知驱动是否知道要通知驱动(步骤6)。

间接描述符

间接描述符是一种批量分配大量描述符的方法,可以增加环的容量。驱动程序在内存中的任何位置存储一个间接描述符表(与常规描述符的布局相同),并在virtqueue中插入一个设置了VIRTQ_DESC_F_INDIRECT (0x4)标志的描述符。描述符的地址和长度与间接表的地址和长度相对应。

如果我们要将链式描述符部分中描述的链添加到间接表中,驱动程序将首先分配2个条目(32个字节)的内存区域来保存后者(在步骤1中分配缓冲区后,图7中的步骤2)。 :

buffer

len

flags

next

0x8000

0x2000

W | N

1个

0xD000

0x2000

w

...

假设它已分配在内存位置0x2000,并且它是第一个可用的描述符。像往常一样,第一步是将其包括在描述符区域(图7中的第3步),因此它看起来像:

描述符区域

buffer

len

flags

next

0x2000

32

|

...

之后步骤与常规描述符相同:驱动程序将在描述符区域中标记有标志的描述符的索引添加到可用环(在此情况下为#0,在图中为步骤4),并通知设备照常操作(第5步)。

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(8)

图7:驱动程序提供了间接描述符

为了使设备能够使用其数据,并使用相同的内存地址来返回其0x3000字节(0x8000-0x9FFF和0xD000-0xDFFF)(步骤6和7,与常规描述符相同)。一旦设备使用了驱动程序,驱动程序就可以释放间接内存或对其进行任何处理,就像处理任何常规缓冲区一样。

数据预处理的数据的特点(virtqueue和virtioring数据如何传播)(9)

图8:设备将间接描述符标记为已使用

带INDIRECT标志的描述符不能设置NEXT或WRITE标志,因此您不能在描述符表中链接间接描述符,并且间接表最多可以包含与描述符表相同数量的描述符。

通知

在许多系统中,已使用和可用的缓冲区通知涉及大量开销。为了缓解这种情况,每个侵权行为都会维护一个标志,以指示何时希望收到通知。请记住,驱动的驱动程序对设备是只读的,而设备的驱动程序对驱动程序是只读的。

我们已经知道了所有这些,并且它的用法非常简单。您唯一需要注意的是此方法的异步性质:禁用或启用它的通信端无法确保另一端会知道更改,因此您可能会错过通知或超出预期。

如果VIRTIO_F_EVENT_IDX功能位是由设备和驱动程序协商的,则启用通知切换得更有效方式:驱动程序和设备可以使用特定的描述符ID来指定在需要通知之前,其他驱动程序可以前进多远,而不是以二进制方式禁用它们。 此ID在结构的末尾使用额外的le16成员进行广告,因此它们的增长如下:

结构布局为:

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; }; };

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

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

猜您喜欢: