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(¤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代码