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

LoyenWang發表於2020-11-07

背景

  • 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. 非虛擬化下的記憶體的訪問

  • CPU訪問實體記憶體前,需要先建立頁表對映(虛擬地址到實體地址的對映),最終通過查表的方式來完成訪問。在ARMv8中,核心頁表基地址存放在TTBR1_EL1中,使用者空間頁表基地址存放在TTBR0_EL0中;
  1. 虛擬化下的記憶體訪問

  • 虛擬化情況下,記憶體的訪問會分為兩個StageHypervisor通過Stage 2來控制虛擬機器的記憶體檢視,控制虛擬機器是否可以訪問某塊實體記憶體,進而達到隔離的目的;
  • Stage 1VA(Virtual Address)->IPA(Intermediate Physical Address),Host的作業系統控制Stage 1的轉換;
  • Stage 2IPA(Intermediate Physical Address)->PA(Physical Address),Hypervisor控制Stage 2的轉換;

猛一看上邊兩個圖,好像明白了啥,仔細一想,啥也不明白,本文的目標就是將這個過程講明白。

在開始細節講解之前,需要先描述幾個概念:

gva - guest virtual address
gpa - guest physical address
hva - host virtual address
hpa - host physical address

  • 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命令來實現的,如下圖:

找到了入口,讓我們進一步揭開神祕的面紗。

2.1 資料結構

關鍵的資料結構如下:

  • 虛擬機器使用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的建立、刪除,更新等操作,話不多說,來圖了:

  • 當使用者要設定記憶體區域時,最終會呼叫到__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_CREATEKVM_MR_MOVEKVM_MEM_READONLY
  • 根據使用者傳遞的引數決定是否需要分配髒頁的bitmap,標識頁是否可用;
  • 最終呼叫kvm_set_memslot來設定和更新slot資訊;

2.2.1 kvm_set_memslot

具體的memslot的設定在kvm_set_memslot函式中完成,slot的操作流程如下:

  • 首先分配一個新的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用於更新整個memslotsmemslots基於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)》的一張圖片:

  • 當使用者態觸發kvm_arch_vcpu_ioctl_run時,會讓Guest OS去跑在Hypervisor上,當Guest OS中出現異常退出到Host時,此時handle_exit將對退出的原因進行處理;

異常處理函式arm_exit_handlers如下,具體呼叫選擇哪個處理函式,是根據ESR_EL2, Exception Syndrome Register(EL2)中的值來確定的。

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_LOWESR_ELx_EC_IABT_LOW兩個異常,這不就是指令異常和資料異常嗎,我們大膽的猜測,HVA->HPA對映的建立就在kvm_handle_guest_abort函式中。

3.1 kvm_handle_guest_abort

先來補充點知識點,可以更方便的理解接下里的內容:

  1. Guest OS在執行到敏感指令時,產生EL2異常,CPU切換模式並跳轉到EL2el1_syncarch/arm64/kvm/hyp/entry-hyp.S)異常入口;
  2. CPU的ESR_EL2暫存器記錄了異常產生的原因;
  3. Guest退出到kvm後,kvm根據異常產生的原因進行對應的處理。

簡要看一下ESR_EL2暫存器:

  • 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函式的註釋吧:

/**
 * 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;

呼叫流程來了:

  • 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_EL2EC,並且判斷是否為ESR_ELx_IABT_LOW,也就是指令異常型別;
  • kvm_vcpu_dabt_isextabt用於判斷是否為同步外部異常,同步外部異常的情況下,如果支援RAS,Host能處理該異常,不需要將異常注入給Guest;
  • 異常如果不是FSC_FAULTFSC_PERMFSC_ACCESS三種型別的話,直接返回錯誤;
  • gfn_to_memslotgfn_to_hva_memslot_prot這兩個函式,是根據IPA去獲取到對應的memslot和HVA地址,這個地方就對應到了上文中第二章節中地址關係的建立了,由於建立了連線關係,便可以通過IPA去找到對應的HVA;
  • 如果註冊了RAM,能獲取到正確的HVA,如果是IO記憶體訪問,那麼HVA將會被設定成KVM_HVA_ERR_BADkvm_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》

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

相關文章