I/O 虚拟化(二):Virtio 协议代码走读

以 Dragonball virtiofs 为例

《I/O 虚拟化(一):Virtqueue 介绍》是从宏观视角解释了 virtqueue 的概念和工作原理,但是不看看代码总觉得不够踏实。这篇文章将会以 Dragonball 项目的 virtiofs 设备 为蓝本,介绍下通用的 virtio 协议在 device 侧(VMM 侧)的实现细节,尽量屏蔽 virtiofs 设备的细节。至于 driver 侧(内核侧)本质上就是 device 侧的反逻辑,如果后面有时间了再单独写一篇文章。

在 driver 准备好数据后,通过 irqfd[1] 通知 device 侧接收数据,在接收到中断信息之后,Dragonball 侧的逻辑主要由 VirtioFsEpollHandler::process() 函数实现。

VirtioFsEpollHandler 的 config 字段的 queues 字段,其类型是 Vec<VirtioQueueConfig<Q>>,其定义如下所示。QueueSync 就是对 Queue 类型的线程安全封装,它们的定义我都列了在下方。这里的 Queue 类型实际上是对 virtqueue 的一种描述,真正的数据需要根据这些描述信息去内存中获取。

前面说过了 VirtioFsEpollHandler::process() 是处理来自 guest 的 events 的主要逻辑,该函数获取了 VirtioQueueConfig 数组的长度(名为 queues_len)。

Event 携带的 data(events.data())表明了本次通知的类型,在 virtiofs 中类型主要有以下几种:

  • Unknown(直接报错)
  • Rate limiter event
  • Patch rate limiter event
  • Queue avail event(在 virtiofs 下 slot 可以理解为 idx,假设有 2 个 queues,那么 slot == 0 就说明第一个 queue 有信息)

我们重点关照的是最后一个 queue avail event,这个事件表示某个 virtqueue 有数据等待被 VMM 处理。首先取走对应 queue 的 eventfd 事件,参见 queue[idx].consume_event(),但直接忽略了 eventfd 传来的值(为什么??那要这个 eventfd 的作用是什么?)。随后调用 process_queue() 处理 queue 中的数据,包含了与 avail vring 和 used vring 的交互。

process_queue() 的功能是遍历 avail vring 获取来自 guest 的请求,处理完后将数据放回 used vring 中。首先获取 avail ring 中可读取的数据,调用 queue_guard.iter(mem.clone()) 返回了一个 avail queue 的迭代器 AvailIter。为了便于理解先提一嘴 mem,的类型是 <AS as GuestAddressSpace>::T(这部分参见 rust-vmm/vm-memory,不在这篇文章中赘述),在 GuestAddressSpace::T 的定义是 Clone + Deref<GuestAddressSpace::M>,所以 mem 是可以被克隆的,而且解引用可以成为 GuestAddressSpace::M 类型(访问 guest 内存)。AvailIter 定义如下所示,其中 last_index 是表示环形队列的 tail,这个是读取 virtq_avail.idx 得到的(见 avail_idx()),next_avail 表示的队列的 head,从 Queue::next_avail 中获取。

Avail ring 存储了 queue 的 flags(u16)和 idx(u16),queue 是一个环形队列(见下图),存的是 descriptor table 的 index(u16),遍历的内容该环形队列的 head 到 tail 这个区间,然后在拿着 index 访问 descriptor table。因此 AvailIter 实现了 Iterator trait 用于遍历 DescriptorChain,每一次循环会获取 index 对应的 desc_chain

下面这段代码说明了是如何获得 DescriptorChain 这个对象的。Offset 就是 2 个 u16 偏移加 next_avail 乘以 2 bytes(u16 长度),这个是 index 的地址,index 的值需要从这个地址读取出来(head_index),最后会将 head_index 包装进 DescriptorChain 对象中。总结一下,假设有一个名为 avail_iterAvailIter 对象,那么调用 avail_iter.iter() 每循环一次就会获取一个新的 DescriptorChain 对象。

Reader 对象是将 desc_chain 中的 descriptors 的 flag 是 read_only 的区域(一小片连续内存)连起来。会调用 Reader::from_descriptor_chain() 获取数据,注意这个方法只会获取当前 index 对应的 desc_table item,有的数据比较大,会通过 next 字段连接多个 items,但是他们都是属于同一个 index(同一个 desc_chain)对应的区域。这个方法最终的功能是把 desc_table 包含的 flag 是 write_only 的数据区域转化为一个 buffer,并将其包装到 Reader 对象中。

Writer 对象是将 desc_chain 中的 descriptors 的 flag 是 read_only 的区域连起来(除了权限其他的与 Reader 保持一致)。(想到一个场景是:假如没有一个 descriptor 是 read-only,这时再写数据要怎么办?或者长度不够的时候又要怎么办?)

handle_message() 是 virtiofs 的私有逻辑了,这里不同的设备(virtio-net、virio-mem 等)处理的逻辑都不尽相同。Virtiofs 支持的操作包括 Init、Write、Open 等等操作磁盘 I/O 的逻辑。总之就是从 Reader 中读取从 device 传来的数据,然后将结果写入到 Writer 的 buffer 中通过 virtqueue 回传给 device。

最后 device 会将这个 index 对应的 desc_chain 添加到 used_queue 中,其实主要流程也和上面读取 avail_queue 差不多。区别之一是 device 向 used_queue 中写入,而非读取。第二个就是 used queue 的数据类型是 virtq_used_elem,除了 index 以外还有一个字段表示长度,我猜测这可以让 driver 迅速回收掉没有用到的空间。

References

  1. https://zhuanlan.zhihu.com/p/547777878
All rights reserved
Except where otherwise noted, content on this page is copyrighted.