hq8501 发表于 2015-10-10 12:25:07

通过KVM_SET_USER_MEMORY_REGION操作虚拟机内存(Kernel 3.10.0 & qemu 2.0.0)

先界定几个术语GPA:Guest 物理地址HVA:HOST 虚拟地址MR:Memory Region,QEMU虚拟机中地址空间的管理结构
在QEMU用户态进程中,会调用KVM_SET_USER_MEMORY_REGION来修改虚拟机的内存,kvm_set_user_memory_region就是内核操作KVM_SET_USER_MEMORY_REGION的接口例如:static int kvm_set_user_memory_region(KVMState *s, KVMSlot *slot){    struct kvm_userspace_memory_region mem;    .......   /*通过KVM_SET_USER_MEMORY_REGION进入Kernel*/    mem.guest_phys_addr = slot->start_addr;    mem.userspace_addr = (unsigned long)slot->ram;    mem.memory_size = slot->memory_size;
    return kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);}

我们看一下,内核是处理如何操作的kvm_vm_compat_ioctl-> kvm_vm_ioctl (case KVM_SET_USER_MEMORY_REGION)-> kvm_vm_ioctl_set_memory_region-> kvm_set_memory_region-> __kvm_set_memory_region


int __kvm_set_memory_region(struct kvm *kvm, struct kvm_userspace_memory_region *mem)
{
       struct kvm_memory_slot old, new/*待添加的slot的内容*/;
      
      ......      
/*设置memslot的base_gfn、npages等域*/
base_gfn = mem->guest_phys_addr >> PAGE_SHIFT;
      /*memory_size为0,就说明npages = 0, npages 在后面会被标记为KVM_MR_DELETE,说明是内存删除动作*/
      npages = mem->memory_size >> PAGE_SHIFT;
      

      /*判断操作的类型*/
      r = -EINVAL;
   if (npages) { /*非删除*/
            if (!old.npages) /*老的页面数为0,说明是新增*/
                     change = KVM_MR_CREATE;
          else { /* Modify an existing slot. */
                  if (base_gfn != old.base_gfn) /*新旧插槽只有gfn不同,那么就是MOVE*/
                           change = KVM_MR_MOVE;
                  else if (new.flags != old.flags)
                              change = KVM_MR_FLAGS_ONLY;
                      else { /* Nothing to change. */
                        r = 0;
                           goto out;
                        }
                }
      } else if (old.npages) { /*待设定的页面数位0,说明是删除*/
             /*memory_size = 0*/
            change = KVM_MR_DELETE;
      } else /* Modify a non-existent slot: disallowed. */
             goto out;

/*
      * 处理和已经存在的memslots的重叠,发现重叠返回-EEXIST
      */
      if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) {
            /* Check for overlaps */
         r = -EEXIST;
             kvm_for_each_memslot(slot, kvm->memslots) {
                   if ((slot->id >= KVM_USER_MEM_SLOTS) ||
                        (slot->id == mem->slot)/*当前要加入的slot,不管,直接跳过*/)
                         continue;
                        /* new_end > slot_base && new_base < slot_end,说明已经有覆盖该段内存了*/
                     if (!((base_gfn + npages <= slot->base_gfn) ||
                           (base_gfn >= slot->base_gfn + slot->npages)))
                           goto out;
                }
      }

