轉載:http://ytliu.info/blog/2014/11/24/shi-shang-zui-xiang-xi-de-kvm-mmu-pagejie-gou-he-yong-fa-jie-xi/
這段時間在研究KVM內存虛擬化的代碼,看的那叫一個痛苦。網上大部分能找到的資料,不管是中文的還是英文的,寫的都非常含糊,很多關鍵的數據結構和代碼都講的閃爍其辭,有些就是簡單的把KVM的文檔翻譯了一下,但是KVM的文檔也讓人(至少讓我)看的挺費解的,只能着眼於代碼,一直掙扎到如今,終於有那么一點開竅了。
於是乎,本着“利己又為人”的原則,我決定將這段時間自己所理解的東西傾情奉獻出,特別是對kvm_mmu_page這個最為關鍵的數據結構,以及它在handle EPT violation時每個域的作用和意義。
需要說明的是,這篇博客並不是一個針對初學者理解“內存虛擬化”的教程,“內存虛擬化”涉及到的很多概念需要讀者去翻閱其它資料來獲取,以下內容均建立在讀者已經了解了“內存虛擬化”的基本概念的基礎上,比如對於什么是影子頁表(Shadow page table),什么是EPT等,請自行google。以下內容大部分是我閱讀目前KVM的文檔和源碼,以及在運行時生成log進行驗證來確定的。
我會盡最大的努力讓以下內容足夠完整和准確,如果讀者發現有什么不清楚或者覺得不正確的地方,望請告知。這篇博文也會實時並且持續更新。
現在開始進入“史上最詳細的”系列:
我們知道在KVM最新的內存虛擬化技術中,采用的是兩級頁表映射tdp (two-dimentional paging),客戶虛擬機采用的是傳統操作系統的頁表,被稱做guest page table (GPT),記錄的是客戶機虛擬地址(GVA)到客戶機物理地址(GPA)的映射;而KVM維護的是第二級頁表extended page table (EPT,注:AMD的體系架構中其被稱為NPT,nested page table,在這篇文章中統一采用Intel的稱法EPT),記錄的是虛擬機物理地址(GPA)到宿主機物理地址(HPA)的映射。
在介紹主體內容之前,需要先統一下幾個縮寫(摘自KVM文檔:linux/Documentation/virtual/kvm/mmu.txt):
- pfn: host page frame number,宿主機中某個物理頁的幀數
- hpa: host physical address,宿主機的物理地址
- hva: host virtual address,宿主機的虛擬地址
- gfn: guest page frame number,虛擬機中某個物理頁的幀數
- gpa: guest physical address,虛擬機的物理地址
- gva: guest virtual address,虛擬機的虛擬地址
- pte: page table entry,指向下一級頁表或者頁的物理地址,以及相應的權限位
- gpte: guest pte,指向GPT中下一級頁表或者頁的gpa,以及相應的權限位
- spte: shadow pte,指向EPT中下一級頁表或者頁的hpa,以及相應的權限位
- tdp: two dimentional paging,也就是我們所說的EPT機制
以上唯一需要解釋的是spte,在這里被叫做shadow pte,如果不了解的話,會很容易和以前的shadow paging機制搞混。
KVM在還沒有EPT硬件支持的時候,采用的是影子頁表(shadow page table)機制,為了和之前的代碼兼容,在當前的實現中,EPT機制是在影子頁表機制代碼的基礎上實現的,所以EPT里面的pte和之前一樣被叫做shadow pte,這個會在之后進行詳細的說明。
兩級頁表尋址 (tdp)
其實這個不是重點,就簡單地貼張圖吧:
在上圖中,包括guest CR3在內,算上PML4E、PDPTE、PDE、PTE,總共有5個客戶機物理地址(GPA),這些GPA都需要通過硬件再走一次EPT,得到下一個頁表頁相對應的宿主機物理地址。
接下來,也就是這篇博文主要的關注點,給定一個GPA,如何通過EPT計算出其相對應的HPA呢?換句話說,如果發生一個EPT violation,即在客戶虛擬機中發現某個GPA沒有映射到相對應的HPA,那么在KVM這一層會進行什么操作呢?
EPT
下圖是EPT的總體結構:
和傳統的頁表一樣,EPT的頁表結構也是分為四層(PML4、PDPT、PD、PT),EPT Pointer (EPTP)指向PML4的首地址,在沒有大頁(huge page)的情況下(大頁會在以后的博文中說明,這篇博文不考慮大頁的情況),一個gpa通過四級頁表的尋址,得到相應的pfn,然后加上gpa最后12位的offset,得到hpa,如下圖所示:
物理頁與頁表頁
在這個過程中,有兩種不同類型的頁結構:物理頁(physical page)和頁表頁(MMU page)。物理頁就是真正存放數據的頁,而頁表頁,顧名思義,就是存放頁表的頁,而且存放的是EPT的頁表。其中,第四級(level-4)頁表,也就是EPTP指向的那個頁表,是所有MMU pages的根(root),它只有一個頁,包含512(4096/8)個頁表項(PML4E),每個頁表項指向一個第三級(level-3)的頁表頁(PDPT),類似的,每個PDPT頁表頁也是512個頁表項指向下一級頁表,直到最后一級(level-1)PT,PT中的每個頁表項(PTE)指向的是一個物理頁的頁幀(pfn)異或上相對應的access bits。
物理頁和頁表頁除了功能和里面存儲的內容不同外,它們被創建的方式也是不同的:
- 物理頁可以通過內核提供的
__get_free_page
來創建,該函數最后會通過底層的alloc_page
來返回一段指定大小的內存區域。 - 頁表頁則是從
mmu_page_cache
獲得,該page cache是在KVM模塊初始化vcpu的時候通過linux內核中的slab機制分配好作為之后MMU pages的cache使用的。
在KVM的代碼實現中,每個頁表頁(MMU page)對應一個數據結構kvm_mmu_page。這個數據結構是理解整個EPT機制的關鍵,接下來的篇幅就主要圍繞這個kvm_mmu_page
進行分析。
ept violation處理流程
在引入這個數據結構之前,我們先來整體了解下在發生ept violation之后KVM是如何進行處理的(也可參考這篇博文):
handle_ept_violation
最終會調用到arch/x86/kvm/mmu.c
里面的tdp_page_fault
。在該函數中,有兩個大的步驟:
- gfn_to_pfn:在這個過程中,通過gfn->memslot->hva->pfn這一系列步驟得到最后的pfn,這個過程以后會專門用一篇博客來描述;
- __direct_map:這個函數所做的事情就是把上一步中得到的pfn和gfn的映射關系反映在EPT中,該過程是這篇博文介紹的重點。
順便提一句,為什么這里叫direct_map
呢,即這里的direct
是什么意思呢?在我的理解中,這個direct
和shadow
是相對應的,direct
是指在EPT的模式下進行映射,而shadow
是在之前shadow paging的模式下進行映射,這主要反映在后面的kvm_mmu_get_page
傳參過程中(請參閱之后的介紹)。
__direct_map
的主要邏輯如下(可參閱這里的解釋):
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
這里的函數代碼將映射的建立分成兩種情況:
arch/x86/kvm/mmu.c
1
2 3 4 |
|
arch/x86/kvm/mmu.c
1
2 3 4 |
|
簡單來說,__direct_map
這個函數是根據傳進來的gpa進行計算,從第4級(level-4)頁表頁開始,一級一級地填寫相應頁表項,這些都是在for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator)
這個宏定義里面實現的,這里不展開。這兩種情況是這樣子的:
- 第一種情況是指如果當前頁表頁的層數(
iterator.level
)是最后一層(level
)的頁表頁,那么直接通過調用mmu_set_spte
(之后會細講)設置頁表項。 - 第二種情況是指如果當前頁表頁
A
不是最后一層,而是中間某一層(leve-4, level-3, level-2),而且該頁表項之前並沒有初始化(!is_shadow_present_pte(*iterator.sptep)
),那么需要調用kvm_mmu_get_page
得到或者新建一個頁表頁B
,然后通過link_shadow_page
將其link到頁表頁A
相對應的頁表項中。
kvm_mmu_get_page
根據代碼可能發生的前后關系,我們先來解釋下第二種情況,即如何新建一個頁表頁,即之前所提到的kvm_mmu_page。
這是kvm_mmu_get_page
的聲明:
arch/x86/kvm/mmu.c
1
2 |
|
首先解釋下傳進來的參數都是什么意思:
- gaddr:產生該ept violation的gpa;
- gfn:gaddr通過某些計算得到的gfn,計算的公式是
(gaddr >> 12) & ~((1 << (level * 9)) - 1)
,這個會在之后進行解釋; - level:該頁表頁對應的level,可能取值為3,2,1;
- direct:在EPT機制下,該值始終為1,如果是shadow paging機制,該值為0;
- access:該頁表頁的訪問權限;
- parent_pte:上一級頁表頁中指向該級頁表頁的頁表項的地址。
下面舉個例子來說明:
假設在__direct_map
中,產生ept violation的gpa為0xfffff000,當前的level為3,這個時候,發現EPT中第3級的頁表頁對應的頁表項為空,那么我們就需要創建一個第2級的頁表頁,然后將其物理地址填在第3級頁表頁對應的頁表項中,那么傳給kvm_mmu_get_page
的參數很可能是這樣子的:
- gaddr:0xfffff000;
- gfn: 0xc0000 (通過
(0xfffff000 >> 12) & ~((1 << (3 - 1) * 9) - 1)
得到); - level:2 (通過
3 - 1
得到); - direct:1;
- access:7(表示可讀、可寫、可執行);
- parent_pte:0xffff8800982f8018(這個是第3級頁表頁相應的頁表項的宿主機虛擬地址hva);
struct kvm_mmu_page
接下來看看這個函數的返回值:struct kvm_mmu_page
:
以上是它的定義,該函數定義在arch/x86/include/asm/kvm_host.h
中。那么它們分別是什么意思呢?這里先有一個大概的解釋(有幾個域還不確定,之后會持續更新),等會兒我們會通過一個具體的例子來說明:
kvm_mmu_page子域 | 解釋 |
---|---|
link | 將該頁結構鏈接到kvm->arch.active_mmu_pages和invalid_list上,標注該頁結構不同的狀態 |
hash_link | KVM中會為所有的mmu_page維護一個hash鏈表,用於快速找到對應的kvm_mmu_page實例,詳見之后代碼分析 |
gfn | 通過kvm_mmu_get_page傳進來的gfn,在EPT機制下,每個kvm_mmu_page對應一個gfn,shadow paging見gfns |
role | kvm_mmu_page_role結構,詳見之后分析 |
spt | 該kvm_mmu_page對應的頁表頁的宿主機虛擬地址hva |
gfns | 在shadow paging機制下,每個kvm_mmu_page對應多個gfn,存儲在該數組中 |
unsync | 用在最后一級頁表頁,用於判斷該頁的頁表項是否與guest的翻譯同步(即是否所有pte都和guest的tlb一致) |
root_rount | 用在第4級頁表,標識有多少EPTP指向該級頁表頁 |
unsync_children | 記錄該頁表頁中有多少個spte是unsync狀態的 |
parent_ptes | 表示有哪些上一級頁表頁的頁表項指向該頁表頁(之后會詳細介紹) |
mmu_valid_gen | 該頁的generation number,用於和kvm->arch.mmu_valid_gen 進行比較,比它小表示該頁是invalid的 |
unsync_child_bitmap | 記錄了unsync的sptes的bitmap,用於快速查找 |
write_flooding_count | 在頁表頁寫保護模式下,用於避免過多的頁表項修改造成的模擬(emulation) |
其中,role
指向了一個union kvm_mmu_page_role
結構,解釋如下:
kvm_mmu_page_role子域 | 解釋 |
---|---|
level | 該頁表頁的層級 |
cr4_pae | 記錄了cr4.pae的值,如果是direct模式,該值為0 |
quadrant | 暫時不清楚 |
direct | 如果是EPT機制,則該值為1,否則為0 |
access | 該頁表頁的訪問權限,參見之后的說明 |
invalid | 表示該頁是否有效(暫時不確定) |
nxe | 記錄了efer.nxe的值(暫時不清楚什么作用) |
cr0_wp | 記錄了cr0.wp的值,表示該頁是否寫保護 |
smep_andnot_wp | 記錄了cr4.smep && !cr0.wp的值(暫時不確定什么作用) |
kvm_mmu_get_page源碼分析
在了解了大部分子域的意義之后,我們來看下kvm_mmu_get_page
的代碼:
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
|
- 一開始會初始化
role
,在EPT機制下,vcpu->arch.mmu.base_role
最開始是被初始化為0的:
arch/x86/kvm/mmu.c
1
2 3 4 5 6 |
|
- 然后調用
for_each_gfn_sp
查找之前已經使用過的kvm_mmu_page
,該宏根據gfn的值在kvm_mmu_page
結構中的hash_link進行,具體可參閱以下代碼:
arch/x86/kvm/mmu.c
1
2 3 4 |
|
- 如果找到了,調用
mmu_page_add_parent_pte
,設置parent_pte對應的reverse map(reverse map一章會在之后對其進行詳細的說明); - 如果該gfn對應的頁表頁不存在,則調用
kvm_mmu_alloc_page
:
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 8 9 10 11 12 |
|
- 改函數調用
mmu_memory_cache_alloc
從之前分配好的mmu page的memory cache中得到一個kvm_mmu_page
結構體實例,然后將其插入kvm->arch.active_mmu_pages
中,同時調用mmu_page_add_parent_pte
函數設置parent pte對應的reverse map。
一個例子
講到這里,我們來看一個例子:
在上圖中,我們假設需要映射gpa(0xfffff000)到其相對應的hpa(0x42faf000)。
另外,對於每一個MMU page,我們都列出了其相對應的kvm_mmu_page
對應的頁結構中幾個比較關鍵的域的值。
對於gpa為0xfffff000
的地址,其gfn為0xfffff
,我們將其用二進制表示出來,並按照EPT entry的格式進行分割:
比如,對於EPT pointer指向的第4級(level-4)頁表頁,它的role.level
為4,它的sp->spt
為該頁表頁的hva
值0xffff8800982f9000
。另外,對於最高層級的頁表頁來說,它的sp->gfn
為0,表示gfn為0的地址可以通過尋址找到該頁表頁。而由於ept entry中第4段的index為0,所以改頁表頁的第1個頁表項(PML4E)指向了下一層的頁表頁。
同樣的,對於第3級(level-3)頁表頁,它的role.level
為3,sp->spt
為該頁表頁的hva
值0xffff8800982f8000
。由上圖可知,在ept entry中,它的上一層(即第4段)的index值為0,所以其sp->gfn
也是0,同樣表示gfn為0的地址可以通過尋址找到該頁表頁。另外,在該層的頁表頁中,其parent_ptes
填的是上一層的頁表頁中指向該頁表頁的頁表項的地址,即第4級頁表頁的第一個頁表項的地址0xffff8800982f9000
,而在ept entry中,由於第3段的index為3,所以該頁表頁的第3個頁表項(PDPTE)指向了下一層的頁表頁。
以此類推,到第2級(level-2)頁表頁,前面幾項都和之前是類似的,而對於sp->gfn
來說,由於它的上一層(第3層)的index值為3,那么通過計算公式(gaddr >> 12) & ~((1 << (level * 9)) - 1)
可以得到以下的值:
將其轉化為十六進制數,即可得到0xc0000
,表示gfn為0xc0000
的地址在尋址過程中會找到該頁表頁。而它的parent_ptes
就指向了第3層頁表頁中第3個頁表項的地址0xffff8800982f8018
,ept entry中第2段的index 0xfff
表示它最后一項頁表項(PDE)指向了下一級的頁表頁。
類似的,可以算出第1級頁表頁的sp->gfn
為0xffe00
,parent_ptes
為0xffff880060db7ff8
,同時,它的最后一個頁表項(PTE)指向了真正的hpa0x42faf000
。
到此為止,gpa被最終映射為hpa,並放映在EPT中,於是下次客戶虛擬機應用程序訪問該gpa的時候就不會再發生ept violation了。
reverse map
似乎講到這里就該結束了?
確實,基本上這篇博文的內容就要接近尾聲了,只是還有那么一小點內容,關於reverse map。
如果你倒回去看會發現,我們還有兩個很重要的函數沒有展開:
- mmu_page_add_parent_pte
- mmu_set_spte
這兩個函數是干什么的呢?其實它們都和reverse map有關。
首先,對於低層級(level-3 to level-1)的頁表頁結構kvm_mmu_page,我們需要設置上一級的相應的頁表項地址,然后通過mmu_page_add_parent_pte
設置其parent_pte的reverse map:
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 |
|
另外一點,我說過,頁分為兩類,物理頁和頁表頁,但是我之前沒有說的一點是,頁表頁本身也被分為兩類,高層級(level-4 to level-2)的頁表頁,和最后一級(level-1)的頁表頁。
對於高層級的頁表頁,我們只需要調用link_shadow_page
,將頁表項的值和相應的權限位直接設置上去就好了,但是對於最后一級的頁表項,我們除了設置頁表項對應的值之外,還需要做另一件事,rmap_add
:
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
可以看到,不管是mmu_page_add_parent_pte
,還是mmu_set_spte
調用的rmap_add
,最后都會調用到pte_list_add
。
那么問題來了,這貨是干嘛的呢?
翻譯成中文的話,reverse map被稱為反向映射,在上面提到的兩個反向映射中,第一個叫parent_ptes,記錄的是頁表頁和指向它的頁表項對應的映射,另一個是每個gfn對應的反向映射rmap,記錄的是該gfn對應的spte。
我們舉rmap為例,給定一個gfn,我們怎么找到其對應的rmap呢?
- 首先,我們通過
gfn_to_memslot
得到這個gfn對應的memory slot(這個機制會在以后的博文中提到); - 通過得到的slot和gfn,算出相應的index,然后從
slot->arch.rmap
數組中取出相應的rmap:
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 8 |
|
有了gfn對應的rmap之后,我們再調用pte_list_add
將這次映射得到的spte加到這個rmap中
arch/x86/kvm/mmu.c
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|