VirtIO实现原理——vring数据结构(转)

转自《VirtIO实现原理——vring数据结构》,添加了一些注释。

文章目录

Guest

数据结构的介绍,我们参照的virtio spec的定义(参考Virtual I/O Device Version 1.0 spec 第二章virtqueues介绍),围绕下面的示意图进行介绍,介绍层次从下到上,从最接近数据buffer到最接近内核块设备进行。有些数据结构初次看可能不是很理解,可以先记住一些概念,后面会通过VirtIO数据传输的流程演示和代码分析,详细解释这些数据结构怎么用,以此加深理解。

  • 数据结构图的左半部分描述了virtio-blk设备与virtio设备的关系,virtqueue与vring_virtqueue的关系,如下:

  1. virtio-blk是一个virtio设备,它看到的队列是virtqueue,里面没有vring的实现,只记录了vring中还有多少空闲的buffer可以使用。

  1. vring_virtqueue是一个virtqueue,它将VRing的实现隐藏在virtqueue下面,当一个virtio-blk设备真正要发送数据时,只要传入virtqueue就能找到VRing并实现数据收发。

  • 数据结构图的右大半部分描述的是VRing的组成,VRing由三部分组成,如下:

  1. Descriptor Table,存放Guest Driver提供的buffer的指针,每个条目指向一个Guest Driver分配的收发数据buffer。注意,VRing中buffer空间的分配永远由Guest Driver负责,Guest Driver发数据时,还需要向buffer填写数据,Guest Driver收数据时,分配buffer空间后通知Host向buffer中填写数据。

  1. 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 Ring从这个位置操作)。Avail Rring是Guest维护,提供给Host用。

  1. Used Ring,同Avail Ring一样,存放Decriptor Table索引。当Host根据Avail Ring中提供的信息从VRing中取出buffer,处理完之后,更新Used Ring,把这一次处理的Descriptor chain头部的地址放到Used Ring中。Host每取一次buffer,就将Used Ring的idx加1,以表示自己工作在Used Ring中的哪个位置(下次更新Used Ring从这个位置操作)。Used Ring是Host维护,提供给Guest用。

  • 以上的描述会在接下来的介绍中进一步解释:

struct vring_desc

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

/* Virtio ring descriptors: 16 bytes.  These can chain together via "next". */
struct vring_desc {
    /* Address (guest-physical). */
    __virtio64 addr;
    /* Length. */
    __virtio32 len;
    /* The flags as indicated above. */
    __virtio16 flags;
    /* We chain unused descriptors via this, too */
    __virtio16 next; 
}; 

addr:数据的物理地址

len:数据的长度

flags:标记数据对于Host是可读还是可写,如果buffer用于发送数据,对Host只读,否则,对Host只写。解释如下:

/* 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用于(Guest)接收数据时,需要向Host提供buffer,
 * 这个时候就标记buffer为只写。反之是(Guest)发送数据,标记为只读
 */
#define VRING_DESC_F_WRITE  2
/* This means the buffer contains a list of buffer descriptors. 
 * desc存的是间接描述符
 */
#define VRING_DESC_F_INDIRECT   4

next:存放下一个buffer在Descriptor Table的位置(index)。

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

struct vring_avail

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

struct vring_avail {
    __virtio16 flags;
    __virtio16 idx;
    __virtio16 ring[];
}; 

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是二级索引。

struct vring_used

Host通过Used Ring向Host提供信息,指示Host处理buffer的位置。

struct vring_used {
    __virtio16 flags;
    __virtio16 idx;
    struct vring_used_elem ring[];
};

/* u32 is used here for ids for padding reasons. */
struct vring_used_elem {
    /* Index of start of used descriptor chain. */
    __virtio32 id;
    /* Total length of the descriptor chain which was used (written to) */
    __virtio32 len;
};

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中处理了多长的数据。这个字段在某些场景下有用。这里不做介绍。

以上三个数据结构的简图如下:

struct vring

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

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

struct virtqueue

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

/**                   
 * virtqueue - a queue to register buffers for sending or receiving.
 * @list: the chain of virtqueues for this device
 * @callback: the function to call when buffers are consumed (can be NULL).
 * @name: the name of this virtqueue (mainly for debugging)
 * @vdev: the virtio device this queue was created for.
 * @priv: a pointer for the virtqueue implementation to use.
 * @index: the zero-based ordinal number for this queue.
 * @num_free: number of elements we expect to be able to fit.
 *
 * A note on @num_free: with indirect buffers, each buffer needs one
 * element in the queue, otherwise a buffer will need one element per
 * sg element.
 */
