2017-05-30
前幾天簡要分析了linux remap機制,雖然還有些許瑕疵,但總算大致分析的比較清楚。今天分析下EPT下的逆向映射機制。EPT具體的工作流程可參考前面博文,本文對於EPT以及其工作流程不做過多介紹,重點介紹逆向映射機制。其實逆向映射機制在最主要的作用就是映射的逆向,說了等於白說,但也不無道理。linux下根據虛擬地址經過頁表轉換得到物理地址。怎么根據物理地址得到對應的虛擬地址呢?這里便用到了逆向映射。逆向映射有什么用呢?最重要的,在頁面換出時,由於物理內存的管理由一套相對獨立的機制在負責,根據物理頁面的活躍程度,對物理頁面進行換出,而此時就需要更新引用了此頁面的頁表了,否則造成不同步而出錯。如果獲取對應的物理頁面對應的pte的地址呢?內核的做法是先通過逆向映射得到虛擬地址,根據虛擬地址遍歷頁表得到pte地址。
在KVM中,逆向映射機制的作用是類似的,但是完成的卻不是從HPA到對應的EPT頁表項的定位,而是從gfn到對應的頁表項的定位。理論上講根據gfn一步步遍歷EPT也未嘗不可,但是效率較低;況且在EPT所維護的頁面不同於host的頁表,理論上講是虛擬機之間是禁止主動的共享內存的,為了提高效率,就有了當前的逆向映射機制。
我們都知道虛擬機的物理內存由多個slot構成,每個slot都是一個kvm_memory_slot結構,表示虛擬機物理內存的一段空間,為了說明問題,不妨先看下該結構:
struct kvm_memory_slot { gfn_t base_gfn; unsigned long npages; /*一個slot有許多客戶機虛擬頁面組成,通過dirty_bitmap標記每一個頁是否可用,一個頁面對應一個位*/ unsigned long *dirty_bitmap; struct kvm_arch_memory_slot arch; unsigned long userspace_addr;//對應的HVA 地址 u32 flags; short id; };
slot本質是qemu進程用戶空間的hva,緊急你是qemu進程的虛擬地址空間,並沒有對應物理地址,各個字段的意義不言自明了。其中有一個kvm_arch_memory_slot結構,我們重點描述。
struct kvm_arch_memory_slot { unsigned long *rmap[KVM_NR_PAGE_SIZES]; struct kvm_lpage_info *lpage_info[KVM_NR_PAGE_SIZES - 1]; };
該結構的rmap字段是指針數組,每種頁面大小對應一項,截止3.10.1版本,KVM的大頁面僅僅支持2M而並沒有考慮1G的頁面,普通的頁面就是4KB了。所以默認狀態下,提到大頁面就是指的2M的頁面。結合上面的kvm_memory_slot結構可以發現,kvm_arch_memory_slot其實是kvm_memory_slot的一個內嵌結構,所以每個slot都關聯一個kvm_arch_memory_slot,也就有一個rmap數組。其實在虛擬機中,qemu為虛擬機分配的頁面主要是大頁面,但是這里為了方面,按照4KB的普通頁面做介紹。
初始化階段
在qemu為虛擬機注冊各個slot的時候,在KVM中會初始化逆向映射的相關內存區。__kvm_set_memory_region-->kvm_arch_create_memslot
在該函數中,用一個for循環為每種頁面類型的rmap分配空間,具體分配代碼如下
lpages = gfn_to_index(slot->base_gfn + npages - 1, slot->base_gfn, level) + 1; slot->arch.rmap[i] = kvm_kvzalloc(lpages * sizeof(*slot->arch.rmap[i])); if (!slot->arch.rmap[i]) goto out_free;
gfn_to_index把一個gfn轉化成該gfn在整個slot中的索引,而這里獲取的其實就是整個slot包含的不同level的頁面數。然后為slot->arch.rmap[i]分配內存,每個頁面對應一個unsigned Long.
建立階段
建立階段自然是在填充EPT的時候了,在KVM中維護EPT的核心函數是tdp_page_fault函數。該函數的處理在之前的文章中也有介紹,在函數尾部會調用rmap_add函數建立逆向映射
static int rmap_add(struct kvm_vcpu *vcpu, u64 *spte, gfn_t gfn) { struct kvm_mmu_page *sp; unsigned long *rmapp; 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); }
page_header是一個內聯函數,主要目的在於獲取kvm_mmu_page,一個該結構描述一個層級的頁表,地址保存在page結構的private字段,然后調用kvm_mmu_page_set_gfn,對kvm_mmu_page進行設置。這不是重點,接着就獲取了gfn對應的rmap的地址,重點看下
static unsigned long *gfn_to_rmap(struct kvm *kvm, gfn_t gfn, int level) { struct kvm_memory_slot *slot; slot = gfn_to_memslot(kvm, gfn); return __gfn_to_rmap(gfn, level, slot); }
首先轉化成到對應的slot,然后調用了__gfn_to_rmap
static unsigned long *__gfn_to_rmap(gfn_t gfn, int level, struct kvm_memory_slot *slot) { unsigned long idx; /*gfn在slot中的index*/ idx = gfn_to_index(gfn, slot->base_gfn, level); /*rmap是一個指針數組,每個項記錄對應層級的gfn對應的逆向映射,index就是下標*/ return &slot->arch.rmap[level - PT_PAGE_TABLE_LEVEL][idx]; }
額。。。到這里就很明確了,我們再次看到了gfn_to_index函數,這里就根據指定的gfn轉化成索引,同時也是在rmap數組的下標,然后就返回對應的表項的地址,沒啥好說的吧……現在地址已經獲取到了,還等什么呢?設置吧,調用pte_list_add函數,該函數也值得一說
static int pte_list_add(struct kvm_vcpu *vcpu, u64 *spte, unsigned long *pte_list) { struct pte_list_desc *desc; int i, count = 0; /*如果*pte_list為空,直接設置逆向映射即可 */ 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; } /*如果已經滿了,就再次擴展more*/ 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; }
先走下函數流程,我們已經傳遞進來gfn對應的rmap的地址,就是pte_list,接下來主要分為三部分;if……else if ……else
首先,如果*ptelist為空,則直接*pte_list = (unsigned long)spte;直接把rmap地址的內容設置成表項地址,到這里為止,so easy……但是這並不能解決所有問題,說到這里看下函數前面的注釋吧
/* * Pte mapping structures: * * If pte_list bit zero is zero, then pte_list point to the spte. * * If pte_list bit zero is one, (then pte_list & ~1) points to a struct * pte_list_desc containing more mappings. * * Returns the number of pte entries before the spte was added or zero if * the spte was not added. * */
根據注釋判斷,pte_list即我們之前的到的rmap最低一位表明這直接指向一個spte還是pte_list_desc,后者用作擴展remap.那么到了else if這里,如果*pte_list不為空且也並沒有指向一個pte_list_desc,那么就壞了,根據gfn定位到了 這個remap項,但是人家已經在用了,怎么辦?解決方案就是通過pte_list_desc擴展下,但是最后要表明這是一個pte_list_desc,所以要吧最后一位置1,然后設置進*pte_list。還是介紹下該結構
struct pte_list_desc { u64 *sptes[PTE_LIST_EXT]; struct pte_list_desc *more; };
結構比較簡單,自身攜帶一個PTE_LIST_EXT大小的指針數組,PTE_LIST_EXT為3,也就是擴展一下可以增加2個表項,數量不多,所以如果還不夠,就通過下面的more擴展。more又指向一個pte_list_desc。好了,接下看我們的else
如果前兩種情況都不對,這就是remap項不為空,且已經指向一個pte_list_desc,同樣的道理我們需要獲取該結構,找到一個能用的地方啊。如何找?
如果desc->sptes已經滿了,且more不為空,則遞歸的遍歷more,while循環出來,就有兩種情況
1、sptes有剩余
2、more為空
此時進行判斷,如果sptes沒滿,直接找到一個空閑的項,進行填充;否則,申請一個pte_list_desc,通過more進行擴展,然后在尋找一個空閑的。
PS:上面是函數的大致流程,可是為何需要擴展呢?之前有提到,初始化的時候為每個頁面都分配了remap空間,如果qemu進程為虛擬機分配的都是4KB的頁面,那么每個頁面均會對應一個位置,這樣僅僅if哪里就可以了,不需要擴展。但是qemu為虛擬機分配的一般是比較大的頁面,就是2M的,但是虛擬機自己分配的很可能是4KB的,這樣,初始化的時候為2M的頁為單位分配rmap空間,就不能保證所有的小頁面都對應一個唯一的remap地址,這樣就用到了擴展。
以馬內利
參考:kvm 3.10.1源碼
