14个版本 (破坏性更新)

0.12.0 2024年3月25日
0.10.0 2023年11月7日
0.9.0 2023年7月4日
0.7.1 2023年2月20日
0.1.0 2021年12月22日

#28操作系统 中排名

Download history 4864/week @ 2024-04-23 4054/week @ 2024-04-30 3897/week @ 2024-05-07 4294/week @ 2024-05-14 4690/week @ 2024-05-21 4320/week @ 2024-05-28 4656/week @ 2024-06-04 4882/week @ 2024-06-11 3874/week @ 2024-06-18 4403/week @ 2024-06-25 4335/week @ 2024-07-02 5418/week @ 2024-07-09 5519/week @ 2024-07-16 4754/week @ 2024-07-23 4758/week @ 2024-07-30 4441/week @ 2024-08-06

每月下载量20,373
21 个crate(18个直接) 中使用

Apache-2.0 OR BSD-3-Clause

360KB
9K SLoC

virtio-queue

virtio-queue crate提供virtio队列、virtio描述符以及此类描述符链的virtio设备实现。规范中定义了两种virtio队列格式:分片virtqueues和打包virtqueues。virtio-queue crate仅支持分片virtqueues格式。virtio-queue API的目的是供virtio设备实现(如块设备或vsock设备)使用。主要的抽象是Queue。此外,crate还定义了队列的状态对象,即QueueState

用法

让我们以使用MMIO总线如何与队列一起工作的具体示例来说明。

首先,重要的是要提到virtio接口的强制部分如下

  • 设备状态字段 → 提供设备初始化例程完成步骤的指示,
  • 特性位 → 驱动程序/设备理解(的)特性,
  • 通知,
  • 一个或多个virtqueues → 驱动程序和设备之间数据传输的机制。

每个virtqueue由三个部分组成

  • 描述符表,
  • 可用环,
  • 已用环。

在启动虚拟机(VM)之前,虚拟机管理程序(VMM)执行以下设置

  1. 使用队列构造函数初始化队列数组。
  2. 将设备注册到MMIO总线,以便驱动程序可以稍后从/向MMIO空间发送读写请求,其中一些请求还设置了队列的状态。
  3. 其他启动前配置,例如为分配给设备的中断注册fd,该fd将稍后被设备用于通知驱动程序它有信息要通信。

VM启动后,驱动程序开始发送读写请求来配置以下内容

  • 支持的功能;
  • 队列参数。以下设置器用于队列设置
    • set_size → 用于设置队列的大小。
    • set_ready → 配置队列到 ready for processing 状态。
    • set_desc_table_addressset_avail_ring_addressset_used_ring_address → 配置队列组成部分的虚拟机地址。
    • set_event_idx → 作为 virtio-device 框架中特性协商的一部分被调用,并启用或禁用 VIRTIO_F_RING_EVENT_IDX 特性。
  • 设备激活。作为此激活的一部分,设备还可以为设备创建一个队列处理器,稍后可用于处理队列。

一旦队列就绪,就可以使用设备。

virtio设备的稳态操作遵循一个模型,其中驱动程序产生描述符链,由设备消耗,并且双方都需要在将新元素放置在关联的环上时通知对方,以避免忙等待。精确的通知机制留给了包含设备和队列的VMM(通常涉及MMIO vm退出和向虚拟机注入中断)。队列实现不关心使用的通知机制,并公开了从外部调用以响应通知事件的方法和功能(例如迭代器)。

使用virtqueue进行数据传输

设备/驱动程序使用队列的基本原理如下,以下图中也有显示

  1. 当虚拟机驱动程序有新的请求(缓冲区)时,它为缓冲区在描述符表中分配空闲描述符,并按需链接。
  2. 驱动程序在可用环条目中添加一个新条目,其中包含描述请求的描述符链的头部索引。
  3. 驱动程序将 idx 增加(新条目的)数量,图中显示了只有一个新条目的简单用例。
  4. 如果未抑制此类通知,则驱动程序将向设备发送可用缓冲区通知。
  5. 设备将在某个时刻消耗该请求,首先通过从可用环读取 idx 字段来实现。这可以直接通过 Queue::avail_idx 实现,但我们不建议将此功能提供给框架的消费者,因为它已经在所有可用描述符链头部迭代器后面被调用。
  6. 设备获取与读取 idx 值相对应的描述符链(的)索引。
  7. 设备从描述符表读取相应的描述符。
  8. 设备通过使用 Queue::add_used 在已用环中添加一个新条目;该条目在规范中定义为 virtq_used_elem,在 virtio-queue 中定义为 VirtqUsedElem。这个结构体包含描述符链的索引以及作为响应请求写入内存的字节数。
  9. 设备增加已用环中的 idx,这是上述提到的 Queue::add_used 操作的一部分。
  10. 如果未禁用此类通知,则设备会将已用缓冲区通知发送给驱动程序。

