通過MMIO的方式實現VIRTIO-BLK裝置(一)

iaGuoZhi發表於2021-07-14

背景知識

什麼是VIRTIO

使用完全虛擬化,Guest不加任何修改就可以執行在任何VMM上,VMM對於Guest是完全透明的。但每次I/O都將導致CPU在Guest模式與Host模式間切換,在I/O操作密集時,這個切換是影響虛擬機器效能的一個重要因素。對於通過軟體方式模擬的虛擬化而言,完全可以制定一個更加高效簡潔地適用於軟體模擬環境下的驅動和模擬裝置互動的標準,於是Virtio誕生了。與完全虛擬化相比,使用Virtio標準的驅動和模擬裝置的互動不再使用暫存器等傳統的I/O方式,而是採用了Virtqueue的方式傳輸資料。這種設計降低了裝置模擬器實現的複雜度,I/O不再受資料匯流排寬度、暫存器寬度等因素的影響,一次I/O傳遞的資料量不受限制,減少了CPU在Guest模式與Host模式之間的切換,提高了虛擬化的效能。

作為一個統一的標準,越來越多的作業系統(例如Linux與Windows)已經提供了對Virtio的支援。

本文將根據Virtio1.0文件講述VIRTIO的實現。

Dirver 與 Device

在VIRTIO中,Driver實現在虛擬機器,是VIRTIO的前端;Device實現在虛擬機器監控器,是VIRTIO的後端。

描述符表

Virtqueue是VIRTIO中資料傳輸的載體,是VIRTIO的核心部分。Virtqueue主要包括三個部分,分別是描述符表(Descriptor Table),可用描述符區域(Available Ring),已用描述符區域(Used Ring)。

VIRTIO要求描述符表,可用描述符表,已用描述符表分別在GPA上連續。

這三個表可以通過下圖表示:

從左到右依次是,可用描述符表,描述符表,已用描述符表。

描述符表

描述符表是Virqqueue的核心,它包括Queue Size個描述符(Queue Size由driver決定,必須是2的指數,它的值儲存在QueueSize暫存器上)。
每個描述符會指向一塊共享記憶體,如果這塊記憶體是驅動寫給裝置的資料,則稱這個描述符為out型別的,如果這塊記憶體是裝置寫給驅動的資料,則稱這個描述符為in型別。

描述符並不是單獨存在的,它們可以通過指標組成描述符鏈,一個描述符表中會有多條描述符鏈,一條描述符鏈記錄一次I/O事件。

描述符有4個欄位,如上圖所示。描述符通過addr指向一塊儲存有I/O資料的共享記憶體,需要注意的是addr儲存的是GPA,當後端需要根據通過addr讀寫該塊共享記憶體時,需要視虛擬機器監控器的實現將GPA轉換成HVA或者HPA。len表示該塊共享記憶體的長度。flags標識了描述符的屬性,當flags * F_NEXT成立,則描述符可以通過next指向下一個描述符;當flags * F_WRITE成立,則這個描述符屬於in型別,否則則是out型別;當flags * F_INDIRECT成立時,共享記憶體上將不是直接儲存資料,而是儲存一連串描述符。next指標則指向描述符鏈中的下一個描述符。

可用描述符表

driver將資料寫入描述符記錄的共享記憶體後,需要讓device知道哪些描述符可以消費(可用)。可用描述符負責完成這個任務。

可用描述符中的ring是一個陣列,因virtqueue中最多可能有Queue Size可用描述符鏈,ring的大小是Queue Size。ring中每個元素都記錄了對應的描述符鏈的第一個描述符的ID,因此ring中一個元素對應一條描述符鏈,也即對應一次I/O事件。可用描述符中idx變數記錄的是driver下一個填充的可用描述符,與之對應的是device將在變數last_avail_idx中記錄上一個處理完的可用描述符,因此在last_avail_idx到idx之間是等待device處理的可用描述符。

已用描述符表

