【原創】Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化

LoyenWang發表於2020-08-29

背景

  • 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. 概述

  • 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處理器的異常模式如下表所示:

然鵝,到了ARMv8,Exception Level(EL)取代了特權級,其中處理器的異常模式與Exception Level的對映關係如下圖:

  • 當異常發生時,處理器將改變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轉換

  • 當有了虛擬機器時,情況就不太一樣了,比如Qemu執行在Linux系統之上時,它只是Linux系統的一個使用者程式,Guest OS所認為自己訪問的實體地址,其實是Linux的使用者程式虛擬地址,到最終的實體地址還需要進一步的對映;
  • Hypervisor可以通過Stage 2轉換來控制虛擬機器的記憶體檢視,控制虛擬機器是否可以訪問某塊實體記憶體,進而達到隔離的目的;

  • 整個地址的對映分成了兩個階段:

    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空間,通常會結合VMIDASID來同時使用;

  • Stage 1Stage 2的轉換頁表中,都包含了屬性的相關裝置,比如訪問許可權,儲存型別等,在兩級轉換的過程中,MMU會整合成一個最終的也有效值,選擇限制更嚴格的屬性,如下圖:

  • 圖中的Device屬性限制更嚴格,則選擇Device型別;
  • Hypervisor如果想要改變預設整合行為,可以通過暫存器HCR_EL2(Hypervisor Configuration Register)來配置,比如設定Non-cacheableWrite-Back Cacheable等特性;

2.2.2 MMIO(Memory-Mapped Input/Output)

Guest OS認為的實體地址空間,實際是IPA地址空間,就像真實物理機中一樣,IPA的地址空間,也分成記憶體地址空間和I/O地址空間:

  • 訪問外設有兩種情況:1)直通訪問真實的外設;2)觸發faultHypervisor通過軟體來模擬;
  • VTTBR_EL2Virtualization Translation Table Base Register,虛擬轉換表基地址暫存器,存放Stage 2轉換的頁表;
  • 為了模擬外設,Hypervisor需要知道訪問的是哪個外設以及訪問的暫存器,讀訪問還是寫訪問,訪問長度是多少,使用哪些暫存器來傳送資料等。Stage 2轉換有一個專門的Hypervisor IPA Fault Address Register, EL2(HPFAR_EL2)暫存器,用於捕獲Stage 2轉換過程中的fault;

軟體模擬外設的示例流程如下:

  • 1)虛擬機器VM中的軟體嘗試訪問串列埠裝置;
  • 2)訪問時Stage 2轉換被block住,並觸發abort異常路由到EL2。異常處理程式查詢ESR_EL2(Exception Syndrome Register)暫存器關於異常的資訊,如訪問長度、目標暫存器,Load/Store操作等,異常處理程式還會查詢HPFAR_EL2暫存器,獲取abort的IPA地址;
  • 3)Hypervisor通過ESR_EL2HPFAR_EL2裡的相關資訊對相關虛擬外圍裝置進行模擬,完成後通過ERET指令返回給vCPU,從發生異常的下一條指令繼續執行;

2.2.3 SMMUs(System Memory Management Units)

訪問記憶體的另外一種case就是DMA控制器。

非虛擬化下DMA控制器的工作情況如下:

  • DMA控制器由核心的驅動程式來控制,能確保作業系統層面的記憶體的保護不會被破壞,使用者程式無法通過DMA去訪問被限制的區域;

虛擬化下DMA控制器,VM中的驅動直接與DMA控制器互動會出現什麼問題呢?如下圖:

  • DMA控制器不受Stage 2轉換的約束,會破壞VM的隔離性;
  • Guest OS以為的實體地址是IPA地址,而DMA看到的地址是真實的實體地址,兩者的視角不一致,為了解決這個問題,需要捕獲每次VM與DMA控制器的互動,並提供轉換,當記憶體出現碎片化時,這個處理低效且容易引入問題;

SMMUs可以用於解決這個問題:

  • SMMU也叫IOMMU,對IO部件提供MMU功能,虛擬化只是SMMU的一個應用;
  • Hypervisor可以負責對SMMU進行程式設計,以便讓上層的控制器和虛擬機器VM以同一個視角對待記憶體,同時也保持了隔離性;

2.3 Trapping and emulation of Instructions

Hypervisor也需要具備捕獲(trap)和模擬指令的能力,比如當VM中的軟體需要配置底層處理器來進行功耗管理或者快取一致性操作時,為了不破壞隔離性,Hypervisor就需要捕獲操作並進行模擬,以便不影響其他的VM。如果設定了捕獲某個操作時,當該操作被執行時會向更高一級的Exception Level觸發異常(比如Hypervisor為EL2),從而在相應的異常處理中完成模擬。

例子來了:

  • 在ARM處理器中執行WFI(wait for interrupt)命令,可以讓CPU處於一個低功耗的狀態;
  • HCR_EL2(Hypervisor Control Register),當該暫存器的TWI==1時,vCPU執行WFI指令會觸發EL2異常,從而Hypervisor可以對其進行模擬,將任務排程到另外一個vCPU即可;

捕獲(traps)的另一個作用是可以用於向Guest OS呈現暫存器的虛擬值,如下:

  • ID_AA64MMFR0_EL1暫存器用於查詢處理器對記憶體系統相關特性的支援,系統可能在啟動階段會讀取該暫存器,Hypervisor可以向Guest OS呈現一個不同的虛擬值;
  • 當vCPU讀取該暫存器時,觸發異常,Hypervisortrap_handler中進行處理,設定一個虛擬值,並最終返回給vCPU;
  • 通過trap來虛擬化一個操作需要大量的計算,包括觸發異常、捕獲,模擬、返回等一系列操作,像ID_AA64MMFR0_EL1暫存器訪問並不頻繁,這種方式問題不大。但是當需要頻繁訪問的暫存器,比如MIDR_EL1MPIDR_EL1等,出於效能的考慮,應該避免陷入到Hypervisor中進行模擬處理,可以通過其他機制,比如提供VPIDR_EL2VMIDR_EL2暫存器,在進入VM前就設定好該值,當讀取MIDR_EL1MPIDR_EL1時,硬體就返回VPIDR_EL2VMIDR_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吧,看過之前中斷子系統系列文章的同學,應該見過下圖:

  • Hypervisor可以將GIC中的Virtual CPU Interface對映到VM中,從而允許VM中的軟體直接與GIC進行通訊,Hypervisor只需要進行配置即可,這樣可以減少虛擬中斷的overhead;

來個虛擬中斷的例子吧:

  1. 外設觸發中斷訊號到GIC;
  2. GIC產生物理中斷IRQ或者FIQ訊號,如果設定了HCR_EL2.IMO/FMO,中斷訊號將被路由到HypervisorHypervisor會檢查中斷訊號轉發給哪個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的內部:

簡化之後是這樣的:

  • ARM體系架構每個處理器都包含了一組通用定時器,從圖中可以看到兩個模組:ComparatorsCounter Module,當Comparators的值小於等於系統的count值時便會產生中斷,我們都知道在作業系統中timer的中斷就是系統的脈搏了;

下圖展示虛擬化系統中執行的vCPU的時序:

  • 物理時間4ms,每個vCPU執行2ms,如果設定vCPU0T=0之後的3ms後產生中斷,那希望是物理時間的3ms後(也就是vCPU0的虛擬時間2ms)產生中斷,還是虛擬時間3ms後產生中斷?ARM體系結構支援這兩種設定;
  • 執行在vCPU上的軟體可以同時訪問兩種時鐘:EL1物理時鐘EL1虛擬時鐘

EL1物理時鐘EL1虛擬時鐘

  • EL1物理時鐘與系統計數器模組直接比較,使用的是wall-clock時間;
  • EL1虛擬時鐘與虛擬計數器比較,而虛擬計數器是在物理計數器上減去一個偏移;
  • Hypervisor負責為當前排程執行的vCPU指定對應的偏移,這種方式使得虛擬時間只會覆蓋vCPU實際執行的那部分時間;

來一張示例圖:

  • 6ms的時間段裡,每個vCPU執行3ms,Hypervisor可以使用偏移暫存器來將vCPU的時間調整為其實際的執行時間;

2.6 Virtualization Host Extensions(VHE)

  • 先丟擲一個問題:通常Host OS的核心都執行在EL1,而控制虛擬化的程式碼執行在EL2,這就意味著傳統的上下文切換,這個顯然是比較低效的;
  • VHE用於支援type-2Hypervisor,這種擴充套件可以讓核心直接跑在EL2,減少host和guest之間共享的系統暫存器數量,同時也減少虛擬化的overhead;

VHE由系統暫存器HCR_EL2E2HTGE兩個位元位來控制,如下圖:

VHE的引入,需要考慮虛擬地址空間的問題,如下圖:

  • 我們在記憶體子系統分析時提到過虛擬地址空間的問題,分為使用者地址空間(EL0)和核心地址空間(EL1),兩者的區域不一致,而在EL2只有一個虛擬地址空間區域,這是因為Hypervisor不支援應用程式,因此也就不需要分成核心空間和使用者空間了;
  • EL0/EL1虛擬地址空間也同時支援ASID(Address Space Identifiers),而EL2不支援,原因也是Hypervisor不需要支援應用程式;

從上兩點可以看出,為了支援Host OS能執行在EL2,需要新增一個地址空間區域,以及支援ASID,設定HCR_EL2.E2H的暫存器位可以解決這個問題,如下圖:

Host OS執行在EL2需要解決的另一個問題就是暫存器訪問重定向,在核心中需要訪問EL1的暫存器,比如TTBR0_EL1,而當核心執行在EL2時,不需要修改核心程式碼,可以通過暫存器的設定來控制訪問流,如下圖:

  • 重定向訪問暫存器引入一個新的問題,Hypervisor在某些情況下需要訪問真正的EL1暫存器,ARM架構引入了一套新的別名機制,以_EL12/_EL02結尾,如下圖,可以在ECH==1EL2訪問TTBR0_EL1

Host OS執行在EL2還需要考慮異常處理的問題,前邊提到過HCR_EL2.IMO/FMO/AMO的位元位可以用來控制物理異常路由到EL1/EL2。當執行在EL0TGE==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

歡迎關注個人公眾號,不定期更新技術文章。

相關文章