前面两篇文章我们一起看了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太快,需要进行通知。