背景分析:
在之前分析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(¤t->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(¤t->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代碼