queue

一个描述符存储四个字段,前两个字段 addrlen 指向描述符引用的内存中的数据,如下面的图所示。 flags 字段用于指示缓冲区是否可读或可写,或者是否在后面有另一个描述符链(VIRTQ_DESC_F_NEXT 标志设置)。 next 字段在 VIRTQ_DESC_F_NEXT 设置时存储下一个描述符的索引。

descriptor

设备实现的要求

  • 可以使用来自 virtio-queue 的抽象,如 DescriptorChain,来解析设备提供的描述符,这些描述符代表设备 I/O 的输入或输出内存区域。描述符本质上是一对(地址,长度)对,随后由设备模型操作使用。我们不检查描述符的有效性,而是期望在设备实现尝试访问相应的区域时进行验证。早期检查会增加不可忽视的额外成本,并且完全依赖它们可能导致检查到使用的时间竞争条件。
  • 在读取/写入缓冲区之前,设备应验证它是否可读/可写。

设计

QueueT 是一个 trait,它允许为单线程上下文和多线程上下文实现不同的 Queue 对象。在 virtio-queue 中提供的实现是

  1. Queue → 用于单线程上下文。
  2. QueueSync → 用于多线程上下文,它只是一个包装在 Arc<Mutex<Queue>> 中的包装器。

除了上述抽象之外,virtio-queue crate 还提供了以下抽象

  • Descriptor → 主要提供对 Descriptor 成员的访问器。
  • DescriptorChain → 提供对 DescriptorChain 成员的访问器和一个 Iterator 实现以迭代 DescriptorChain,还有一个抽象用于迭代仅可读或仅可写的设备描述符(DescriptorChainRwIter)。
  • AvailIter - 是队列中所有可用描述符链头的消耗性迭代器。

保存/恢复队列

Queue 允许通过 state 函数保存状态,该函数返回一个 QueueState。可以使用 QueueState::try_from 使用之前保存的状态创建 Queue 对象。VMM 应在从先前保存的状态恢复 Queue 时检查错误。

通知抑制

《virtio-queue》库的大部分代码都由通知抑制支持组成。正如前面所述,当可用环中出现新条目时,驱动程序可以将可用缓冲区通知发送到设备,当使用环中出现新条目时,设备可以将使用缓冲区通知发送到驱动程序。在某些情况下,每次这些场景发生时发送通知可能并不高效,例如,当驱动程序正在处理使用环时,它就不需要接收另一个使用缓冲区通知。抑制通知的机制在规范的下文中详细介绍。

《Queue》抽象提出了以下步骤序列以处理新的可用环条目

  1. 设备首先禁用通知,通过使用Queue::disable_notification让驱动程序知道它正在处理可用环,并且不想被打断。设备通过以下任一条件禁用通知:未协商VIRTIO_F_EVENT_IDX,且在使用的环的flags字段中设置了VIRTQ_USED_F_NO_NOTIFY,或者已协商VIRTIO_F_EVENT_IDX,且avail_event值未更新,即它保持为已由驱动程序通知的可用环的最新idx值。
  2. 设备通过使用AvailIter迭代器处理新条目。
  3. 设备现在可以通过使用Queue::enable_notification启用通知。设备通过以下任一条件启用通知:未协商VIRTIO_F_EVENT_IDX,且在使用的环的flags字段中设置了0,或者已协商VIRTIO_F_EVENT_IDX,且avail_event值设置为未由驱动程序通知的可用环中最小的idx值。这样,设备可以确保不会错过任何通知。

应该循环执行上述步骤,以处理驱动程序在我们重新启用通知之前添加新条目的不太可能的情况。

在驱动程序端,《Queue》提供了needs_notification方法,每次设备将新条目添加到使用环时都应使用它。根据used_event值和最后使用的值(signalled_used),needs_notification返回true,以让设备知道它应该向虚拟机发送通知。

假设

我们假设《Queue》实现的使用者不会在检查ready位是否设置之前尝试使用队列。这可以通过调用Queue::is_valid来验证,除了这个之外,它还检查三个队列部分是否是有效的内存区域。我们假设消费者仅在单线程环境中使用AvailIter::go_to_previous_position。我们假设用户将从文档中推荐的方式从可用环中消费条目,即设备开始处理可用环条目,禁用通知,处理条目,然后重新启用通知。

许可证

此项目受以下任一许可证的许可:

依赖关系

~1-1.7MB
~32K SLoC