virtio系列-packed virtqueue

11 篇文章 12 订阅

virtio packed virtqueue

spilt virtqueue因其简约的设计而备受欢迎,但是它有一个基本的问题:avail, used ring 是分离的,cpu cache miss的概率比较大,从硬件角度来看意味着每个descriptor的读写操作都需要几个PC事务。packed virtqueue通过将三个ring合并到一起来改善这个问题.不过这种方式看起来非常复杂,远不如split virtqueue简约。在split版本中如果认识到在获取driver提供的数据之后device可以丢弃或者重写覆写这块buffer区域,基本就能理解packed virtqueue的实现了。

在virtio设备初始化阶段,split virtqueue和packed virtqueue的过程是相同的:协商feature, 申请virtqueue,packed virtqueue的布局如下:

struct virtq_desc { 
        le64 addr;
        le32 len;
        le16 id;
        le16 flags;
};

id域不再是device寻找buffer的索引了,它只是一个对driver有意义的值。
driver同时维护了一个内部的bit位表示ring的回绕情况,初始化为1.每次回绕都会反转该位,我们称为wrapper_count,只能为0和1。
和split 中相同,第一步是更新addr,len, id, flags域。但是packed增加了两个新的flag:AVAIL(0x7) and USED(0x15)。为了标记一个desc为avail,driver必须置位AVAIL(0x7)为它的回绕标志,对于used是相反的。
一个二进制标记很容易实现,但是它会阻碍一些又用的优化,后面会讲。

通过下面的例子讲packed 数据是如何流转的.
driver现在有一个device可写的buffer:起始于0x80000000 ,长度为0x1000。之后更新desc指向这块buffer, flags中AVAIL位和wrapper_count,系统初始化为1,所以目前flags位:W|A, 整个desc如下:

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10000W|A

注意,avail idx和used idx列只是为了展示状态,他们并不在descriptor table中,只是device和driver的内部状态,每一端都需要直到需要下一次该从哪里读或者写,同时device还需要追踪driver的wrapper_counter.

最后,driver需要通知device,如step3.
如图中所示,没有avail,used ring,只有一个descriptor table.
在这里插入图片描述

和driver一样,device内部也维护一个wrapper_counter,初始化为1,同时它假设driver内的wrapper count是初始状态是1。当device第一次寻找driver发布的buffer时,它会需找ring中的第一满足条件的desc:flag.AVAIL, flag.USED等于driver内部的wrapper counter.
当device消费完数据,通知driver时,和split used ring一样,需要更新写入数据的长度,id为消费的desc id。最后device会设置flag.AVAIL和USED,和它内部维护的wrapper counter相同。
如下图,descriptor table如下。当flag.AVAIL == flag.USED == device.wrapper_counter时,device知道这个buffer已经返回给driver.返回的地址不重要,重要的是id。

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10000W|A|U

在这里插入图片描述

当driver使用完descriptor table之后,会反转内部的wrapper_counter。所以在第二轮,再次设置avail desc时就完全不一样了,flags.AVAIL = 0并且flags.USED=1 表示avail,同样的device会意识到这种情况。让我们看一个例子:

如果我们descriptor table中有两个desc时,driver内部的wrapper_counter初始化为1,并且填充了两个avail buffer后,driver反转它内部的wrapper_counter,此时为0,此时descriptor table如下表:

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10000W|A
0x810000000x10001W|A

在此之后,device意识到descritor id 0 和 id 1都已经是可用状态: flags.AVAIL 和 它内部的wrapper_counter相同,并且没有设置 flags.USED. 如果device使用了desc id 1的buffer,descriptor table如下标,注意此时desc id 0仍然属于desc.

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10001W|A|U
0x810000000x10001W|A

现在,driver 看到desc id 1已经被使用过了,因为 flags.AVAIL == flags.USED == device.wrapper_counter.
如果device 继续使用了desc id 0,descriptor table就如下表:

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10001W|A|U
0x810000000x10000W|A|U

但是更有趣的是仍然乱序使用,device目前只使用desc id 1, driver 看到desc id 1已经被使用,driver再次使用了 desc id 1作为avail buffer, 如下图,两个id都是 1,而且device都可用

Avail idxAddressLengthIDFlagsUsed idx
0x810000000x10001W|(!A)|U
0x810000000x10001W|A

注意:在上次的wrap之后,driver需要反转它的wrapper_counter和used flag.当device 回绕寻找avail buffer时,它需要寻找这种符合条件的desc,很明显, index 1 的 desc id 1符合条件,之后回绕之后反转wrapper_counter, 此时index 0 和 index 1 的 buffer都可用,此时buffer id 0和 id 1都属于device,它可以再次决定首先使用desc id 1.

Chained descriptors

链式的descriptor工作方式:在desc中没有 next 域来指向下一个 desc 的索引,因为 下一个 desc一定会在它的下一个位置。但是在split used ring中, 只需要返回chain desc的头部,根据next 就可以找到后面所有的 desc; 在packed virtqueue中需要返回 尾部的desc.

