• qemu对虚拟机的内存管理(一)


    在分析了KVM中对虚拟机各级地址(gva->gpa->hva->hpa)的转换之后,想要知道qemu中又是如何完成各级地址转换的,因此对qemu中对虚拟机内存管理的相关数据结构与源码进行了分析。qemu中对于虚拟机内存管理涉及的数据结构较多,仅gpa->hpa的转换过程涉及的数据结构就有:MemoryRegion, AddressSpace, MemoryRegionSection, Flatview, FlatRange, RAMBlock, RAMList等。

    这几个数据结构的关系刚接触时有些混乱,以下试图从gpa到hva的转换来整理这几个数据结构之间的关系。

    qemu源码版本为qemu-2.8.0

     一、MemoryRegion

    QEMU通过MemoryRegion来管理虚拟机内存,通过内存属性,GUEST物理地址等特点对内存分类,就形成了多个MemoryRegion,这些MemoryRegion 通过树状组织起来,挂接到根MemoryRegion下。每个MemoryRegion树代表了一类作用的内存,如系统内存空间(system_memory)或IO内存空间(system_io),这两个是qemu中的两个全局MemoryRegion。

    struct MemoryRegion {
        Object parent_obj;
    
        /* All fields are private - violators will be prosecuted */
    
        /* The following fields should fit in a cache line */
        bool romd_mode;
        bool ram;
        bool subpage;
        bool readonly; /* For RAM regions */
        bool rom_device;
        bool flush_coalesced_mmio;
        bool global_locking;
        uint8_t dirty_log_mask;
        RAMBlock *ram_block; //指向对应的RAMBlock
        Object *owner;
        const MemoryRegionIOMMUOps *iommu_ops;
    
        const MemoryRegionOps *ops;
        void *opaque;
        MemoryRegion *container; //指向父MR
        Int128 size; //区域大小
        hwaddr addr; //在父MR中的偏移量
        void (*destructor)(MemoryRegion *mr);
        uint64_t align;
        bool terminates;
        bool ram_device;
        bool enabled;
        bool warning_printed; /* For reservations */
        uint8_t vga_logging_count;
        MemoryRegion *alias; //指向实体MR
        hwaddr alias_offset;// 起始地址 (GPA) 在实体 MemoryRegion 中的偏移量
        int32_t priority;
        QTAILQ_HEAD(subregions, MemoryRegion) subregions; //子区域链表头
        QTAILQ_ENTRY(MemoryRegion) subregions_link; //子区域链表结点
        QTAILQ_HEAD(coalesced_ranges, CoalescedMemoryRange) coalesced;
        const char *name;
        unsigned ioeventfd_nb;
        MemoryRegionIoeventfd *ioeventfds;
        QLIST_HEAD(, IOMMUNotifier) iommu_notify;
        IOMMUNotifierFlag iommu_notify_flags;
    };

    MemoryRegion 表示在 Guest memory layout 中的一段内存,可将 MemoryRegion 划分为以下三种类型:

    • 根级 MemoryRegion: 直接通过 memory_region_init 初始化,没有自己的内存,用于管理 subregion。如 system_memory
    • 实体 MemoryRegion: 通过 memory_region_init_ram 初始化,有自己的内存 (从 QEMU 进程地址空间中分配),大小为 size 。如 ram_memory(pc.ram) 、 pci_memory(pci) 等。 这种MemoryRegion中真正的分配物理内存,最主要的就是pc.ram和pci。分配的物理内存的作用分别是内存、PCI地址空间以及fireware空间。QEMU是用户空间代码,分配的物理内存返回的是hva,hva保存至RAMBlock的host域。通过实体MemoryRegion对应的RAMBlock可以管理HVA。
    • 别名 MemoryRegion: 通过 memory_region_init_alias 初始化,没有自己的内存,表示实体 MemoryRegion(如 pc.ram) 的一部分,通过 alias 成员指向实体 MemoryRegion,alias_offset 代表了该别名MemoryRegion所代表内存起始GPA相对于实体 MemoryRegion 所代表内存起始GPA的偏移量。如 ram_below_4g 、ram_above_4g 等。

    代码中常见的 MemoryRegion 关系为:

                       alias
    ram_memory (pc.ram) - ram_below_4g(ram-below-4g)
                        - ram_above_4g(ram-above-4g)
    
                         sub
    system_memory(system) - ram_below_4g(ram-below-4g)
                          - ram_above_4g(ram-above-4g)
                          - pcms->hotplug_memory.mr        热插拔内存

    实际上虚拟机的ram申请时是一次性申请的一个完成的ram,记录在一个MR中,之后又对此ram按照size进行了划分,形成subregion,而subregion 的alias便指向原始的MR,而alias_offset 便是在原始ram中的偏移。对于系统地址空间的ram,会把刚才得到的subregion注册到系统中,父MR是刚才提到的全局MR system_memory,subregions_link是链表节点。addr是子MR相对于父MR的偏移,在函数pc_memory_init()函数中有对实体MemoryRegion和别名MemoryRegion的初始化:

    void pc_memory_init(PCMachineState *pcms,
                        MemoryRegion *system_memory,
                        MemoryRegion *rom_memory,
                        MemoryRegion **ram_memory)
    {
        int linux_boot, i;
        MemoryRegion *ram, *option_rom_mr;
        MemoryRegion *ram_below_4g, *ram_above_4g;
        FWCfgState *fw_cfg;
        MachineState *machine = MACHINE(pcms);
        PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
    
        assert(machine->ram_size == pcms->below_4g_mem_size +
                                    pcms->above_4g_mem_size);
    
        linux_boot = (machine->kernel_filename != NULL);
    
        /* Allocate RAM.  We allocate it as a single memory region and use
         * aliases to address portions of it, mostly for backwards compatibility
         * with older qemus that used qemu_ram_alloc().
         */
        ram = g_malloc(sizeof(*ram));
        memory_region_allocate_system_memory(ram, NULL, "pc.ram",
                                             machine->ram_size); //初始化实体MR pc.ram
        *ram_memory = ram;
        ram_below_4g = g_malloc(sizeof(*ram_below_4g));
        memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,
                                 0, pcms->below_4g_mem_size); //初始化别名MR ram_below_4g,将其alias指向ram,alias_offset为0
        memory_region_add_subregion(system_memory, 0, ram_below_4g); //将别名MRram_below_4g添加为system_memory的subregion,设置偏移addr为0
        e820_add_entry(0, pcms->below_4g_mem_size, E820_RAM);
        if (pcms->above_4g_mem_size > 0) {
            ram_above_4g = g_malloc(sizeof(*ram_above_4g));
            memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram,
                                     pcms->below_4g_mem_size,
                                     pcms->above_4g_mem_size); //初始化别名MR ram_above_4g,将其alias指向ram,alias_offset为below_4g_mem_size
            memory_region_add_subregion(system_memory, 0x100000000ULL,
                                        ram_above_4g);//同上述ram_below_4g,初始化并添加ram_above_4g,设置偏移addr为0x100000000ull,即4g
            e820_add_entry(0x100000000ULL, pcms->above_4g_mem_size, E820_RAM);
        }
    void memory_region_init_alias(MemoryRegion *mr,
                                  Object *owner,
                                  const char *name,
                                  MemoryRegion *orig,
                                  hwaddr offset,
                                  uint64_t size)
    {
        memory_region_init(mr, owner, name, size);
        mr->alias = orig; //别名MR的alias指向原实体MR
        mr->alias_offset = offset; //alias_offset表示偏移
    }
    static void memory_region_add_subregion_common(MemoryRegion *mr,
                                                   hwaddr offset,
                                                   MemoryRegion *subregion)
    {
        assert(!subregion->container);
        subregion->container = mr;
        subregion->addr = offset; //将addr设置为offset
        memory_region_update_container_subregions(subregion);
    }
    
    void memory_region_add_subregion(MemoryRegion *mr,
                                     hwaddr offset,
                                     MemoryRegion *subregion)
    {
        subregion->priority = 0;
        memory_region_add_subregion_common(mr, offset, subregion);
    }

    可见subregion的addr即为相对于父MR的偏移,对于ram_below_4g,addr为0,对于ram_above_4g,偏移则为4g,而alias_offset为相对于实体MR的偏移量,对于ram_below_4g,alias_offset为0,对于ram_above_4g,alias_offset为ram_below_4g_size,即为4g。

     二、RAMBlock

    上面提到了qemu为虚拟机分配的内存的hva保存在RAMblock的host域,RAMBlock的定义如下:

    struct RAMBlock {
        struct rcu_head rcu;                                        // 用于保护 Read-Copy-Update
        struct MemoryRegion *mr;                                    // 对应的 MemoryRegion
        uint8_t *host;                                              // 对应的 HVA
        ram_addr_t offset;                                          // 在 ram_list 地址空间中的偏移 (要把前面 block 的 size 都加起来)
        ram_addr_t used_length;                                     // 当前使用的长度
        ram_addr_t max_length;                                      // 总长度
        void (*resized)(const char*, uint64_t length, void *host);  // resize 函数
        uint32_t flags;
        /* Protected by iothread lock.  */
        char idstr[256];                                            // id
        /* RCU-enabled, writes protected by the ramlist lock */
        QLIST_ENTRY(RAMBlock) next;                                 // 指向在 ram_list.blocks 中的下一个 block
        int fd;                                                     // 映射文件的文件描述符
        size_t page_size;                                           // page 大小,一般和 host 保持一致
    };

    一个RAMBlock表示一段虚拟内存,host域指向申请的ram的虚拟地址,即hva。所有的RAMBlock通过next字段连接起来,表头保存在全局RAMList中,offset表示当前RAMBlock在RAMList中的偏移。每个RAMBlock都有一个唯一的MemoryRegion对应,但需要注意的是不是每个MemoryRegion都有RAMBlock对应。

    在函数pc_memory_init()中为实体memoryregion分配内存时,调用了函数memory_region_allocate_system_memory(),非numa架构下调用函数allocate_system_memory_nonnuma(),继而调用memory_region_init_ram_from_file():

    #ifdef __linux__
    void memory_region_init_ram_from_file(MemoryRegion *mr,
                                          struct Object *owner,
                                          const char *name,
                                          uint64_t size,
                                          bool share,
                                          const char *path,
                                          Error **errp)
    {
        memory_region_init(mr, owner, name, size);
        mr->ram = true;
        mr->terminates = true;
        mr->destructor = memory_region_destructor_ram;
        mr->ram_block = qemu_ram_alloc_from_file(size, mr, share, path, errp); //实体MR指向的RAM_BLOCK为qemu_ram_alloc_from_file函数返回的RAMBlock
        mr->dirty_log_mask = tcg_enabled() ? (1 << DIRTY_MEMORY_CODE) : 0;
    }
    #endif

    函数 qemu_ram_alloc_from_file()中申请并设置RAMBlock,RAMBlock->host 为函数file_ram_alloc()函数的返回值,该函数使用对应路径的(设备)文件来分配内存,调用qemu_ram_mmap()通过mmap方式进行内存分配,可见RAMBlock->host 则为分配的内存的hva的起始地址。

    static void *file_ram_alloc(RAMBlock *block,
                                ram_addr_t memory,
                                const char *path,
                                Error **errp)
    {
        ......
        area = qemu_ram_mmap(fd, memory, block->mr->align,
                             block->flags & RAM_SHARED);//通过mmap在qemu的进程地址空间中进行地址分配
        if (area == MAP_FAILED) {
            error_setg_errno(errp, errno,
                             "unable to map backing store for guest RAM");
            goto error;
        }

     上述为对实体MemoryRegion “pc.ram” 内存的分配,在为别名MemoryRegion“ram-below-4g”和“ram-above-4g”初始化时调用的是函数memory_region_init_alias(), 该函数调用memory_region_init()

    void memory_region_init(MemoryRegion *mr,
                            Object *owner,
                            const char *name,
                            uint64_t size)
    {
        object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);
        mr->size = int128_make64(size);
        if (size == UINT64_MAX) {
            mr->size = int128_2_64();
        }
        mr->name = g_strdup(name);
        mr->owner = owner;
        mr->ram_block = NULL; //别名MR的ram_block设置为null
        .......
    }

    在该函数中将别名MR的ram_block设置为NULL,而“pc.ram”指向的ram_block是有内容的,可见不是所有的MemoryRegion都有对应的RAMBlock,对于分配的RAMBlock,最后会将其插入到全局链表RAMList中。

    上述对结构体MemoryRegion和RAMblock的分析可知,对于系统内存而言(不考虑io)实体MemoryRegion是有具体内存的,而别名MemoryRegion是对实体MR不同分段的一个指向,其alias指向实体MR。别名MR都是根级MR system_memory的subregion,通过RAMBlock,可以知道一个MemoryRegion对应内存的hva,其关系大致如下:

     三、AddressSpace

    从GPA与hva的角度来看,如果以结构体MemoryRegion为核心的话,RAMBlock可以看成是对该片内存区域hva的关联,而AddressSpace在我看来可以看做是对该片内存区域GPA的一个关联,从其注释AddressSpace: describes a mapping of addresses to #MemoryRegion objects也可看出。

    这里我有一个疑问:在qemu-2.3.0版本的源码中,结构体MemoryRegion中有一个变量ram_addr表示该片内存区域的GPA的起始地址,而在qemu-2.8.0中,结构体MemoryRegion中没有了这个变量。猜想对于实体MR而言,其addr变量是否就表示为该片内存区域GPA的起始地址,如果是的话,那么对于subregion而言,其alias_offset加上实体addr即可表示该片MemoryRegion的GPA起始地址,加上实体MR对应的RAMBlock,应该就可以实现GPA到HVA的映射了,那么AddressSpace的作用又是什么,其意义何在?先提出这个疑问,看看后续能否得到解答。

    /**
     * AddressSpace: describes a mapping of addresses to #MemoryRegion objects
     */
    struct AddressSpace {
        /* All fields are private. */
        struct rcu_head rcu;
        char *name;
        MemoryRegion *root; //指向根MR
        int ref_count;
        bool malloced;
    
        /* Accessed via RCU.  */
        struct FlatView *current_map;                               // 指向当前维护的 FlatView,在 address_space_update_topology 时作为 old 比较
    
        int ioeventfd_nb;
        struct MemoryRegionIoeventfd *ioeventfds;
        struct AddressSpaceDispatch *dispatch;                      // 负责根据 GPA 找到 HVA
        struct AddressSpaceDispatch *next_dispatch;
        MemoryListener dispatch_listener;
        QTAILQ_HEAD(memory_listeners_as, MemoryListener) listeners;
        QTAILQ_ENTRY(AddressSpace) address_spaces_link;
    };

    结构体AddressSpace用来表示虚拟机的一片地址空间,不同的设备使用的地址空间不同,但qemu x86中只有两种, address_space_memory和address_space_io,这也是两个全局的address_space变量,所有设备的地址空间都被映射到了这两个上面。其root指向根MemoryRegion, 对于全局变量address_space_memory而言,其root指向系统全局的system_memory,address_space_io的root则指向system_io.由于根MR可能有自己的若干个subregion,因此每个AddressSpace一般包含一系列MemoryRegion,形成树状结构。

    AddressSpace中的current_map指向当前维护的FlatView:

    /*
     * Note that signed integers are needed for negative offsetting in aliases
     * (large MemoryRegion::alias_offset).
     */
    struct AddrRange {
        Int128 start; //起始
        Int128 size; //大小
    };
    
    /* Range of memory in the global map.  Addresses are absolute. */
    struct FlatRange {
        MemoryRegion *mr; //指向所属的MR
        hwaddr offset_in_region; //在MR中的offset
        AddrRange addr; //本FR代表的区间
        uint8_t dirty_log_mask;
        bool romd_mode;
        bool readonly;
    };
    
    /* Flattened global view of current active memory hierarchy.  Kept in sorted
     * order.
     */
    struct FlatView {
        struct rcu_head rcu;
        unsigned ref; //引用计数,为0就销毁
        FlatRange *ranges; //对应的flatrange数组
        unsigned nr; //flatrange数目
        unsigned nr_allocated;
    };

    FlatView管理MR展开后得到的所有FlatRange,ranges是一个数组,记录FlatView下所有的FlatRange,每个FlatRange对应一段虚拟机物理地址区间,各个FlatRange不会重叠,按照地址的顺序保存在数组中。具体的范围由一个AddrRange结构描述,其描述了地址和大小。当memory region发生变化的时候,执行memory_region_transaction_commit,address_space_update_topology,address_space_update_topology_pass最终完成更新FlatView的目标。

    FlatView结构如下,图源见水印:

    由图片可知每个FlatRange的中的AddrRange的start为该段内存区间GPA的首地址,size则描述了该段区间的大小。那么结构体FlatRange中的offset_in_region是什么,是该flatrange相对于所属MR的offset?

    与flatrange对应的是MemoryRegionSection:

    /**
     * MemoryRegionSection: describes a fragment of a #MemoryRegion
     *
     * @mr: the region, or %NULL if empty
     * @address_space: the address space the region is mapped in
     * @offset_within_region: the beginning of the section, relative to @mr's start
     * @size: the size of the section; will not exceed @mr's boundaries
     * @offset_within_address_space: the address of the first byte of the section
     *     relative to the region's address space
     * @readonly: writes to this section are ignored
     */
        struct MemoryRegionSection {
        MemoryRegion *mr;                           // 指向所属 MemoryRegion
        AddressSpace *address_space;                // 所属 AddressSpace
        hwaddr offset_within_region;                // 起始地址 (HVA) 在 MemoryRegion 内的偏移量
        Int128 size;
        hwaddr offset_within_address_space;         // 在 AddressSpace 内的偏移量,如果该 AddressSpace 为系统内存,则为 GPA 起始地址
        bool readonly;
    };

    MemoryRegionSection 指向 MemoryRegion 的一部分 ([offset_within_region, offset_within_region + size]),是注册到 KVM 的基本单位。

    将 AddressSpace 中的 MemoryRegion 映射到线性地址空间后,由于重叠的关系,原本完整的 region 可能会被切分成片段,于是产生了 MemoryRegionSection。

    其中偏移offset_within_region描述的是该section在其所属的MR中的偏移,一个address_space可能有多个MR构成,因此该offset是局部的。而offset_within_address_space是在整个地址空间中的偏移,是全局的offset,如果AddressSpace为系统内存,则该偏移则为GPA的起始地址。

    到这里,借助函数kvm_set_phys_mem()中组装kvmslot,并通过kvm_userspace_memory_region将qemu的内存分布信息传递给kvm的部分过程整理一下上述数据结构中GPA到HVA的对应关系:

    static void kvm_set_phys_mem(KVMMemoryListener *kml,
                                 MemoryRegionSection *section, bool add)
    {
        KVMState *s = kvm_state;
        KVMSlot *mem, old;
        int err;
        MemoryRegion *mr = section->mr;
        bool writeable = !mr->readonly && !mr->rom_device;
        hwaddr start_addr = section->offset_within_address_space; //获取GPA
        ram_addr_t size = int128_get64(section->size);
        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. Pad the start
           address to next and truncate size to previous page boundary. */
        delta = qemu_real_host_page_size - (start_addr & ~qemu_real_host_page_mask);
        delta &= ~qemu_real_host_page_mask;
        if (delta > size) {
            return;
        }
        start_addr += delta; //页对齐修正
        size -= delta;
        size &= qemu_real_host_page_mask;
        if (!size || (start_addr & ~qemu_real_host_page_mask)) {
            return;
        }
    
        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;
            }
        }
    
        ram = memory_region_get_ram_ptr(mr) + section->offset_within_region + delta; //获取hva
      .......
    }

    GPA:在该函数中传入的参数为MemoryRegionSection,根据region section在AddressSpace中的偏移,即offset_within_address_space,加上页对齐修正(delta)得到该section的GPA,填入start_addr。

    HVA: hva是通过该section所属的MR的起始HVA + 该region section在所属MR中的偏移量(offset_within_region)+页对齐修正(delta)得到。

    该region section所属MR的起始HVA通过函数memory_region_get_ram_ptr()得到,该函数内容如下:

    void *memory_region_get_ram_ptr(MemoryRegion *mr)
    {
        void *ptr;
        uint64_t offset = 0;
    
        rcu_read_lock();
        while (mr->alias) { //追溯到实体MR为止
            offset += mr->alias_offset;
            mr = mr->alias;
        }
        assert(mr->ram_block);
        ptr = qemu_map_ram_ptr(mr->ram_block, offset); //实体MR有对应的RAMBlock
        rcu_read_unlock();
    
        return ptr;
    }
    
    void *qemu_map_ram_ptr(RAMBlock *ram_block, ram_addr_t addr)
    {
        RAMBlock *block = ram_block;
    
        if (block == NULL) {
            block = qemu_get_ram_block(addr);
            addr -= block->offset;
        }
    
        if (xen_enabled() && block->host == NULL) {
            /* We need to check if the requested address is in the RAM
             * because we don't want to map the entire memory in QEMU.
             * In that case just map until the end of the page.
             */
            if (block->offset == 0) {
                return xen_map_cache(addr, 0, 0);
            }
    
            block->host = xen_map_cache(block->offset, block->max_length, 1);
        }
        return ramblock_ptr(block, addr);
    }
    
    static inline void *ramblock_ptr(RAMBlock *block, ram_addr_t offset)
    {
        assert(offset_in_ramblock(block, offset));
        return (char *)block->host + offset; //hva的起始地址加上所有偏移得到最终hva
    }

    在 memory_region_get_ram_ptr 中,如果当前MR是另一个MR的 alias,则会向上追溯,一直追溯到非 alias region(实体 region) 为止。将追溯过程中的 alias_offset 加起来,可以得到当前 region 在实体 region 中的偏移量。由于实体 region 具有对应的 RAMBlock,所以调用函数 qemu_map_ram_ptr ,将实体 region 对应的 RAMBlock 的 host 和总 offset 加起来,得到当前 region 的起始 HVA。

    在函数qemu_map_ram_ptr()中,如果传入的ram_block为空,还可以根据当前region在实体region中的偏移量找到对应的ramblock,其调用qemu_get_ram_block()

    static RAMBlock *qemu_get_ram_block(ram_addr_t addr)
    {
        RAMBlock *block;
    
        block = atomic_rcu_read(&ram_list.mru_block);//首先看是不是处于最近使用的block中
        if (block && addr - block->offset < block->max_length) { //addr即为当前region相对于实体region的offset,若offset-当前block.offset小于该block的大小,说明该region对应的内存处于该block中
            return block;
        }
        QLIST_FOREACH_RCU(block, &ram_list.blocks, next) { //不在最近使用的block中,则遍历RAMList的所有block
            if (addr - block->offset < block->max_length) {
                goto found;
            }
        }
    
        fprintf(stderr, "Bad ram offset %" PRIx64 "
    ", (uint64_t)addr);
        abort();
        .......
    }

    由于每一段内存都对应一个RAMblock,通过当前region相对于实体region的offset可以知道这段内存的大小,如果该段大小减去某个RAMBlock的offset小于该block的size,说明该段内存对应的hva在这段block中,否则则查找下一个。比如第一个block的offset为0,如果addr小于该block的大小,那么该block就是这段内存区域对应的block。

    一些猜想及疑问:

    1、虚拟机的GPA是从0开始的,由系统内存的初始化过程可以看出(不考虑io),初始时分配了一整片内存“pc.ram”及对应的RAMBlock,因此猜想MemoryRegion “pc.ram”的addr为起始GPA,即为0,其他region到该实体region的各级alias_offset之和应该就是该region的起始GPA。又MemoryRegionSection中的offset_within_address_space表示在所属AddressSpace中的偏移量,若该AS为系统内存,则为GPA的起始地址。那么各级subregion的alias_offset相加,再加上实体MR的addr是否就等于MemoryRegionSection中的offset_within_address_space。个人感觉应该是,但不确定,可通过实验进行相关验证。

    2、若上述猜想是对的,那么由MemoryRegion及RAMBlock即可得到GPA到HVA的对应关系,那么之前提出的疑问:AddressSpace的意义何在?分析qemu的源码可知AddressSpace绑定了相关listener,当发生变化时会触发相关的listener,不能单从GPA到HVA的映射来考虑AddressSpace的意义。两个全局的AddressSpace(address_space_io,address_space_memory)串起了属于系统内存和io内存的所有memoryRegion,当内存发生变化时,会触发相关listener。所以个人认为AddressSpace可以更好地对不同级别的MemoryRegion进行管理,而不需要为各个MemoryRegion注册绑定listener。且由源码可以看出,MemoryRegion的偏移更偏向于应用得到该region对应于起始hva的偏移,从而计算该region的起始hva,而AddressSpace更偏向于应用于得到起始GPA。(若实体MR的addr为起始GPA,那么该MR到实体MR的偏移之和也可以用于得到该region的起始GPA,但源码中并没有应用此种方式,因为AddressSpace中的相关变量已经可以表示起始GPA了)

    3、结构体FlatRange.addr.start就可以表示该段FlatRange的起始GPA,那么该结构体中的offset_in_region是什么,是其相对于所属的MR的offset,其意义又是什么?该问题从函数listener_add_address_space()中可以得到一些解答:

    static void listener_add_address_space(MemoryListener *listener,
                                           AddressSpace *as)
    {
        FlatView *view;
        FlatRange *fr;
        .......
    
        view = address_space_get_flatview(as);//获取as中的flatview
        FOR_EACH_FLAT_RANGE(fr, view) { //遍历flatview中的每个flatrange
            MemoryRegionSection section = { //新建一个memoryregionsection 并进行赋值
                .mr = fr->mr,
                .address_space = as,
                .offset_within_region = fr->offset_in_region, 
                .size = fr->addr.size,
                .offset_within_address_space = int128_get64(fr->addr.start),
                .readonly = fr->readonly,
            };     
        ......
    }

    由上述代码也可以看出FlatRange和MemoryRegionSection的对应关系,MemoryRegionSection中的offset_within_region即为FlatRange的offset_in_region,因此均表示为在所属MR中的偏移,若所属MR为全局MR,则表示为在全局MR中的偏移。同样的,MemoryRegionSection中的offset_within_address_space即为FlatRange.addr.start,表示GPA的起始地址。

    补充一个在虚拟机退出时如何根据GPA找到HVA:https://www.anquanke.com/post/id/86412 链接中的第四小节对此进行了分析,主要原理是由AddressSpaceDispatch中的6级页表PhysPageMap实现,该页表的最后一级指向MemoryRegionSection,由MemoryRegionSection可以得到GPA对应的MR,由此得到HVA。

    后续会分析结构体AddressSpace注册的listerner的一些操作,以及qemu如何把内存管理的信息传至KVM中,以及如何进行视图的更新。

    以上仅是对qemu中管理虚拟机内存的一些数据结构的整理,由于个人理解及分析不够,存在着一些疑问及猜想,难免有不对的地方,欢迎大家提出疑问,指正错误。

     参考:https://www.cnblogs.com/ck1020/p/6729224.html

    https://www.binss.me/blog/qemu-note-of-memory/

    http://oenhan.com/qemu-memory-struct

    https://blog.csdn.net/leoufung/article/details/48781205

     

  • 相关阅读:
    Mysql知识总结
    Unity3D UGUI 自动调节大小
    关于 Rijndael 加密
    配置java环境
    二叉查找树
    序列化和反序列化
    关于文件保存/关闭时报错:文件正由另一进程使用,因此该进程无法访问此文件。
    关于Unity中NGUI图片精灵响应鼠标的方法
    用人类的话来描述 里氏转换
    C#中string的相关方法
  • 原文地址:https://www.cnblogs.com/ccxikka/p/9477530.html
Copyright © 2020-2023  润新知