QEMU之内存虚拟化

news2024/11/18 3:25:26

内存虚拟化方案

最直观的方案,将QEMU进程的虚拟地址空间的一部分作为虚拟机的物理地址。但该方案有一个问题:

在这里插入图片描述

在物理机上,CPU对内存的访问在保护模式下是通过分段分页实现的,在该模式下,CPU访问时使用的是虚拟地址,必须通过硬件MMU进行转换,将虚拟地址转换成物理地址才能够访问到实际的物理内存:

虚拟地址
物理地址
CPU
MMU
物理地址

显然,在虚拟机内部,其也是访问的虚拟地址,要想让其访问到实际的物理内存,必须先将这个地址转换成虚拟机的物理地址,然后将物理地址转换成QEMU的虚拟地址,最后将QEMU的虚拟地址转换成物理机上的物理地址,才能访问到数据。这就引出MMU的虚拟化

EPT(Extended Page Table),VMX架构引入了EPT(Extended Page Table, 扩展页表)机制来实现VM物理地址空间的隔离, EPT机制实现原理与x86/x64的分页机制是一致的。当guest软件发出指令访问内存时, guest最终生成GPA(Guest-Physical Address)。EPT页表结构定义在host端, 处理器接受到guest传来guest-physical address后, 通过EPT页表结构转换为HPA(Host-physical address), 从而访问平台上的物理地址。

纯软MMU

在CPU没有支持EPT之前,通过影子页表(Shadow Page Table)来实现虚拟机地址到host主机物理地址的转换。效率较低,KVM中维护影子页表。

影子页表
GVA
HPA
支持EPT

EPT方案中,CPU的寻址模式在VM non-root operation下会发生变化,其会使用两个页表。

在这里插入图片描述

如果开启EPT,当CPU进行VM Entry时,会使用EPT功能,虚拟机对内部自身页表有着完全的控制,CPU先将虚拟机内部的虚拟地址转换为虚拟机物理地址,通过这个过程查找虚拟机内部页表,然后CPU会将这个地址转换为宿主机的物理地址,通过这个过程查找宿主机中的EPT页表。当CPU产生VM Exit时,EPT会关闭,这个时候CPU在宿主机上又会按照传统的单页表方式寻址。虚拟机中的页表就像物理机操作系统页表一样,页表里面保存的是虚拟机物理地址,由自己维护,EPT页表则由宿主机维护,里面记录着虚拟机物理地址到宿主机的物理地址的转换。

QEMU内存虚拟化初始化

基本数据结构

AddressSpace结构体,用来表示一个虚拟机或者虚拟CPU能够访问的所有物理地址。注意这里的访问和能够访问是两回事,与进程的地址空间一样,一个进程的虚拟地址空间为4GB(32位下),这并不是说操作系统需要为进程分配这么大的空间。同样,QEMU中的AddressSpace表示的是一段地址空间,整个系统可以有一个全局的地址空间,CPU可以有自己的地址空间视角,设备也可以有自己的地址空间视角。

/**
 * struct AddressSpace: describes a mapping of addresses to #MemoryRegion objects
 */
struct AddressSpace {
    /* private: */
    struct rcu_head rcu;
    char *name;
    MemoryRegion *root;

    /* Accessed via RCU.  */
    struct FlatView *current_map;

    int ioeventfd_nb;
    struct MemoryRegionIoeventfd *ioeventfds;
    QTAILQ_HEAD(, MemoryListener) listeners;
    QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};

QEMU的其他子系统可以注册地址空间变更的事件,所有注册的信息都通过listeners连接起来。所有的AddressSpace通过address_spaces_link这个node连接起来,链表头是address_spaces。

内存管理中另一个结构是MemoryRegion,它表示的是虚拟机的一段内存区域。MemoryRegion是内存模拟中的核心结构,整个内存的模拟都是通过MemoryRegion构成的无环图完成的,图的叶子节点是实际分配给虚拟机的物理内存或者是MMIO,中间的节点则表示内存总线,内存控制器是其他MemoryRegion的别名。

