KVM的ept機制


轉載: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)

其實這個不是重點,就簡單地貼張圖吧:

tdp

在上圖中,包括guest CR3在內,算上PML4E、PDPTE、PDE、PTE,總共有5個客戶機物理地址(GPA),這些GPA都需要通過硬件再走一次EPT,得到下一個頁表頁相對應的宿主機物理地址。

接下來,也就是這篇博文主要的關注點,給定一個GPA,如何通過EPT計算出其相對應的HPA呢?換句話說,如果發生一個EPT violation,即在客戶虛擬機中發現某個GPA沒有映射到相對應的HPA,那么在KVM這一層會進行什么操作呢?

EPT

下圖是EPT的總體結構:

ept overview

和傳統的頁表一樣,EPT的頁表結構也是分為四層(PML4、PDPT、PD、PT),EPT Pointer (EPTP)指向PML4的首地址,在沒有大頁(huge page)的情況下(大頁會在以后的博文中說明,這篇博文不考慮大頁的情況),一個gpa通過四級頁表的尋址,得到相應的pfn,然后加上gpa最后12位的offset,得到hpa,如下圖所示:

ept walk

物理頁與頁表頁

在這個過程中,有兩種不同類型的頁結構:物理頁(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是如何進行處理的(也可參考這篇博文):

ept violation handle

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是什么意思呢?在我的理解中,這個directshadow是相對應的,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 
static int __direct_map(args...) {  ...  for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) {  if (iterator.level == level) {  mmu_set_spte(vcpu, iterator.sptep, ACC_ALL,  write, &emulate, level, gfn, pfn,  prefault, map_writable);  ...  break;  }   if (!is_shadow_present_pte(*iterator.sptep)) {  u64 base_addr = iterator.addr;   base_addr &= PT64_LVL_ADDR_MASK(iterator.level);  pseudo_gfn = base_addr >> PAGE_SHIFT;  sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr,  iterator.level - 1,  1, ACC_ALL, iterator.sptep);   link_shadow_page(iterator.sptep, sp, true);  }  }  return emulate; } 

這里的函數代碼將映射的建立分成兩種情況:

arch/x86/kvm/mmu.c

1
2 3 4 
if (iterator.level == level) {  mmu_set_spte(...);  ... } 

arch/x86/kvm/mmu.c

1
2 3 4 
else if (!is_shadow_present_pte(*iterator.sptep)) {  kvm_mmu_get_page(...);  link_shadow_page(...); } 

簡單來說,__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 
static struct kvm_mmu_page *kvm_mmu_get_page(struct kvm_vcpu *vcpu, gfn_t gfn,  gva_t gaddr, unsigned level, int direct, unsigned access, u64 *parent_pte); 

首先解釋下傳進來的參數都是什么意思:

  • 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

kvm_mmu_page definition

以上是它的定義,該函數定義在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 
static struct kvm_mmu_page *kvm_mmu_get_page(...) {  ...  role = vcpu->arch.mmu.base_role;  role.level = level;  role.direct = direct;  if (role.direct)  role.cr4_pae = 0;  role.access = access;  ...  for_each_gfn_sp(vcpu->kvm, sp, gfn) {  ...  mmu_page_add_parent_pte(vcpu, sp, parent_pte);  ...  return sp;  }  ...  sp = kvm_mmu_alloc_page(vcpu, parent_pte, direct);  if (!sp)  return sp;  sp->gfn = gfn;  sp->role = role;  hlist_add_head(&sp->hash_link,  &vcpu->kvm->arch.mmu_page_hash[kvm_page_table_hashfn(gfn)]);  ...  sp->mmu_valid_gen = vcpu->kvm->arch.mmu_valid_gen;  init_shadow_page_table(sp);  return sp; } 
  • 一開始會初始化role,在EPT機制下,vcpu->arch.mmu.base_role最開始是被初始化為0的:

arch/x86/kvm/mmu.c

1
2 3 4 5 6 
static void init_kvm_tdp_mmu(struct kvm_vcpu *vcpu) {  ...  context->base_role.word = 0;  ... } 
  • 然后調用for_each_gfn_sp查找之前已經使用過的kvm_mmu_page,該宏根據gfn的值在kvm_mmu_page結構中的hash_link進行,具體可參閱以下代碼:

arch/x86/kvm/mmu.c

1
2 3 4 
#define for_each_gfn_sp(_kvm, _sp, _gfn) \  hlist_for_each_entry(_sp, \  &(_kvm)->arch.mmu_page_hash[kvm_page_table_hashfn(_gfn)], hash_link) \  if ((_sp)->gfn != (_gfn)) {} else 
  • 如果找到了,調用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 
static struct kvm_mmu_page *kvm_mmu_alloc_page(...) {  struct kvm_mmu_page *sp;   sp = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_header_cache);  sp->spt = mmu_memory_cache_alloc(&vcpu->arch.mmu_page_cache);  ...  list_add(&sp->link, &vcpu->kvm->arch.active_mmu_pages);  sp->parent_ptes = 0;  mmu_page_add_parent_pte(vcpu, sp, parent_pte);  return sp; } 
  • 改函數調用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。

一個例子

講到這里,我們來看一個例子:

direct map

在上圖中,我們假設需要映射gpa(0xfffff000)到其相對應的hpa(0x42faf000)。

另外,對於每一個MMU page,我們都列出了其相對應的kvm_mmu_page對應的頁結構中幾個比較關鍵的域的值。

對於gpa為0xfffff000的地址,其gfn為0xfffff,我們將其用二進制表示出來,並按照EPT entry的格式進行分割:

direct map ept entry

比如,對於EPT pointer指向的第4級(level-4)頁表頁,它的role.level為4,它的sp->spt為該頁表頁的hva0xffff8800982f9000。另外,對於最高層級的頁表頁來說,它的sp->gfn為0,表示gfn為0的地址可以通過尋址找到該頁表頁。而由於ept entry中第4段的index為0,所以改頁表頁的第1個頁表項(PML4E)指向了下一層的頁表頁。

同樣的,對於第3級(level-3)頁表頁,它的role.level為3,sp->spt為該頁表頁的hva0xffff8800982f8000。由上圖可知,在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)可以得到以下的值:

direct map ept entry l1

將其轉化為十六進制數,即可得到0xc0000,表示gfn為0xc0000的地址在尋址過程中會找到該頁表頁。而它的parent_ptes就指向了第3層頁表頁中第3個頁表項的地址0xffff8800982f8018,ept entry中第2段的index 0xfff 表示它最后一項頁表項(PDE)指向了下一級的頁表頁。

類似的,可以算出第1級頁表頁的sp->gfn0xffe00parent_ptes0xffff880060db7ff8,同時,它的最后一個頁表項(PTE)指向了真正的hpa0x42faf000

direct map ept entry l1

到此為止,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 
static void mmu_page_add_parent_pte(...) {  if (!parent_pte)  return;   pte_list_add(vcpu, parent_pte, &sp->parent_ptes); } 

