1. 程式人生 > >Virtio 基本概念和設備操作

Virtio 基本概念和設備操作

nali out 為我 written 字節 1.5 提醒 eof 設備配置

轉載自https://www.ibm.com/developerworks/cn/linux/1402_caobb_virtio/index.html

Linux Kernel 支持很多 Hypervisor,比如 KVM、Xen 和 VMware 的 VMI 等。每個 Hypervisor 都有自己獨特的 block、network、console 等設備模型,設備驅動多樣化的特性和優化方式使得各個平臺共有性的東西越來越少,亟需提供一種通用的框架和標準接口來減少各 Hypervisor 虛擬化設備之間的差異,從而減少驅動開發的負擔。

虛擬化主要包括處理器的虛擬化, 內存的虛擬化以及 I/O 的虛擬化等,從 2006 年開始,KVM 上設備 I/O 虛擬化的性能問題也顯現了出來,此時由 Rusty Russell 開發的 virtio 引起了開發者們的註意並逐漸被 KVM 等虛擬化平臺接納並作為了其 I/O 虛擬化最主要的一個通用框架。

Virtio 使用 virtqueue 來實現其 I/O 機制,每個 virtqueue 就是一個承載大量數據的 queue。vring 是 virtqueue 的具體實現方式,針對 vring 會有相應的描述符表格進行描述。框架如下圖所示:

圖 1.virtio 框架

技術分享圖片

virtio 提供了一套有效,易維護、易開發、易擴展的中間層 API。virtio 使用 Feature Bits 來進行功能擴展,使用 vring buffer 傳輸數據。使用 virtio 的設備在配置上於其他 PCI 設備沒有太多不同,只不過它只應用於虛擬化環境。

Virtio 設備具備以下特點:

1. 簡單易開發

virtio PCI 設備使用通用的 PCI 的中斷和 DMA 機制,對於設備驅動開發者來說不會帶來困難。

2. 高效

virtio PCI 設備使用針對輸入和輸出使用不同的 vring,規避了可能的由高速緩存帶來的影響。

3. 標準

virtio PCI 不假定其所處的環境一定需要對 PCI 的支持,實際上當前很多 virtio 設備已經在非 PCI 總線上實現了,這些設備根本不需要 PCI。

4. 可擴展

virtio PCI 設備包含一組 Feature Bits,在設備安裝過程中,可以告知 guest OS。設備和驅動之間相互協調,驅動可以根據設備提供的特性以及驅動自身能夠支持的特性來最終確定在 guest OS 裏面能夠使用的設備特性。這樣可以顧及到設備的前後兼容性。

因此,對與 guest OS 來說,只需要添加一個 PCI 設備驅動,然後 Hypervisor 添加設備的 vring 支持即可以添加一個 virtio 設備。

本文面向對 virtio 設備有一定了解的程序員,對 virtio 驅動和虛擬設備開發人員提供一定的參考和幫助。

基本概念

設備的類型

virtio 設備的 Vendor ID (廠商編號)為 0x1AF4,Device ID(設備編號)範圍為 0x1000~0x103F,subsystem device ID(子系統設備編號)如下:

表 1. virtio device ID

virtio 設備的配置空間

需要使用 PCI 設備的第一塊 I/O region 來對 PCI 設備進行配置。針對 virtio 設備來說,在 device 特定的配置區域後會有一塊區域存放 virtio header(下表)。

表 2.virtio 設備的配置空間

最開始的 32bits 為設備的 feature bits,緊跟著的 32bits 為 Guest(driver) feature bits,然後依次為 Queue Address(32 bits),Queue Size(16bits),Queue Select(16bits),Queue Notify(16bits),Device Status(8 bits),ISR status(8 bits)。

如果設備開啟了 MSI-X(Message Signalled Interrupt-Extended),則在上述 bits 後添加了兩個域:

表 3.virtio 設備的配置空間--MSI-X 開啟下的附加配置

緊接著這些通常的 bits,可能會有指定設備專屬的 headers:

表 4.virtio 設備的配置空間-設備專屬配置

各段的具體含義和作用會在後面給予解釋。

特征位(Feature Bits)

使用 feature bits(32bits)來指定設備支持的功能和特性。

0~23:根據設備類型的不同而不同

24~32:保留位,用於 queue 和 feature 協商機制的擴展