if (change == KVM_MR_CREATE) {
         /*初始化HVA的内容*/
            new.userspace_addr = mem->userspace_addr;
             /*设定memslot中arch相关的内容*/
          if (kvm_arch_create_memslot(&new, npages))
                   goto out_free;
   }

         if ((change == KVM_MR_DELETE) || (change == KVM_MR_MOVE)) {
            r = -ENOMEM;
             slots = kmemdup(kvm->memslots, sizeof(struct kvm_memslots),GFP_KERNEL);
               if (!slots)
                      goto out_free;
         slot = id_to_memslot(slots, mem->slot);
               slot->flags |= KVM_MEMSLOT_INVALID;

            old_memslots = install_new_memslots(kvm, slots, NULL);

            /* slot was deleted or moved, clear iommu mapping */
             kvm_iommu_unmap_pages(kvm, &old);
          /* From this point no new shadow pages pointing to a deleted,
             * or moved, memslot will be created.
             *
                * validation of sp->gfn happens in:
         *      - gfn_to_hva (kvm_read_guest, gfn_to_pfn)
               *      - kvm_is_visible_gfn (mmu_check_roots)
            */
            kvm_arch_flush_shadow_memslot(kvm, slot);
                slots = old_memslots;
    }

      ......
}

从上面的代码看来:如果memory_size大于0,那么这次内存操作的动作为“向虚拟机中添加内存或者更新内存布局”如果memory_size等于0,那么这次内存操作的动作为“将guest_phys_addr 为起始地址的那块内存块从虚拟机中删除”
OK,知道了上面的内容,让我们来理解一个QEMU里面的关键函数,这个函数会在后面讲解QEMU内存初始化流程的博文中再次提到。它就是kvm_set_phys_mem,通过该函数,QEMU可以修改虚拟机注册在内核中的内存,达到为虚拟机添加/删除/移动内存的目的
在讲解前,我们先了解一下什么是overlap。overlap就是重叠的意思,见图1                           图1: overlap内存所谓slot可以理解为物理机上的一个内存插槽上插入的内存,已有slot,就是虚拟机中已经有的内存当新加入的section管理的地址范围,完全落入已有slot的范围内(见图1),这样就叫做overlap,注意是完全落入哦,重叠一部分不算,参加函数kvm_lookup_overlapping_slot/*

* 在KVMSlot插槽数组中查找同制定范围重叠的KVMSlot

* 注意: 这里完全覆盖才算找到

*/

static KVMSlot *kvm_lookup_overlapping_slot(KVMState *s,hwaddr start_addr, hwaddr end_addr)

{


    for (i = 0; i < s->nr_slots; i++) {

      KVMSlot *mem = &s->slots;

       .....

          /*找到有地址空间重叠的KVMSlot区域*/

      if (end_addr > mem->start_addr &&

            start_addr < mem->start_addr + mem->memory_size) {

            found = mem;

      }

    }

    return found;

}

那么QEMU会怎么处理这种overlap的情况呢,是这样的:1.将已有slot分为三个部分,prefix slot + overlap + suffix slot2.将已有的slot内存,通过KVM_SET_USER_MEMORY_REGION,从内核KVM模块中删除3.通过KVM_SET_USER_MEMORY_REGION,向内核kvm模块注册添加prefix slot内存4.通过KVM_SET_USER_MEMORY_REGION,向内核kvm模块注册添加suffix slot内存5.通过KVM_SET_USER_MEMORY_REGION,向内核kvm模块注册添加overlap内存,起始overlap内存就是本次这次的section的内存OK,这样就将内存加入进去了。
一定有人会问,为什么不直接利用原来的内存,为什么要新分配呢?我的理解是这样的,kvm_set_phys_mem是一个操作内存的统一接口,添加内存和删除内存都通过改接口,如果是删除overlap部分,kvm_set_phys_mem的分段工作就不言而喻了。而且如果添加的section的属性变了,如从RAM变成了ROM,那么重新进行添加也是必要的。
当然,prefix slot 和 suffix slot 的长度可以为0,不用多解释了,就是正好覆盖到起始或结束位置
好了,有了上面的理解,我们就来可以分析一下kvm_set_phys_mem函数了主要流程如图2所示,就不多废话了
                           图2
下面是代码分析

/*设定虚拟机物理内存*/

static void kvm_set_phys_mem(MemoryRegionSection *section, bool add)