在这里插入图片描述

struct MemoryRegion {
    Object parent_obj;

    /* private: */

    /* The following fields should fit in a cache line */
    bool romd_mode;
    bool ram;
    bool subpage;
    bool readonly; /* For RAM regions */
    bool nonvolatile;
    bool rom_device;
    bool flush_coalesced_mmio;
    uint8_t dirty_log_mask;
    bool is_iommu;
    RAMBlock *ram_block;
    Object *owner;

    const MemoryRegionOps *ops;
    void *opaque;
    MemoryRegion *container;
    Int128 size;
    hwaddr addr;
    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;
    hwaddr alias_offset;
    int32_t priority;
    QTAILQ_HEAD(, MemoryRegion) subregions;
    QTAILQ_ENTRY(MemoryRegion) subregions_link;
    QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
    const char *name;
    unsigned ioeventfd_nb;
    MemoryRegionIoeventfd *ioeventfds;
    RamDiscardManager *rdm; /* Only for RAM */
};
QEMU虚拟内存初始化

虚拟机虚拟化内存是在/hw/i386/pc_piix.c中进行的,内存分为低端内存和高端内存,之所以会有这个区分是因为一些有传统设备的虚拟机,其设备必须使用一些地址空间在4GB以下的内存。

/* PC hardware initialisation */
static void pc_init1(MachineState *machine,
                     const char *host_type, const char *pci_type)
{
    ...
        if (!pcms->max_ram_below_4g) {
            pcms->max_ram_below_4g = 0xe0000000; /* default: 3.5G */
        }
        lowmem = pcms->max_ram_below_4g;
        if (machine->ram_size >= pcms->max_ram_below_4g) {
            if (pcmc->gigabyte_align) {
                if (lowmem > 0xc0000000) {
                    lowmem = 0xc0000000;
                }
                if (lowmem & (1 * GiB - 1)) {
                    warn_report("Large machine and max_ram_below_4g "
                                "(%" PRIu64 ") not a multiple of 1G; "
                                "possible bad performance.",
                                pcms->max_ram_below_4g);
                }
            }
        }

        if (machine->ram_size >= lowmem) {
            x86ms->above_4g_mem_size = machine->ram_size - lowmem;
            x86ms->below_4g_mem_size = lowmem;
        } else {
            x86ms->above_4g_mem_size = 0;
            x86ms->below_4g_mem_size = machine->ram_size;
        }
    ...
}

KVM内存虚拟化

虚拟机MMU初始化

VMCS中VM execution区域里的secondary processor-based VM-execution control字段的第二位用来表示是是否开启EPT,在KVM初始化的时候会调用架构相关的hardware_setup函数,hardware_setup函数会调用setup_vmcs_config,在其中读取MSR_IA32_VMX_PROCBASED_CTLS2,将寄存器存放在vmcs_conf->cpu_based_2nd_exec_ctrl中。

KVM在创建VCPU的过程中会创建虚拟机MMU,具体是在函数kvm_arch_vcpu_init中调用kvm_mmu_create。

