KVm中EPT逆向映射機制分析


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源碼

 


免責聲明!

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



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