这是这系列文章的第二篇,我们第一篇粗略看了看Virtio的数据结构,接下来我们大概来看看VIrtio设备具体的数据流转流程,看看究竟数据在device端和driver端是如何交互的。
前面有文章已经讲了virtQueue的具体数据结构,不了解的大家可以打开看看。
聊聊VirtIO的数据结构 —— Split virt queue
我们这里先不讨论通知的方式,单独讨论数据以及数据结构的流转,通知的方式我们后面单开文章再说。
device提供avail过程的总结
其实整体的流程不是特别复杂,我们以Virtio1.0及以下的Split 模式的Virt queue来看看队列具体是如何进行工作的。
总结起来其实就以下几步:
- driver提供buffer,放到desc table之中(如果是chain模式的话,可能是多个buffer组成的chain,简化来看我们不用管它)
- 更新avail ring, driver 将已经放入buffer的desc ring的idx信息(如果是chain模式,则是chain的头部的idx)放入avail ring中
- 以上步骤1和步骤2可能执行多次
- driver需要提供一个内存屏障来保证device能够在以上步骤之后正确取到desc ring中的信息和avail ring中的desc idx信息。
- 更新avail ring的header中的idx, driver尝试更新avail ring中的idx(与上述2步骤区别,2步骤更新的是desc ring中的信息和avail ring中的信息,这里更新的是avail ring的header中的idx!)
- driver提供内存屏障,保证device在检查通知抑制的时候avail ring的header中的idx已经更新了
- 发送notify到device, driver发送notify到device,通知设备已经有包的buffer来了可以用了哥们儿。(当然中断抑制不能是开的)
driver回used的过程
数据路径上,不讨论通知的形式的时候,其实设备写used是相对简单的一个流程。
检查avail ring的idx,去desc table中对应的idx找到实际的地址以及len信息,取到对应的buffer或者数据。完成之后通知driver即可。(通知的流程这篇文章我们先不讨论,后面再细说)
avail更新的详细的流程叙述
1. driver提供bufffer
buffer可能是由一个或者多个元素组成。我们假设一个buffer的元素b
- 获取下一个可用的desc table中的entry,我们称为d
- d.addr指向我们的buffer b的开头
- d.len设置为我们buffer b的长度
- 如果b是设备可写的,那么去设置一下VIRTQ_DESC_F_WRITE这个flag,否则设置为0
- 如果接下来还有buffer:
5.1 d.next指向下面的buffer的开头
5.2 设置VIRTQ_DESC_F_NEXT这个表示位
如果d.next设置了,那么就是chain模式了,以链表的形式串联了一堆buffer。
2. 更新avail ring
之前我们已经看过了avail ring的数据结构:
struct vring_avail {
__virtio16 flags;
__virtio16 idx;
__virtio16 ring[q_size];
};
这里更新的是
avail->ring[avail->idx % qsz] = head;
注意,未更新的avail->idx指向的是下一次可使用ring的idx。
3. 更新idx
这里才会去更新idx,这里和第二步之间需要有内存屏障强保序
avail->idx += added;