virtio後端驅動詳解

原文鏈接:https://www.cnblogs.com/ck1020/p/5939777.html

2016-10-08

virtIO是一種半虛擬化驅動,廣泛用於在XEN平臺和KVM虛擬化平臺,用於提高客戶機IO的效率,事實證明,virtIO極大的提高了VM IO 效率,配備virtIO前後端驅動的情況下,客戶機IO效率基本達到和宿主機一樣的水平。咱們本次的分析以qemu-kvm架構的虛擬化平臺爲基礎,分析virtIO前後端驅動。當然後端就指有qemu實現的虛擬PCI設備,而前端自然就是客戶操作系統中的virtIO驅動。需要前後配合才能完成數據的傳輸。

本節的重點在於首先對virtIO進行總體介紹,然後結合LInux 3.11.1內核代碼對後端驅動進行分析。


一、virtIO 總體介紹

 對於virtIO的基本介紹如前面所述,而virtIO的出現所解決的另一個問題就是給衆多的虛擬化平臺提供了一個統一的IO模型,KVM、XEN、VMWare等均可以利用virtIO進行IO虛擬化,在提高IO效率的同時也極大的減少了自家軟件開發的工作量。那麼對於virtIO基本介紹我們就不詳細深入,事實上,前面已經足以說明virtIO是何方神聖,下面主要是深入內在分析virtIO 其實現原理。


二、現有設備虛擬化存在的問題

這裏以網絡驅動爲例,其他的設備驅動類似。先看傳統的利用簡單虛擬網卡進行網絡虛擬化的情況,這種情況下數據包的收發模式根物理機沒有本質區別。虛擬網卡發現有數據包到來,需要先接收下來,這個過程會發生數據的複製,然後向客戶機注入軟件中斷,客戶機接收到信號,然後處理中斷,最終完成數據包的處理。這種模式下工作效率的瓶頸主要兩點:

1、數據的複製

2、根模式和非根模式的頻繁切換

即使目前qemu的虛擬網卡已經使用DMA的方式直接把數據寫入到客戶機內存,然後通知客戶機,但是仍然免不了數據複製帶來的性能開銷。

那麼有沒有一種方式能夠徹底的解決上面兩個問題呢??當然要徹底消除根模式和非根模式的切換是不可能的,畢竟虛擬機還是有Hypervisor管理的,但是我們可以最大程度的減少這種不必要的切換。virtIO爲這一問題提供了比較好的解決方案。

第一個問題:數據的複製

在virtIO實現了零複製。不管是什麼虛擬化平臺,虛擬機是運行在host內存中或者說虛擬機共享同一塊內存,那麼既然如此我們就沒有必要在同一塊內存不同區域之間複製數據,而可以進行簡單的地址重映射即可。以KVM虛擬機爲例,虛擬機運行在HOST的內存中,且在HOST上表現爲一個普通的qemu進程。前面我們分析qemu網絡虛擬化的時候已經分析,宿主機接收到數據包會根據目的MAC進行數據包的轉發,如果是發往客戶機的,則把數據包轉發到一個虛擬端口(tap設備模擬),其本質實際上只是把數據共享到一個用戶空間應用程序(通過虛擬設備),這裏我們就是指qemu,這個過程是不需要我們操心的。數據到了qemu,其實這裏有一個Net client來接收,

 第二個問題:根模式和非根模式的頻繁切換、

