【原創】Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化
阿新 • • 發佈:2020-08-30
# 背景
- `Read the fucking source code!` --By 魯迅
- `A picture is worth a thousand words.` --By 高爾基
說明:
1. KVM版本:5.9.1
2. QEMU版本:5.0.0
3. 工具:Source Insight 3.5, Visio
# 1. 概述
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232438753-619940029.png)
- KVM虛擬化離不開底層硬體的支援,本文將介紹ARMv8架構處理器對虛擬化的支援,包括記憶體虛擬化、中斷虛擬化、I/O虛擬化等內容;
- ARM處理器主要用於移動終端領域,近年也逐漸往伺服器領域靠攏,對虛擬化也有了較為完善的支援;
- `Hypervisor`軟體,涵蓋的功能包括:記憶體管理、裝置模擬、裝置分配、異常處理、指令捕獲、虛擬異常管理、中斷控制器管理、排程、上下文切換、記憶體轉換、多個虛擬地址空間管理等;
- 本文描述的ARMv8虛擬化支援,對於理解`arch/arm64/kvm`下的程式碼很重要,脫離硬體去看Architecture-Specific程式碼,那是耍流氓;
開始旅程!
# 2. ARMv8虛擬化
## 2.1 Exception Level
- ARMv7之前的架構,定義了一個處理器的異常處理模式,比如`USR, FIQ, IRQ, SVC, ABT, UND, SYS, HYP, MON`等,各個異常模式所處的特權級不一樣,比如`USR`模式的特權級就為`PL0`,對應為使用者態程式執行;
- 處理器的異常模式可以在特權級軟體控制下進行主動切換,比如修改`CPSR`暫存器,也可以被動進行異常模式切換,典型的比如中斷來臨時切換到`IRQ模式`;
ARMv7處理器的異常模式如下表所示:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232458451-1431013725.png)
然鵝,到了ARMv8,`Exception Level(EL)`取代了特權級,其中處理器的異常模式與`Exception Level`的對映關係如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232734015-1125516073.png)
- 當異常發生時,處理器將改變`Exception Level`(相當於ARMv7中的處理器模式切換),來處理異常型別;
- 圖中可以看出`Hypervisor`執行在`EL2`,而`Guest OS`執行在`EL1`,可以通過`HVC (Hypervisor Call)`指令向`Hypervisor`請求服務,響應虛擬化請求時就涉及到了`Exception Level`的切換;
## 2.2 Stage 2 translation
`Stage 2轉換`與記憶體虛擬化息息相關,這部分內容不僅包括常規的記憶體對映訪問,還包含了基於記憶體對映的I/O(`MMIO`)訪問,以及系統記憶體管理單元(`SMMUs`)控制下的記憶體訪問。
### 2.2.1 記憶體對映
OS在訪問實體記憶體前,需要先建立頁表來維護虛擬地址到實體地址的對映關係,看過之前記憶體管理分析的同學應該熟悉下邊這張圖,這個可以認為是`Stage 1轉換`:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232749769-1606100875.png)
- 當有了虛擬機器時,情況就不太一樣了,比如Qemu執行在Linux系統之上時,它只是Linux系統的一個使用者程序,`Guest OS`所認為自己訪問的實體地址,其實是Linux的使用者程序虛擬地址,到最終的實體地址還需要進一步的對映;
- `Hypervisor`可以通過`Stage 2轉換`來控制虛擬機器的記憶體檢視,控制虛擬機器是否可以訪問某塊實體記憶體,進而達到隔離的目的;
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232831670-243204921.png)
- 整個地址的對映分成了兩個階段:
1. `Stage 1: VA(Virutal Address) -> IPA(Intermediate Physical Address)`,作業系統控制`Stage 1轉換`;
2. `Stage 2: IPA(Intermediate Physical Address) -> PA(Physical Address)`,`Hypervisor`控制`Stage 2轉換`;
- `Stage 2轉換`與`Stage 1`轉換機制很類似,不同點在於`Stage 2轉換`時判斷記憶體型別是normal還是device時,是存放進頁表資訊裡了,而不是通過`MAIR_ELx`暫存器來判斷;
- 每個虛擬機器(VM,Virtual Machine)都會分配一個`VMID`,用於標識`TLB entry`所屬的VM,允許在TLB中同時存在多個不同VM的轉換;
- 作業系統會給應用程式分配一個`ASID(Address Space Identifier)`,也可以用於標識`TLB entry`,屬於同一個應用程式的`TLB entry`都有相同的`ASID`,不同的應用程式可以共享同一塊`TLB快取`。每個VM都有自己的`ASID`空間,通常會結合`VMID`和`ASID`來同時使用;
- `Stage 1`和`Stage 2`的轉換頁表中,都包含了屬性的相關裝置,比如訪問許可權,儲存型別等,在兩級轉換的過程中,`MMU`會整合成一個最終的也有效值,選擇限制更嚴格的屬性,如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232932829-1993666529.png)
- 圖中的`Device`屬性限制更嚴格,則選擇`Device`型別;
- `Hypervisor`如果想要改變預設整合行為,可以通過暫存器`HCR_EL2(Hypervisor Configuration Register)`來配置,比如設定`Non-cacheable`, `Write-Back Cacheable`等特性;
### 2.2.2 `MMIO(Memory-Mapped Input/Output)`
`Guest OS`認為的實體地址空間,實際是`IPA`地址空間,就像真實物理機中一樣,`IPA`的地址空間,也分成記憶體地址空間和`I/O`地址空間:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829232949035-1560977763.png)
- 訪問外設有兩種情況:1)直通訪問真實的外設;2)觸發`fault`,`Hypervisor`通過軟體來模擬;
- `VTTBR_EL2`:`Virtualization Translation Table Base Register`,虛擬轉換表基地址暫存器,存放`Stage 2轉換`的頁表;
- 為了模擬外設,`Hypervisor`需要知道訪問的是哪個外設以及訪問的暫存器,讀訪問還是寫訪問,訪問長度是多少,使用哪些暫存器來傳送資料等。`Stage 2轉換`有一個專門的`Hypervisor IPA Fault Address Register, EL2(HPFAR_EL2)`暫存器,用於捕獲`Stage 2轉換`過程中的fault;
軟體模擬外設的示例流程如下:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233001772-1160277084.png)
- 1)虛擬機器VM中的軟體嘗試訪問串列埠裝置;
- 2)訪問時`Stage 2轉換`被block住,並觸發abort異常路由到`EL2`。異常處理程式查詢`ESR_EL2(Exception Syndrome Register)`暫存器關於異常的資訊,如訪問長度、目標暫存器,Load/Store操作等,異常處理程式還會查詢`HPFAR_EL2`暫存器,獲取abort的IPA地址;
- 3)`Hypervisor`通過`ESR_EL2`和`HPFAR_EL2`裡的相關資訊對相關虛擬外圍裝置進行模擬,完成後通過`ERET`指令返回給`vCPU`,從發生異常的下一條指令繼續執行;
### 2.2.3 `SMMUs(System Memory Management Units)`
訪問記憶體的另外一種case就是DMA控制器。
非虛擬化下DMA控制器的工作情況如下:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233027257-538027833.png)
- DMA控制器由核心的驅動程式來控制,能確保作業系統層面的記憶體的保護不會被破壞,使用者程式無法通過DMA去訪問被限制的區域;
虛擬化下DMA控制器,VM中的驅動直接與DMA控制器互動會出現什麼問題呢?如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233042445-1554003389.png)
- DMA控制器不受`Stage 2轉換`的約束,會破壞VM的隔離性;
- Guest OS以為的實體地址是IPA地址,而DMA看到的地址是真實的實體地址,兩者的視角不一致,為了解決這個問題,需要捕獲每次VM與DMA控制器的互動,並提供轉換,當記憶體出現碎片化時,這個處理低效且容易引入問題;
`SMMUs`可以用於解決這個問題:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233058320-1717242981.png)
- `SMMU`也叫`IOMMU`,對IO部件提供MMU功能,虛擬化只是SMMU的一個應用;
- `Hypervisor`可以負責對`SMMU`進行程式設計,以便讓上層的控制器和虛擬機器VM以同一個視角對待記憶體,同時也保持了隔離性;
## 2.3 Trapping and emulation of Instructions
`Hypervisor`也需要具備捕獲(`trap`)和模擬指令的能力,比如當VM中的軟體需要配置底層處理器來進行功耗管理或者快取一致性操作時,為了不破壞隔離性,`Hypervisor`就需要捕獲操作並進行模擬,以便不影響其他的VM。如果設定了捕獲某個操作時,當該操作被執行時會向更高一級的`Exception Level`觸發異常(比如`Hypervisor`為EL2),從而在相應的異常處理中完成模擬。
例子來了:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233146348-464186261.png)
- 在ARM處理器中執行`WFI(wait for interrupt)`命令,可以讓CPU處於一個低功耗的狀態;
- `HCR_EL2(Hypervisor Control Register)`,當該暫存器的`TWI==1`時,vCPU執行`WFI`指令會觸發EL2異常,從而`Hypervisor`可以對其進行模擬,將任務排程到另外一個vCPU即可;
捕獲(`traps`)的另一個作用是可以用於向Guest OS呈現暫存器的虛擬值,如下:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233202178-1880863710.png)
- `ID_AA64MMFR0_EL1`暫存器用於查詢處理器對記憶體系統相關特性的支援,系統可能在啟動階段會讀取該暫存器,`Hypervisor`可以向Guest OS呈現一個不同的虛擬值;
- 當vCPU讀取該暫存器時,觸發異常,`Hypervisor`在`trap_handler`中進行處理,設定一個虛擬值,並最終返回給vCPU;
- 通過`trap`來虛擬化一個操作需要大量的計算,包括觸發異常、捕獲,模擬、返回等一系列操作,像`ID_AA64MMFR0_EL1`暫存器訪問並不頻繁,這種方式問題不大。但是當需要頻繁訪問的暫存器,比如`MIDR_EL1`和`MPIDR_EL1`等,出於效能的考慮,應該避免陷入到`Hypervisor`中進行模擬處理,可以通過其他機制,比如提供`VPIDR_EL2`和`VMIDR_EL2`暫存器,在進入VM前就設定好該值,當讀取`MIDR_EL1`和`MPIDR_EL1`時,硬體就返回`VPIDR_EL2`和`VMIDR_EL2`的值,避免了陷入處理;
## 2.4 Virtualizing exceptions
- `Hypervisor`對虛擬中斷的處理比較複雜,`Hypervisor`本身需要機制來在EL2處理中斷,還需要機制來將外設的中斷訊號傳送到目標虛擬機器VM(或vCPU)上,為了使能這些機制,ARM體系架構包含了對虛擬中斷的支援(vIRQs,vFIQs,vSErrors);
- 處理器只有在EL0/EL1執行狀態下,才能收到虛擬中斷,在EL2/EL3狀態下不能收到虛擬中斷;
- `Hypervisor`通過設定`HCR_EL2`暫存器來控制向EL0/EL1傳送虛擬中斷,比如為了使能vIRQ,需要設定`HCR_EL2.IMO`,設定後便會將物理中斷髮送至EL2,然後使能將虛擬中斷髮送至EL1;
有兩種方式可以產生虛擬中斷:1)在處理器內部控制`HCR_EL2`暫存器;2)通過GIC中斷控制器(v2版本以上);其中方式一使用比較簡單,但是它只提供了產生中斷的方式,需要`Hypervisor`來模擬VM中的中斷控制器,通過捕獲然後模擬的方式,會帶來overhead,當然不是一個最優解。
讓我們來看看`GIC`吧,看過之前中斷子系統系列文章的同學,應該見過下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233259504-1725365094.png)
- `Hypervisor`可以將GIC中的`Virtual CPU Interface`對映到VM中,從而允許VM中的軟體直接與GIC進行通訊,`Hypervisor`只需要進行配置即可,這樣可以減少虛擬中斷的overhead;
來個虛擬中斷的例子吧:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233333557-1792763750.png)
1. 外設觸發中斷訊號到GIC;
2. GIC產生物理中斷`IRQ`或者`FIQ`訊號,如果設定了`HCR_EL2.IMO/FMO`,中斷訊號將被路由到`Hypervisor`,`Hypervisor`會檢查中斷訊號轉發給哪個`vCPU`;
3. `Hypervisor`設定GIC,將該物理中斷訊號以虛擬中斷的形式傳送給某個`vCPU`,如果此時處理器執行在EL2,中斷訊號會被忽略;
4. `Hypervisor`將控制權返回給`vCPU`;
5. 處理器執行在EL0/EL1時,虛擬中斷會被接受和處理
- ARMv8處理器中斷遮蔽由`PSTATE`中的位元位來控制(比如`PSTATE.I`),虛擬化時位元位的作用有些不一樣,比如設定`HCR_EL2.IMO`時,表明物理IRQ路由到EL2,並且對EL0/EL1開啟`vIRQs`,因此,當執行在EL0/EL1時,`PSTATE.I`位元位針對的是虛擬`vIRQs`而不是物理的`pIRQs`。
## 2.5 Virtualizing the Generic Timers
先來看一下SoC的內部:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233343231-187249989.png)
簡化之後是這樣的:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233351132-1430832040.png)
- ARM體系架構每個處理器都包含了一組通用定時器,從圖中可以看到兩個模組:`Comparators`和`Counter Module`,當`Comparators`的值小於等於系統的count值時便會產生中斷,我們都知道在作業系統中`timer`的中斷就是系統的脈搏了;
下圖展示虛擬化系統中執行的`vCPU`的時序:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233408771-223266346.png)
- 物理時間4ms,每個`vCPU`執行2ms,如果設定`vCPU0`在`T=0`之後的3ms後產生中斷,那希望是物理時間的3ms後(也就是`vCPU0`的虛擬時間2ms)產生中斷,還是虛擬時間3ms後產生中斷?ARM體系結構支援這兩種設定;
- 執行在`vCPU`上的軟體可以同時訪問兩種時鐘:`EL1物理時鐘`和`EL1虛擬時鐘`;
`EL1物理時鐘`和`EL1虛擬時鐘`:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233415764-710020648.png)
- `EL1物理時鐘`與系統計數器模組直接比較,使用的是`wall-clock`時間;
- `EL1虛擬時鐘`與虛擬計數器比較,而虛擬計數器是在物理計數器上減去一個偏移;
- `Hypervisor`負責為當前排程執行的`vCPU`指定對應的偏移,這種方式使得虛擬時間只會覆蓋`vCPU`實際執行的那部分時間;
來一張示例圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233422824-60522162.png)
- 6ms的時間段裡,每個`vCPU`執行3ms,`Hypervisor`可以使用偏移暫存器來將`vCPU`的時間調整為其實際的執行時間;
## 2.6 Virtualization Host Extensions(VHE)
- 先丟擲一個問題:通常`Host OS`的核心都執行在EL1,而控制虛擬化的程式碼執行在EL2,這就意味著傳統的上下文切換,這個顯然是比較低效的;
- `VHE`用於支援`type-2`的`Hypervisor`,這種擴充套件可以讓核心直接跑在EL2,減少host和guest之間共享的系統暫存器數量,同時也減少虛擬化的overhead;
`VHE`由系統暫存器`HCR_EL2`的`E2H`和`TGE`兩個位元位來控制,如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233435229-1455157188.png)
`VHE`的引入,需要考慮虛擬地址空間的問題,如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233442416-1824423239.png)
- 我們在記憶體子系統分析時提到過虛擬地址空間的問題,分為使用者地址空間(`EL0`)和核心地址空間(`EL1`),兩者的區域不一致,而在`EL2`只有一個虛擬地址空間區域,這是因為`Hypervisor`不支援應用程式,因此也就不需要分成核心空間和使用者空間了;
- `EL0/EL1`虛擬地址空間也同時支援`ASID(Address Space Identifiers)`,而`EL2`不支援,原因也是`Hypervisor`不需要支援應用程式;
從上兩點可以看出,為了支援`Host OS`能執行在`EL2`,需要新增一個地址空間區域,以及支援`ASID`,設定`HCR_EL2.E2H`的暫存器位可以解決這個問題,如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233529711-813181793.png)
`Host OS`執行在`EL2`需要解決的另一個問題就是暫存器訪問重定向,在核心中需要訪問`EL1`的暫存器,比如`TTBR0_EL1`,而當核心執行在`EL2`時,不需要修改核心程式碼,可以通過暫存器的設定來控制訪問流,如下圖:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233538193-425961869.png)
- 重定向訪問暫存器引入一個新的問題,`Hypervisor`在某些情況下需要訪問真正的`EL1`暫存器,ARM架構引入了一套新的別名機制,以`_EL12/_EL02`結尾,如下圖,可以在`ECH==1`的`EL2`訪問`TTBR0_EL1`:
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233544630-856509126.png)
`Host OS`執行在`EL2`還需要考慮異常處理的問題,前邊提到過`HCR_EL2.IMO/FMO/AMO`的位元位可以用來控制物理異常路由到`EL1/EL2`。當執行在`EL0`且`TGE==1`時,所有物理異常都會被路由到`EL2`(除了SCR_EL3控制的),這是因為`Host Apps`執行在`EL0`,而`Host OS`執行在`EL2`。
## 2.7 總結
- 本文涉及到記憶體虛擬化(stage 2轉換),I/O虛擬化(包含了SMMU,中斷等),中斷虛擬化,以及指令`trap and emulation`等內容;
- 基本的套路就是請求虛擬化服務時,路由到`EL2`去處理,如果有硬體支援的則硬體負責處理,否則可以通過軟體進行模擬;
- 儘管本文還沒涉及到程式碼分析,但是已經大概掃了一遍了,大體的輪廓已經瞭然於胸了,說了可能不信,我現在都有點小興奮了;
# 參考
`《ArmV8-A virtualization.pdf》`
`《vm-support-ARM-may6-2019.pdf》`
`《aarch64_virtualization_100942_0100_en.pdf》`
`《ARM Cortex-A Series Programmer's Guide for ARMv8-A》`
[arm64: Virtualization Host Extension support](https://lwn.net/Articles/650524/)
歡迎關注個人公眾號,不定期更新技術文章。
![](https://img2020.cnblogs.com/blog/1771657/202008/1771657-20200829233610911-651763717.jpg)