device將已經處理好的IO請求對應的描述符記錄在已用描述符中,從這裡可以看出,可用,已用這兩個概念都是對device而言的。一個需要注意的點是,可用描述符和已用描述符都是指向描述符鏈,它們只是說明該條描述符鏈的狀態,並不是代表描述符鏈的in/out型別。

與可用描述符不同的是,已用描述符的陣列中每個元素的大小是8byte,它不僅記錄了描述符鏈第一個描述符的ID,還記錄了device向描述符鏈中寫入的byte數。已用描述符通過idx和last_used_idx記錄了等待driver回收的描述符鏈。idx由裝置維護,表示裝置下一個處理完的描述符鏈將記錄在已用描述符表中的位置,last_used_idx由驅動維護,記錄的是驅動上一個回收的描述符鏈在已用描述符表中的位置。

VIRTIO MMIO暫存器(部分

在MMIO實現VIRTIO的情況下,每個VIRTIO裝置都有一個MMIO REGION。這個REGION在裝置樹中的宣告如圖:

上圖表示GPA 0x1e000 到 0x1e200 的地址段是這個virtio_block裝置的MMIO REGION。這個REGION中分佈著VIRTIO MMIO暫存器。

42是這個virtio裝置對應的中斷號,注意42需要加上SPI中斷的基礎值:32,因此這個virtio driver實際上能夠識別的中斷號是74。

一些重要的MMIO 暫存器如下:

DeviceFeatures & DeviceFeaturesSel

裝置通過DeviceFeatures暫存器告訴驅動裝置支援的一些機制,比如VIRTIO_RING_F_INDIRECT_DESC這個bit就是告訴driver:device支援virtqueue通過indirection擴大共享記憶體區域。driver只能夠在device提供的機制上工作,不能夠在device沒有提供該機制的情況下執行對應的程式碼。由於DeviceFeatures的區域要大於4bytes,driver需要通過DeviceFeaturesSel暫存器用檢視DeviceFeatures的部分bits。

DriverFeatures & DriverFeaturesSel

驅動通過DriverFeatures暫存器告訴裝置,驅動支援了裝置的哪些機制,DriverFeaturesSel則用於裝置檢視DriverFeatures。

QueueSel

對於某些virtio裝置,比如virtio-net,virtio-console會包括多個virtqueue。為了讓裝置知道該對在哪條virtqueue上進行處理,driver會通過QueueSel暫存器告訴驅動後續的操作是在哪條virtqueue進行的。

QueueReady

driver會通過寫QueueReady暫存器通知裝置,當前virtqueue已經初始化好了,裝置可以通過讀描述符暫存器來獲得virtqueue的地址。

QueueNotify

當driver準備了新的可用描述符時,會通過寫QueueNotify暫存器通知device進行處理。

InterruptStatus

Virtio裝置可以通過傳送中斷通知虛擬機器,每個virtio裝置有一個對應的中斷號(這個中斷號在裝置樹中宣告),虛擬機器在收到中斷後,會根據中斷號找到對應的driver,driver則需要通過InterruptStatus暫存器搞清楚產生這次中斷的事件是什麼,比如bit 0表示已用描述符更新,bit 1表示裝置配置空間更新。

QueueDescLow & QueueDescHigh

driver通過寫這兩個暫存器告訴device描述符表的GPA。由於每個MMIO暫存器只有32個bit,因此需要兩個暫存器。

QueueAvailLow & QueueAvailHigh

driver通過寫這兩個暫存器告訴device可用描述符表的GPA。

QueueUsedLow & QueueUsedHigh

driver通過寫這兩個暫存器告訴device已用描述符表的GPA。

Config

Config不是一個暫存器,而是一個區域,這個區域由device進行配置,每種device會有不一樣的配置區域。
下圖展示的就是block裝置配置空間的資料結構。

參考資料

《深度探索Linux系統虛擬化:原理與實現》
《Virtual I/O Device Version 1.0》
《Linux虛擬化KVM-Qemu分析(十一)之virtqueue》
https://github.com/minosproject/minos/

下一期將介紹實現VIRTIO-BLK裝置時,虛擬機器image,rootfs,dtb檔案的製作

相關文章