這裏考慮下爲何會有線程或者說操作系統中用戶模式和內核模式都是同樣的道理,線程出現的很大一部分原因就是進程切換代價太高,需要保存和恢復的東西太多,以至於每次切換都要做很多重複的工作,這纔有了線程或者說是輕量級的進程。那麼在這裏,根模式和非根模式也是這個道理,只是這個模式的切換比進程的切換需要消耗更多的資源,因爲每次切換保存的不在是一個普通進程的上下文,而是一個虛擬機的上下文,儘管虛擬機在HOST上同樣是表現爲一個進程,但是其保存的資源更多。仍然以網絡數據包的傳送爲例。傳統的方式,物理網卡每接收到一個數據幀就需要中斷CPU,讓CPU處理調用相應的中斷服務函數處理數據幀。那麼虛擬網卡也是如此,HOST每轉發一個數據包到客戶機的虛擬網卡,在不使用DMA的情況下就一個數據幀觸發一個軟中斷,客戶機就必須VM-exit處理中斷,然後VM-entry,該過程不僅包含了數據的複製還包含了根模式非根模式之間的頻繁切換。而即使qemu採用DMA的方式把數據幀直接寫入到客戶機內存,然後通知客戶機,同樣免不了數據複製帶來的開銷。


 

三、 virtIO的實現

基於上面描述的問題,virtIo給出了比較好的而解決方案。 而事實上,virtIO的出現不僅僅是解決了效率的問題。其更是爲各大虛擬化引擎提供了一個統一的外部設備驅動。

我們先從存儲的角度分析一個數據幀。首先一個數據幀可能會需要多個buffer塊才能完成存儲;而一個buffer在這裏指我們調用函數申請的虛擬地址空間,對應到物理內存可能包含有多個物理內存塊。

qemu中用VirtQueueElement結構表示一個邏輯buffer,用VRingDesc結構描述一個物理內存塊,用一個描述符數組集中管理所有的描述符。而前後端的配合通過兩個ring來實現:VRingAvailVRingUsed。當HOST需要向客戶機發送數據時,先從對應的virtqueue獲取客戶機設置好的buffer空間(實際的buffer空間由客戶機添加到virtqueue),每次取出一個buffer,相關信息記錄到VirtQueueElement結構中,然後對其進行地址映射,因爲這裏記錄的buffer信息是客戶機的物理地址,需要映射成HOST的虛擬地址纔可以對其進行訪問。每完成一個VirtQueueElement 即buffer的的寫入,就需要記錄VirtQueueElement相關信息到VRingUsed,並撤銷地址映射。一個數據幀寫入完成後會設置VRingUsed的idx字段並對客戶機注入軟件中斷以通知客戶機。

 數據幀的邏輯存儲結構如下:

 

而物理內存塊由一個全局的描述符表統一管理,具體的管理如下圖所示:

 

後端驅動工作模式正是如此,下面我們還是深入分析下幾個重要的數據結構:

複製代碼

 1 struct VirtQueue
 2 {
 3     VRing vring;//每個queue一個vring
 4     hwaddr pa;//記錄virtqueue中關聯的描述符表的地址
 5     /*last_avail_idx對應ring[]中的下標*/
 6     uint16_t last_avail_idx;//上次寫入的最後一個avail_ring的索引,下次給客戶機發送的時候需要從avail_ring+1
 7     /* Last used index value we have signalled on */
 8     uint16_t signalled_used;
 9 
10     /* Last used index value we have signalled on */
11     bool signalled_used_valid;
12 
13     /* Notification enabled? */
14     bool notification;
15 
16     uint16_t queue_index;
17 
18     int inuse;
19 
20     uint16_t vector;
21     void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);
22     VirtIODevice *vdev;
23     EventNotifier guest_notifier;
24     EventNotifier host_notifier;
25 };

複製代碼

 VirtQueue是一個虛擬隊列,之所以稱之爲隊列是從其管理buffer的角度。HOST和客戶機也正是通過VirtQueue來操作buffer。每個buffer包含一個VRing結構,對buffer的操作實際上是通過VRing來管理的。pa是描述符表的物理地址。last_avail_index對應VRingAvail中ring[]數組的下標,表示上次最後使用的一個buffer首個desc對應的ring[]中的下標。這裏聽起來有點亂,麼關係,後面會詳細解釋。暫且先介紹這幾個。

 

複製代碼

