virtio-ring与virtio-queue

virtio_ring与virtio_queue

参考链接:https://blog.csdn.net/weixin_43836778/article/details/103777170,并在此基础上做了整合.

注意有virtio_ring.c但是没有virtio_queue.c。因此先把两个数据结构的头文件分析完了,再分析实现

这部分不是unikraft特有的,它就是把linux内核里这块给拿过来了。

vring由3部分组成

  • Descriptor Table,**存放Guest Driver提供的buffer的指针,每个条目指向一个Guest Driver分配的收发数据buffer。**注意,VRing中buffer空间的分配永远由Guest Driver负责,Guest Driver发数据时,还需要向buffer填写数据,Guest Driver收数据时,分配buffer空间后通知Host向buffer中填写数据
  • Avail Ring,**存放Decriptor Table索引,指向Descriptor Table中的一个entry。**当Guest Driver向Vring中添加buffer时,可以一次添加一个或多个buffer,所有buffer组成一个Descriptor chain,Guest Driver添加buffer成功后,需要将Descriptor chain头部的地址记录到Avail Ring中,让Host端能够知道新的可用的buffer是从VRing的哪个地方开始的。Host查找Descriptor chain头部地址,需要经过两次索引Buffer Adress = Descriptor Table[Avail Ring[last_avail_idx]],last_avail_idx是Host端记录的Guest上一次增加的buffer在Avail Ring中的位置。**Guest Driver每添加一次buffer,就将Avail Ring的idx加1,以表示自己工作在Avail Ring中的哪个位置。Avail Rring是Guest维护,提供给Host用。
  • Used Ring,同Used Ring一样,存放Decriptor Table索引。当Host根据Avail Ring中提供的信息从VRing中取出buffer,处理完之后,更新Used Ring,把这一次处理的Descriptor chain头部的地址放到Used Ring中。Host每取一次buffer,就将Used Ring的idx加1,以表示自己工作在Used Ring中的哪个位置。Used Ring是Host维护,提供给Guest用

个人总结:

这个机制就是为了说明Host后端是怎么把前端的buffer给拿过来的,其实就是host后端通过自己的used ring索引。

具体的,前端添加buffer到descriptor chain中,并将这组buffer的头部存到available ring中,具体数据结构是vring_avail。

接着host后端来取信息了,首先是从available ring中拿到available last idx,然后通过available ring[idx],获取buffer在descriptor table中的地址,最后在descriptor table中映射到buffer。访问过的ring就放到used ring里。

1. virtio.h

vring_desc

vring_desc是一个buffer描述符,可以认为它代表了一个Guest内存的buffer。指向要传输的数据。所有的vring_desc组成一个Descriptor Table,Table的条目数就是virtqueue的队列深度,表示Guest一次性最多可以存放的数据buffer,qemu默认设置为128。见上图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Virtqueue descriptors: 16 bytes.
* These can chain together via "next".
*/
struct vring_desc {
/* Address (guest-physical). */
__virtio_le64 addr;
/* Length. */
__virtio_le32 len;
/* The flags as indicated above. */
__virtio_le16 flags;
/* We chain unused descriptors via this, too */
__virtio_le16 next;
};

addr:数据的物理地址,此处应该是物理地址
len:数据的长度
flags:标记数据对于Host是可读还是可写,如果buffer用于发送数据,对Host只读,否则,对Host只写。解释如下

1
2
3
4
5
6
7
8
9
10
11
12
/* This marks a buffer as continuing via the next field. 
* 表示该buffer之后还有buffer,所有buffer可以通过next连成一个Descriptor chain
*/
#define VRING_DESC_F_NEXT 1
/* This marks a buffer as write-only (otherwise read-only).
* 表示该buffer只能写,当buffer用于接收数据时,需要向Host提供buffer,这个时候就标记buffer为写。反之是发送数据,标记为读
*/
#define VRING_DESC_F_WRITE 2
/* This means the buffer contains a list of buffer descriptors.
* 不做讨论
*/
#define VRING_DESC_F_INDIRECT 4

