背景知識
什么是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文件的制作