1 typedef struct VRing
2 {
3     unsigned int num;//描述符表中表項的個數
4     unsigned int align;
5     hwaddr desc;//指向描述符表
6     hwaddr avail;//指向VRingAvail結構
7     hwaddr used;//指向VRingUsed結構
8 } VRing;

複製代碼

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

複製代碼

1 typedef struct VRingDesc
2 {
3     uint64_t addr;//buffer 的地址
4     uint32_t len;//buffer的大小,需要注意當描述符作爲節點連接一個描述符表時,描述符項的個數爲len/sizeof(VRingDesc)
5     uint16_t flags;
6     uint16_t next;
7 } VRingDesc;

複製代碼

 

 

複製代碼

 1 /*一個數據幀可能有多個VirtQueueElement,VirtQueueElement中的index */
 2 typedef struct VirtQueueElement
 3 {
 4     unsigned int index;
 5     unsigned int out_num;
 6     unsigned int in_num;
 7     /*in_addr和 out_addr保存的是客戶機的物理地址,而in_sg和out_sg中的地址是host的虛擬地址,兩者之間需要映射*/
 8     hwaddr in_addr[VIRTQUEUE_MAX_SIZE];
 9     hwaddr out_addr[VIRTQUEUE_MAX_SIZE];
10     struct iovec in_sg[VIRTQUEUE_MAX_SIZE];
11     struct iovec out_sg[VIRTQUEUE_MAX_SIZE];
12 } VirtQueueElement;

複製代碼

 再說這個VirtQueueElement,前面也說過其對應的是一個邏輯buffer塊。而一個邏輯buffer塊由多個物理內存塊組成。index記錄該邏輯buffer塊的首個物理內存塊對應的描述符在描述符表中的下標,out_num和in_num是指輸出和輸入塊的數量。因爲這裏一個邏輯buffer可能包含可讀區和可寫區,即有些物理塊是可讀的而有些物理塊是可寫的,out_num記錄可讀塊的數量,而in_num記錄可寫塊的數量。in_addr是一個數組,記錄的是可讀塊的物理地址,out_addr同理。但是由於物理地址是客戶機的,所以要想在HOST訪問,需要把這些地址映射成HOST的虛擬地址,下面兩個就是保存的對應物理塊在HOST的虛擬地址和長度。

 1 typedef struct VRingAvail

複製代碼

 2 {
 3     uint16_t flags;//限制host是否向客戶機注入中斷
 4     uint16_t idx;
 5     uint16_t ring[0];//這是一個索引數組,對應在描述符表中表項的下標,代表一個buffer的head,即一個buffer有多個description組成,其head會記錄到
 6                     //ring數組中,使用的時候需要從ring數組中取出一個head纔可以
 7 } VRingAvail;
 8 
 9 typedef struct VRingUsedElem
10 {
11     uint32_t id;
12     uint32_t len;//應該表示它代表的數據段的長度
13 } VRingUsedElem;
14 
15 typedef struct VRingUsed
16 {
17     uint16_t flags;//用於限制客戶機是否增加buffer後是否通知host
18     uint16_t idx;//
19     VRingUsedElem ring[0];//意義同VRingAvail
20 } VRingUsed;

複製代碼

 這三個放在一起說是因爲這三個之間聯繫密切,而筆者也曾被這幾個關係搞得暈頭轉向。先說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的首個物理塊的信息。

virtqueue中的last_avail_idx記錄ring[]數組中首個可用的buffer頭部。即根據last_avail_idx查找ring[],根據ring[]數組得到desc表的下標。然後last_avail_idx++。

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

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

 小結:上面結合重要的數據結構分析了virtIO後端驅動的工作模式,雖然筆者儘可能的想要分析清楚,但是總感覺表達能力有限,不足之處還請諒解!下面會結合qemu源代碼就具體的網絡數據包的接收,做簡要的分析。


 

 當然,開始還是從virtio_net_receive函數開始

