virtio系列-split virtqueue数据流

本文详细解读了virtqueue的工作原理,包括buffers的分配与消费、driver/device间的通信机制,特别是splitvirtqueue的结构和优化,以及indirect descriptors的使用,重点介绍了notifications的优化策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

virtio split virtqueue数据流

基本概念解释

buffers和notifications:
一个virtqueue的buffer是由guest来分配,host来消费的,host可以读和写这些buffer。
一个buffer只能是只读或者只写的,但是不能读和写同时存在。
buffer的描述符可以是通过指针链接起来的,即indriect descriptor,通过将消息扩展会更加方便,例如在一个buffer中放入和2000字节的数据和使用两个1000字节的数据buffer存放效果是一样的。

driver,device互相通知的方式:
virtqueue只规定了语义,并没有规定实现方式。
device->driver一般通过中断,driver->devcie通过notifications。
driver和device还提供了一种优化特性,抑制对方的通知来减少不必要的通知损耗。

split virtqueue

split virtqueue将virtqueue分为三个部分(又称为table, virtqueue),每一部分都是一个环形buffer,每一部分可以被driver或者device写,但是不会允许双方都可写。规范中并没有要求这三部分是物理连续的,但是为了实现方便,实际的驱动中是将三个部分连续存放的。

descriptor Area:用来描述buffer,下面两部分需要根据索引查找该表才能找到实际的buffer
Driver Area: driver生产的data,device来消费,被称为avail virtqueue,该表只有driver可写
Device Area: device生产的data,driver来消费,该表只有device可写

virtqueue需要在driver中申请,因为需要双方都可以访问,没有比driver中申请管理更直接的方式了。buffer的地址直接存放在driver中,而device需要通过地址转换gpa->hva才能访问这些buffer。device有很多方式可来访问,取决于接入的方式:

  1. qemu中的仿真device,guest的地址一定位于qemu进程内的
  2. 对于其他的仿真设备,像vhost-net, vhost-user,需要通过内存共享映射来完成,例如POSIX shared memory,使用文件描述符通过vhost协议来共享这块内存
  3. 对于一个真实的硬件设备,需要一个硬件级别的地址转换,通常是IOMMU
    在这里插入图片描述

descriptor area

它实际上是一个环形的buffer,又可以被称为descriptor ring,是第一个需要理解的ring。它包括一个数组,数组中每一项的元素包括指向guest buffer的地址和长度。另外每一个desc还包含一些其他信息。例如这个desc指向的不是真实的buffer而是一组desc时需要标为INDIRECT。如果这组buffer标记位device只写,设置WRITE,反之如果只读则清楚WRITE。

下面是desc的结构:

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

avail area

它也是一个环形buffer, 又被称为avail vring。driver需要给device提供数据,而数据的元数据存放在descriptor ring中,所以此时填入其中的是descriptor ring中的index,device根据index去desc ring中找到对应的desc,然后获得地址信息:gpa和len,最后再转换成hva来消费这些数据。

注意,driver放置这些buffer并不意味着device需要立即消费他们
例如virtio-net提供了一些buffer,当有网络包到来时将网络数据放到这些buffer中,此时device才算消费了这些buffer。

avail ring有两个重要的域:idx和flags。 idx指向下一个driver可用的desc ring index。flags的最低位表明driver是否需要中断通知VIRTQ_AVAIL_F_NO_INTERRUPT。在这两个域之后,是和desc ring相同长度的数组,其中存放的是descriptor ring中的index。

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

图1展示了descriptor table中指向了一块地址为 0x8000,长度为2000字节的buffer,此时descriptor已经有desc指向了这块区域,不过avail ring还没有任何的数据。
在这里插入图片描述
第一步:driver分配内存;第二步需要更新并填充desc指向这块buffer。

在填充完descriptor entry之后,driver需要发布这块desc到avail ring中。它将desc index 0写入avail ring数组的第一项,之后更新idx域。结果如下图2。如果提供了chained buffer时,只需要将descriptor的头写入avail 的数组中,avail idx只需要加1,和添加一个desc的情况处理是相同的。
在这里插入图片描述
现在,driver不应该再修改avail idx,desc以及指向的buffer了,他们现在已经归属device管理了。现在driver需要通知device,如果需要通知则发送notifications,如下图中的step4。
在这里插入图片描述

avail ring中需要处理所有的descriptor ring中的desc,所以他们的数组项一定是相同的。descriptor ring中项数是2的次方,所以在到达ring尾部时可以自动回绕。如果ring的size是256,idx 1, 257,513…引用的是相同的desc,这样两方都不必困扰会遇到无效的idx。

注意:
desc可以以任何的顺序添加到avail ring中,不必要一定从idx 0开始或者从上一次结束的位置开始。实际上使用时通常是从0或上一次结束的位置开始下一个desc,除非乱序有什么必然的好处。

chained descriptor