有 2 組 feature bits,device 端列出支持的特性寫入 device feature bits 域,guest 端把它支持的 feature bits 寫入 guest feature bits 域,雙方相互協商,開始協商的唯一途徑是 reset device。

在設備配置 header 裏面添加一個新的域通常會提供一個 feature bit,所以 guest 應當在讀取新配置域之前檢查 feature bits.

考慮到設備的前後兼容性,如果 device 使用新的 feature bits,guest 無法把新的 feature bits 寫入 guest feature bits,guest 進入向後兼容的模式。同樣如果 guest driver 使用了 device 無法支持的 feature bits,guest 在 device feature bits 中看不到 device 不支持的 feature bits,guest 也同樣進入向後兼容的模式。

設備狀態(Device Status)

Device Status 域主要由 guest 來更新,表示當前 drive 的狀態。狀態包括:

0:寫入 0 表示重啟該設備

1:Acknowledge,表明 guest 已經發現了一個有效的 virtio 設備

2:Driver,表明 guest 已經可以驅動該設備,guest 已經成功註冊了設備驅動

3:Driver_OK,表示 guest 已經正確安裝了驅動,準備驅動設備

4:FAILED,在安裝驅動過程中出錯

每次試圖重新初始化設備前,需要設置 Device Status 為 0。

配置修改/隊列向量/中斷

如果 MSI-X 沒有啟用的話,在 header 20 bytes 偏移的地方為設備指定的配置空間,而在 MSI-X 開啟的情況下,移動到 Queue Vector 的後面。

PCI 設備具有並且啟用 MSI-X 中斷時,在 virtio header 裏會有 4 bytes 的區域用於把“配置更改”和“queue 中斷/events”映射到對應的 MSI-X 中斷向量,通過寫入 MSI-X table 入口號(有效值範圍:0x0~0x7FF)來映射中斷,寫入 VIRTIO_MSI_NO_VECTOR 來關閉中斷取消映射。

1 #define VIRTIO_MSI_NO_VECTOR 0xFFFF

讀取這些寄存器返回映射到指定 event 上的 vector,如果取消映射了返回 NO_VECTOR。默認情況下,為 NO_VECTOR。

映射一個 event 到 vector 上需要分配資源,可能會失敗,此時讀取寄存器的值,返回 NO_VECTOR。當映射成功後,驅動必須讀取這些寄存器的值來確認映射成功。如果映射失敗的化,可能會嘗試映射較少的 vector 或者關閉 MSI-X 中斷。

在 Linux3.5.2 內核中,驅動會首先嘗試為“配置修改”中斷映射一個 vector,為每個 queue events 映射一個 vector。如果映射失敗,會為所有的 queue events 映射一個共享的 vector.如果再失敗,則關閉 MSI-X 中斷,為每個 event 和“配置修改”中斷申請常規中斷。

設備的專屬配置

此配置空間包含了虛擬設備特殊的一些配置信息,可由 guest 讀寫。

比如網絡設備含有一個 VIRTIO_NET_F_MAC 特征位,表明 host 想要設備包含一個特定的 MAC 地址,相應的設備專屬配置空間中存有此 MAC 地址。

這種專屬的配置空間和特征位的使用可以實現設備特性功能的擴展。

virtio 設備配置的操作

針對 virtio 設備配置的操作主要包括四個部分——讀寫特征位,讀寫配置空間,讀寫狀態位,重啟設備,如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 struct virtio_config_ops { void (*get)(struct virtio_device *vdev, unsigned offset,void *buf, unsigned len); void (*set)(struct virtio_device *vdev, unsigned offset, const void *buf, unsigned len); u8 (*get_status)(struct virtio_device *vdev); void (*set_status)(struct virtio_device *vdev, u8 status); void (*reset)(struct virtio_device *vdev); int (*find_vqs)(struct virtio_device *, unsigned nvqs, struct virtqueue *vqs[], vq_callback_t *callbacks[], const char *names[]); void (*del_vqs)(struct virtio_device *); u32 (*get_features)(struct virtio_device *vdev); void (*finalize_features)(struct virtio_device *vdev); const char *(*bus_name)(struct virtio_device *vdev); };

get():讀取某配置域的值

set():設置寫入某配置域

get_status():讀取狀態位

set_status():寫入狀態位

reset():重啟設備主要是清除狀態位並重新配置,還有清除掉所有的中斷及其回調。當然也可以用於 guest 恢復驅動。