複製代碼

 1 static ssize_t virtio_net_receive(NetClientState *nc, const uint8_t *buf, size_t size)
 2 {
 3     VirtIONet *n = qemu_get_nic_opaque(nc);
 4     VirtIONetQueue *q = virtio_net_get_subqueue(nc);
 5     VirtIODevice *vdev = VIRTIO_DEVICE(n);
 6     struct iovec mhdr_sg[VIRTQUEUE_MAX_SIZE];
 7     struct virtio_net_hdr_mrg_rxbuf mhdr;
 8     unsigned mhdr_cnt = 0;
 9     size_t offset, i, guest_offset;
10 
11     if (!virtio_net_can_receive(nc)) {
12         return -1;
13     }
14 
15     /* hdr_len refers to the header we supply to the guest */
16     if (!virtio_net_has_buffers(q, size + n->guest_hdr_len - n->host_hdr_len)) {
17         return 0;
18     }
19 
20     if (!receive_filter(n, buf, size))
21         return size;
22 
23     offset = i = 0;
24     /*這裏循環一次,就get到一個buffer*/
25     while (offset < size) {
26         VirtQueueElement elem;//一個elem表示一個buffer的數據
27         int len, total;
28         //sg作爲一個指針,指向elem.in_sg,即sg=elem.in_sg。指向的是同一片內存區
29         const struct iovec *sg = elem.in_sg;
30 
31         total = 0;
32         //從virtqueue中取出所有的描述符信息,返回的是in_sg和out_sg的數量總和,如果爲0表示這隊列並沒有實際的空間,無法裝入數據
33         if (virtqueue_pop(q->rx_vq, &elem) == 0) {
34             if (i == 0)
35                 return -1;
36             error_report("virtio-net unexpected empty queue: "
37                     "i %zd mergeable %d offset %zd, size %zd, "
38                     "guest hdr len %zd, host hdr len %zd guest features 0x%x",
39                     i, n->mergeable_rx_bufs, offset, size,
40                     n->guest_hdr_len, n->host_hdr_len, vdev->guest_features);
41             exit(1);
42         }
43 
44         if (elem.in_num < 1) {
45             error_report("virtio-net receive queue contains no in buffers");
46             exit(1);
47         }
48 
49         if (i == 0) {
50             assert(offset == 0);
51             if (n->mergeable_rx_bufs) {
52                 mhdr_cnt = iov_copy(mhdr_sg, ARRAY_SIZE(mhdr_sg),
53                                     sg, elem.in_num,
54                                     offsetof(typeof(mhdr), num_buffers),
55                                     sizeof(mhdr.num_buffers));
56             }
57             receive_header(n, sg, elem.in_num, buf, size);
58             offset = n->host_hdr_len;
59             total += n->guest_hdr_len;
60             guest_offset = n->guest_hdr_len;
61         } else {
62             guest_offset = 0;
63         }
64         /* copy in packet.  ugh */
65         /*進行數據的寫入*/
66         len = iov_from_buf(sg, elem.in_num, guest_offset,buf + offset, size - offset);
67         /*total表示完成複製的數據*/
68         total += len;
69         /*offset表示下一刻要複製的數據到buffer頭的偏移*/
70         offset += len;
71         /* If buffers can't be merged, at this point we
72          * must have consumed the complete packet.
73          * Otherwise, drop it. */
74         if (!n->mergeable_rx_bufs && offset < size) {
75 #if 0
76            
77 #endif
78             return size;
79         }
80 
81         /* signal other side i 代表第幾個buffer*/
82         /*到這裏數據已經寫入完成,需要撤銷映射,更新ring的相關字段*/
83         virtqueue_fill(q->rx_vq, &elem, total, i++);
84     }
85 
86     if (mhdr_cnt) {
87         stw_p(&mhdr.num_buffers, i);
88         iov_from_buf(mhdr_sg, mhdr_cnt,
89                      0,
90                      &mhdr.num_buffers, sizeof mhdr.num_buffers);
91     }
92     /*i表示數據包使用了多少個邏輯buffer即elem*/
93     virtqueue_flush(q->rx_vq, i);//這裏會更新used_ring的idx,表明這些buffer 已經消費,可以回收
94     virtio_notify(vdev, q->rx_vq);//通知客戶機
95 
96     return size;
97 }

