背景分析:

在之前分析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 = ;
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 == )
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 + ); 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 = ; 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, , pages, NULL);
up_read(&mm->mmap_sem); /* Have to be a bit careful with return values */
if (nr > ) {
if (ret < )
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 : ) | _PAGE_INVALID | _PAGE_SPECIAL; ptep = ((pte_t *) pmd_deref(pmd)) + pte_index(addr);
do {
pte = *ptep;
barrier();
if ((pte_val(pte) & mask) != )
return ;
VM_BUG_ON(!pfn_valid(pte_pfn(pte)));
page = pte_page(pte);
if (!page_cache_get_speculative(page))
return ;
if (unlikely(pte_val(pte) != pte_val(*ptep))) {
put_page(page);
return ;
}
pages[*nr] = page;
(*nr)++; } while (ptep++, addr += PAGE_SIZE, addr != end); return ;
}

这里就根据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代码

05-11 18:08