get_features():讀取 feature bits

finalize_features():確認最終使用的特征並寫入 Guest 特征位

Virtqueue

每個設備擁有多個 virtqueue 用於大塊數據的傳輸。virtqueue 是一個簡單的隊列,guest 把 buffers 插入其中,每個 buffer 都是一個分散-聚集數組。驅動調用 find_vqs()來創建一個與 queue 關聯的結構體。virtqueue 的數目根據設備的不同而不同,比如 block 設備有一個 virtqueue,network 設備有 2 個 virtqueue,一個用於發送數據包,一個用於接收數據包。Balloon 設備有 3 個 virtqueue.

針對 virtqueue 的操作包括:

1)

1 2 3 4 5 6 7 .int virtqueue_add_buf( struct virtqueue *_vq, struct scatterlist sg[], unsigned int out, unsigned int in, void *data, gfp_t gfp)

add_buf()用於向 queue 中添加一個新的 buffer,參數 data 是一個非空的令牌,用於識別 buffer,當 buffer 內容被消耗後,data 會返回。

2).virtqueue_kick():

Guest 通知 host 單個或者多個 buffer 已經添加到 queue 中,調用 virtqueue_notify(),notify 函數會向 queue notify(VIRTIO_PCI_QUEUE_NOTIFY)寄存器寫入 queue index 來通知 host。

3).void *virtqueue_get_buf(struct virtqueue *_vq, unsigned int *len)

返回使用過的 buffer,len 為寫入到 buffer 中數據的長度。獲取數據,釋放 buffer,更新 vring 描述符表格中的 index。

4).virtqueue_disable_cb()

示意 guest 不再需要再知道一個 buffer 已經使用了,也就是關閉 device 的中斷。驅動會在初始化時註冊一個回調函數,disable_cb()通常在這個 virtqueue 回調函數中使用,用於關閉再次的回調發生。

5).virtqueue_enable_cb()

與 disable_cb()剛好相反,用於重新開啟設備中斷的上報。

Vring

virtio_ring 是 virtio 傳輸機制的實現,vring 引入 ring buffers 來作為我們數據傳輸的載體。

virtio_ring 包含 3 部分:

描述符數組(descriptor table)用於存儲一些關聯的描述符,每個描述符都是一個對 buffer 的描述,包含一個 address/length 的配對。

可用的 ring(available ring)用於 guest 端表示那些描述符鏈當前是可用的。

使用過的 ring(used ring)用於表示 Host 端表示那些描述符已經使用。

Ring 的數目必須是 2 的次冪。

描述符和描述符表格

vring descriptor 用於指向 guest 使用的 buffer。

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

多個 buffer 組成一個 list 由 descriptor table 指向這些 list。

約定俗成,每個 list 中,read-only buffers 放置在 write-only buffers 前面。

圖 2.descriptor table

技術分享圖片

Indirect Descriptors

有些設備可能需要同時完成大量數據傳輸的大量請求,設備 VIRTIO_RING_F_INDIRECT_DESC 特性能夠滿足這種需求。為了增加 ring 的容量,vring 可以指向一個可以處於內存中任何位置 indirect descriptors table,而這個 table 指向一組 vring descriptors,而這些 vring descriptor 分別指向一組 buffer list(如圖所示)。當然 indirect descriptors table 中的 descriptor 不能再次指向 indirect descriptors table。單個 indirect descriptor table 可以包含 read-only 和 write-only 的 descriptors,帶有 write-only flag 的 descriptor 會被忽略。

圖 3.indirect decriptors

技術分享圖片

Available Ring

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

idx=idx&(vring.num-1)

圖 4.available ring

技術分享圖片

Used Ring

Used ring 指向 device(host)使用過的 buffers。Used ring 和 Available ring 之間在內存中的分布會有一定間隙,從而避免了 host 和 guest 兩端由於 cache 的影響而會寫入到 virtqueue 結構體的同一部分的情況。

flags 用於 device 告訴 guest 再次添加 buffer 到 available ring 時不再提醒,也就是說 guest 添加 buffers 到 available ring 時不必進行 kick 操作。

Used vring element 包含 id 和 len,id 指向 descriptor chain 的入口,與之前 guest 寫入到 available ring 的入口項一致。

len 為寫入到 buffer 中的字節數。