複製代碼

 

 參數想必不用過多解釋,NetClientState代表當前網絡接收端,buffer指向數據,size是數據的長度。前面的驗證這裏我們就暫且忽略了,重點從while循環開始,這裏聲明瞭一個局部變量VirtQueueElement用於從virt_queue中取元素,同時在循環體外邊設置兩個移動指針offset和i初始化成0.二者的意義後面自然明瞭,而循環體內部還有一個toal字段,我們最後一起說。往下看就調用了virtqueue_pop函數,該函數具體後面在分析,功能就是從隊列中取出一個邏輯buffer,並記錄相關信息到elem中。關於VirtQueueElement前面已經分析過;蝦米那一個if是在開始寫入數據之前,判斷下是否支持合併buffer,是的話需要記錄buffer的數量,並寫入頭部到buffer中(注意是最開始那個buffer塊)。接着就要iov_from_buf函數往buffer塊中寫入數據了,每次按照一個邏輯buffer的大小進行寫入直到寫入數據結束。完成後調用virtqueue_fill函數,主要是撤銷地址的映射並更新在VRingUsed中的ring[]映射,即獲取一個VRingUsedElem,記錄剛纔完成寫入的VirtQueueElement信息,主要是index和長度。就這樣一直循環到寫入數據結束。這樣到最後就調用virtqueue_flushvirtio_notify函數更新VRingUsed中的idx並通知客戶機。

大致處理流程是這樣的,細節方面,我們看先如何從一個virt_queue中取出元素的:

複製代碼

 1 int virtqueue_pop(VirtQueue *vq, VirtQueueElement *elem)
 2 {
 3     unsigned int i, head, max;
 4     hwaddr desc_pa = vq->vring.desc;
 5 
 6     if (!virtqueue_num_heads(vq, vq->last_avail_idx))
 7         return 0;
 8 
 9     /* When we start there are none of either input nor output. */
10     elem->out_num = elem->in_num = 0;
11     /*初始化成主描述表的表項數*/
12     max = vq->vring.num;
13     /*獲取的是vringavail中的ring[avail]的值,對應描述符表中某個表項的下標*/
14     i = head = virtqueue_get_head(vq, vq->last_avail_idx++);
15     
16     if (vq->vdev->guest_features & (1 << VIRTIO_RING_F_EVENT_IDX)) {
17         vring_avail_event(vq, vring_avail_idx(vq));//設置了 
18         }
19     //如果描述符項指向的是另一個描述符數組
20     if (vring_desc_flags(desc_pa, i) & VRING_DESC_F_INDIRECT) {
21         //描述符數組的大小應該是VRingDesc大小的整數倍
22         if (vring_desc_len(desc_pa, i) % sizeof(VRingDesc)) {
23             error_report("Invalid size for indirect buffer table");
24             exit(1);
25         }
26 
27         /* loop over the indirect descriptor table */
28         max = vring_desc_len(desc_pa, i) / sizeof(VRingDesc);
29         desc_pa = vring_desc_addr(desc_pa, i);//獲取另一個描述符數組的地址
30         i = 0;//從第一個描述符項開始
31     }
32     /* Collect all the descriptors */
33     do {
34         struct iovec *sg;
35         //判斷第i個描述符的屬性
36         if (vring_desc_flags(desc_pa, i) & VRING_DESC_F_WRITE) {
37             if (elem->in_num >= ARRAY_SIZE(elem->in_sg)) {
38                 error_report("Too many write descriptors in indirect table");
39                 exit(1);
40             }
41             elem->in_addr[elem->in_num] = vring_desc_addr(desc_pa, i);
42             /**/
43             sg = &elem->in_sg[elem->in_num++];
44         } else {
45             if (elem->out_num >= ARRAY_SIZE(elem->out_sg)) {
46                 error_report("Too many read descriptors in indirect table");
47                 exit(1);
48             }
49             elem->out_addr[elem->out_num] = vring_desc_addr(desc_pa, i);
50             sg = &elem->out_sg[elem->out_num++];
51         }
52 
53         sg->iov_len = vring_desc_len(desc_pa, i);
54 
55         /* If we've got too many, that implies a descriptor loop. */
56         if ((elem->in_num + elem->out_num) > max) {
57             error_report("Looped descriptor");
58             exit(1);
59         }
60     } while ((i = virtqueue_next_desc(desc_pa, i, max)) != max);
61 
62     /* Now map what we have collected */
63     /*現在獲取到了客戶機設置的avail space,現在需要map到host內存,然後寫入數據*/
64     virtqueue_map_sg(elem->in_sg, elem->in_addr, elem->in_num, 1);
65     virtqueue_map_sg(elem->out_sg, elem->out_addr, elem->out_num, 0);
66     /*記錄該數據段在整條數據幀中的索引*/
67     elem->index = head;
68     /*隊列的使用元素加一*/
69     vq->inuse++;
70 
71     trace_virtqueue_pop(vq, elem, elem->in_num, elem->out_num);
72     return elem->in_num + elem->out_num;
73 }

