zoukankan      html  css  js  c++  java
  • 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代码

  • 相关阅读:
    八数码问题及其扩展
    java注释
    康托展开和逆康托展开
    模线性方程组
    欧拉函数
    扩展欧几里德
    商城项目实战 | 2.2 Android 仿京东商城——自定义 Toolbar (二)
    商城项目实战 | 2.1 Android 仿京东商城——自定义 Toolbar (一)
    商城项目实战 | 1.1 Android 仿京东商城底部布局的选择效果 —— Selector 选择器的实现
    Android使用Path实现仿最新淘宝轮播广告底部弧形有锯齿的问题以及解决办法
  • 原文地址:https://www.cnblogs.com/ck1020/p/6753206.html
Copyright © 2011-2022 走看看