virtqueue:數據結構以及通信機制

何爲前後端–virtio:

說到前後端就要提到virtio,virtio是IBM提出的實現虛擬機內部和宿主機之前數據交換的一種方式,與全虛擬化方式比較(即通過qemu完全模擬設備的方式),性能有了較大的提升。

簡單來講,在virtio體系中分爲前端驅動和後端驅動兩個部分。前端驅動我們一般可以理解爲虛擬機內部的虛擬網卡的驅動,當然Windows和Linux的驅動是不同的;後端驅動virtio是宿主機上的部分,實現可能會有不同的方式,如內核模式的vhost,用戶態vhost-user等等,但是本質的功能是類似的,就是將前端驅動發出的報文轉發到後端虛擬設備上,同理將收到的報文傳入實例內的前端驅動。

virtio如何交換數據–virtqueue、vring

總的來說,virtqueue 即前後端環形隊列進行數據交換的實際數據鏈路。guest 把 buffers 插入其中,每個 buffer 都是一個分散-聚集數組。驅動調用 find_vqs()來創建一個與 queue 關聯的結構體。virtqueue 的數目根據設備的不同而不同。network 設備有 2 個 virtqueue,一個用於發送數據包,一個用於接收數據包。

virtqueue由 描述符列表(descriptor table)、可用環表(available ring)、已用環表(used ring)3部分組成:

struct vring {
	unsigned int num;
	struct vring_desc *desc;
	struct vring_avail *avail;
	struct vring_used *used;
}
  • vring_desc:放置了所有真正的報文數據。
  • vring_avail 與vring_used :
    • 在發送報文的時候,前端驅動將報文在desc中的索引放在avail隊列中,後端驅動從這個隊列裏獲取報文進行轉發,處理完之後將這些報文放入used隊列。
    • 在接受報文的時候,前端驅動將空白的內存塊放入avail隊列中(當然也只是報文在desc隊列中的索引而已),後端接受報文將內容填充後,將這些含數據的報文放入used隊列。

這三個隊列都是固定長度的環形隊列,當然實現僅僅是對相應索引號對最大長度去餘而已。下面這張圖形象地表明三個隊列和前後端驅動的關係:
在這裏插入圖片描述

virtqueue:數據結構以及通信機制

我們以 前端的發送隊列爲例,注意所有的結構信息都是在虛擬機內部可見的,可以通過core dump查看:

struct vring_virtqueue {
vq = {
list = {
next = 0xffff881027e3d800,
prev = 0xffff881026d9b000
},
callback = 0xffffffffa0149450,
name = 0xffff881027e3ee88 "output.0", ->>表明是發送隊列
vdev = 0xffff881023776800,
priv = 0xffff8810237d03c0
},
vring = {
num = 256, ->>所有的隊列長度
desc = 0xffff881026d9c000, ->> desc隊列
avail = 0xffff881026d9d000, ->> avail隊列
used = 0xffff881026d9e000 ->> used隊列
},
broken = false,
indirect = true,
event = true,
num_free = 0, ->> 隊列目前有多少空閒元素了,如果已經爲0表明隊列已經阻塞,前端將無法發送報文給後端
free_head = 0, ->> 指向下一個空閒的desc元素
num_added = 0, ->>是最近一次操作向隊列中添加報文的數量
last_used_idx = 52143, 這是前端記錄他看到最新的被後端用過的索引(idx),是前端已經處理到的used隊列的idx。前端會把這個值寫到avail隊列的最後一個元素,這樣後端就可以得知前端已經處理到used隊列的哪一個元素了。

<> ->> last_avail_idx 前端不會碰,而且前端的virtqueue結構裏就沒有這個值,這個代表後端已經處理到avail隊列的哪個元素了,前端靠這個信息來做限速,後端是把這個值寫在used隊列的最後一個元素,這樣前端就可以讀到了。

notify = 0xffffffffa005a350,
queue_index = 1,
data = 0xffff881026d9f078
}

crash> struct vring_avail 0xffff881026d9d000
struct vring_avail {
flags = 0,
idx = 52399, ->> avail隊列的下個可用元素的索引
ring = 0xffff881026d9d004 ->> 隊列數組
}

crash> struct vring_used
struct vring_used {
__u16 flags;
__u16 idx; ->> used隊列的下個可用元素的索引
struct vring_used_elem ring[]; ->> 隊列數組
}

下面我們再深入分析下後端驅動的幾個重要數據結構:

struct VirtQueue
 {
     VRing vring;//每個queue一個vring
     hwaddr pa;//記錄virtqueue中關聯的描述符表的地址
     /*last_avail_idx對應ring[]中的下標*/
     uint16_t last_avail_idx;//上次寫入的最後一個avail_ring的索引,下次給客戶機發送的時候需要從avail_ring+1
     /* Last used index value we have signalled on */
     uint16_t signalled_used;
 
     /* Last used index value we have signalled on */
     bool signalled_used_valid;
 
     /* Notification enabled? */
     bool notification;
 
     uint16_t queue_index;
 
     int inuse;
 
     uint16_t vector;
     void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);
     VirtIODevice *vdev;
     EventNotifier guest_notifier;
     EventNotifier host_notifier;
 };