複製代碼

 首先調用下函數virtqueue_num_heads檢查下是否又可用的buffer head,然後初始化elem的out_numin_num爲0,接着以 vq->last_avail_idx爲索引從VRingAvailring數組中獲取一個buffer head索引,並賦值給i和head。head要作爲index寫入到elem.index,i作爲一個計數器記錄一共多少物理buffer,即多少個desc。接着判斷客戶機是否支持VIRTIO_RING_F_EVENT_IDX,對於這個看一下官方的解釋:

1 /* The Guest publishes the used index for which it expects an interrupt
2  * at the end of the avail ring. Host should ignore the avail->flags field. */
3 /* The Host publishes the avail index for which it expects a kick
4  * at the end of the used ring. Guest should ignore the used->flags field. */
5 #define VIRTIO_RING_F_EVENT_IDX        29

簡單點說,就是客戶機希望HOST在使用完可用buffer後,中斷虛擬機,忽略avail->flags ,同時HOST希望客戶機使用完HOST添加的used buffer後,通知HOST,忽略 used->flags。

 然後判斷剛纔獲取的head指向的desc是否是一個indirect,是的話表明該描述符項指向一個單獨的描述符表,此描述符表單獨構成一個buffer

然後從下面的do循環開始,就要開始獲取各個物理buffer信息了,這裏分爲兩類:可讀和可寫。可寫的物理buffer地址(客戶機物理地址)記錄到in_addr數組中;可讀的記錄到out_addr數組中;並記錄in_numout_num;這樣知道最後一個desc。獲取完成後調用virtqueue_map_sg函數映射剛纔我們獲取到的各個物理buffer的地址,並記錄到in_sgout_sg數組中,這樣纔可以在HOST訪問到。

 這樣經過virtqueue_pop函數,HOST已經獲取了相應buffer的信息,下一步就是往buffer中寫入數據 了。

寫入數據的過程就比較簡單,這裏就不贅述。那麼寫入數據完成後,調用的virtqueue_fill函數做了哪些工作呢?