int kvm_mmu_create(struct kvm_vcpu *vcpu)
{
    uint i;
    int ret;

    vcpu->arch.mmu_pte_list_desc_cache.kmem_cache = pte_list_desc_cache;
    vcpu->arch.mmu_pte_list_desc_cache.gfp_zero = __GFP_ZERO;

    vcpu->arch.mmu_page_header_cache.kmem_cache = mmu_page_header_cache;
    vcpu->arch.mmu_page_header_cache.gfp_zero = __GFP_ZERO;

    vcpu->arch.mmu_shadow_page_cache.gfp_zero = __GFP_ZERO;

    vcpu->arch.mmu = &vcpu->arch.root_mmu;
    vcpu->arch.walk_mmu = &vcpu->arch.root_mmu;

    vcpu->arch.root_mmu.root_hpa = INVALID_PAGE;
    vcpu->arch.root_mmu.root_pgd = 0;
    vcpu->arch.root_mmu.translate_gpa = translate_gpa;
    for (i = 0; i < KVM_MMU_NUM_PREV_ROOTS; i++)
        vcpu->arch.root_mmu.prev_roots[i] = KVM_MMU_ROOT_INFO_INVALID;

    vcpu->arch.guest_mmu.root_hpa = INVALID_PAGE;
    vcpu->arch.guest_mmu.root_pgd = 0;
    vcpu->arch.guest_mmu.translate_gpa = translate_gpa;
    for (i = 0; i < KVM_MMU_NUM_PREV_ROOTS; i++)
        vcpu->arch.guest_mmu.prev_roots[i] = KVM_MMU_ROOT_INFO_INVALID;

    vcpu->arch.nested_mmu.translate_gpa = translate_nested_gpa;

    ret = alloc_mmu_pages(vcpu, &vcpu->arch.guest_mmu);
    if (ret)
        return ret;

    ret = alloc_mmu_pages(vcpu, &vcpu->arch.root_mmu);
    if (ret)
        goto fail_allocate_root;

    return ret;
 fail_allocate_root:
    free_mmu_pages(&vcpu->arch.guest_mmu);
    return ret;
}
EPT表的创建

EPT的缺页处理是由函数tdp_page_fault完成的。当虚拟机内部进行内存访问的时候,MMU首先会根据虚拟机操作系统的页表把GVA转换成GPA,然后根据EPT页表把GPA转换成HPA,这就是所谓的两级页表转换,也就是tdp(two dimission page)的来源。GVA转换为GPA的过程中,如果发生缺页异常,这个异常会由虚拟机操作系统内核处理;GPA转换成HPA的过程中,如果发生缺页异常,虚拟机会产生退出,并且退出原因为EXIT_REASON_EPT_VIOLATION,其对应的处理函数为handle_ept_violation,这个函数就会调用tdp_page_fault来完成缺页异常的处理。

vcpu_enter_guest函数会完成虚拟机的进入和退出,函数最后调用架构相关的handle_exit回调函数,Intel CPU对应的是vmx_handle_exit,该函数会根据退出原因调用kvm_vmx_exit_handlers函数表中的一个函数,EPT异常会调用handle_ept_violation。

static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
    ...
    [EXIT_REASON_GDTR_IDTR]              = handle_desc,
    [EXIT_REASON_LDTR_TR]              = handle_desc,
    [EXIT_REASON_EPT_VIOLATION]          = handle_ept_violation,
    [EXIT_REASON_EPT_MISCONFIG]           = handle_ept_misconfig,
    ...
};
static int handle_ept_violation(struct kvm_vcpu *vcpu)
{
    unsigned long exit_qualification;
    gpa_t gpa;
    u64 error_code;

    exit_qualification = vmx_get_exit_qual(vcpu);

    if (!(to_vmx(vcpu)->idt_vectoring_info & VECTORING_INFO_VALID_MASK) &&
            enable_vnmi &&
            (exit_qualification & INTR_INFO_UNBLOCK_NMI))
        vmcs_set_bits(GUEST_INTERRUPTIBILITY_INFO, GUEST_INTR_STATE_NMI);

    gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);
    trace_kvm_page_fault(gpa, exit_qualification);

    ...[省略]

    vcpu->arch.exit_qualification = exit_qualification;

    if (unlikely(allow_smaller_maxphyaddr && kvm_mmu_is_illegal_gpa(vcpu, gpa)))
        return kvm_emulate_instruction(vcpu, 0);

    return kvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0);
}

MMIO机制

在x86下访问设备的资源有两种方式:一种是通过Port I/O,即PIO;另一种是通过MemoryMapped I/O,即MMIO。虚拟机的VMCS中定义了两个IO bitmap,共2页,总共有4096×8×2=65536个位,每一个位如果进行了置位则表示虚拟机对该端口的读写操作会退出到KVM,KVM可以自己处理这些PIO的请求,但是更多时候KVM会将对PIO的请求分派到QEMU,这就是PIO的实现机制。那么MMIO应该怎么实现呢?答案是EPT。MMIO的机制简单介绍如下。

1)QEMU申明一段内存作为MMIO内存,这不会导致实际QEMU进程的内存分配。

2)SeaBIOS会分配好所有设备MMIO对应的基址。

3)当Guest第一次访问MMIO的地址时候,会发生EPT violation,产生VM Exit。

4)KVM创建一个EPT页表,并设置页表项特殊标志。

5)虚拟机之后再访问对应的MMIO地址的时候就会产生EPT misconfig,从而产生VM Exit,退出到KVM,然后KVM负责将该事件分发到QEMU。

虚拟机脏页跟踪

开启EPT并建立EPT的页表后,虚拟机中对内存的访问都是通过两级页表完成的,这是在硬件中自动完成的。但是有的时候需要知道虚拟的物理内存中有哪些内存被改变了,也就是记录虚拟机写过的内存,写过的内存叫作内存脏页,记录写过的脏页情况叫作内存脏页跟踪,脏页跟踪是热迁移的基础。热迁移能够将虚拟机从一台宿主机(源端)迁移到另一台宿主机(目的端)上,并且对客户机的影响极小,迁移过程主要就是将虚拟机的内存页迁移到目的端,在热迁移进行内存迁移的同时,虚拟机会不停地写内存,如果虚拟机在QEMU迁移了该页之后又对该页写入了新数据,那么QEMU就需要重新迁移该页,所以QEMU需要跟踪虚拟机的脏页情况。

脏页跟踪的实现

应用层软件QEMU在需要进行脏页跟踪时,会设置memslot的flags为KVM_MEM_LOG_DIRTY_PAGES,在__kvm_set_memory_region函数中,当检测到这个标识设置的时候,会调用kvm_create_dirty_bitmap创建一个脏页位图。

int __kvm_set_memory_region(struct kvm *kvm,
                const struct kvm_userspace_memory_region *mem)
{
    ...

    /* Allocate/free page dirty bitmap as needed */
    if (!(new.flags & KVM_MEM_LOG_DIRTY_PAGES))
        new.dirty_bitmap = NULL;
    else if (!new.dirty_bitmap) {
        r = kvm_alloc_dirty_bitmap(&new);
        if (r)
            return r;
    ...
}
EXPORT_SYMBOL_GPL(__kvm_set_memory_region);
static int kvm_alloc_dirty_bitmap(struct kvm_memory_slot *memslot)
{
    unsigned long dirty_bytes = 2 * kvm_dirty_bitmap_bytes(memslot);

    memslot->dirty_bitmap = kvzalloc(dirty_bytes, GFP_KERNEL_ACCOUNT);
    if (!memslot->dirty_bitmap)
        return -ENOMEM;

    return 0;
}

kvm_alloc_dirty_bitmap()分配的空间是实际的2倍。

当应用层需要知道虚拟机的内存访问情况时,调用虚拟机所属ioctl(KVM_GET_DIRTY_LOG)可以得到脏页位图。KVM中对这个ioctl的处理函数是kvm_vm_ioctl_get_dirty_log,该函数调用kvm_get_dirty_log_protect来完成实际工作。QEMU每次调用ioctl(KVM_GET_DIRTY_LOG)都能够获得虚拟机上一次进行该调用之后到现在之间的脏页情况。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1476222.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法--时空复杂度分析以及各个数据量对应的可使用的算法(C++;1s内)

这里写目录标题 由数据范围反推算法时间复杂度以及算法内容一级目录二级目录二级目录二级目录 一级目录二级目录二级目录二级目录 一级目录二级目录二级目录二级目录 一级目录二级目录二级目录二级目录 由数据范围反推算法时间复杂度以及算法内容 一级目录 二级目录 二级目录…

龙蜥 Anolis OS8.4 设置IP

1、配置文件路径 /etc/sysconfig/network-scripts/ [rootlocalhost ~]# cd /etc/sysconfig/network-scripts/ [rootlocalhost network-scripts]# ls ifcfg-ens32 进入配置文件路径后&#xff0c;展示。ifcfg-ens32这个不同的服务器不一样&#xff0c;本次虚拟机所对应的是ens3…

labelme 使用笔记

下载和安装 labelme官网地址 在Anaconda环境下 conda create -n labelme python3.6 conda activate labelme # go https://anaconda.org/ to find pkg conda install conda-forge/label/cf202003::labelme安装好了&#xff0c;查看版本和使用帮助 labelme -V labelme -h用l…

【机器人最短路径规划问题(栅格地图)】基于蚁群算法求解

基于蚁群算法求解机器人最短路径规划问题的仿真结果 仿真结果 收敛曲线变化趋势 蚁群算法求解最优解的机器人运动路径 各代蚂蚁求解机器人最短路径的运动轨迹

pandas/geopandas 笔记:逐record的轨迹dataFrame转成逐traj_id的轨迹dataFrame

我们现在有这样的一个dataframe&#xff0c;名字为dart 我们需要这样一个DataFrame&#xff0c;每一行有两列&#xff0c;一列是new_installation_id&#xff0c;表示这个轨迹的id&#xff1b;另一列就是这个new_installation_id的轨迹 dart_new dart[[new_installation_id]]…

使用HTML5画布(Canvas)模拟图层(Layers)效果

使用HTML5画布&#xff08;Canvas&#xff09;模拟图层&#xff08;Layers&#xff09;效果 在图形处理和计算机图形学中&#xff0c;图层&#xff08;Layers&#xff09;是指将图像分成不同的可独立编辑、组合和控制的部分的技术或概念。每个图层都可以包含不同的图形元素、效…

你真的了解C语言的枚举和联合吗~

目录 1. 枚举1.1 枚举类型的定义1.2 枚举的优点1.3 枚举的使用 2. 联合&#xff08;共用体&#xff09;2.1 联合类型的定义2.2 联合的特点2.3 使用联合体判断当前机器的大小端2.4 联合大小的计算 1. 枚举 枚举顾名思义就是一一列举。 把可能的取值一一列举。 比如我们现实生活…

华为云磁盘挂载

华为云磁盘挂载 磁盘挂载情况 fdisk -l 2. 查看当前分区情况 df -h 3.给新硬盘添加新分区 fdisk /dev/vdb 4.分区完成&#xff0c;查询所有设备的文件系统类型 blkid 发现新分区并没有文件系统类型&#xff08;type为文件系统具体类型&#xff0c;有ext3,ext4,xfs,iso9660等…

如何一步一步地优化LVGL的丝滑度

经过一番周折将LVGL移植到了STM32F407单片机上&#xff0c;底层驱动的LCD是st7789&#xff0c;移植时的条件和环境如下&#xff1a; ●LVGL用的是单缓冲&#xff0c;一次刷新10行&#xff1b; ●刷新函数用的是最原始的一个一个打点的方式&#xff1b; ●ST7789底层发送数据用的…

寒假开学在即,怎么寄行李才能便宜省钱呢?

在度过了一个充实愉快的假期之后&#xff0c;小伙伴们就要踏上新的征程了&#xff0c;来面对新学期的到来&#xff0c;可是&#xff0c;面对这么多不知道怎么安排的行李可就把人给愁死了&#xff0c;如果通过驿站寄行李的话&#xff0c;又要花费一大笔快递费了&#xff0c;可是…

IAudioManager.cpp源码解读

IAudioManager.cpp源码如下&#xff1a; 源码路径&#xff1a;https://cs.android.com/android/platform/superproject/main//main:frameworks/native/services/audiomanager/IAudioManager.cpp;drc84410fbd18148d422d3581201c67f1a72a6658c4;l147?hlzh-cn /** Copyright (C)…

基于springboot实现线上阅读系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现线上阅读系统演示 摘要 随着社会发展速度的愈来愈快&#xff0c;以及社会压力变化的越来越快速&#xff0c;致使很多人采取各种不同的方法进行解压。大多数人的稀释压力的方法&#xff0c;是捧一本书籍&#xff0c;心情地让自己沉浸在情节里面&#xff0c;以…

【Linux】TCP应用与相关API守护进程

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;优惠多多。&#xff08;联系我有折扣哦&#xff09; 文章目录 1. 相关使用接口2. 代码实现2.1 日志组件2.2 Server端2.3 Client端2.3 bug解决 3. 守…

动态规划|【斐波那契数列模型 】|面试题08.01三步问题

目录 题目 思路 普通思路 动态规划思路 1.状态表示 2.状态转移方程 3.初始化 4.填表顺序 5.返回值 代码 空间优化 题目 题目链接 面试题 08.01. 三步问题https://leetcode.cn/problems/three-steps-problem-lcci/ 三步问题。有个小孩正在上楼梯&#xff0c;楼梯有n…

【JAVA日志】关于日志系统的架构讨论

目录 1.日志系统概述 2.环境搭建 3.应用如何推日志到MQ 4.logstash如何去MQ中取日志 5.如何兼顾分布式链路追踪 1.日志系统概述 关于日志系统&#xff0c;其要支撑的核心能力无非是日志的存储以及查看&#xff0c;最好的查看方式当然是实现可视化。目前市面上有成熟的解决…

今天面试了一个工作4年的测试工程师,一问连自动化基础都不知道,还反过来怼我..

金三银四黄金期&#xff0c;我们公司也开始大量招人了&#xff0c;我这次是公司招聘的面试官之一&#xff0c;主要负责一些技术上的考核&#xff0c;这段时间还真让我碰到了不少奇葩求职者 昨天公司的HR小席刚跟我吐槽&#xff1a;这几个星期没有哪天不加班的&#xff01;各种…

LTD264次升级 | 对接AsiaPay • 人民币买外币商品 •知识付费订单可关闭 • 专栏支持VIP免支付购买

​ 1、对接AsiaPay第三方支付平台&#xff0c;支持人民币买外币商品&#xff1b; 2、知识付费购买优化 3、账号绑定的微信号可解除绑定&#xff1b; 4、其他已知问题修复与优化&#xff1b; 01 商城 1) 新增海外跨境支付系统AsiaPay 在本次升级中&#xff0c;商城支付系统新增…

【面试题】在浏览器地址栏输入URL后会发生什么

1. 地址栏输入后的本地操作 当我们在浏览器的地址栏中&#xff0c;输入xxx内容后&#xff0c;浏览器的进程首先会判断输入的内容&#xff1a; 如果是普通的字符&#xff0c;那浏览器会使用默认的搜索引擎去对于输入的xxx生成URL。如若输入的是网址&#xff0c;那浏览器会拼接…

StarRocks实战——滴滴OLAP的技术实践与发展方向

原文大佬的这篇StarRocks实践文章整体写的很深入&#xff0c;介绍了StarRocks数仓架构设计、物化视图加速实时看板、全局字典精确去重等内容&#xff0c;这里直接摘抄下来用作学习和知识沉淀。 目录 一、背景介绍 1.1 滴滴OLAP的发展历程 1.2 OLAP引擎存在的痛点 1.2.1 运维…

在 Jupyter Notebook 中查看所使用的 Python 版本和 Python 解释器路径

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 我们在做 Python 开发时&#xff0c;有时在我们的服务器上可能安装了多个 Python 版本。 使用 conda info --envs 可以列出所有的 conda 环境。当在 Linux 服务器上使用 which python 命令时&#xff0…