next:存放下一个buffer在Descriptor Table的位置。
注意,next不是存放的物理地址,通过其类型不难判断,next是存放的下一个buffer在Descriptor Table的索引

vring_avail

Guest通过Avail Ring向Host提供buffer,指示Guest增加的buffer位置和当前工作的位置

1
2
3
4
5
6
struct vring_avail {
__virtio_le16 flags;
__virtio_le16 idx;
__virtio_le16 ring[];
/* Only if VIRTIO_F_EVENT_IDX: __virtio_le16 used_event; */
};
  • flags:用于指示Host当它处理完buffer,将Descriptor index写入Used Ring之后,是否通过注入中断通知Guest。如果flags设置为0,Host每处理完一次buffer就会中断通知Guest,从而触发VMExit,增加开销。如果flags为1,不通知Guest。这是一种比较粗糙的方式,要么不通知,要么通知。还有一种比较优雅的方式,叫做VIRTIO_F_EVENT_IDX特性,它根据前后端的处理速度,来判断是否进行通知。如果该特性开启,那么flags的意义将会改变,Guest必须把flags设置为0,然后通过used_event机制实现通知。used_event机制会在后面进行介绍。
  • idx:指示Guest下一次添加buffer时的在Avail Ring所处的位置,换句话说,idx存放的ring[]数组索引,ring[idx]存放才是下一次添加的buffer头在Descriptor Table的位置
  • ring:存放Descriptor Table索引的环,是一个数组,长度是队列深度加1个。其中最后一个用作Event方式通知机制,见下图。VirtIO实现了两级索引,一级索引指向Descriptor Table中的元素,Avail Ring和Used Ring代表的是一级索引,核心就是这里的ring[]数组成员。二级索引指向buffer的物理地址,Descriptor Table是二级索引

vring_used

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* __virtio_le32 is used here for ids for padding reasons. */
struct vring_used_elem {
/* Index of start of used descriptor chain. */
__virtio_le32 id;
/* Total length of the descriptor chain which was written to. */
__virtio_le32 len;
};

struct vring_used {
__virtio_le16 flags;
__virtio_le16 idx;
struct vring_used_elem ring[];
/* Only if VIRTIO_F_EVENT_IDX: __virtio_le16 avail_event; */
};

flags:用于指示Guest当它添加完buffer,将Descriptor index写入Avail Ring之后,是否发送notification通知Host。**如果flags设置为0,Guest每增加一次buffer就会通知Host,如果flags为1,不通知Host。**Used Ring flags的含义和Avail Ring flags的含义类似,都是指示前后端数据处理完后是否通知对方。同样的,当VIRTIO_F_EVENT_IDX特性开启时,flags必须被设置成0,Guest使用avail_event方式通知Host

idx:指示Host下一次处理的buffer在Used Ring所的位置

ring:存放Descriptor Table索引的环。意义和Avail Ring中的ring类似,都是存放指向Descriptor Table的索引。但Used Ring不同的是,它的元素还增加了一个len字段,用来表示Host在buffer中处理了多长的数据。这个字段在某些场景下有用。这里不做介绍

vring

VRing包含数据传输的所有要素,包括Descriptor Table,Avail Ring和Used Ring,其中Descriptor Table是一个数组,每个Entry描述一个数据的buffer,Descriptor Table存放的是指针,Avail Ring和Used Ring中的ring数组则不同,它们存放的是索引,用来间接记录Descriptor chain。

1
2
3
4
5
6
7
8
9
10
struct vring {
/* VRing的队列深度,表示一个VRing有多少个buffer */
unsigned int num;
/* 指向Descriptor Table */
struct vring_desc *desc;
/* 指向Avail Ring */
struct vring_avail *avail;
/* 指向Used Ring */
struct vring_used *used;
};

接着这里还实现了些vring的相关函数,后面与vqueue结合起来看的

2. virtqueue.h

virtqueue

