文章目錄
Quest
數據結構的介紹,我們參照的virtio spec的定義(參考Virtual I/O Device Version 1.0 spec 第二章virtqueues介紹),圍繞下面的示意圖進行介紹,介紹層次從下到上,從最接近數據buffer到最接近內核塊設備進行。有些數據結構初次看可能不是很理解,可以先記住一些概念,後面會通過VirtIO數據傳輸的流程演示和代碼分析,詳細解釋這些數據結構怎麼用,以此加深理解。
- 數據結構圖的左半部分描述了virtio-blk設備與virtio設備的關係,virtqueue與vring_virtqueue的關係,如下:
- virtio-blk是一個virtio設備,它看到的隊列是virtqueue,裏面沒有vring的實現,只記錄了vring中還有多少空閒的buffer可以使用
- vring_virtqueue是一個virtqueue,它將VRing的實現隱藏在virtqueue下面,當一個virtio-blk設備真正要發送數據時,只要傳入virtqueue就能找到VRing並實現數據收發
- 數據結構圖的右大半部分描述的是VRing的組成,VRing由三部分組成,如下:
- 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用
- 以上的描述會在接下來的介紹中進一步解釋
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用於接收數據時,需要向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的索引
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對應的主機虛擬地址