複製代碼

 1 void virtqueue_fill(VirtQueue *vq, const VirtQueueElement *elem,
 2                     unsigned int len, unsigned int idx)
 3 {
 4     unsigned int offset;
 5     int i;
 6 
 7     trace_virtqueue_fill(vq, elem, len, idx);
 8 
 9     offset = 0;
10     for (i = 0; i < elem->in_num; i++) {
11         size_t size = MIN(len - offset, elem->in_sg[i].iov_len);
12 
13         cpu_physical_memory_unmap(elem->in_sg[i].iov_base,
14                                   elem->in_sg[i].iov_len,
15                                   1, size);
16 
17         offset += size;
18     }
19 
20     for (i = 0; i < elem->out_num; i++)
21         cpu_physical_memory_unmap(elem->out_sg[i].iov_base,
22                                   elem->out_sg[i].iov_len,
23                                   0, elem->out_sg[i].iov_len);
24     /*idx表明這是第幾個elem,一個elem代表一個buffer,而一個buffer會有一個description 的 head,這裏idx對應head*/
25     idx = (idx + vring_used_idx(vq)) % vq->vring.num;
26 
27     /* Get a pointer to the next entry in the used ring. */
28     vring_used_ring_id(vq, idx, elem->index);
29     vring_used_ring_len(vq, idx, len);
30 }

複製代碼

因爲前面已經將數據寫入,所以就HOST而言已經不需要再次訪問前面映射的地址,那麼這裏就需要撤銷映射以便於下次映射另外的物理buffer,同時需要在

 VRingUsed的ring中添加元素映射我們使用過的elem,後面的vring_used_ring_idvring_used_ring_len就是完成這樣的工作。這樣,待整條數據寫入完成(可能使用多個elem即多個邏輯buffer),在最後調用virtqueue_flush函數集體更新VRingUsed的idx,最後調用virtio_notify通知客戶機!!


 

virtIO和客戶機Driver的通知機制:

這些需要結合具體的PCI設備架構, 之前有分析過PCI設備地址映射,這裏簡要說下,PCI設備的寄存器通過其BAR空間映射到IO地址空間或者內存地址空間,映射範圍記錄在BAR中,virtIO 作爲一個PCI設備有幾個控制寄存器或者理解成控制區域,大致佈局爲:

  • Common configuration
  • Notifications
  • ISR Status
  • Device-specific configuration
  • PCI configuration access

在qemu中也都定義了對應的宏與之匹配

這裏我們只關注ISR Status,它是一個32bit的寄存器,結構佈局如下:

當0位被設置的時候表明這是由virtQueue引起的中斷

 當1位被設置的時候表明這是因爲配置變化引起的中斷

 在HOST完成數據寫入,需要notify客戶機時,調用前面提到的virtio_notify函數

 

複製代碼

 1 void virtio_notify(VirtIODevice *vdev, VirtQueue *vq)
 2 {
 3     if (!vring_notify(vdev, vq)) {
 4         return;
 5     }
 6 
 7     trace_virtio_notify(vdev, vq);
 8     vdev->isr |= 0x01;
 9     virtio_notify_vector(vdev, vq->vector);
10 }

複製代碼

 

 可以看到這裏設置了0位爲1,然後調用virtio_notify_vector函數,virtio_notify_vector其實就是簡單的調用了virtio_pci_notify函數,virtio_pci_notify這裏根據不同的類型,採用不同的方式向客戶機注入中斷,這裏主要由MSI-x和普通注入兩種方式。

複製代碼

 1 static void virtio_pci_notify(DeviceState *d, uint16_t vector)
 2 {
 3     VirtIOPCIProxy *proxy = to_virtio_pci_proxy_fast(d);
 4 
 5     if (msix_enabled(&proxy->pci_dev))
 6         msix_notify(&proxy->pci_dev, vector);
 7     else {
 8         VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);
 9         pci_set_irq(&proxy->pci_dev, vdev->isr & 1);
10     }
11 }

複製代碼

 

 而客戶機通知HOST就是想一個地址寫入16位 的 queue index即可。比較關鍵的是Queue Notify Address的計算,這裏我們下節介紹前端驅動的時候再講!

 

後記:

由於筆者能力有限,文章中難免有錯誤或者不準確之處,若有老師發現,懇請指點!!謝謝!

 

 

 

 

參考:

1、virtiIO:Towards a De-Tacto Standard For Virtual I/O Devices

2、Virtual I/O Device (VIRTIO) Version 1.0

2、qemu1.7.1 源代碼

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