另外一點,我說過,頁分為兩類,物理頁和頁表頁,但是我之前沒有說的一點是,頁表頁本身也被分為兩類,高層級(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 
static void mmu_set_spte(...) {  ...   if (set_spte(vcpu, sptep, pte_access, level, gfn, pfn, speculative,  true, host_writable)) {  ...  }  ...  if (is_shadow_present_pte(*sptep)) {  if (!was_rmapped) {  rmap_count = rmap_add(vcpu, sptep, gfn);  ...  }  }   ... }  static int rmap_add(struct kvm_vcpu *vcpu, u64 *spte, gfn_t gfn) {  ...  sp = page_header(__pa(spte));  kvm_mmu_page_set_gfn(sp, spte - sp->spt, gfn);  rmapp = gfn_to_rmap(vcpu->kvm, gfn, sp->role.level);  return pte_list_add(vcpu, spte, rmapp); } 

可以看到,不管是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 
static unsigned long *__gfn_to_rmap(gfn_t gfn, int level,  struct kvm_memory_slot *slot) {  unsigned long idx;   idx = gfn_to_index(gfn, slot->base_gfn, level);  return &slot->arch.rmap[level - PT_PAGE_TABLE_LEVEL][idx]; } 

有了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 
static int pte_list_add(struct kvm_vcpu *vcpu, u64 *spte,  unsigned long *pte_list) {  struct pte_list_desc *desc;  int i, count = 0;   if (!*pte_list) {  rmap_printk("pte_list_add: %p %llx 0->1\n", spte, *spte);  *pte_list = (unsigned long)spte;  } else if (!(*pte_list & 1)) {  rmap_printk("pte_list_add: %p %llx 1->many\n", spte, *spte);  desc = mmu_alloc_pte_list_desc(vcpu);  desc->sptes[0] = (u64 *)*pte_list;  desc->sptes[1] = spte;  *pte_list = (unsigned long)desc | 1;  ++count;  } else {  rmap_printk("pte_list_add: %p %llx many->many\n", spte, *spte);  desc = (struct pte_list_desc *)(*pte_list & ~1ul);  while (desc->sptes[PTE_LIST_EXT-1] && desc->more) {  desc = desc->more;  count += PTE_LIST_EXT;  }  if (desc->sptes[PTE_LIST_EXT-1]) {  desc->more = mmu_alloc_pte_list_desc(vcpu);  desc = desc->more;  }  for (i = 0; desc->sptes[i]; ++i)  ++count;  desc->sptes[i] = spte;  }  return count; } 

看到這里你可能還是一頭霧水,rmap到底是什么,為什么加一個rmap的項要那么復雜?

好吧,其實我的理解是這樣的:

  • 首先,rmap就是一個數組,這個數組的每個項都對應了這個gfn反向映射出的某個spte的地址;
  • 其次,由於大部分情況下一個gfn對應的spte只有一個,也就是說,大部分情況下這個數組的大小是1;
  • 但是,這個數組也可能很大,大到你也不知道應該把數組的大小設到多少合適;
  • 所以,總結來說,rmap是一個不確定大小,但是大部分情況下大小為1的數組。

那么,怎么做?

我想說,這是一個看上去很完美的設計!

由於spte的地址只可能是8的倍數(自己想為什么),所以其第一位肯定是0,那么我們就利用這個特點:

  • 我們用一個unsigned long *來表示一個rmap,即上文中的pte_list
  • 如果這個pte_list為空,則表示這個rmap之前沒有創建過,那么將其賦值,即上文中0->1的情況;
  • 如果這個pte_list不為空,但是其第一位是0,則表示這個rmap之前已經被設置了一個值,那么需要將這個pte_list的值改為某個struct pte_list_desc的地址,然后將第一位設成1,來表示該地址並不是單純的一個spte的地址,而是指向某個struct pte_list_desc,這是上文中1->many的情況;
  • 如果這個pte_list不為空,而且其第一位是1,那么通過訪問由這個地址得到的struct pte_list_desc,得到更多的sptes,即上文中many->many的情況。

struct pte_list_desc結構定義如下:

arch/x86/kvm/mmu.c

1
2 3 4 
struct pte_list_desc {  u64 *sptes[PTE_LIST_EXT];  struct pte_list_desc *more; }; 

它是一個單鏈表的節點,每個節點都存有3個spte的地址,以及下一個節點的位置。

好了,最后一個問題,rmap到底有什么用?

當然,信息總歸是有用的,特別是這些和映射相關的信息。

舉個例子吧,假如操作系統需要進行頁面回收或換出,如果宿主機需要把某個客戶機物理頁換到disk,那么它就需要修改這個頁的物理地址gpa對應的spte,將其設置成不存在。

那么這個該怎么做呢?

當然,你可以用軟件走一遍ept頁表,找到其對應的spte。但是,這樣太慢了!這個時候你就會想,如果有一個gfn到spte的反向映射豈不方便很多!於是,reverse map就此派上用場。

這里最后說一點,如果說有這么一個需求:宿主機想要廢除當前客戶機所有的MMU頁結構,那么如何做最快呢?

當然,你可以從EPTP開始遍歷一遍所有的頁表頁,處理掉所有的MMU頁面和對應的映射,但是這種方法效率很低。

如果你還記得之前kvm_mmu_page結構里面的mmu_valid_gen域的話,你就可以通過將kvm->arch.mmu_valid_gen加1,那么當前所有的MMU頁結構都變成了invalid,而處理掉頁結構的過程可以留給后面的過程(如內存不夠時)再處理,這樣就可以加快這個過程。

而當mmu_valid_gen值達到最大時,可以調用kvm_mmu_invalidate_zap_all_pages手動廢棄掉所有的MMU頁結構。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM