android qemu-kvm内存管理和IO映射

简介: 为什么内存管理和IO映射要放一起呢?因为IO映射有memory map io(MMIO)和port map io(PMIO)两种,其中MMIO和内存管理有关的。 MMIO和普通内存的访问的汇编指令是相同的;PMIO有自己的汇编指令。

为什么内存管理和IO映射要放一起呢?因为IO映射有memory map io(MMIO)和port map io(PMIO)两种,其中MMIO和内存管理有关的。

MMIO和普通内存的访问的汇编指令是相同的;PMIO有自己的汇编指令。

kvm如果执行到了PMIO的指令,那么退出状态是KVM_EXIT_IO。

kvm怎么知道某段内存是MMIO,从而退出状态是KVM_EXIT_MMIO,而某段内存是普通内存,由kvm处理缺页,而不退出呢?这是本文想要探究的一个东西。

PS:kvm退出状态是KVM_EXIT_IO或者KVM_EXIT_MMIO,那么将由qemu进行读写虚拟设备IO端口的模拟,如何模拟?

PMIO的看《android qemu-kvm i8254 pit虚拟设备》,MMIO的看《android emulator虚拟设备分析第一篇之battery》。



阅读本文前,需要对MMU,页表,虚拟内存空间,物理内存空间有一些了解。对于kvm的影子页表的工作原理,kvm的vcpu的执行,以及kvm的ioctl有一些了解。

前提知识:

KVM之内存虚拟化:http://royluo.org/2016/03/13/kvm-mmu-virtualization/,重点看GVA,GPA,HVA,HPA,影子页表的东西

kvm api:https://kernel.org/doc/Documentation/virtual/kvm/api.txt,重点看ioctl的东西,并对kvm的使用有所印象

kvm api使用实例:https://lwn.net/Articles/658511/https://lwn.net/Articles/658512/ 

QEMU-MEMORY-MANAGEMENT.TXT:https://android.googlesource.com/platform/external/qemu.git/+/master/docs/QEMU-MEMORY-MANAGEMENT.TXT

本文使用的android版本是5.1.0,x86的img;host机器是intel x86_64,ubuntu12.04



先来一个大图:



普通内存的申请(external/qemu/hw/i386/pc.c):

     /*
      * Allocate a single contiguous RAM so that the goldfish
      * framebuffer can work well especially when the frame buffer is
      * large.
      */
     ram_addr = qemu_ram_alloc(NULL, "pc.ram", below_4g_mem_size);
     cpu_register_physical_memory(0, below_4g_mem_size, ram_addr);


由于kvm可以使用硬件提供的EPT影子页表,所以我们只需要将HVA和GPA的关系通过ioctl告知kvm即可(KVM_SET_USER_MEMORY_REGION)。

先介绍一下qemu内存管理中重要的ram_list和RAMBlock

RAMList ram_list = { .blocks = QTAILQ_HEAD_INITIALIZER(ram_list.blocks) };


 typedef struct RAMList {
     QemuMutex mutex;
     unsigned long *dirty_memory[DIRTY_MEMORY_NUM];
     RAMBlock *mru_block;
     QTAILQ_HEAD(ram, RAMBlock) blocks;
     uint32_t version;
 } RAMList;

 typedef struct RAMBlock {
     uint8_t *host;
     ram_addr_t offset;
     ram_addr_t length;
     uint32_t flags;
     char idstr[256];
     /* Reads can take either the iothread or the ramlist lock.
      * Writes must take both locks.
      */
     QTAILQ_ENTRY(RAMBlock) next;
     int fd;
 } RAMBlock;

ram_list有个锁,是RAMBlock的链表,按照length从大到小排序。RAMBlock的host是HVA,offset是qemu自己引入的介于HVA和GPA之间的一层,length是大小,idstr是名称。每一个RAMBlock对应一个slot,也就是内存插槽。


qemu_ram_alloc直接调用qemu_ram_alloc_from_ptr,主要是申请HVA,求取offset(可放下size的最小空闲空间),设置RAMBlock结构体并按从大到小顺序插入ram_list:

ram_addr_t qemu_ram_alloc_from_ptr(DeviceState *dev, const char *name,
                                   ram_addr_t size, void *host)
{
    RAMBlock *block, *new_block;
    ram_addr_t old_ram_size, new_ram_size;

    //last_ram_offset求取phys_offset空间已用的大小,old_ram_size为phys_offset空间已用页面个数
    old_ram_size = last_ram_offset() >> TARGET_PAGE_BITS;

    //页对齐
    size = TARGET_PAGE_ALIGN(size);
    new_block = g_malloc0(sizeof(*new_block));
    new_block->fd = -1;

    /* This assumes the iothread lock is taken here too.  */
    qemu_mutex_lock_ramlist();
    //new_block->mr = mr;

    //在phys_offset空间中寻找可以存放size的最小的空闲空间的位置
    new_block->offset = find_ram_offset(size);
    if (host) {
        new_block->host = host;
        new_block->flags |= RAM_PREALLOC_MASK;
    }
//...else if...xen... 
    else {
//...if...mem_path...
        if (!new_block->host) {
            //申请HVA,此时未分配HGA,HGA是在kvm发现缺页时再分配的
            new_block->host = phys_mem_alloc(size);
            if (!new_block->host) {
                fprintf(stderr, "Cannot set up guest memory '%s': %s\n",
                        name, strerror(errno));
                exit(1);
            }
//...HAXM...

            memory_try_enable_merging(new_block->host, size);
        }
    }
    new_block->length = size;

//...if...dev..
    // 设置RAMBlock的名字
    pstrcat(new_block->idstr, sizeof(new_block->idstr), name);

    /* Keep the list sorted from biggest to smallest block.  */
    QTAILQ_FOREACH(block, &ram_list.blocks, next) {
        if (block->length < new_block->length) {
            break;
        }
    }
    if (block) {
        QTAILQ_INSERT_BEFORE(block, new_block, next);
    } else {
        QTAILQ_INSERT_TAIL(&ram_list.blocks, new_block, next);
    }
    ram_list.mru_block = NULL;

    ram_list.version++;
    qemu_mutex_unlock_ramlist();

    //dirty ram
    new_ram_size = last_ram_offset() >> TARGET_PAGE_BITS;
    if (new_ram_size > old_ram_size) {
        int i;
        for (i = 0; i < DIRTY_MEMORY_NUM; i++) {
            ram_list.dirty_memory[i] =
                bitmap_zero_extend(ram_list.dirty_memory[i],
                                   old_ram_size, new_ram_size);
       }
    }
    cpu_physical_memory_set_dirty_range(new_block->offset, size);

    //无用
    qemu_ram_setup_dump(new_block->host, size);

    if (kvm_enabled())
        //无用
        kvm_setup_guest_memory(new_block->host, size);

    return new_block->offset;
}


cpu_register_physical_memory最终调用的是cpu_register_physical_memory_log,主要对普通内存KVM_SET_USER_MEMORY_REGION,对MMIO不KVM_SET_USER_MEMORY_REGION。然后设置一下普通内存的phys_offset随着每一页的设置而增大,MMIO的phys_offset和io_index相关,不变:

void cpu_register_physical_memory_log(hwaddr start_addr,
                                         ram_addr_t size,
                                         ram_addr_t phys_offset,
                                         ram_addr_t region_offset,
                                         bool log_dirty)
{
    hwaddr addr, end_addr;
    PhysPageDesc *p;
    CPUState *cpu;
    ram_addr_t orig_size = size;
    subpage_t *subpage;

    if (kvm_enabled())
        // 如果是普通内存,那么KVM_SET_USER_MEMORY_REGION
        // 如果是MMIO,那么修改ram_list和RAMBlock,使对应的内存段没有KVM_SET_USER_MEMORY_REGION,其他内存段不变
        kvm_set_phys_mem(start_addr, size, phys_offset);
//...haxm...

    if (phys_offset == IO_MEM_UNASSIGNED) {
        region_offset = start_addr;
    }
    region_offset &= TARGET_PAGE_MASK;
    // 页对齐
    size = (size + TARGET_PAGE_SIZE - 1) & TARGET_PAGE_MASK;
    end_addr = start_addr + (hwaddr)size;

    addr = start_addr;
    do {
        p = phys_page_find(addr >> TARGET_PAGE_BITS);
        if (p && p->phys_offset != IO_MEM_UNASSIGNED) {
            ram_addr_t orig_memory = p->phys_offset;
            hwaddr start_addr2, end_addr2;
// ...if...subpage...
            else {
                p->phys_offset = phys_offset;
                if ((phys_offset & ~TARGET_PAGE_MASK) <= IO_MEM_ROM ||
                    (phys_offset & IO_MEM_ROMD))
                    // 普通内存的phys_offset需要增加的
                    // MMIO的phys_offset和io_index相关,不管申请几页,都不变
                    phys_offset += TARGET_PAGE_SIZE;
            }
        } else {
            p = phys_page_find_alloc(addr >> TARGET_PAGE_BITS, 1);
            p->phys_offset = phys_offset;
            p->region_offset = region_offset;
            if ((phys_offset & ~TARGET_PAGE_MASK) <= IO_MEM_ROM ||
                (phys_offset & IO_MEM_ROMD)) {
                // 普通内存的phys_offset需要增加的
                // MMIO的phys_offset和io_index相关,不管申请几页,都不变
                phys_offset += TARGET_PAGE_SIZE;
            } else {
                hwaddr start_addr2, end_addr2;
// ...if...subpage...
            }
        }
        region_offset += TARGET_PAGE_SIZE;
        addr += TARGET_PAGE_SIZE;
    } while (addr != end_addr);

    /* since each CPU stores ram addresses in its TLB cache, we must
       reset the modified entries */
    /* XXX: slow ! */
    CPU_FOREACH(cpu) {
        tlb_flush(cpu->env_ptr, 1);
    }
}

phys_page_find_alloc中实现了每一个页都有对应的PhysPageDesc,主要记录了该页对应的phys_offset,从而可以找到HVA以及io_index。注意l1_phys_map是一个多级页表,不仅仅是两级的。


kvm_set_phys_mem用于分配slot,重新分配slot,普通内存KVM_SET_USER_MEMORY_REGION,MMIO不KVM_SET_USER_MEMORY_REGION

void kvm_set_phys_mem(hwaddr start_addr,
                      ram_addr_t size,
                      ram_addr_t phys_offset)
{
    KVMState *s = kvm_state;
    ram_addr_t flags = phys_offset & ~TARGET_PAGE_MASK;
    KVMSlot *mem, old;
    int err;

// ...页面不对齐的检测...

    /* KVM does not support read-only slots */
    phys_offset &= ~IO_MEM_ROM;

    while (1) {
        // 寻找内存空间有重合的slot,重新分配slot的
        mem = kvm_lookup_overlapping_slot(s, start_addr, size);
        if (!mem) {
            break;
        }

        // 普通内存,且完全重合,不需要重新分配
        if (flags < IO_MEM_UNASSIGNED && start_addr >= mem->start_addr &&
            (start_addr + size <= mem->start_addr + mem->memory_size) &&
            (phys_offset - start_addr == mem->phys_offset - mem->start_addr)) {
            /* The new slot fits into the existing one and comes with
             * identical parameters - nothing to be done. */
            return;
        }

        old = *mem;

        // 需要重新分配,先注销之前的
        /* 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();
        }

// ......

        // 重合前的一段,重新分配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->phys_offset = old.phys_offset;
            mem->flags = 0;

            err = kvm_set_user_memory_region(s, mem);
            if (err) {
                fprintf(stderr, "%s: error registering prefix slot: %s\n",
                        __func__, strerror(-err));
                abort();
            }
        }

        // 重合后的一段,重新分配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->phys_offset = old.phys_offset + size_delta;
            mem->flags = 0;

            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;

    // 重合的一段,重新分配slot,如果是MMIO,则不分配,且不kvm_set_user_memory_region,将缺页且KVM无法处理,导致KVM_EXIT_MMIO
    /* KVM does not need to know about this memory */
    if (flags >= IO_MEM_UNASSIGNED)
        return;

    mem = kvm_alloc_slot(s);
    mem->memory_size = size;
    mem->start_addr = start_addr;
    mem->phys_offset = phys_offset;
    mem->flags = 0;

    // kvm ioctl KVM_SET_USER_MEMORY_REGION
    err = kvm_set_user_memory_region(s, mem);
    if (err) {
        fprintf(stderr, "%s: error registering slot: %s\n", __func__,
                strerror(-err));
        abort();
    }
}


kvm_set_user_memory_region用于普通内存的KVM_SET_USER_MEMORY_REGION,guest_phys_addr就是GPA,userspace_addr就是HVA,需要根据phys_offset,在ram_list中进行RAMBlock的查找和匹配,然后找到RAMBlock->host,也就是HVA:

static int kvm_set_user_memory_region(KVMState *s, KVMSlot *slot)
{
    struct kvm_userspace_memory_region mem;

    mem.slot = slot->slot;
    mem.guest_phys_addr = slot->start_addr;
    mem.memory_size = slot->memory_size;
    mem.userspace_addr = (unsigned long)qemu_get_ram_ptr(slot->phys_offset);
    mem.flags = slot->flags;
    if (s->migration_log) {
        mem.flags |= KVM_MEM_LOG_DIRTY_PAGES;
    }
    return kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);
}


MMIO内存的申请(external/qemu/hw/android/goldfish/device.c),申请的MMIO都是页对齐的,基本上都是申请了1页。具体内容需要看《android emulator虚拟设备分析第一篇之battery》:

 int goldfish_device_add(struct goldfish_device *dev,
                        CPUReadMemoryFunc **mem_read,
                        CPUWriteMemoryFunc **mem_write,
                        void *opaque)
 {
     int iomemtype;
     goldfish_add_device_no_io(dev);
     iomemtype = cpu_register_io_memory(mem_read, mem_write, opaque);
     cpu_register_physical_memory(dev->base, dev->size, iomemtype);
     return 0;
 }




目录
相关文章
|
25天前
|
存储 开发工具 Android开发
构建高效的Android应用:从内存管理到用户界面
【5月更文挑战第29天】 随着智能手机的普及,Android应用的开发变得日益重要。然而,许多开发者在开发过程中忽视了性能优化,导致应用运行缓慢,用户体验差。本文将深入探讨如何通过有效的内存管理和用户界面优化,提升Android应用的性能。我们将详细介绍内存泄漏的原因和解决方案,以及如何使用Android的新特性来创建流畅的用户界面。无论你是新手还是经验丰富的开发者,都可以从本文中获得有用的技巧和建议。
|
25天前
|
移动开发 监控 Android开发
构建高效Android应用:从内存优化到电池寿命代码之美:从功能实现到艺术创作
【5月更文挑战第28天】 在移动开发领域,特别是针对Android系统,性能优化始终是关键议题之一。本文深入探讨了如何通过细致的内存管理和电池使用策略,提升Android应用的运行效率和用户体验。文章不仅涵盖了现代Android设备上常见的内存泄漏问题,还提出了有效的解决方案,包括代码级优化和使用工具进行诊断。同时,文中也详细阐述了如何通过减少不必要的后台服务、合理管理设备唤醒锁以及优化网络调用等手段延长应用的电池续航时间。这些方法和技术旨在帮助开发者构建更加健壮、高效的Android应用程序。
|
1天前
|
大数据 API Android开发
Android MemoryFile 共享内存
Android MemoryFile 共享内存
7 0
|
1天前
|
缓存 Java Linux
Android 匿名内存深入分析
Android 匿名内存深入分析
6 0
|
25天前
|
监控 Android开发 UED
构建高效的Android应用:从内存管理到用户体验
【5月更文挑战第28天】 在移动开发的世界中,打造一个既快速又高效的Android应用是每个开发者追求的目标。本文将深入探讨如何通过合理的内存管理策略、优化的代码实践和用户界面设计的提升来增强应用性能。我们将剖析内存泄漏的根源,提供解决方案,探索Kotlin与Java在性能上的差异,并分析如何利用Android系统提供的工具监控和改善应用表现。此外,我们还将讨论Material Design的应用以及其对用户体验的影响。通过这些技术的综合运用,你将能够为用户提供更流畅、响应更快的使用体验。
|
27天前
|
监控 Java Android开发
构建高效Android应用:采用Kotlin进行内存优化的策略
【5月更文挑战第26天】随着移动设备的普及,用户对应用程序的性能要求越来越高。在资源受限的Android平台上,内存管理成为提升性能的关键因素之一。本文将深入探讨使用Kotlin语言开发Android应用时,如何通过智能内存管理策略来提高应用性能和用户体验。我们将分析内存泄露的原因,介绍有效的内存优化技巧,并通过实例代码展示如何在Kotlin中实现这些优化措施。
|
28天前
|
缓存 Java Android开发
构建高效的Android应用:内存优化策略解析
【5月更文挑战第25天】在移动开发领域,性能优化一直是一个不断探讨和精进的课题。特别是对于资源受限的Android设备来说,合理的内存管理直接关系到应用的流畅度和用户体验。本文深入分析了Android内存管理的机制,并提出了几种实用的内存优化技巧。通过代码示例和实践案例,我们旨在帮助开发者识别和解决内存瓶颈,从而提升应用性能。
|
28天前
|
存储 算法 Java
Android 应用开发中的内存优化策略
【5月更文挑战第25天】 在移动设备上,资源的有限性要求开发者对应用进行严格的性能优化。特别是对于Android平台,由于设备的多样性和碎片化问题,内存管理成为确保应用流畅运行的关键因素之一。本文将探讨几种实用的内存优化技术,包括避免内存泄漏、合理使用数据结构和算法、优化图片资源以及利用Android系统的垃圾回收机制。文章的目的是为Android开发者提供一套有效的内存管理工具集,帮助他们构建更高效、更稳定的应用。
|
1月前
|
缓存 移动开发 Android开发
构建高效Android应用:从内存优化到电池寿命
【5月更文挑战第18天】在移动开发领域,一个优秀的Android应用不仅要拥有流畅的用户界面和丰富的功能,更要在设备资源有限的前提下保持高效运行。本文将探讨Android应用开发中关键的性能优化策略,包括内存使用优化、CPU使用减少和电池寿命延长等方面。通过分析常见的性能瓶颈和提供实用的解决方案,帮助开发者打造更高效、更受欢迎的Android应用。
|
1月前
|
移动开发 监控 Android开发
构建高效Android应用:从内存优化到电池续航
【5月更文挑战第22天】 在移动开发的世界中,一个流畅且高效的Android应用是区分优秀与平庸的关键因素。本文深入探讨了如何通过内存管理和电池使用效率的优化来提升应用性能,确保最终用户获得无缝且持久的体验。我们将透过具体策略和编码实践,揭示开发过程中可实施的改进措施,旨在帮助开发者克服常见的性能瓶颈,打造更高质量的Android应用。

热门文章

最新文章