virtqueue用作在Guest与Host之间传递数据,Host可以在用户态(qemu)实现,也可以在内核态(vhost)实现。一个virtio设备可以是磁盘,网卡或者控制台,可以拥有一个或者多个virtqueue,每个virtqueue独立完成数据收发。virtqueue数量多少根据设备的需求来定,比如网卡,通常有两个virtqueue,一个用来接收数据,一个用来发送数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* Type declarations
*/
struct virtqueue;
struct virtio_dev;
typedef int (*virtqueue_callback_t)(struct virtqueue *, void *priv);
typedef int (*virtqueue_notify_host_t)(struct virtio_dev *, __u16 queue_nr);

/**
* Structure to describe the virtqueue.
*/
struct virtqueue {
/* Reference the virtio_dev it belong to */
struct virtio_dev *vdev;
/* Virtqueue identifier */
__u16 queue_id;
/* Notify to the host */
virtqueue_notify_host_t vq_notify_host;
/* Callback from the virtqueue */
virtqueue_callback_t vq_callback;
/* Next entry of the queue */
UK_TAILQ_ENTRY(struct virtqueue) next;
/* Private data structure used by the driver of the queue */
void *priv;
};

后面声明了很多对于vq和vring的操作函数,统一在virtio_ring.c里说

3. virtio_ring.c

这里就主要提一下vring在vqueue的入队和出队的过程

vring_virtqueue

virtqueue是virtio设备看到的队列形式,真正实现数据传输的VRing不会被设备看见,它隐藏在virtqueue的下面,和virtqueue一起,组成了vring_virtqueue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct virtqueue_desc_info {
void *cookie;
__u16 desc_count;
};

struct virtqueue_vring {
/* 设备看到的VRing */
struct virtqueue vq;

/* Descriptor Ring */
// 实现数据传输的VRing结构
struct vring vring;

/* Reference to the vring */
void *vring_mem;

/* Keep track of available descriptors */
__u16 desc_avail;

/* Index of the next available slot */
__u16 head_free_desc;

/* Index of the last used descriptor by the host */
__u16 last_used_desc_idx;

/* Cookie to identify driver buffer */
struct virtqueue_desc_info vq_info[];
};

入队过程分析virtqueue_buffer_enqueue

丛代码层面意思看,入队过程就是guest os向desc chain里加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
int virtqueue_buffer_enqueue(struct virtqueue *vq, void *cookie,
struct uk_sglist *sg, __u16 read_bufs,
__u16 write_bufs)
{
// 这里的sg是某种信号量列表
__u32 total_desc = 0;
__u16 head_idx = 0, idx = 0;
struct virtqueue_vring *vrq = NULL;

UK_ASSERT(vq);

vrq = to_virtqueue_vring(vq); // virtqueue -> virtqueue_vring
total_desc = read_bufs + write_bufs; // 每个描述符对应一个buf
if (unlikely(total_desc < 1 || total_desc > vrq->vring.num)) {
uk_pr_err("%"__PRIu32" invalid number of descriptor\n",
total_desc);
return -EINVAL;
} else if (vrq->desc_avail < total_desc) {
uk_pr_err("Available descriptor:%"__PRIu16", Requested descriptor:%"__PRIu32"\n",
vrq->desc_avail, total_desc);
return -ENOSPC;
}
/* Get the head of free descriptor */
head_idx = vrq->head_free_desc; // 找到首地址塞buf
UK_ASSERT(cookie);
/* Additional information to reconstruct the data buffer */
vrq->vq_info[head_idx].cookie = cookie;
vrq->vq_info[head_idx].desc_count = total_desc;

/**
* We separate the descriptor management to enqueue segment(s).
*/
idx = virtqueue_buffer_enqueue_segments(vrq, head_idx, sg,
read_bufs, write_bufs); // buf写入virtqueue,具体过程见下面的分析
/* Metadata maintenance for the virtqueue */
vrq->head_free_desc = idx; // 下一个vrq里的开始id
vrq->desc_avail -= total_desc; // 可用描述符个数更新

uk_pr_debug("Old head:%d, new head:%d, total_desc:%d\n",
head_idx, idx, total_desc);

virtqueue_ring_update_avail(vrq, head_idx); // last available idx
return vrq->desc_avail;
}

