qemu進程頁表和EPT的同步問題


背景分析:

在之前分析EPT violation的時候,沒有太注意qemu進程頁表和EPT的關系,從虛擬機運行過程分析,虛擬機訪存使用自身頁表和EPT完成地址轉換,沒有用到qemu進程頁表,所以也就想當然的認為虛擬機使用的物理頁面在qemu進程的頁表中沒有體現。但是最近才發現,自己的想法是錯誤的。LInux kernel作為核心管理層,具體物理頁面的管理有其管理,再怎么說,虛擬機在host上表現為一個qemu進程,而內存管理器只能根據qemu進程頁表管理其所擁有的物理頁面,否則,linux kernel怎么知道哪些物理頁面是屬於qemu進程的?這是問題1;還有一個問題就是用一個實例來講,virtio 的實現包含前后端驅動兩個部分,前后端其實通過共享內存的方式實現數據的0拷貝。具體來講,虛擬機把數據填充好以后,通知qemu,qemu得到通過把對應的GPA轉化成HVA,如果兩個頁表不同步,怎么保證訪問的是同一塊內存?

帶着上面的問題,我又重新看了下EPT的維護流程,終於發現了問題,事實上,KVM並不負責物理頁面的分配,而是請求qemu分配后把對應的地址傳遞過來,然后自己的維護EPT。也就是說,在qemu進程建立頁表后,EPT才會建立。下面詳細描述下,整體流程大致如圖所示:

handle_ept_violation是處理EPT未命中時候的處理函數,最終落到tdp_page_fault函數中。有個細節就是該函數在維護EPT之前,已經申請好了pfn即對應的物理頁框號,具體見try_async_pf函數,其實之前也注意過這個函數,就是沒多想!!唉……

static bool try_async_pf(struct kvm_vcpu *vcpu, bool prefault, gfn_t gfn,
             gva_t gva, pfn_t *pfn, bool write, bool *writable)
{
    bool async;

    *pfn = gfn_to_pfn_async(vcpu->kvm, gfn, &async, write, writable);

    if (!async)
        return false; /* *pfn has correct page already */

    if (!prefault && can_do_async_pf(vcpu)) {
        trace_kvm_try_async_get_page(gva, gfn);
        if (kvm_find_async_pf_gfn(vcpu, gfn)) {
            trace_kvm_async_pf_doublefault(gva, gfn);
            kvm_make_request(KVM_REQ_APF_HALT, vcpu);
            return true;
        } else if (kvm_arch_setup_async_pf(vcpu, gva, gfn))
            return true;
    }

    *pfn = gfn_to_pfn_prot(vcpu->kvm, gfn, write, writable);

    return false;
}

 這里其主要作用的有兩個函數和gfn_to_pfn_prot,二者均調用了static pfn_t __gfn_to_pfn(struct kvm *kvm, gfn_t gfn, bool atomic, bool *async,bool write_fault, bool *writable)函數,區別在於第四個參數bool *async,前者不為NULL,而后者為NULL。先跟着gfn_to_pfn_async函數往下走,該函數直接調用了__gfn_to_pfn(kvm, gfn, false, async, write_fault, writable);可以看到這里atomic參數被設置成false。

 

static pfn_t __gfn_to_pfn(struct kvm *kvm, gfn_t gfn, bool atomic, bool *async,
              bool write_fault, bool *writable)
{
    struct kvm_memory_slot *slot;

    if (async)
        *async = false;

    slot = gfn_to_memslot(kvm, gfn);

    return __gfn_to_pfn_memslot(slot, gfn, atomic, async, write_fault,
                    writable);
}

 

 在__gfn_to_pfn函數中,如果async不為NULL,則初始化成false,然后根據gfn獲取對應的slot結構。接下來調用__gfn_to_pfn_memslot(slot, gfn, atomic, async, write_fault,writable);該函數主要做了兩個事情,首先根據gfn和slot得到具體得到host的虛擬地址,然后就是調用了hva_to_pfn函數根據虛擬地址得到對應的pfn。

 

static pfn_t hva_to_pfn(unsigned long addr, bool atomic, bool *async,
            bool write_fault, bool *writable)
{
    struct vm_area_struct *vma;
    pfn_t pfn = 0;
    int npages;

    /* we can do it either atomically or asynchronously, not both */
    /*這里二者不能同時為真*/
    BUG_ON(atomic && async);
    /*主要實現邏輯*/
    if (hva_to_pfn_fast(addr, atomic, async, write_fault, writable, &pfn))//查tlb緩存
        return pfn;

    if (atomic)
        return KVM_PFN_ERR_FAULT;
    /*如果前面沒有成功,則調用hva_to_pfn_slow*/
    npages = hva_to_pfn_slow(addr, async, write_fault, writable, &pfn);//快表未命中,查內存頁表
    if (npages == 1)
        return pfn;

    down_read(&current->mm->mmap_sem);
    if (npages == -EHWPOISON ||
          (!async && check_user_page_hwpoison(addr))) {
        pfn = KVM_PFN_ERR_HWPOISON;
        goto exit;
    }

    vma = find_vma_intersection(current->mm, addr, addr + 1);

    if (vma == NULL)
        pfn = KVM_PFN_ERR_FAULT;
    else if ((vma->vm_flags & VM_PFNMAP)) {
        pfn = ((addr - vma->vm_start) >> PAGE_SHIFT) +
            vma->vm_pgoff;
        /*如果PFN不是MMIO*/
        BUG_ON(!kvm_is_mmio_pfn(pfn));
    } else {
        if (async && vma_is_valid(vma, write_fault))
            *async = true;
        pfn = KVM_PFN_ERR_FAULT;
    }
exit:
    up_read(&current->mm->mmap_sem);
    return pfn;
}

 

 在本函數中涉及到兩個重要函數hva_to_pfn_fast和hva_to_pfn_slow,首選是前者,在前者失敗后,調用后者。hva_to_pfn_fast核心是調用了__get_user_pages_fast函數,而hva_to_pfn_slow函數的主體其實是get_user_pages_fast函數,可以看到這里兩個函數就查了一個前綴,前者默認頁表項已經存在,直接通過遍歷頁表得到對應的頁框;而后者不做這種假設,如果有頁表項沒有建立,還需要建立頁表項,物理頁面沒有分配就需要分配物理頁面。考慮到這里是KVM,在開始EPT violation時候,虛擬地址肯定沒有分配具體的物理地址,所以這里調用后者的可能性比較大。get_user_pages_fast函數的前半部分基本就是__get_user_pages_fast,所以這里我們簡要分析下get_user_pages_fast函數。

