前言
- kvm實現內存虛擬化,需要充分利用intel的硬件機制EPT。所謂EPT的硬件機制,就是開啓EPT之後,當處在客戶態的CPU訪問物理地址(所謂的GPA,比如CR3,頁表項中的物理地址)時,會通過VMCS中的EPT pointer發起查找,在EPT頁結構中查詢對應的主機物理地址(HPA),訪問過程中如果遇到某個頁結構的條目不存在,會產生缺頁異常。軟件負責分配頁表或者頁結構。
- 看下面這張圖,EPT初始化時,軟件只在VMCS中填入了EPT PML4 Table的物理地址,並未EPT PML4 Table分配了內存空間。當CPU訪問的GPA落在PML4 Table其中一個條目包含的內存區間時,硬件需要根據這個條目中存放的主機物理地址去訪問下一級的頁結構EPT PDPT,但初始化時這張表是空的,條目肯定不存在,因此產生缺頁異常,然後軟件會負責分配EPT PDPT的內存並把地址填寫到EPT PML4 Table的條目中。缺頁異常處理完成後,硬件再次訪問該條目,已然發現可以正常訪問了。接着繼續訪問下一級的頁結構直到最終找到HPA對應的主機物理頁地址。從上面的描述中可以確定三點:
- PML4 Table,PDPT,PDT,PT這些頁結構,都是放在內存中的,它們由軟件來分配管理。
- PML4 Table,PDPT,PDT,PT這些頁結構的創建,都是軟件在缺頁異常的處理過程中完成的。
- EPT頁表以及中間頁結構的創建,都通過缺頁異常觸發。內核負責維護這些頁結構。
- 本文主要介紹內核如何組織EPT的頁結構。從形態上來看,每一級的頁結構都包含若干頁表項,每個頁表項都指向一個內存頁,最後一級的頁表項指向真正的物理頁,中間的指向一個存放頁表的內存頁。對於在EPT中存放頁表的頁結構,內核統一用一個概念來抽象,即
SPT——Shadow Page Table
。SPT中的每一個條目稱爲SPTE——Shadow Page Table Entry
。爲什麼要用這麼彆扭的名字?因爲這個數據結構不是爲EPT頁結構專門設計的,而是爲影子頁表設計的,影子頁表中也需要維護一套頁結構,EPT在實現時就複用了這個結構。因此SPT可以在代碼上兼容兩種內存虛擬化加速的實現(影子頁表和EPT),EPT在內核代碼中還被稱爲tdp(two dimensional paging),用於區分傳統的影子頁表。
數據結構
SPT
- 首先介紹EPT的頁結構,就是前言中說的spt。從上圖中可以知道,EPT有4級,最上面一級是Level-4(PML4 Table),然後依次時Level-3(PDPT),Level-2(PDT),Level-1(PT)。這些頁結構,在kvm中都用kvm_mmu_page來表示,每張表的條目內容都存放下一級頁表的物理地址。如下:
struct kvm_mmu_page {
......
gfn_t gfn; /* 1 */
union kvm_mmu_page_role role; /* 2 */
u64 *spt; /* 3 */
......
};
1. 這個頁結構包含的虛機物理內存區域的起始頁框號
2. 頁結構在EPT頁層級中的角色
3. 頁結構的核心成員,指向一張物理頁的基址,存放着512個頁表項(sptes)
- 我們知道,一個頁結構就是一個4k大小的物理頁,它的核心要素只有一個,就是物理頁地址,在kvm_mmu_page中對應spt這個成員。那爲什麼一個頁結構除了spt還有好些其它成員呢?這是因爲管理需要,假設現在有兩個4k大小的物理頁,它們分別分配用來存放PML4 Table和PDPT這兩個頁結構,那硬件在查找HPA時應該先查找哪個表呢?顯然應該先查PML4 Table,因爲它的頁表項包含PDPT的物理地址,這時候需要一個標籤來表明頁結構在EPT頁表中的層級,用不同的角色來區分不同層級的頁結構。這個標籤kvm用kvm_mmu_page_role來表示,如下所示,所以一個頁結構至少還應該包含這個role成員。除了層級不一樣,其實每個頁結構的頁表項可能也有細微差別,比如指向物理頁的頁結構和指向spt的頁結構。kvm_mmu_page可以看作是所有頁結構的父類,不同層級的頁結構繼承這個父類的特性。
union kvm_mmu_page_role {
unsigned word;
struct {
unsigned level:4; /* 4 */
unsigned direct:1; /* 5 */
unsigned invalid:1; /* 6 */
......
};
};
4. spt在頁結構中的層級,比如level=1就是最底層,每個條目指向一個物理頁,level=4最上層,它的地址被VMCS的EPT pointer記錄
5. 表明頁表項是否直接指向了物理頁
6. spt是否無效,如果被設置成無效,就不應該被使用了,因此稍後它可能被銷燬
MMU
- MMU(Memory Manager Unit),在普通情況下代指用來實現線性地址到物理地址轉換的內存管理單元,輸入是HVA,輸出是HPA。在kvm內存虛擬化中,它用來代指實現客戶機物理地址到主機物理地址轉換的內存管理單元。當客戶機需要訪問一個GPA是,它將GPA送入mmu,mmu返回一個對應的HPA。kvm mmu的輸入是GPA,輸出是HPA。kvm中mmu的數據結構如下:
struct kvm_mmu {
......
int (*page_fault)(struct kvm_vcpu *vcpu, gva_t gva, u32 err, /* 1 */
bool prefault);
hpa_t root_hpa; /* 2 */
u8 root_level; /* 3 */
bool direct_map; /* 4 */
......
1. mmu需要實現GPA到HPA的轉換,不只基於x86平臺的EPT實現,也可以基於AMD平臺的NPT,或者影子頁表,因此mmu定義一套地址轉換的通
用操作,具體平臺有自己的實現,對於x86平臺上,page_fault的實現就是tdp_page_fault
2. mmu實現地址轉換依賴的當然是spt,因此它的結構裏面必須包含spt的入口,這樣通過mmu可以找到ept頁結構的入口點。root_hpa就是這個入
口點,它存放的是一個物理頁地址,這個物理頁的內容是spt,root_hpa也可以看做是指向根spt的指針。
3. 當客戶機採用不同的分頁模式時,頁結構的層級頁各有不同,root_level表示的是最頂層的頁結構是第幾級
4. 區分mmu是基於影子頁表實現還是基於內存硬件機制實現,direct_map爲true時表示基於硬件(EPT/NPT)實現
- 對於intel提供的EPT頁表機制,kvm抽象出kvm_mmu和kvm_mmu_page數據結構來描述,如下圖所示,intel手冊中EPT的頁結構就是這裏的一個SPT,頁結構裏面的每個表項就是這裏的spte。
創建SPT
- 當虛機訪問物理地址時,硬件會通過VMCS的EPTP開始查整個EPT頁表,如果訪問的物理內存不存在,產生缺頁異常——EPT violation,退出到內核態後由handle_ept_violation處理缺頁異常。內核根據引發缺頁的客戶機物理地址頁框號gfn找到它所在的spte,如果spte不在內存中,說明它指向的下一級頁表還沒有創建,這時就會創建SPT。
- 從虛機內存缺頁到SPT創建,整個流程如下:
vmx_handle_exit
handle_ept_violation
kvm_mmu_page_fault
vcpu->arch.mmu.page_fault <=> tdp_page_fault
__direct_map
static int __direct_map(struct kvm_vcpu *vcpu, gpa_t v, int write,
int map_writable, int level, gfn_t gfn, kvm_pfn_t pfn,
bool prefault)
{
......
for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) {
if (iterator.level == level) { /* 1 */
mmu_set_spte(vcpu, iterator.sptep, ACC_ALL, /* 2 */
write, &emulate, level, gfn, pfn,
prefault, map_writable);
}
if (!is_shadow_present_pte(*iterator.sptep)) { /* 3 */
u64 base_addr = iterator.addr;
base_addr &= PT64_LVL_ADDR_MASK(iterator.level); /* 4 */
pseudo_gfn = base_addr >> PAGE_SHIFT;
sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr, /* 5 */
iterator.level - 1,
1, ACC_ALL, iterator.sptep);
link_shadow_page(iterator.sptep, sp); /* 6 */
}
}
......
1. 遍歷vcpu的mmu所管理spt頁表,如果要查找的gfn所在的層級與spt頁表層級相同,說明找到了gfn所在的頁表
2. 找到頁表之後,根據gfn計算其在spt頁表的索引,就可以設置gfn對應的頁表項了,這個流程在後面分析
3. 如果頁表遍歷的層級和gfn所在頁表層級不同,並且它的下一級頁表沒有了,需要分配內存創建頁表了
4. 根據當前頁表迭代器的起始地址計算頁框號
5. 查找內存頁表,如果沒有就創建
6. 將找到的內存頁表地址寫入上一級的頁表項中
- kvm_mmu_get_page函數中有創建頁表的關鍵動作:
kvm_mmu_get_page
kvm_mmu_alloc_page
sp = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_header_cache) /* 7 */
sp->spt = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache) /* 8 */
7. 從cache中分配kvm_mmu_page的內存
8. 分配kvm_mmu_page結構關鍵成員,頁表spt
- 頁表分配完成後,需要將頁表的地址填寫到上級頁表的頁表項中:
link_shadow_page(iterator.sptep, sp)
spte = __pa(sp->spt) | shadow_present_mask | PT_WRITABLE_MASK | /* 9 */
shadow_user_mask | shadow_x_mask | shadow_me_mask;
mmu_spte_set(sptep, spte) /* 10 */
9. 將頁表的物理地址取出,這裏能夠看到,SPT處於中間層級的頁表項記錄的就是主機內存的物理地址
10.填入對應的頁表項
設置SPT
- 在缺頁流程處理中,當發現gfn所在區間的頁表不存在,就分配,然後繼續遍歷,直到最終找到gfn對應的頁表項,設置頁表項內容,設置反向映射rmap的內容:
mmu_set_spte
set_spte
rmap_add(vcpu, sptep, gfn)