1. 程式人生 > >【原創】Linux虛擬化KVM-Qemu分析(五)之記憶體虛擬化

【原創】Linux虛擬化KVM-Qemu分析(五)之記憶體虛擬化

# 背景 - `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 4. 文章同步在部落格園:`https://www.cnblogs.com/LoyenWang/` # 1. 概述 `《Linux虛擬化KVM-Qemu分析(二)之ARMv8虛擬化》`文中描述過記憶體虛擬化大體框架,再來回顧一下: 1. 非虛擬化下的記憶體的訪問 ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233823163-1248765431.png) - CPU訪問實體記憶體前,需要先建立頁表對映(虛擬地址到實體地址的對映),最終通過查表的方式來完成訪問。在ARMv8中,核心頁表基地址存放在`TTBR1_EL1`中,使用者空間頁表基地址存放在`TTBR0_EL0`中; 2. 虛擬化下的記憶體訪問 ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233831393-633851095.png) - 虛擬化情況下,記憶體的訪問會分為兩個`Stage`,`Hypervisor`通過`Stage 2`來控制虛擬機器的記憶體檢視,控制虛擬機器是否可以訪問某塊實體記憶體,進而達到隔離的目的; - `Stage 1`:`VA(Virtual Address)->IPA(Intermediate Physical Address)`,Host的作業系統控制`Stage 1`的轉換; - `Stage 2`:`IPA(Intermediate Physical Address)->PA(Physical Address)`,Hypervisor控制`Stage 2`的轉換; 猛一看上邊兩個圖,好像明白了啥,仔細一想,啥也不明白,本文的目標就是將這個過程講明白。 在開始細節講解之前,需要先描述幾個概念: ```c gva - guest virtual address gpa - guest physical address hva - host virtual address hpa - host physical address ``` ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233844101-140556043.png) - Guest OS中的虛擬地址到實體地址的對映,就是典型的常規操作,參考之前的記憶體管理模組系列文章; 鋪墊了這麼久,來到了本文的兩個主題: 1. `GPA->HVA`; 2. `HVA->HPA`; 開始吧! # 2. GPA->HVA 還記得上一篇文章`《Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)》`中的Sample Code嗎? KVM-Qemu方案中,GPA->HVA的轉換,是通過`ioctl`中的`KVM_SET_USER_MEMORY_REGION`命令來實現的,如下圖: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233855813-554323553.png) 找到了入口,讓我們進一步揭開神祕的面紗。 ## 2.1 資料結構 關鍵的資料結構如下: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233904748-592916078.png) - 虛擬機器使用`slot`來組織實體記憶體,每個`slot`對應一個`struct kvm_memory_slot`,一個虛擬機器的所有`slot`構成了它的實體地址空間; - 使用者態使用`struct kvm_userspace_memory_region`來設定記憶體`slot`,在核心中使用`struct kvm_memslots`結構來將`kvm_memory_slot`組織起來; - `struct kvm_userspace_memory_region`結構體中,包含了`slot`的ID號用於查詢對應的`slot`,此外還包含了實體記憶體起始地址及大小,以及HVA地址,HVA地址是在使用者程序地址空間中分配的,也就是Qemu程序地址空間中的一段區域; ## 2.2 流程分析 資料結構部分已經羅列了大體的關係,那麼在`KVM_SET_USER_MEMORY_REGION`時,圍繞的操作就是`slots`的建立、刪除,更新等操作,話不多說,來圖了: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233915050-1368514616.png) - 當用戶要設定記憶體區域時,最終會呼叫到`__kvm_set_memory_region`函式,在該函式中完成所有的邏輯處理; - `__kvm_set_memory_region`函式,首先會對傳入的`struct kvm_userspace_memory_region`的各個欄位進行合法性檢測判斷,主要是包括了地址的對齊,範圍的檢測等; - 根據使用者傳遞的`slot`索引號,去查詢虛擬機器中對應的`slot`,查詢的結果只有兩種:1)找到一個現有的slot;2)找不到則新建一個slot; - 如果傳入的引數中`memory_size`為0,那麼會將對應`slot`進行刪除操作; - 根據使用者傳入的引數,設定`slot`的處理方式:`KVM_MR_CREATE`,`KVM_MR_MOVE`,`KVM_MEM_READONLY`; - 根據使用者傳遞的引數決定是否需要分配髒頁的bitmap,標識頁是否可用; - 最終呼叫`kvm_set_memslot`來設定和更新`slot`資訊; ### 2.2.1 kvm_set_memslot 具體的`memslot`的設定在`kvm_set_memslot`函式中完成,`slot`的操作流程如下: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233927534-60352225.png) - 首先分配一個新的`memslots`,並將原來的`memslots`內容複製到新的`memslots`中; - 如果針對`slot`的操作是刪除或者移動,首先根據舊的`slot id`號從`memslots`中找到原來的`slot`,將該`slot`設定成不可用狀態,再將`memslots`安裝回去。這個安裝的意思,就是RCU的assignment操作,不理解這個的,建議去看看之前的RCU系列文章。由於`slot`不可用了,需要解除stage2的對映; - `kvm_arch_prepare_memory_region`函式,用於處理新的`slot`可能跨越多個使用者程序VMA區域的問題,如果為裝置區域,還需要將該區域對映到`Guest IPA`中; - `update_memslots`用於更新整個`memslots`,`memslots`基於PFN來進行排序的,新增、刪除、移動等操作都是基於這個條件。由於都是有序的,因此可以選擇二分法來進行查詢操作; - 將新增新的`slot`後的`memslots`安裝回KVM中; - `kvfree`用於將原來的`memslots`釋放掉; ### 2.2.2 kvm_delete_memslot `kvm_delete_memslot`函式,實際就是呼叫的`kvm_set_memslot`函式,只是`slot`的操作設定成`KVM_MR_DELETE`而已,不再贅述。 # 3. HVA->HPA 光有了GPA->HVA,似乎還是跟`Hypervisor`沒有太大關係,到底是怎麼去訪問實體記憶體的呢?貌似也沒有看到去建立頁表對映啊? 跟我走吧,帶著問題出發! 之前記憶體管理相關文章中提到過,使用者態程式中分配虛擬地址vma後,實際與實體記憶體的對映是在`page fault`時進行的。那麼同樣的道理,我們可以順著這個思路去查詢是否HVA->HPA的對映也是在異常處理的過程中建立的?答案是顯然的。 回顧一下前文`《Linux虛擬化KVM-Qemu分析(四)之CPU虛擬化(2)》`的一張圖片: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233941226-1298320533.png) - 當用戶態觸發`kvm_arch_vcpu_ioctl_run`時,會讓`Guest OS`去跑在`Hypervisor`上,當`Guest OS`中出現異常退出到`Host`時,此時`handle_exit`將對退出的原因進行處理; 異常處理函式`arm_exit_handlers`如下,具體呼叫選擇哪個處理函式,是根據`ESR_EL2, Exception Syndrome Register(EL2)`中的值來確定的。 ```c static exit_handle_fn arm_exit_handlers[] = { [0 ... ESR_ELx_EC_MAX] = kvm_handle_unknown_ec, [ESR_ELx_EC_WFx] = kvm_handle_wfx, [ESR_ELx_EC_CP15_32] = kvm_handle_cp15_32, [ESR_ELx_EC_CP15_64] = kvm_handle_cp15_64, [ESR_ELx_EC_CP14_MR] = kvm_handle_cp14_32, [ESR_ELx_EC_CP14_LS] = kvm_handle_cp14_load_store, [ESR_ELx_EC_CP14_64] = kvm_handle_cp14_64, [ESR_ELx_EC_HVC32] = handle_hvc, [ESR_ELx_EC_SMC32] = handle_smc, [ESR_ELx_EC_HVC64] = handle_hvc, [ESR_ELx_EC_SMC64] = handle_smc, [ESR_ELx_EC_SYS64] = kvm_handle_sys_reg, [ESR_ELx_EC_SVE] = handle_sve, [ESR_ELx_EC_IABT_LOW] = kvm_handle_guest_abort, [ESR_ELx_EC_DABT_LOW] = kvm_handle_guest_abort, [ESR_ELx_EC_SOFTSTP_LOW]= kvm_handle_guest_debug, [ESR_ELx_EC_WATCHPT_LOW]= kvm_handle_guest_debug, [ESR_ELx_EC_BREAKPT_LOW]= kvm_handle_guest_debug, [ESR_ELx_EC_BKPT32] = kvm_handle_guest_debug, [ESR_ELx_EC_BRK64] = kvm_handle_guest_debug, [ESR_ELx_EC_FP_ASIMD] = handle_no_fpsimd, [ESR_ELx_EC_PAC] = kvm_handle_ptrauth, }; ``` 用你那雙水汪汪的大眼睛掃描一下這個函式表,發現`ESR_ELx_EC_DABT_LOW`和`ESR_ELx_EC_IABT_LOW`兩個異常,這不就是指令異常和資料異常嗎,我們大膽的猜測,`HVA->HPA`對映的建立就在`kvm_handle_guest_abort`函式中。 ## 3.1 `kvm_handle_guest_abort` 先來補充點知識點,可以更方便的理解接下里的內容: 1. Guest OS在執行到敏感指令時,產生EL2異常,CPU切換模式並跳轉到`EL2`的`el1_sync`(`arch/arm64/kvm/hyp/entry-hyp.S`)異常入口; 2. CPU的`ESR_EL2`暫存器記錄了異常產生的原因; 3. Guest退出到kvm後,kvm根據異常產生的原因進行對應的處理。 簡要看一下`ESR_EL2`暫存器: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107233952862-20535881.png) - `EC`:Exception class,異常類,用於標識異常的原因; - `ISS`:Instruction Specific Syndrome,ISS域定義了更詳細的異常細節; - 在`kvm_handle_guest_abort`函式中,多處需要對異常進行判斷處理; `kvm_handle_guest_abort`函式,處理地址訪問異常,可以分為兩類: 1. 常規記憶體訪問異常,包括未建立頁表對映、讀寫許可權等; 2. IO記憶體訪問異常,IO的模擬通常需要Qemu來進行模擬; 先看一下`kvm_handle_guest_abort`函式的註釋吧: ```c /** * kvm_handle_guest_abort - handles all 2nd stage aborts * * Any abort that gets to the host is almost guaranteed to be caused by a * missing second stage translation table entry, which can mean that either the * guest simply needs more memory and we must allocate an appropriate page or it * can mean that the guest tried to access I/O memory, which is emulated by user * space. The distinction is based on the IPA causing the fault and whether this * memory region has been registered as standard RAM by user space. */ ``` - 到達Host的abort都是由於缺乏Stage 2頁錶轉換條目導致的,這個可能是Guest需要分配更多記憶體而必須為其分配記憶體頁,或者也可能是Guest嘗試去訪問IO空間,IO操作由使用者空間來模擬的。兩者的區別是觸發異常的IPA地址是否已經在使用者空間中註冊為標準的RAM; 呼叫流程來了: ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107234014086-1464598545.png) - `kvm_vcpu_trap_get_fault_type`用於獲取`ESR_EL2`的資料異常和指令異常的`fault status code`,也就是`ESR_EL2`的ISS域; - `kvm_vcpu_get_fault_ipa`用於獲取觸發異常的IPA地址; - `kvm_vcpu_trap_is_iabt`用於獲取異常類,也就是`ESR_EL2`的`EC`,並且判斷是否為`ESR_ELx_IABT_LOW`,也就是指令異常型別; - `kvm_vcpu_dabt_isextabt`用於判斷是否為同步外部異常,同步外部異常的情況下,如果支援RAS,Host能處理該異常,不需要將異常注入給Guest; - 異常如果不是`FSC_FAULT`,`FSC_PERM`,`FSC_ACCESS`三種類型的話,直接返回錯誤; - `gfn_to_memslot`,`gfn_to_hva_memslot_prot`這兩個函式,是根據IPA去獲取到對應的memslot和HVA地址,這個地方就對應到了上文中第二章節中地址關係的建立了,由於建立了連線關係,便可以通過IPA去找到對應的HVA; - 如果註冊了RAM,能獲取到正確的HVA,如果是IO記憶體訪問,那麼HVA將會被設定成`KVM_HVA_ERR_BAD`。`kvm_is_error_hva`或者`(write_fault && !writable)`代表兩種錯誤:1)指令錯誤,向Guest注入指令異常;2)IO訪問錯誤,IO訪問又存在兩種情況:2.1)Cache維護指令,則直接跳過該指令;2.2)正常的IO操作指令,呼叫`io_mem_abort`進行IO模擬操作; - `handle_access_fault`用於處理訪問許可權問題,如果記憶體頁無法訪問,則對其許可權進行更新; - `user_mem_abort`,用於分配更多的記憶體,實際上就是完成Stage 2頁表對映的建立,根據異常的IPA地址,已經對應的HVA,建立對映,細節的地方就不表了。 來龍去脈摸清楚了,那就草草收場吧,下回見了。 # 參考 `《Arm Architecture Registers Armv8, for Armv8-A architecture profile》` 歡迎關注個人公眾號,不定期分享技術文章。 ![](https://img2020.cnblogs.com/blog/1771657/202011/1771657-20201107234044212-17246550