int get_user_pages_fast(unsigned long start, int nr_pages, int write,
            struct page **pages)
{
    struct mm_struct *mm = current->mm;
    unsigned long addr, len, end;
    unsigned long next;
    pgd_t *pgdp, pgd;
    int nr = 0;

    start &= PAGE_MASK;
    addr = start;
    len = (unsigned long) nr_pages << PAGE_SHIFT;
    end = start + len;
    if ((end < start) || (end > TASK_SIZE))
        goto slow_irqon;

    /*
     * local_irq_disable() doesn't prevent pagetable teardown, but does
     * prevent the pagetables from being freed on s390.
     *
     * So long as we atomically load page table pointers versus teardown,
     * we can follow the address down to the the page and take a ref on it.
     */
    local_irq_disable();
    pgdp = pgd_offset(mm, addr);
    do {
        pgd = *pgdp;
        barrier();
        next = pgd_addr_end(addr, end);
        if (pgd_none(pgd))
            goto slow;
        if (!gup_pud_range(pgdp, pgd, addr, next, write, pages, &nr))
            goto slow;
    } while (pgdp++, addr = next, addr != end);
    local_irq_enable();

    VM_BUG_ON(nr != (end - start) >> PAGE_SHIFT);
    return nr;

    {
        int ret;
slow:
        local_irq_enable();
slow_irqon:
        /* Try to get the remaining pages with get_user_pages */
        start += nr << PAGE_SHIFT;
        pages += nr;

        down_read(&mm->mmap_sem);
        ret = get_user_pages(current, mm, start,
            (end - start) >> PAGE_SHIFT, write, 0, pages, NULL);
        up_read(&mm->mmap_sem);

        /* Have to be a bit careful with return values */
        if (nr > 0) {
            if (ret < 0)
                ret = nr;
            else
                ret += nr;
        }

        return ret;
    }
}

 

函數開始獲取虛擬頁框號和結束地址,在咱們分析的情況下,一般這里就是一個頁面的大小。然后調用local_irq_disable禁止本地中斷,開始遍歷當前進程的頁表。pgdp是在頁目錄表中的偏移+一級頁表基址。進入while循環,獲取二級表的基址,next在這里基本就是end了,因為前面申請的僅僅是一個頁面的長度。可以看到這里如果表項內容為空,則goto到了slow,即要為其建立表項。這里暫且略過。先假設其存在,繼續調用gup_pud_range函數。在x86架構下,使用的二級頁表而在64位下使用四級頁表。64位暫且不考慮,所以中間兩層處理其實就是走個過場。這里直接把pgdp指針轉成了pudp即pud_t類型的指針,接下來還是進行同樣的工作,只不過接下來調用的是gup_pmd_range函數,該函數取出表項的內容,往下一級延伸,重點看其調用的gup_pte_range函數。

static inline int gup_pte_range(pmd_t *pmdp, pmd_t pmd, unsigned long addr,
        unsigned long end, int write, struct page **pages, int *nr)
{
    unsigned long mask;
    pte_t *ptep, pte;
    struct page *page;

    mask = (write ? _PAGE_RO : 0) | _PAGE_INVALID | _PAGE_SPECIAL;

    ptep = ((pte_t *) pmd_deref(pmd)) + pte_index(addr);
    do {
        pte = *ptep;
        barrier();
        if ((pte_val(pte) & mask) != 0)
            return 0;
        VM_BUG_ON(!pfn_valid(pte_pfn(pte)));
        page = pte_page(pte);
        if (!page_cache_get_speculative(page))
            return 0;
        if (unlikely(pte_val(pte) != pte_val(*ptep))) {
            put_page(page);
            return 0;
        }
        pages[*nr] = page;
        (*nr)++;

    } while (ptep++, addr += PAGE_SIZE, addr != end);

    return 1;
}

 

這里就根據pmd和虛擬地址的二級偏移,定位到二級頁表項的地址ptep,在循環中,就取出ptep的內容,不出意外就是物理頁面的地址及pfn,后面調用了page = pte_page(pte);實質是把pfn轉成了page結構,然后設置參數中的page數組。沒有錯誤就返回1.上面就是整個頁表遍歷的過程。如果失敗了,就為其維護頁表並分配物理頁面,注意這里如果當初申請的是多個頁面,就一並處理了,而不是一個頁面一個頁面的處理。實現的主體是get_user_pages函數,該函數是__get_user_pages函數的封裝,__get_user_pages比較長,我們這里i就不在介紹,感興趣的朋友可以參考具體的代碼或者其他資料。

 

參考資料:

linux內核3.10.1代碼

 


免責聲明!

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



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