{

    KVMState *s = kvm_state;

    KVMSlot *mem, old;

    int err;

    MemoryRegion *mr = section->mr;

    bool log_dirty = memory_region_is_logging(mr);

    bool writeable = !mr->readonly && !mr->rom_device;

    bool readonly_flag = mr->readonly || memory_region_is_romd(mr);

    hwaddr start_addr = section->offset_within_address_space; /*start_addr为该段section内存GPA的起始地址*/

    ram_addr_t size = int128_get64(section->size); /*start_addr为该段section内存的大小*/

    void *ram = NULL;

    unsigned delta;


    /* kvm works in page size chunks, but the function may be called

       with sub-page size and unaligned start address. */

    /*页面对齐,对页面起始地址和长度进行微调*/

    delta = TARGET_PAGE_ALIGN(size) - size;

    if (delta > size) {

      return;

    }

    start_addr += delta;

    size -= delta;

    size &= TARGET_PAGE_MASK;

    if (!size || (start_addr & ~TARGET_PAGE_MASK)) {

      return;

    }


   /*如果注册的内存是ROM,进行权限校验*/

    if (!memory_region_is_ram(mr)) {

      if (writeable || !kvm_readonly_mem_allowed) {

            return;

      } else if (!mr->romd_mode) {

            /* If the memory device is not in romd_mode, then we actually want

             * to remove the kvm memory slot so all accesses will trap. */

            add = false;

      }

    }


   /*

   * memory_region_get_ram_ptr 是MR管理的内存块的HVA

   * HVA + section->offset_within_region + delta; 就是section所在HVA

   * ram 就是section所在HVA

   */

    ram = memory_region_get_ram_ptr(mr) + section->offset_within_region + delta;


    while (1) {

          /*查找和制定区间重叠的KVMSLOT*/

      mem = kvm_lookup_overlapping_slot(s, start_addr, start_addr + size);

          /* 如果没有重叠,就跳出循环, 直接添加内存*/

      if (!mem) {

            break;

      }


          /*从这里开始,说明找到了mem和制定内存返回重叠了*/


      if (add && start_addr >= mem->start_addr &&

            (start_addr + size <= mem->start_addr + mem->memory_size) &&

            (ram - start_addr == mem->ram - mem->start_addr)) {

            /* The new slot fits into the existing one and comes with

             * identical parameters - update flags and done. */

            kvm_slot_dirty_pages_log_change(mem, log_dirty);

            return;

      }


          /*临时保存原有slot信息*/

      old = *mem;


      if (mem->flags & KVM_MEM_LOG_DIRTY_PAGES) {

            kvm_physical_sync_dirty_bitmap(section);

      }


          /*通过将memory_size设置为0,通过KVM_SET_USER_MEMORY_REGION从内核KVM模块中删除已经注册的内存*/

      /* unregister the overlapping slot */

      mem->memory_size = 0;

          /*设定虚拟机物理内存*/

      err = kvm_set_user_memory_region(s, mem);

      if (err) {

            fprintf(stderr, "%s: error unregistering overlapping slot: %s\n",

                  __func__, strerror(-err));

            abort();

      }


      /* Workaround for older KVM versions: we can't join slots, even not by

         * unregistering the previous ones and then registering the larger

         * slot. We have to maintain the existing fragmentation. Sigh.

         *

         * This workaround assumes that the new slot starts at the same

         * address as the first existing one. If not or if some overlapping

         * slot comes around later, we will fail (not seen in practice so far)

         * - and actually require a recent KVM version. */

          /*

          * 一般不会走这里,因为broken_set_mem_region为0,暂时不看

          */

      if (s->broken_set_mem_region &&

            old.start_addr == start_addr && old.memory_size < size && add) {

            mem = kvm_alloc_slot(s);

            mem->memory_size = old.memory_size;

            mem->start_addr = old.start_addr;

            mem->ram = old.ram;

            mem->flags = kvm_mem_flags(s, log_dirty, readonly_flag);


            err = kvm_set_user_memory_region(s, mem);

            if (err) {

                fprintf(stderr, "%s: error updating slot: %s\n", __func__,

                        strerror(-err));

                abort();

            }


            start_addr += old.memory_size;

            ram += old.memory_size;

            size -= old.memory_size;

            continue;

      }


          /*

          * 通过KVM_SET_USER_MEMORY_REGION,向KVM内核模块注册prefix slot部分内存

          * 通过临时保存的old,计算prefix slot的起始地址和大小         

          */

      /* register prefix slot */

      if (old.start_addr < start_addr) {

            mem = kvm_alloc_slot(s);

            mem->memory_size = start_addr - old.start_addr;

            mem->start_addr = old.start_addr;

            mem->ram = old.ram;

            mem->flags =kvm_mem_flags(s, log_dirty, readonly_flag);


            err = kvm_set_user_memory_region(s, mem);

            if (err) {

                fprintf(stderr, "%s: error registering prefix slot: %s\n",

                        __func__, strerror(-err));

#ifdef TARGET_PPC

                fprintf(stderr, "%s: This is probably because your kernel's " \

                              "PAGE_SIZE is too big. Please try to use 4k " \

                              "PAGE_SIZE!\n", __func__);

#endif

                abort();

            }

      }


          /*

          * 通过KVM_SET_USER_MEMORY_REGION,向KVM内核模块注册suffix slot部分内存

          * 通过临时保存的old,计算suffix slot的起始地址和大小         

          */

      /* register suffix slot */

      if (old.start_addr + old.memory_size > start_addr + size) {

            ram_addr_t size_delta;


            mem = kvm_alloc_slot(s);

            mem->start_addr = start_addr + size;

            size_delta = mem->start_addr - old.start_addr;

            mem->memory_size = old.memory_size - size_delta;

            mem->ram = old.ram + size_delta;

            mem->flags = kvm_mem_flags(s, log_dirty, readonly_flag);


            err = kvm_set_user_memory_region(s, mem);

            if (err) {

                fprintf(stderr, "%s: error registering suffix slot: %s\n",

                        __func__, strerror(-err));

                abort();

            }

      }

    }


    /* in case the KVM bug workaround already "consumed" the new slot */

    if (!size) {

      return;

    }

    if (!add) {

      return;

    }


   /*新分配一个slot,将section的信息填入其中,并通过KVM_SET_USER_MEMORY_REGION向KVM内核模块中加入/删除内存*/

    mem = kvm_alloc_slot(s);

    mem->memory_size = size;

    mem->start_addr = start_addr;

    mem->ram = ram;

    mem->flags = kvm_mem_flags(s, log_dirty, readonly_flag);


    err = kvm_set_user_memory_region(s, mem);

    if (err) {

      fprintf(stderr, "%s: error registering slot: %s\n", __func__,

                strerror(-err));

      abort();

    }

}

这里补充说明一个关键数据结构MemoryRegionSection,这个结构指定了一段内存,这段内存将被注册到内核KVM模块,进行加入或删除操作。不多说了,上代码struct MemoryRegionSection {

    MemoryRegion *mr;

    AddressSpace *address_space;

   /*

   * 相当于在region内偏移量,region上面挂载了一块从HOST上分配的内存,通过这个offset,就可以计算这个section在HOST内存上的HVA了

   */

    hwaddr offset_within_region;   

   /*该段内存的大小*/

    Int128 size;

   /*

   * AS内偏移量,该值是GPA,相当于从GUEST物理地址0处开始的偏移量,也就是说,这个值是该段内存GPA的起始地址

   * 这很好理解,如果AS代表的是系统内存,那么AS内的偏移量当然是物理地址

   */

    hwaddr offset_within_address_space;

   /*指明是ROM还是RAM*/

    bool readonly;

};   
版权声明:本文为博主原创文章,未经博主允许不得转载。
页: [1]
查看完整版本: 通过KVM_SET_USER_MEMORY_REGION操作虚拟机内存(Kernel 3.10.0 & qemu 2.0.0)