聊聊Virtio的前后端通知

前面两篇文章我们一起看了Virtio的数据结构设计以及工作的模式,接下来我们需要看到一块稍微复杂一些的设计,即前后端的Notify。

VIRTIO_F_EVENT_IDX

这个标志位在协商和未协商的时候,前后端的通知方式是不同的,我们分开来看,假设这个时候我们还是在收包的流程中,驱动侧有一个可用的buffer可以提供给设备侧使用,我们看看这个buffer的提供是如何通知给设备侧的。

1. 未协商VIRTIO_F_EVENT_IDX标志位

驱动侧:

avail ring的flags可以设置为0或者1
driver可以将flags设置为1来告诉设备不需要进行通知

设备侧:

如果flags读到是1,那么设备不允许给driver发送通知
如果flags读到是0,那么设备必须给driver发送通知

2. 协商了VIRTIO_F_EVENT_IDX标志位

协商了VIRTIO_F_EVENT_IDX标志位的情况下,avail ring的数据结构和used ring的数据结构都会有相应改变。
其中avail ring

struct virtq_avail {
#define VIRTQ_AVAIL_F_NO_INTERRUPT 1
	le16 flags;
	le16 idx;
	le16 ring[ /* Queue Size */ ];
	le16 used_event; /* Only if VIRTIO_F_EVENT_IDX */
};

used ring

struct virtq_used {
#define VIRTQ_USED_F_NO_NOTIFY 1
	le16 flags;
	le16 idx;
	struct virtq_used_elem ring[ /* Queue Size */];
	le16 avail_event; /* Only if VIRTIO_F_EVENT_IDX */
};

可以看到在数据结构的末尾多了一个used_event和avail_event
这个域非常关键,对于自身,他是只读的,对于对端,他是可写的。
上面这句是什么意思呢?

简单举例一下,
guest会将一个used ring的idx更新到avail_ring末尾的used_event域中,host中的avail ring idx在到达这个buffer时会给guest一个通知。
同样,host也会将一个avail ring的idx放到used ring末尾的avail event中,告诉guest,如果回写这个buffer的时候记得kick给我。

注意: idx和event需要区分开,idx是下一个可以消费(回写)的desc编号,而avail_event/used_event是已经完成的编号。这里可能有一个1的差距嗷!

dive into kernel,看看实现

我们在linux里面看看具体是如何实现的
virtqueue_kick_prepare_split函数是在split模式下准备kick工作,即guest回写used时,判断是否需要通知avail ring的一方。

static bool virtqueue_kick_prepare_split(struct virtqueue *_vq)
{
	struct vring_virtqueue *vq = to_vvq(_vq);
	u16 new, old;
	bool needs_kick;

	START_USE(vq);
	/* We need to expose available array entries before checking avail
	 * event. */
	virtio_mb(vq->weak_barriers);

	old = vq->split.avail_idx_shadow - vq->num_added;
	new = vq->split.avail_idx_shadow;
	vq->num_added = 0;

	LAST_ADD_TIME_CHECK(vq);
	LAST_ADD_TIME_INVALID(vq);

	if (vq->event) {
		needs_kick = vring_need_event(virtio16_to_cpu(_vq->vdev,
					vring_avail_event(&vq->split.vring)),
					      new, old);
	} else {
		needs_kick = !(vq->split.vring.used->flags &
					cpu_to_virtio16(_vq->vdev,
						VRING_USED_F_NO_NOTIFY));
	}
	END_USE(vq);
	return needs_kick;
}

我们着重看后面几个
vq->event其实是在之前判断的VIRTIO_F_EVENT_IDX标志位是否使能

未使能VIRTIO_F_EVENT_IDX

如果未使能,那么走下面那个分支,首先需要看的是VRING_USED_F_NO_NOTIFY这个域,表明是否可以通知。接着我们去看看used ring中的flags域,如果为0,且那么needs kick为1,即可以通知,如果flags为1,不允许通知,与我们上述的描述是相同的。

使能VIRTIO_F_EVENT_IDX

咱们看的东西就多了,首先是这几行

	old = vq->split.avail_idx_shadow - vq->num_added;
	new = vq->split.avail_idx_shadow;
	vq->num_added = 0;

avail_idx_shadow在内核中的注释是

	/*
	 * Last written value to avail->idx in
	 * guest byte order.
	 */
	 u16 avail_idx_shadow;

也就是上次avail_idx的值
而num_added是我们上次同步后新增的值

	/* Number we've added since last sync. */
	unsigned int num_added;

接下来我们看两个宏定义:
vring_need_event和``

/* The following is used with USED_EVENT_IDX and AVAIL_EVENT_IDX */
/* Assuming a given event_idx value from the other side, if
 * we have just incremented index from old to new_idx,
 * should we trigger an event? */
static inline int vring_need_event(__u16 event_idx, __u16 new_idx, __u16 old)
{
	/* Note: Xen has similar logic for notification hold-off
	 * in include/xen/interface/io/ring.h with req_event and req_prod
	 * corresponding to event_idx + 1 and new_idx respectively.
	 * Note also that req_event and req_prod in Xen start at 1,
	 * event indexes in virtio start at 0. */
	return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);
}
#define vring_avail_event(vr) (*(__virtio16 *)&(vr)->used->ring[(vr)->num])

avail_event看起来相对比较简单,别怕那么多杂七杂八的符号,其实就是取了used_ring最后那个域里面avail_event的值。
重点其实是need_event的计算。
这里其实形参中已经把new和old给拿进来了,还相对好看,简单来说就是在
new - avail_event - 1 < new - old的时候, 发送kick通知
我们再简化一下如上的判断,
当我们从另一端更新idx时是否应该触发通知呢?
取决于旧的idx —— old和新的idx —— new 以及之前存进来的avail_event的值。
event_idx较大时,如上不等式成立,返回true,说明后端消费buffer太快,需要进行通知。

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值