回到used ring,每次我们使用chained descriptors,used idx都会滞后于avail idx. 在一次性传输多个avail desc给device,我们只会在avail ring中放一个头部的 idx, device 使用完之后也只会返回一个头部的idx . 在split ring中,这个不是问题,但是在packed virtqueue中这个就会引入一个问题。

最直接的方法是,device 标记 chained desc中的每一个desc为 USED。 但是这种方式代价比较大,因为我们修改的是共享内存,会引入cache颠簸性问题。
但是, driver 已经知道这个chain, 它可以忽略最后一个id之前的所有的chain desc,这也就是我们为什么需要比较used/avail 和driver/device wrapper_counter: 在跳转后,我们不知道下一个 desc是否是可用的,或者
如下表, 我们一个desc table中有4个desc, driver通过chain desc发布了三个avail desc:

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10000W|A
0x810000000x10001W|A
0x820000000x10002W|A
0

在此之后, device 发现index 0之后的desc是可用的,然后将其标记为used,然后发布给driver: 只在 index 0位置处写入了flags.USED, 并且 id = 2; 当driver 查看是否已经被使用,它将会跳过index 1 和 2, driver知道这3个desc是chained 关系。

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x10002W|A|U
0x810000000x10001W|A
0x820000000x10002W|A
0

现在 driver 发布了另外带有2 个desc的chained desc, 在这个过程中它会反转内部的wrapper,状态如下表:

Avail idxAddressLengthIDFlagsUsed idx
0x810000000x10001W|(!A)|U
0x810000000x10001W|A
0x820000000x10002W|A
0x800000000x10000W|A

之后,device 使用这2个desc的chained desc, 现在只需要更新chained desc中的第一个: id = 0, flags.AVAIL = flags.USED = device.wrapper_counter;

Avail idxAddressLengthIDFlagsUsed idx
0x810000000x10001W|(!A)|U
0x810000000x10001W|A
0x820000000x10002W|A
0x800000000x10000W|A|U

尽管index 0位置处的desc看起来像是avail的:flags.AVAIL != flags.USED, 但是device通过判断它的flags组合知道它不是,正确的标志应该是flags.AVAIL = 0, flags.USED = 1;

Indirect descriptors

indirect descriptor工作机制和split virtqueue非常相似。首先driver 分配一个 indirect desc 的空间,它和普通的packed virtqueue desc中的布局是完全相同的。 之后设置每一个indirect desc指向的buffer信息,如下图中的step 1-2; 最终在desc 中设置,指向indirect desc table,并且标记上 VIRTQ_DESC_F_INDIRECT ,如下图step 3,起始地址和长度对应于indirect desc table的信息。

在这里插入图片描述

在packed virtqueue中, buffer的顺序完全依赖于indirect desc table中的顺序,在indirect desc table中 id 是完全没有意义的,唯一有意义的flag是VIRTQ_DESC_F_WRITE。 driver 通知 device的条件和普通的packed virtqueue是相同的。

假如我们要发布三个buffer,通过indirect table,首先需要申请三个desc空间的indirect desc table,长度为48字节,更新indirect table项指向三个buffer:

Avail idxAddressLengthIDFlagsUsed idx
0x800000000x1000W
0x810000000x1000W
0x820000000x1000W

假如indirect table的地址在 0x83000000 处,我们需要更新desc table, 第一个可用的位置为index 0:

Avail idxAddressLengthIDFlagsUsed idx
0x83000000480A|I

当indirect buffer被消费后,device 将会在index 0 返回 id 0, flags.USED. desc表和普通的packed desc表基本相似,只是flags.INDIRECT被置位了。

Avail idxAddressLengthIDFlagsUsed idx
0x80000000480A|U|I

在此之后,device就不能再访问这个indirect table除非driver将其再次发布为avail状态,此时driver可以释放这块内存或者重用它。

Notifications

和split virtqueue一样, device/driver 每一端都维护了一个相同的结构来控制对方发送通知给自己。driver维护的对于device来说是只读的,同样的,device维护的对于driver来说也是只读的。

struct pvirtq_event_suppress { 
        le16 desc;
        le16 flags; 
};

flags的值的意义:
0: 通知总是enabled的
1:通知总是disable的
2: 当desc满足条件时再通知
如果flags == 2, 另一方判断是否通知的条件:

  1. 内部的wrapper counter == desc的最高位
  2. desc的index == desc & 0x7fff
    这种方式通过VIRTIO_F_RING_EVENT_IDX来呈现,需要device/driver都支持并且在初始化过程中协商好;
    这些机制都不时100%可靠的,因为当我们设置这些值时可能对方已经发送了notifications, 如果真的不想被通知,那就直接禁止它吧。
    注意,packed virtqueue的size并不一定要求是2的次方,所以notifications结构可以和desc一同放在同样的一个页内,这样在某些实现上有优势。
  • 0
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值