與前端類似,HOST和客戶機也正是通過VirtQueue來操作buffer。每個buffer包含一個VRing結構,對buffer的操作實際上是通過VRing來管理的。pa是描述符表的物理地址。last_avail_index對應VRingAvail中ring[]數組的下標,表示上次最後使用的一個buffer首個desc對應的ring[]中的下標。這裏先介紹VRing:

typedef struct VRing
{
    unsigned int num;//描述符表中表項的個數
    unsigned int align;
    hwaddr desc;//指向描述符表
    hwaddr avail;//指向VRingAvail結構
    hwaddr used;//指向VRingUsed結構
} VRing;

前面說過,VRing管理buffer,其實事實上,VRing是通過描述符表管理buffer的。究竟是怎麼個管理法?這裏num表示描述符表中的表項數。align是對其粒度。desc表示描述符表的物理地址。avail是VRingAvail的物理地址,而used是VRingUsed的物理地址。每一個描述符表項都對應一個物理塊,參考下面的數據結構,每個表項都記錄了其對應物理塊的物理地址,長度,標誌位,和next指針。同一buffer的不同物理塊正是通過這個next指針連接起來的。

typedef struct VRingDesc
{
    uint64_t addr;//buffer 的地址
    uint32_t len;//buffer的大小,需要注意當描述符作爲節點連接一個描述符表時,描述符項的個數爲len/sizeof(VRingDesc)
    uint16_t flags;
    uint16_t next;
} VRingDesc;
  • addr:guest 物理地址
  • len:buffer 的長度
  • flags:flags 的值含義包括:
    • VRING_DESC_F_NEXT:用於表明當前 buffer 的下一個域是否有效,也間接表明當前 buffer 是否是 buffers list 的最後一個。
    • VRING_DESC_F_WRITE:當前 buffer 是 read-only 還是 write-only。
    • VRING_DESC_F_INDIRECT:表明這個 buffer 中包含一個 buffer 描述符的 list
  • next:所有的 buffers 通過 next 串聯起來組成 descriptor table
{
typedef struct VRingAvail
    uint16_t flags;//限制host是否向客戶機注入中斷
    uint16_t idx;
    uint16_t ring[0];//這是一個索引數組,對應在描述符表中表項的下標,代表一個buffer的head,即一個buffer有多個description組成,其head會記錄到
                    //ring數組中,使用的時候需要從ring數組中取出一個head纔可以
} VRingAvail;

typedef struct VRingUsedElem
{
    uint32_t id;
    uint32_t len;//應該表示它代表的數據段的長度
} VRingUsedElem;

typedef struct VRingUsed
{
    uint16_t flags;//用於限制客戶機是否增加buffer後是否通知host
    uint16_t idx;//
    VRingUsedElem ring[0];//意義同VRingAvail
} VRingUsed;

兩個字段基本一致,flags是標識位主要限制HOST和客戶機的通知。VRing中的flags限制當HOST寫入數據完成後是否向客戶機注入中斷,而VRingUsed中的flags限制當客戶機增加buffer後,是否通知給HOST。這一點在高流量的情況下很有效。就像現在網絡協議棧中的NAPI,爲何採用中斷加輪訓而不是採用單純的中斷或者輪詢。

二者也都有idx。VRingAvail中的idx表明客戶機驅動下次添加buffer使用的ring下標,VRingUsed中的idx表明qemu下次添加VRingUsedElem使用的ring下標。

然後兩者都有一個數組,VRingAvail中的ring數組記錄的是可用buffer 的head index.即

if ring[0]=2
then desctable[2] 記錄的就是一個邏輯buffer的首個物理塊的信息。

Available ring 指向 guest 提供給設備的描述符,它指向一個 descriptor 鏈表的頭。Available ring 結構如下圖所示。
其中標識 flags 值爲 0 或者 1,1 表明 Guest 不需要 device 使用完這些 descriptor 時上報中斷。idx 指向我們下一個 descriptor 入口處,idx 從 0 開始,一直增加,使用時需要取模:idx=idx&(vring.num-1)

在這裏插入圖片描述
virtqueue中的last_avail_idx記錄ring[]數組中首個可用的buffer頭部。即根據last_avail_idx查找ring[],根據ring[]數組得到desc表的下標。然後last_avail_idx++。

每次HOST向客戶機發送數據就需要從這裏獲取一個buffer head。

當HOST完成數據的寫入,可能會產生多個VirtQueueElement,即使用多個邏輯buffer,每個VirtQueueElement的信息記錄到VRingUsed的VRingUsedElem數組中,一個元素對應一個VRingUsedElem結構,其中id記錄對應buffer的head,len記錄長度。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章