struct virtqueue {
    struct list_head list;
    void (*callback)(struct virtqueue *vq);
    const char *name;
    struct virtio_device *vdev;
    unsigned int index;
    unsigned int num_free;    // virtqueue中剩余的buffer数量,初始化时该大小是virtqueue深度
    void *priv;
};
  • 当virtio设备支持多队列特性时,virtqueue数量可配置,比如为一个virtio-blk磁盘配置4个队列,主机侧:

1: libvirt配置
 <disk type='file' device='disk'>
    <driver name='qemu' type='qcow2' queues='4'/>
    ...
 </disk>
2: qemu配置
-device virtio-blk-pci,num-queues=4...
  • qemu在启动时如果解析到virtio-blk设备的num-queues被设置大于1,就会为磁盘设置VIRTIO_BLK_F_MQ特性,表示后端支持多队列,如果前端guest驱动也支持多队列,那么多队列可以设置成功,如果前端驱动不支持多队列特性,那么队列会回退到默认值1

if (s->conf.num_queues > 1) {
        virtio_add_feature(&features, VIRTIO_BLK_F_MQ);    // 添加多队列特性
}

static Property virtio_blk_properties[] = {
    DEFINE_PROP_UINT16("num-queues", VirtIOBlock, conf.num_queues, 1)    // virtio-blk默认队列数为1
    ...
}

成功设置磁盘多队列之后,虚拟机内部查看如下:

多队列可以提高IO性能,libvirt的官方推荐配置是多队列个数与vcpu个数相同,让每个vcpu可以处理一个队列,当虚拟机IO压力大的时候,IO数据可以平均到各个队列分别让每个cpu单独处理,从而提高传输效率

struct vring_virtqueue

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

struct vring_virtqueue {
    struct virtqueue vq;                                  /* 1 */
    /* Actual memory layout for this queue */    
            
    struct vring vring;                                   /* 2 */
    /* Can we use weak barriers? */
    bool weak_barriers;    
    
    /* Other side has made a mess, don't try any more. */
    bool broken;    
    
    /* Host supports indirect buffers */
    bool indirect;
    
    /* Host publishes avail event idx */
    bool event;                                            /* 3 */
    
    /* Head of free buffer list. */
    unsigned int free_head;                              /* 4 */
    
    /* Number we've added since last sync. */
    unsigned int num_added;                                /* 5 */
    
    /* Last used index we've seen. */
    u16 last_used_idx;
    
    /* Last written value to avail->flags */
    u16 avail_flags_shadow;            

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

    /* How to notify other side. FIXME: commonalize hcalls! */
    bool (*notify)(struct virtqueue *vq);
    ......
};

1. 设备看到的VRing

2. 实现数据传输的VRing结构

3. 是否开启Event通知机制

4. 当前Descriptor Table中空闲buffer的起始位置

5. 上一次通知Host后,Guest往VRing上添加了多少次buffer,每添加一次buffer,num_added加1,每kick一次Host清空

6. Guest每添加一次buffer,avail_idx_shadow加1,每删除一次buffer,avail_idx_shadow减1

7. virtio队列通知后端的具体实现,初始化队列的时候注册,对于基于pci总线的virtio队列,对应的实现为vp_notify

Host

VirtQueueElement

VirtQueueElement是后端在从virtio环取buffer时临时存放描述符的数据结构,由于buffer是前端提供的,需要区分哪些描述符用于虚机发送数据,哪些描述符用于虚机接收数据。对于发送数据,buffer对于后端来说是只读的,对于接受数据,后端需要往buffer中写数据,所以是可写的。

typedef struct VirtQueueElement
{
    unsigned int index;                /* 1 */
    unsigned int len;                
    unsigned int ndescs;            /* 2 */
    unsigned int out_num;            /* 3 */
    unsigned int in_num;            /* 4 */
    hwaddr *in_addr;                /* 5 */
    hwaddr *out_addr;                /* 6 */
    struct iovec *in_sg;            /* 7 */
    struct iovec *out_sg;            /* 8 */
} VirtQueueElement;

1. 当前元素在描述符表取描述符时的起始索引

2. 当前元素包含的总描述符个数

3. 当前元素中包含的发送描述符个数,即descriptor table entry个数

4. 当前元素中包含的接受描述符个数

5. 接受buffer的起始虚机物理地址GPA

6. 发送buffer的起始虚机物理地址

7. 接受buffer对应的主机虚拟地址HVA,由qemu从GPA转换而来

8. 发送buffer对应的主机虚拟地址

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值