driver可以通过chained descriptor一次性提供给device多个buffer区域。如果desc flags的NEXT域置位,表示之后还有desc,下一个idx存放在NEXT中,通过next链接起来多个desc形成一个链,直到desc flags.NEXT没有置位。

注意chain desc并不共享flags:一部分buffer可以是只读,一部分buffer可以是只写的。在这种情况下,只写的buffer放在只读的buffer后面。

下图中,driver需要通知给device两个buffer,分别放在了desc 0和1中,随后更新avail idx和ring[], idx只是加1,ring[0]指向desc 0位置处。
在这里插入图片描述

Used ring

device通过used ring来将消费过的buffer返回给driver。和avail ring一样,它同样也有flags和idx,布局和用途相同,不需要通知的flags现在叫VIRTQ_USED_F_NO_NOTIFY.
在这之后,它也维护了一个used descriptor数组,在这个数组项,device需要返回descriptor的idx,如果buffer是device可写的还需要返回长度。

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

在使用chained descriptor时,也时只需要返回chain desc 的头位置,还有所有desc已经写的长度,不包括device只读数据的长度。device并不会修改descriptor table,它只会读/写buffer。这就是下图中的step 5
在这里插入图片描述

在这里插入图片描述
最终,通过avail flag检查driver是否需要通知,需要则通过约定方式通知driver(一般是中断)

Indirect descriptors

上面的普通或者chained方式,一个 descriptor desc只能存储一块buffer的元数据,当提供大量的小块buffer给device时势必需要很多的desc,这样就要求virtqueue size往往按照最坏的情景来申请资源,会多占点内存。所以就增加了一个indirect descriptor,相当于原来的一级指针数据扩充成了二级指针数据,间接地扩充了vring的长度。
driver可以额外申请一个indriect descriptor的表,它的使用方式和virtqueue中的descriptor是完全相同的,然后在virtqueue desc中插入indirect descriptor的信息,和普通的buffer基本相同,只是需要标记flag VIRTQ_DESC_F_INDIRECT来表明它指向的是一个indirect descriptor table而不是普通的buffer,desc的长度对应了indirect table本身所占的长度。
如果我们想要在indirect table中使用chain descriptor,如下图中我们需要插入两个buffer,首先driver需要首先申请indirect table的空间,从0x2000处申请到了合适的空间,即两个desc,更新desc指向buffer基址;indirect desc 0还需要标记flag NEXT并且next域指向下一个desc的索引1

BufferLenFlagsNext
0x80000x2000W|N1
0xD0000x2000W

之后跟新descriptor table中的项,buffer地址指向indirect table地址0x2000,len为indirect table长度,flags标记上INDIRECT。

BufferLenFlagsNext
0x200032I

在此之后,和正常的操作相同,更新avail idx和avail ring[0],然后通知device
在这里插入图片描述

device使用indirect+ chained buffer时,将会根据descriptor flags获取indirect table,再从indirect table中遍历chained descriptor拿到总共0x3000长度的buffer( 0x8000-0x9FFF 和 0xD000-0xDFFF))。一旦device通过used ring通知已经消费完后,driver就可以释放indirect table,indirect生命周期到此结束。

在这里插入图片描述

descriptor如果被标记了INDIRECT,它指向的不再是普通的buffer而是buffer的元数据,所以不可以再标记WRITE flags。同时也不能再标记NEXT标记,目前只支持在indirect table中存放chained descriptor而不支持将indirect table像普通buffer一样形成chained descriptor. 同时,单个indirect table中存放的desc数目不能超过queue size,它只是间接地扩大了queue地size,但是一次性超越了queue size可能会导致device或者driver出现问题。

Notifications优化

在许多系统中,used和avail buffer地通知会有明显的损耗。为了缓解这个问题,virtqueue都提供了一项特性来抑制通知:告知对方自己是否希望被通知。driver提供的flag只能被device读,反之亦然。
需要注意的是,这种方式是异步的,device/driver一方disable或者enable时,另一方并不会直到这个事件,此时你有可能会错过通知。所以目前这种方式基本上不再被使用。
另外一种有效的方式是通过driver和device协商使用VIRTIO_F_EVENT_IDX,不再是disable通知而是指定一个notification index来暗示对方何时需要发送notification,这个index位于ring的尾部,ring实际的布局是:

struct virtq_avail {             
  le16 flags;                    
  le16 idx;                        
  le16 ring[ /* Queue Size */ ];
  le16 used_event;                
}; 
 struct virtq_used { 
   le16 flags;
   le16 idx;  
   struct virtq_used_elem ring[Q. size];
   le16 avail_event;
};

这样,driver每次发布一个avail buffer时都检查used ring中的avail_event: 如果driver的idx域等于avail_event,需要发送notification到device,此时会忽略used ring的VIRTQ_USED_F_NO_NOTIFY标志。
同样的,如果driver支持VIRTIO_F_EVENT_IDX ,device每次发一个used buffer都会检查avail ring中的used_event.
这种方式可以减少无意义的notification带来的损耗。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值