摘抄於此:
https://www.openeuler.org/zh/blog/yorifang/2020-10-24-arm-virtualization-overview.html
摘要:
ARM處理器在移動領域已經大放異彩占據瞭絕對優勢,但在服務器領域ARM仍處於發展上升階段。 為瞭能夠和X86在服務器領域展開競爭,ARM也逐漸對虛擬化擴展有瞭較為完善的支持。 本文的目的是介紹一下ARMv8處理器的虛擬化擴展中的一些相關知識點, 將主要從ARM體系結構、內存虛擬化、中斷虛擬化、I/O虛擬化等幾個方面做一些概括總結。 本文將盡可能的在特性層面將ARM和X86做一些對比,以加深我們對於ARM Virtualizaiton Extension的印象。
0. ARMv8 System Architecture
在進入正題之前先回顧一下ARMv8體系結構的一些基本概念。
ARMv8支持兩種執行狀態:AArch64和AArch32。
AArch64 64-bit執行狀態:
- 提供31個64bit的通用處理器,其中X30是Procedure link register
- 提供一個64bit的程序寄存器PC,堆棧指針(SPs)和Exception link registers(ELRs)
- 提供瞭32個128bit的寄存器以支持SIMD矢量和標量浮點運算
- 定義瞭4個Exception Level (EL0-EL3)
- 支持64bit虛擬機地址(virtual address)
- 定義瞭一些PSTATE eletems來存儲PE的狀態
- 過後ELn後綴來表示不同Exception Level下可以操作的系統寄存器
AArch32 32-bit執行狀態:
- 提供瞭13個32bit通用寄存器,1個32bit的PC,SP和Link Register(LR)
- 為Hyper Mode下的異常返回值提供給瞭一個單一的ELR
- 提供32個64bit的寄存器來支持SIMD矢量和標量浮點運算支持
- 提供瞭2個指令集,A32和T32,
- 支持ARMv7-A Exception Mode,基於PE modes並且可以對應到ARMv8的Exception model中
- 使用32bit的虛擬地址
- 使用一個單一的CPSR來保存PE的狀態
ARM 內存模型:
- 非對齊的內存訪問將產生一個異常
- 限制應用程序訪問指定的內存區域
- 程序執行中的虛擬地址將被翻譯成物理地址
- 內存訪問順序受控
- 控制cache和地址翻譯的結構
- 多個PE之間共享內存的訪問同步
ARM 內存管理(參考ARM Address Translation):
- ARMv8-A架構支持的最大物理內存地址寬度是48bit,支持4KB、16KB、或者64KB的頁面大小
- 使用虛擬內存管理機制,VA的最高有效位(MSB)為0時MMU使用TTBR0的轉換表來翻譯,VA的最高有效位為1時MMU使用TTBR1的轉換表來翻譯
- EL2和EL3有TTBR0,但是沒有TTBR1,這意味著EL2和EL3下隻能使用0x0~0x0000FFFF_FFFFFFFF范圍的虛擬地址空間
————————————————————————- AArch64 Linux memory layout with 4KB pages + 4 levels:: Start End Size Use 0000000000000000 0000ffffffffffff 256TB user ffff000000000000 ffffffffffffffff 256TB kernel ————————————————————————-
OK,假裝我們現在的ARMv8-A已經有瞭一個初步的瞭解,下面再從幾個大的維度去看下ARMv8對虛擬化是怎麼支持的。
1. ARMv8 Virtualization Extension Overview
ARM為瞭支持虛擬化擴展在CPU的運行級別上引入瞭Exception Level(異常級別)的概念,AArch64對應的Exception Level視圖如下圖:
- EL0:用戶態程序的運行級別,Guest內部的App也運行在這個級別
- EL1:內核的運行級別,Guest的內核也運行在這個級別
- EL2:Hypervisor的運行級別,Guest在運行的過程中會觸發特權指令後陷入到EL2級別,將控制權交給Hypervisor
- EL3:Monitor Mode,CPU在Secure World和 Normal World直接切換的時候會先進入EL3,然後發生World切換
註:當CPU的Virtualization Extension被disable的時候,軟件就運行在EL0和EL1上,這時候EL1有權限訪問所有的硬件。
ARM的異常級別可以分為同步(synchronous)和異步(asynchronous)兩類:
- 同步異常:未定義異常,未對齊異常,陷阱執行異常,系統調用異常。例如:SVC、HVC、SMC等。 SVC被Application用於申請OS的特權操作或者訪問系統資源,HVC被用來申請hypervisor服務;SMC則用於申請firmware服務。
- 異步異常:中斷
與ARMv8不同的是,在X86為支持CPU虛擬化引入瞭Root Mode和None-Root Mode的概念和一套特殊的VMX指令集, 其中非根模式是Guest CPU的執行環境,根模式是Host CPU的執行環境。 根模式、非根模式與CPU的特權級別是兩個完全獨立的概念,二者完全正交, 也就是說非根模式下支持和根模式下一樣的用戶態(Ring 3)、內核態(Ring 0)特權級。 而這和ARM是不同的,ARM CPU是依靠在不同的EL之間切換來支持虛擬化模式切換。 但二者都有一個相同點:那就是ARM和X86在虛擬化模式下如果執行瞭敏感指令會分別退出到EL2和Root Mode之間。 同時,X86上為瞭更好地支持Root/Non-root Mode在內存中實現瞭一個叫做VMCS的數據結構, 用來保存和恢復Root/None-root模式切換過程中的寄存器信息,VMX指令集則專門用來操作VMCS數據結構。 但在RISC-style的ARM處理器上,則沒有類似的實現,而是讓Hypervisor軟件自己來決定哪些信息需要保存和恢復, 這在一定程度上帶來瞭一些靈活性[Ref1]。
為瞭優化多個異常級別帶來的虛擬化上下文切換開銷,ARMv8中引入瞭虛擬化主機擴展特性(virtual Host Extension, VHE) Ref2。 其實現原理是直接將宿主機內核運行在EL2上。為瞭實現EL2引入HCR_EL2寄存器,用來控制這一特性。 VHE帶來的好處是減少瞭2次異常級別的切換,隻需要保存和恢復部分Host的上下文, 減少瞭虛擬化的開銷,帶來瞭性能的提升。
2. Memory Virtualization
在ARMv8-A上,每個tarnslation regime可能包括1個stage,也可能包括2個sate。 每個Exception Level都有自己的地址翻譯機制,使用不同的頁表基地址寄存器,地址翻譯可以細分到stage, 大部分的EL包括一個stage的地址翻譯過程, Non-Secure EL1&0包括瞭2個stage的地址翻譯過程。 每個stage都有自己獨立的一系列Translation tables,每個stage都能獨立的enable或者disable。 每個stage都是將輸入地址(IA)翻譯成輸出地址(OA)[Ref3]。
所以在虛擬化場景下,ARM和X86上的方案是類似的,都是采用兩階段地址翻譯實現GPA -> HPA的地址翻譯過程。 虛擬機運行在None-secure EL1&0,當虛擬機內的進程訪問GVA的時候MMU會將GVA翻譯成IPA(intermediate physical address,中間物理地址:GPA), 這就是所謂的stage 1地址翻譯。然後MMU會再次將IPA翻譯成HPA,這就是所謂的stage 2地址翻譯。
在不同的Eexception Level下有不同的Address Space,那麼如何去控制不同地址空間的翻譯呢?
ARMv8-A上有一個TCR(Translation Control Register)寄存器來控制地址翻譯。 例如:對於EL1&0來說,由於在該運行模式下VA存在2個獨立的映射空間(User Space和Kernel Space), 所以需要兩套頁表來完成地址翻譯,這2個頁表的及地址分別放在TTBR0_EL1和TTBR1_EL1中。
對於每一個地址翻譯階段:
- 有一個system control register bit來使能該階段的地址翻譯
- 有一個system control register bit來決定翻譯的時候使用的大小端策略
- 有一個TCR寄存器來控制整個階段的地址翻譯過程
- 如果某個地址翻譯階段支持將VA映射到兩個subranges,那麼該階段的地址翻譯需要為每個VA subrange提供不同的TTBR寄存器
內存虛擬化也沒有太多可以說道,理解瞭原理之後就可以去梳理KVM相關代碼,相關代碼實現主要在arch/arm/mm/mmu.c裡面。
3. I/O Virtualization
設備直通的目的是能夠讓虛擬機直接訪問到物理設備,從而提升IO性能。 在X86上使用VT-d技術就能夠實現設備直通,這一切都得益於VFIO驅動和Intel IOMMU的加持。 那麼在ARMv8-A上為瞭支持設備直通,又有哪些不同和改進呢?
同X86上一樣,ARM上的設備直通關鍵也是要解決DMA重映射和直通設備中斷投遞的問題。 但和X86上不一樣的是,ARMv8-A上使用的是SMMU v3.1來處理設備的DMA重映射, 中斷則是使用GICv3中斷控制器來完成的,SMMUv3和GICv3在設計的時候考慮瞭更多跟虛擬化相關的實現, 針對虛擬化場景有一定的改進和優化。
先看下SMMUv3.1的在ARMv8-A中的使用情況以及它為ARM設備直通上做瞭哪些改進[Ref4]。 SMMUv3規定必須實現的特性有:
- SMMU支持2階段地址翻譯,這和內存虛擬化場景下MMU支持2階段地址翻譯類似, 第一階段的地址翻譯被用做進程(software entity)之間的隔離或者OS內的DMA隔離, 第二階段的地址翻譯被用來做DMA重映射,即將Guest發起的DMA映射到Guest的地址空間內。
- 支持16bit的ASIDs
- 支持16bit的VMIDs
- 支持SMMU頁表共享,允許軟件選擇一個已經創建好的共享SMMU頁表或者創建一個私有的SMMU頁表
- 支持49bit虛擬地址 (matching ARMv8-A’s 2×48-bit translation table input sizes),SMMUv3.1支持52bit VA,IPA,PA
SMMUv3支持的可選特性有:
- Stage1和Stage2同時支持AArch32(LPAE: Large Page Address Extension)和AArch64地址翻譯表格式(兼容性考慮)
- 支持Secure Stream (安全的DMA流傳輸)
- 支持SMMU TLB Invalidation廣播
- 支持HTTU(Hardware Translation Table Update)硬件自動刷新頁表的Access/Dirty標志位
- 支持PCIE ATS和PRI(PRI特性非常厲害,後面單獨介紹)
- 支持16K或者64K頁表粒度
我們知道,一個平臺上可以有多個SMMU設備,每個SMMU設備下面可能連接著多個Endpoint, 多個設備互相之間可能不會復用同一個頁表,需要加以區分,SMMU用StreamID來做這個區分, 通過StreamID去索引Stream Table中的STE(Stream Table Entry)。 同樣x86上也有類似的區分機制,不同的是x86是使用Request ID來區分的,Request ID默認是PCI設備分配到的BDF號。 不過看SMMUv3 Spec,又有說明:對於PCI設備StreamID就是PCI設備的RequestID, 好吧,兩個名詞其實表示同一個東西,隻是一個是從SMMU的角度去看就成為StreamID,從PCIe的角度去看就稱之為RequestID。 同時,一個設備可能被多個進程使用,多個進程有多個頁表,設備需要對其進行區分,SMMU使用SubstreamID來對其進行表示。 SubstreamID的概念和PCIe PASID是等效的,這隻不過又是在ARM上的另外一種稱呼而已。 SubstreamID最大支持20bit和PCIe PASID的最大寬度是一致的。
STE裡面都有啥呢?Spec裡面有說明:
- STE裡面包含一個指向stage2地址翻譯表的指針,並且同時還包含一個指向CD(Context Descriptor)的指針
- CD是一個特定格式的數據結構,包含瞭指向stage1地址翻譯表的基地址指針
理論上,多個設備可以關聯到一個虛擬機上,所以多個STE可以共享一個stage2的翻譯表。 類似的,多個設備(stream)可以共享一個stage1的配置,因此多個STE可以共享同一個CD。
Stream Table是存在內存中的一張表,在SMMU設備初始化的時候由驅動程序創建好。 Stream Table支持2種格式,Linear Stream Table 和 2-level Stream Table, Linear Stream Table就是將整個Stream Table在內存中線性展開為一個數組,優點是索引方便快捷,缺點是當平臺上外設較少的時候浪費連續的內存空間。 2-level Stream Table則是將Stream Table拆成2級去索引,優點是更加節省內存。
在使能SMMU兩階段地址翻譯的情況下,stage1負責將設備DMA請求發出的VA翻譯為IPA並作為stage2的輸入, stage2則利用stage1輸出的IPA再次進行翻譯得到PA,從而DMA請求正確地訪問到Guest的要操作的地址空間上。
在stage1地址翻譯階段:硬件先通過StreamID索引到STE,然後用SubstreamID索引到CD, CD裡面包含瞭stage1地址翻譯(把進程的GVA/IOVA翻譯成IPA)過程中需要的頁表基地址信息、per-stream的配置信息以及ASID。 在stage1翻譯的過程中,多個CD對應著多個stage1的地址翻譯,通過Substream去確定對應的stage1地址翻譯頁表。 所以,Stage1地址翻譯其實是一個(RequestID, PASID) => GPA的映射查找過程。 註意:隻有在使能瞭stage1地址翻譯的情況下,SubstreamID才有意義,否則該DMA請求會被丟棄。
在stage2地址翻譯階段:STE裡面包含瞭stage2地址翻譯的頁表基地址(IPA->HPA)和VMID信息。 如果多個設備被直通給同一個虛擬機,那麼意味著他們共享同一個stage2地址翻譯頁表[Ref5]。
值得註意的是:CD中包含一個ASID,STE中包含瞭VMID,CD和VMID存在的目的是作為地址翻譯過程中的TLB Tag,用來加速地址翻譯的過程。
系統軟件通過Command Queue和Event Queue來和SMMU打交道,這2個Queue都是循環隊列。 系統軟件將Command放到隊列中SMMU從隊列中讀取命令來執行,同時設備在進行DMA傳輸或者配置發生錯誤的時候會上報事件, 這些事件就存放在Event Queue當中,系統軟件要及時從Event Queue中讀取事件以防止隊列溢出。
SMMU支持兩階段地址翻譯的目的隻有1個,那就是為瞭支持虛擬化場景下的SVM特性(Shared Virtual Memory)。 SVM特性允許虛擬機內的進程都能夠獨立的訪問直通給虛擬機的直通設備,在進程自己的地址空間內向設備發起DMA。 SVM使得虛擬機裡面的每個進程都能夠獨立使用某個直通設備,這能夠降低應用編程的復雜度,並提升安全性。
為瞭實現虛擬化場景下的SVM,QEMU需要模擬一個vSMMU(或者叫vIOMMU)的設備。 虛擬機內部進程要訪問直通設備的時候,會調用Guest驅動創建PASID Table(虛擬化場景下這個表在Guest內部), 在這個場景下PASID將作為虛擬機內進程地址空間的一個標志,設備在發起DMA請求的時候會帶上PASID Prefix,這樣SMMU就知道如何區分瞭。 創建PASID Table的時候會訪問vSMMU,這樣Guest就將PASID Table的地址(GPA)傳給瞭QEMU, 然後QEMU再通過VFIO的IOCTL調用(VFIO_DEVICE_BIND_TASK)將表的信息傳給SMMU, 這樣SMMU就獲得瞭Guest內部進程的PASID Table的shadow信息,它就知道該如何建立Stage1地址翻譯表瞭。
所以,在兩階段地址翻譯場景下,Guest內部DMA請求的處理步驟
Step1: Guest驅動發起DMA請求,這個DMA請求包含GVA + PASID Prefix Step2: DMA請求到達SMMU,SMMU提取DMA請求中的RequestID就知道這個請求是哪個設備發來的,然後去StreamTable索引對應的STE Step3: 從對應的STE表中查找到對應的CD,然後用PASID到CD中進行索引找到對應的S1 Page Table Step4: IOMMU進行S1 Page Table Walk,將GVA翻譯成GPA(IPA)並作為S2的輸入 Step5: IOMMU執行S2 Page Table Walk,將GPA翻譯成HPA,done!
縱觀SMMUv3,從設計上來和Intel IOMMU的設計和功能基本類似,畢竟這裡沒有太多可以創新的地方。 但ARM SMMUv3有2個比較有意思的改進點: 一個是支持Page Request Interface(PRI),PRI是對ATS的進一步改進。當設備支持PRI特性的時候, 設備發送DMA請求的時候可以缺頁IOPF(IO Page Fault),這就意味著直通虛擬機可以不需要進行內存預占, DMA缺頁的時候SMMU會向CPU發送一個缺頁請求,CPU建立好頁表之後對SMMU進行回復,SMMU這時候再將內容寫到DMA Buffer中。 另外一個改進就是,DMA寫內存之後產生臟頁可以由硬件自動更新Access/Dirty Bit, 這樣就對直通設備熱遷移比較友好,但這個功能是需要廠商選擇性支持的, 而且在這種場景下如何解決SMMU和MMU的Cache一致性是最大的挑戰。
4. Interrupt Virtualization
ARM的中斷系統和x86區別比較大,x86用的是IOAPIC/LAPIC中斷系統,ARM則使用的是GIC中斷控制器, 並且隨著ARM的演進陸續出現瞭GICv2,GICv3,GICv4等不同版本, 看瞭GICv3手冊感覺著玩兒設計得有點復雜,並不像x86上那樣結構清晰。
GICv1和GICv2最大隻支持8個PE,這放在現在顯然不夠用瞭。 所以,GICv3對這裡進行改進,提出瞭affinity routing機制以支持更多的PE。
GICv3定義瞭以下中斷類型[Ref6]: ARM上的中斷類型:
- LPI(Locality-specific Peripheral Interrupt) LPI始終是基於消息的中斷,邊緣觸發、經過ITS路由,它們的配置保存在表中而不是寄存器,比如PCIe的MSI/MSI-x中斷,GITS_TRANSLATER控制中斷
- SGI (Software Generated Interrupt) 軟件觸發的中斷,軟件可以通過寫GICD_SGIR寄存器來觸發一個中斷事件,一般用於核間通信(對應x86 IPI中斷)
- PPI(Private Peripheral Interrupt) 私有外設中斷,這是每個核心私有的中斷,PPI太冗長會送達到指定的CPU上,邊緣觸發或者電平觸發、有Active轉態,應用場景有CPU本地時鐘,類似於x86上的LAPIC Timer Interrupt
- SPI(Shared Peripheral Interrupt) 公用的外部設備中斷,也定義為共享中斷,邊緣觸發或者電平觸發、有Active轉態,可以多個CPU或者說Core處理,不限定特定的CPU,SPI支持Message格式(GICv3),GICD_SETSPI_NSR設置中斷,GICD_CLRSPI_NSR清除中斷
ARM上的中斷又可以分為兩類: 一類中斷要通過Distributor分發的,例如SPI中斷。 另一類中斷不通過Distributor的,例如LPI中斷,直接經過ITS翻譯後投遞給某個Redistributor。
INTID | Interrupt Type | Notes |
---|---|---|
0-15 | SGI | Banked per PE |
16-31 | PPI | Banked per PE |
32-1019 | SPI | |
1020-1023 | Special Interrupt Number | Used to signal special cases |
1024-8191 | Reserved | |
8192- | LPI |
ARM上又搞出來一個Affinity Routing的概念,GICv3使用Affinity Routing來標志一個特定的PE或者是一組特定的PE, 有點類似於x86上的APICID/X2APIC ID機制。ARM使用4個8bit的域來表示affinity,格式如:
<affinity level 3>.<affinity level 2>.<affinity level 1>.<affinity level 0>
例如,現在有個ARM Big.Little架構的移動處理器SOC,擁有2個Cluster,小核心擁有4個Cortex-A53大核心擁有2個A72,那麼可以表示為:
0.0.0.[0:3] Cores 0 to 3 of a Cortex-A53 processor 0.0.1.[0:1] Cores 0 to 1 of a Cortex-A72 processor
GICv3的設計上和x86的IOAPIC/LAPIC架構差異甚遠,GICv3的設計架構如下圖所示:
GICv3中斷控制器由Distributor,Redistributor和CPU Interface三個部分組成。 Distributor負責SPI中斷管理並將中斷發送給Redistributor,Redistributor管理PPI,SGI,LPI中斷,並將中斷投遞給CPU Interface, CPU Interface負責將中斷註入到Core裡面(CPU Interface本身就在Core內部)。
Distributor的主要功能有:
- 中斷優先級管理和中斷分發
- 啟用和禁用SPI
- 為每個SPI設置設置中斷優先級
- 為每個SPI設置路由信息
- 設置每個SPI的中斷觸發屬性:邊沿觸發或者電平觸發
- 生成消息格式的SPI中斷
- 控制SPI中斷的active狀態和pending狀態
每個PE都對應有一個Redistributor與之相連,Distributor的寄存器是memory-mapped, 並且它的配置是全局生效的,直接影響所有的PE。Redistributor的主要功能有:
- 使能和禁用SGI和PPI
- 設置SGI和PPI的中斷優先級
- 設置PPI的觸發屬性:電平觸發或者邊沿觸發
- 為每個SGI和PPI分配中斷組
- 控制SGI和PPI的狀態
- 控制LPI中斷相關數據結構的基地址
- 對PE的電源管理的支持
每個Redistributor都和一個CPU Interface相連, 在GICv3中CPU Interface的寄存器是是通過System registers(ICC_*ELn)來訪問的。 在使用這些寄存器之前軟件必須使能系統寄存器,CPU Interface的主要功能:
- 控制和使能CPU的中斷處理。如果中斷disable瞭,即使Distributor分發瞭一個中斷事件到CPU Interface也會被Core屏蔽掉。
- 應答中斷
- 進行中斷優先級檢測和中斷deassert
- 為PE設置一個中斷優先級mask標志,可以選擇屏蔽中斷
- 為PE定義搶占策略
- 為PE斷定當前pending中斷的最高優先級(優先級仲裁)
GICv3中為瞭處理LPI中斷,專門引入瞭ITS(Interrupt Translation Service)組件。 外設想發送LPI中斷時(比如PCI設備的MSI中斷),就去寫ITS的寄存器GITS_TRANSLATER,這個寫操作就會觸發一個LPI中斷。 ITS接收到LPI中斷後,對其進行解析然後發送給對應的redistributor,然後再由redistributor發送給CPU Interface。 那麼這個寫操作裡面包含瞭哪些內容呢?主要是2個關鍵域。
- EventID:這個是寫入到GITS_TRANSLATER的值,EventID定義瞭外設要觸發的中斷號,EventID可以和INTID一樣,或者經過ITS翻譯後得到一個INTID
- DeviceID:這個是外設的標志,實現是自定義的,例如可以使用AXI的user信號傳遞。
ITS使用3種類型的表來完成LPI的翻譯和路由:
- Device Table: 將DeviceID映射到Interrupt Translation Table中
- Interrupt Translation Table:包含瞭EventID到INTID映射關系之間和DeviceID相關的信息,同時也包含瞭INTID Collection
- Collection Table:將collections映射到Redistributor上
整個流程大概是:
Step1: 外設寫GITS_TRANSLATER,ITS使用DeviceID從Device Table中索引出這個外設該使用哪個Interrupt Translation Table Step2: 使用EventID去選中的Interrupt Translation Table中索引出INTID和對應的Collection ID Step3: 使用Collection ID從Collection Table中選擇對應的Collection和路由信息 Step4: 把中斷送給目標Redistributor
看來看去總覺得GICv3中斷控制器設計比較復雜,不如x86上那樣結構清晰,目前隻是理瞭個大概,要深入理解再到代碼級熟悉還得花不少時間。 上面說瞭這麼多,還是在將GICv3控制器的邏輯,具體QEMU/KVM上是怎麼實現的還得去看代碼,為瞭提升中斷的性能, GICv3的模擬是直接放到KVM裡面實現的。比如說virtio設備的MSI中斷,那肯定類型上是LPI中斷,QEMU模擬的時候機制上還是使用irqfd方式來實現的, 前面也有從代碼角度去分析過,後面再單獨從代碼層級去分析具體的實現方案。
5. Overview
ARM體系結構和x86存在不少差異,其中差異最大的還是中斷控制器這塊,這裡需要投入事件好好分析一下. 內存虛擬化和I/O虛擬化這塊二者可能細節上有些不同,但背後的原理還是近似的. 例如:SMMUv3在設計上和Intel IOMMU都支持瞭二次地址翻譯,但SMMU有針對性的改進點. 更多的詳細的ARM虛擬化知識和細節需要閱讀ARMv8的spec文檔深入去瞭解。
6. References
- ARMv8 Architecture Reference Manual
- Virtualization Host Extensions
- ARMv8-A Address Translation Version 1.0
- ARM64 Address Translation
- SMMU architecture version 3.0 and version 3.1
- GICv3 Software Overview Official Release
- SVM on ARM SMMUv3
- SVM and PASID