1 2 3 4 5 6 struct vring_used_elem { /* Index of start of used descriptor chain. */ __u32 id; /* Total length of the descriptor chain which was used (written to) */ __u32 len; };

Virtio 設備操作

設備的初始化

1. 重啟設備狀態,狀態位寫入 0

2. 設置狀態為 ACKNOWLEDGE,guest(driver)端當前已經識別到了設備

3. 設置狀態為 Driver,guest 知道如何驅動當前設備

4. 設備特定的安裝和配置:特征位的協商,virtqueue 的安裝,可選的 MSI-X 的安裝,讀寫設備專屬的配置空間等

5. 設置狀態為 Driver_OK 或者 Failed(如果中途出現錯誤)

6. 當前設備初始化完畢,可以進行配置和使用

設備的安裝和配置

設備操作包括兩個部分:driver(guest)提供 buffers 給設備,處理 device(host)使用過的 buffers。

初始化 virtqueue

該部分代碼的實現在 virtio-pci.c 裏 setup_vps()裏面,具體為:

1.選擇 virtqueue 的索引,寫入 Queue Select 寄存器

2.讀取 queue size 寄存器獲得 virtqueue 的可用數目

3.分配並清零 4096 字節對齊的連續物理內存用於存放 virtqueue(調用 alloc_pages_exact()).把內存地址除以 4096 寫入 Queue Address 寄存器(VIRTIO_PCI_QUEUE_ADDR_SHIFT)

4.可選情況下,如果 MSI-X 中斷機制啟用,選擇一個向量用於 virtqueue 請求的中斷,把對應向量的 MSI-X 表格入口號寫入 Queue Vector 寄存器域,然後再次讀取該域以確認返回正確值。

Virtqueue 所需要的字節數由下面的公式獲得:

1 2 3 ((sizeof(struct vring_desc) * num + sizeof(__u16) * (3 + num) + align - 1) & ~(align – 1)) + sizeof(__u16) * 3 + sizeof(struct vring_used_elem) * num

其中,num 為 virtqueue 的數目。

Guest 向設備提供 buffer

1.把 buffer 添加到 description table 中,填充 addr,len,flags

2.更新 available ring head

3.更新 available ring 中的 index

4.通知 device,通過寫入 virtqueue index 到 Queue Notify 寄存器

Device 使用 buffer 並填充 used ring

device 端使用 buffer 後填充 used ring 的過程如下:

1.virtqueue_pop()——從描述符表格(descriptor table)中找到 available ring 中添加的 buffers,映射內存

2.從分散-聚集的 buffer 讀取數據

3.virtqueue_fill()——取消內存映射,更新 ring[idx]中的 id 和 len 字段

4.virtqueue_flush()——更新 vring_used 中的 idx

5.virtio_notify()——如果需要的話,在 ISR 狀態位寫入 1,通知 guest 描述符已經使用

中斷處理

在 MSI-X 關閉的情況下,設備端會設置 ISR bit lower bit 並發送 PCI 中斷給客戶機,

客戶機端會讀取 ISR lower bit,同時會清零 ,如果 lower bit 為 0,則無中斷.

如果有中斷,遍歷一遍該設備每個 virtqueue 上的 used rings,來判斷是否有中斷服務需要處理。

在 MSI-X 開啟的情況下,設備端會為設備請求一個 MSI-X interrupt message 並設置 Queue Vector 寄存器的值為 MSI-X table entry,如果 Queue Vector 為 NO_VECTOR,不再請求 interrupt message。

客戶機端會遍歷映射到該 MSI-X vector 每個 virtqueue 上的 used rings,來判斷是否有中斷服務需要處理。

Config Changed

設備端如果改變其 configure space,也存在兩種情況。

客戶機端會在 MSI-X 關閉的情況下,讀取 ISR 高位,判斷是否為 1,掃描所有 virtqueue 上的 used rings,觸發驅動對 config changed 的處理函數。在 MSI-X 開啟的情況下,與中斷相同,同樣請求 MSI-X interrupt message,將 Configuration Vector 設置為 MSI-X table entry。

總結

virtio 是 KVM 虛擬化技術中 IO 虛擬化的一個重要框架,現在有很多虛擬設備都使用了 virtio。本文著重介紹了 virtio 的基本概念和設備操作,可以方便讀者更深一層地理解 virtio,同時也對 virtio 感興趣的朋友能夠從本文中獲取幫助。

Virtio 基本概念和設備操作