static inline int virtqueue_buffer_enqueue_segments(
struct virtqueue_vring *vrq,
__u16 head, struct uk_sglist *sg, __u16 read_bufs,
__u16 write_bufs)
{
int i = 0, total_desc = 0;
struct uk_sglist_seg *segs;
__u16 idx = 0;

total_desc = read_bufs + write_bufs;

for (i = 0, idx = head; i < total_desc; i++) {
segs = &sg->sg_segs[i]; // 存放物理地址和长度信息,即信号量就是描述符
vrq->vring.desc[idx].addr = segs->ss_paddr;
vrq->vring.desc[idx].len = segs->ss_len;
vrq->vring.desc[idx].flags = 0; // flag根据下标更新
if (i >= read_bufs) // 区分读写队列,对于写的buf,标记为仅可写
vrq->vring.desc[idx].flags |= VRING_DESC_F_WRITE;

if (i < total_desc - 1) // 标记终止符,所以对于write_buf实际上是011的标识,可写且有下一个
vrq->vring.desc[idx].flags |= VRING_DESC_F_NEXT;
idx = vrq->vring.desc[idx].next; // vring描述符是链表,而vrq是数组结构,更新下标
}
return idx; // descriptors表的最后一个idx
}

static inline void virtqueue_ring_update_avail(struct virtqueue_vring *vrq,
__u16 idx)
{
__u16 avail_idx;

avail_idx = vrq->vring.avail->idx & (vrq->vring.num - 1);
/* Adding the idx to available ring */
vrq->vring.avail->ring[avail_idx] = idx;
/**
* Write barrier to make sure we push the descriptor on the available
* descriptor and then increment available index.
*/
wmb();
vrq->vring.avail->idx++; // 回到最开始,这就是available last idx
}

出队过程分析virtqueue_buffer_denqueue

从vrq里读信息到host,并更新used ring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
int virtqueue_buffer_dequeue(struct virtqueue *vq, void **cookie, __u32 *len)
{
struct virtqueue_vring *vrq = NULL;
__u16 used_idx, head_idx;
struct vring_used_elem *elem;

UK_ASSERT(vq);
UK_ASSERT(cookie);
vrq = to_virtqueue_vring(vq);

/* No new descriptor since last dequeue operation */
if (!virtqueue_hasdata(vq))
return -ENOMSG;
used_idx = vrq->last_used_desc_idx++ & (vrq->vring.num - 1);
elem = &vrq->vring.used->ring[used_idx]; // 根据used ring提供的下标,取出对应vring
/**
* We are reading from the used descriptor information updated by the
* host.
*/
rmb();
head_idx = elem->id;
if (len)
*len = elem->len;
*cookie = vrq->vq_info[head_idx].cookie;
virtqueue_detach_desc(vrq, head_idx); //
vrq->vq_info[head_idx].cookie = NULL;
return (vrq->vring.num - vrq->desc_avail); // 返回的是下一个available_idx in vrq
}

static inline void virtqueue_detach_desc(struct virtqueue_vring *vrq,
__u16 head_idx)
{
struct vring_desc *desc;
struct virtqueue_desc_info *vq_info;

desc = &vrq->vring.desc[head_idx]; // desc存储了buffer的物理地址 长度,flag,next等信息
vq_info = &vrq->vq_info[head_idx];
vrq->desc_avail += vq_info->desc_count; // 更新队列下一个可用描述符下标
vq_info->desc_count--;

while (desc->flags & VRING_DESC_F_NEXT) { // 遍历所有vrq中有next的信息,然后更新vq信息
desc = &vrq->vring.desc[desc->next];
vq_info->desc_count--;
}

/* The value should be empty */
UK_ASSERT(vq_info->desc_count == 0); // 都出队完后 vq_info对应描述符数量应该为空

/* Appending the descriptor to the head of list */
desc->next = vrq->head_free_desc;
vrq->head_free_desc = head_idx;
}