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

 

posted @ 2017-04-23 20:16 jack.chen Views(...) Comments(...) Edit 收藏

以马内利