virt-report虚拟化社区动态

QEMU 内存初始化与地址空间管理

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、从内存配置到客户机地址空间

QEMU 启动一台 8 GiB 内存的虚拟机,需要先准备保存 RAM 内容的宿主内存,再把它安排到客户机物理地址空间中。这个地址空间还容纳设备寄存器、ROM 和保留窗口。QEMU 用 MemoryRegion 描述各区域及其连接关系,用 AddressSpace 确定访问入口,并把展开后的 RAM 映射登记到 KVM。

本文以 Linux 宿主上的 pc-q35-10.2、KVM 加速和 -m 8G 为例,沿着后备内存创建、Q35 地址布局、FlatView 生成、KVM 内存槽注册和实际访问展开。示例使用默认匿名 RAM 后端,未配置 NUMA、内存预分配或机密虚拟机。文中的 GiB 按 230 字节计算。

同一份 RAM 在这条路径中有不同的表示:RAMBlock 保存宿主存储信息,RAM MemoryRegion 描述这块存储,机器通过别名决定它出现在客户机的哪些地址,KVM 内存槽登记 GPA 与宿主用户地址的对应关系。先顺着初始化过程建立这几层联系,再看一次高端 RAM 访问,就能理解它们怎样共同工作。

源码版本:QEMU 10.2.0。

二、创建 RAM 后端与宿主映射

以下命令把机器停在客户机执行之前,便于查看内存布局。它无需系统盘;在 HMP monitor 中执行 info mtree -f 可查看映射,执行 quit 退出。运行环境需要支持 KVM,并安装相应版本的 QEMU 与固件。

qemu-system-x86_64 \
  -machine pc-q35-10.2,accel=kvm \
  -cpu host -m 8G \
  -nodefaults -display none -S -monitor stdio

-m 8G 进入 MachineState.ram_size。创建机器容器时,cpu_exec_init_all() 已经建立名为 system 的根区域和全局 address_space_memory;随后 KVM 初始化创建内核虚拟机,并向这个 AddressSpace 注册内存监听器。根容器大小设为 UINT64_MAX,用于承接机器布局;-m 决定的 RAM 容量只占其中一部分。此时 8 GiB RAM 尚未挂入根容器。地址空间入口初始化

RAM 从配置、分配到 KVM 注册的初始化顺序
先建立系统地址空间并注册 KVM 监听器,再创建 RAM 后端和机器布局。区域变化生成 FlatView,经监听器同步到 KVM 内存槽;实际宿主页与二级页表映射通常随访问建立。
先建立系统地址空间并注册 KVM 监听器,再创建 RAM 后端和机器布局。区域变化生成 FlatView,经监听器同步到 KVM 内存槽;实际宿主页与二级页表映射通常随访问建立。 打开原图
Mermaid 源码
sequenceDiagram
  accTitle: QEMU Q35 内存初始化与 KVM 内存槽登记
  accDescr: QEMU 10.2 先建立名为 system 的根区域及名为 memory 的 AddressSpace,再初始化 KVM 并注册内存监听器。RAM backend 创建 RAM MemoryRegion 与 RAMBlock,host 字段保存宿主虚拟地址,随后 machine.ram 引用该区域。Q35 依次把低端和高端 RAM 别名加入地址空间。拓扑事务生成新的 FlatView,监听器收集区间变化,在 commit 阶段登记 KVM memslots。内存槽数量取决于可见 RAM 区间;内存槽保存客户机地址与宿主后备的关系,二阶段页表由后续的 KVM MMU 路径建立。
  %% QEMU baseline: 698104725efad4b29079d857dfdebbd804e34c99
  %% system/vl.c: qemu_create_machine, configure_accelerators, late backends
  %% hw/core/machine.c: machine_run_board_init, machine_consume_memdev
  %% hw/i386/pc.c: pc_memory_init; system/memory.c: transaction commit
  %% accel/kvm/kvm-all.c: kvm_region_add, kvm_region_commit
  participant S as 启动与 Q35
  participant M as QEMU 内存
  participant K as KVM 内核
  Note over S,K: 阶段 1:地址空间与监听器
  S->>M: 建立 system 根与 AddressSpace
  S->>K: 创建 KVM 虚拟机
  S->>M: 注册 KVM 内存监听器
  Note over S,K: 阶段 2:分配 8 GiB RAM 后备
  S->>M: 创建 RAM 后端
  M->>M: 分配 RAMBlock 与宿主映射
  M-->>S: machine.ram 引用 RAM 区域
  Note over S,K: 阶段 3:发布 Q35 的 RAM 布局
  loop 挂接低端、高端 RAM 别名
    S->>M: 加入别名,提交拓扑事务
    M->>M: 生成 FlatView,暂存区间变化
    M->>M: 发布新视图
    M->>K: 监听器 commit,登记内存槽
  end
  Note over M,K: 内存槽登记 GPA、长度与 HVA
  Note over M,K: 二级映射随后由 KVM MMU 建立

1. 默认后端的创建

进入 machine_run_board_init() 后,本例没有指定 memory-backend 或 mem-path,机器类的 create_default_memdev() 创建一个 memory-backend-ram 对象,名称为 pc.ram,size 为 8 GiB。对象完成初始化时调用其分配函数,再把该后端关联到 machine->memdev。接着 machine_consume_memdev() 取得后端的 MemoryRegion,保存为 machine->ram,并登记 RAM 的迁移信息。默认后端创建、后端接入机器

machine_run_board_init()
  create_default_memdev()
    memory-backend-ram 对象完成初始化
      ram_backend_memory_alloc()
        memory_region_init_ram_flags_nomigrate()
          qemu_ram_alloc() → qemu_ram_alloc_internal()
            ram_block_add() → qemu_anon_ram_alloc()
  machine_consume_memdev() → machine->ram
  machine_class->init() → pc_q35_init()

ram_backend_memory_alloc() 为后端内嵌的 backend->mr 初始化 RAM 区域;qemu_ram_alloc() 创建相应 RAMBlock,关联 mr->ram_block。RAMBlock 保存宿主基址 host、当前长度 used_length、最大长度 max_length 等信息。本例只有一份 8 GiB 的主 RAM 后备,之后的高低端别名共享它。RAM 后端分配、RAMBlock 创建

2. 地址映射与物理页分配

在本例的私有匿名内存路径上,qemu_anon_ram_alloc() 调用 qemu_ram_mmap(),通过 mmap 建立宿主虚拟地址映射。设 RAMBlock 的宿主基址为 B,则后备偏移 x 对应用户地址 B + x。这段用户地址可以连续,承载它的宿主物理页无需连续。匿名映射入口、mmap 实现

默认未启用 prealloc。宿主通常随访问处理缺页,提供零页或分配实际页面;固件加载、设备初始化也可能提前访问部分内存。因此,后端创建成功并不表示进程已经占用 8 GiB 物理内存。启用预分配会把更多取页工作提前到初始化阶段,增加启动耗时和初始驻留内存,同时降低运行初期的缺页开销。KVM 的二级页表还需要另外建立,预分配宿主页不会自动完成这一层。宿主后备分配入口、预分配条件

三、Q35 的低端 RAM、高端 RAM 与设备窗口

RAM 后端准备完成后,pc_q35_init() 计算机器布局,再由 pc_memory_init() 把 RAM 挂入系统地址空间。x86 的 4 GiB 以下需要容纳 PCI MMIO、固件和传统设备窗口,所以 8 GiB RAM 会分段映射。本例保持 max-ram-below-4g 默认值;Q35 对这个容量选择 2 GiB 低端 RAM,其余 6 GiB 从客户机物理地址 4 GiB 开始安排。低端与高端容量计算

8 GiB RAM 的客户机地址与后备偏移
低端 RAM 别名覆盖 GPA 0 至 2 GiB,高端别名覆盖 GPA 4 至 10 GiB,分别引用同一后备内存的前 2 GiB 与后 6 GiB。2 至 4 GiB 留给设备及固件等窗口;低端内部还会叠加传统设备映射。区间上界均不包含。
低端 RAM 别名覆盖 GPA 0 至 2 GiB,高端别名覆盖 GPA 4 至 10 GiB,分别引用同一后备内存的前 2 GiB 与后 6 GiB。2 至 4 GiB 留给设备及固件等窗口;低端内部还会叠加传统设备映射。区间上界均不包含。 打开原图
Mermaid 源码
flowchart TB
  accTitle: Q35 的 8 GiB RAM 基础别名布局
  accDescr: 此图不按比例绘制。QEMU 10.2 Q35 在默认低端 RAM 上限下,把八 GiB 后备划为两个无复制别名。客户机物理地址零至二 GiB 对应后备零至二 GiB,四至十 GiB 对应后备二至八 GiB。二至四 GiB 为 PCI 和固件等窗口,其中可以存在未映射区。低端基础 RAM 布局内部还会叠加 VGA、ROM 等区域。十 GiB 仅为本例高端 RAM 的末端,不代表整个客户机物理地址空间的上限。
  %% QEMU baseline: 698104725efad4b29079d857dfdebbd804e34c99
  %% hw/i386/pc_q35.c: lowmem = 0x80000000 for ram_size >= 0xb0000000
  %% hw/i386/x86.c: above_4g_mem_start = 4 * GiB
  %% hw/i386/pc.c: pc_memory_init creates two aliases into machine->ram
  %% Keep all mapping edges inside each row; only row clusters connect vertically.
  %% This preserves each row's LR direction when Mermaid lays out the TB graph.
  subgraph LOW[低端 RAM 别名]
    direction LR
    low["GPA [0, 2 GiB)
局部有 VGA / ROM 覆盖"]
    first["后备 [0, 2 GiB)
长度 2 GiB"]
    low --> first
  end
  subgraph GAP[" "]
    direction LR
    hole["GPA [2, 4 GiB)
PCI / 固件等窗口
含未映射区"]
    shared["同一份 8 GiB 后备
两个 alias 无需复制"]
    hole ~~~ shared
  end
  subgraph HIGH[高端 RAM 别名]
    direction LR
    high["GPA [4, 10 GiB)
10 GiB 为本例 RAM 末端"]
    rest["后备 [2, 8 GiB)
长度 6 GiB"]
    high --> rest
  end
  LOW ~~~ GAP
  GAP ~~~ HIGH
  style GAP fill:transparent,stroke:transparent;
  classDef memory fill:#e4f1ec,stroke:#438575,color:#173e34;
  classDef window fill:#fff4db,stroke:#b89a55,color:#59491e;
  classDef note fill:#eaf0f6,stroke:#8198ac,color:#263c4c;
  class low,high,first,rest memory;
  class hole window;
  class shared note;
客户机物理地址范围布局主 RAM 后备偏移
[0, 2 GiB)ram-below-4g,低端别名[0, 2 GiB)
[2 GiB, 4 GiB)为 PCI、固件等保留的地址范围,其中可有未映射区不对应这一段主 RAM
[4 GiB, 10 GiB)ram-above-4g,高端别名[2 GiB, 8 GiB)

表中给出主 RAM 的两段别名边界。低端内部还会出现 VGA、ROM、芯片组控制的覆盖区域,客户机操作系统最终得到的可用内存表也会扣除相应保留部分。10 GiB 是本例高端 RAM 的结束位置,整个机器还可以在更高地址安排 64 位 PCI 窗口或其他区域。

pc_memory_init() 的核心操作如下。两次 memory_region_init_alias() 都引用 machine->ram,第二个别名从该区域的 2 GiB 偏移开始;memory_region_add_subregion() 决定别名在客户机地址空间中的位置。

memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g",
                         machine->ram, 0, x86ms->below_4g_mem_size);
memory_region_add_subregion(system_memory, 0, ram_below_4g);

memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g",
                         machine->ram, x86ms->below_4g_mem_size,
                         x86ms->above_4g_mem_size);
memory_region_add_subregion(system_memory, x86ms->above_4g_mem_start,
                            ram_above_4g);

这里没有复制 RAM 内容,也没有再申请一份 6 GiB 内存。别名把“存放在哪里”和“客户机从哪里访问”分开,使 RAM 后端保持原来的存储布局。e820_add_entry() 同时记录 RAM 范围,供后续固件内存信息使用;它与建立 QEMU 内部映射承担不同工作。RAM 别名与 E820 范围

四、区域、地址空间与平坦视图

MemoryRegion 表达局部布局:区域有自己的大小,挂到容器后才取得相对于容器的地址。AddressSpace 选择一个根,表示 CPU 或设备从这个入口看到的空间。RAMBlock 保存 RAM 后端的宿主存储信息;一块 RAM 可以通过多个别名出现在不同地址,不需要复制其内容。

结构关键字段职责
MemoryRegionaddr、size、container、subregions、priority区域在局部拓扑中的位置、范围与遮盖关系
RAM / MMIO 区域ram_block 或 ops、opaqueRAM 后端存储,或设备读写回调及其状态
别名区域alias、alias_offset把目标区域的一段暴露在新的位置
AddressSpaceroot、current_map、listeners确定访问入口,持有当前 FlatView 和更新监听器
FlatViewranges、nr、dispatch、ref保存展开后的不重叠区间、查找结构和生命周期信息
FlatRangeaddr、mr、offset_in_region、readonly把地址空间中的一段映射到叶子区域的指定偏移

本例中,machine->ram 指向 8 GiB 的 pc.ram;ram-above-4g 是指向它的别名,自己的 alias_offset 为 2 GiB;address_space_memory.root 则指向 system 根。RAMBlock 的 offset 用于 QEMU 内部 RAM 地址管理,与客户机 GPA、单个 RAMBlock 内的偏移各有不同含义。

定义见 MemoryRegion、AddressSpace 与 FlatView、FlatRange 与 MemoryRegionSection。这些对象与 QOM 的所有权树也有区别:owner 负责生命周期,container 描述映射位置。一个区域的所属设备不会因为映射地址改变而更换。

五、生成 FlatView、注册 KVM 与访问高端 RAM

1. 把区域树展开为可见区间

机器和设备逐步挂接区域,每次最外层内存事务提交时,内存框架处理待更新的地址空间。render_memory_region() 先展开高优先级子区域,再由当前终止区域填充剩余空洞。纯容器的 terminates 为 false,只有已展开的子区域会进入结果。别名先修正地址基址并递归到目标,只读等属性随路径向下传递。生成结果经过相邻区间合并,再建立 AddressSpaceDispatch。

对本例的高端 RAM,展开结果记录 GPA 起点为 4 GiB、长度为 6 GiB、目标区域为 pc.ram、区域起始偏移为 2 GiB。客户机通过高端别名找到后备内存的关系,被保存为可以直接查询的区间。低端的设备覆盖会进一步切分 RAM 可见范围,因此两个 RAM 别名不等于最终只有两个 FlatRange 或 KVM 内存槽。拓扑展开、生成视图和 dispatch

2. 将 GPA 与宿主地址登记到 KVM

KVM 的 MemoryListener 已在板级初始化之前注册。内存框架比较视图并通知区域变化;kvm_region_add() 先把新增区间排队,监听器的 kvm_region_commit() 再调用 kvm_set_phys_mem() 处理这些区间。对普通 RAM,它取得区域的宿主基址和偏移,填入 KVMSlot,最后经 kvm_set_user_memory_region() 调用 KVM 的内存注册 ioctl。宿主能力决定使用 KVM_SET_USER_MEMORY_REGION 或 REGION2 接口。

本例高端 RAM 提供的对应关系是 GPA 4 GiB → HVA B + 2 GiB,长度为 6 GiB。内核由此知道这一段客户机 RAM 的宿主用户地址,随后才能取得对应的宿主物理页并建立二级映射。内存槽注册本身不会预建全部 EPT;首次访问与失效处理见KVM 内存映射与 EPT 缺页处理。区间与宿主地址换算、更新排队与提交

3. 沿 GPA 0x100001000 找到同一份 RAM

现在访问客户机物理地址 0x100001000,即 4 GiB + 4 KiB。这个位置落入高端 RAM,换算后的后备偏移为 2 GiB + 4 KiB。若 QEMU 用户态通过 address_space_rw() 访问它,会读取 AddressSpace.current_map,由 dispatch 查找 MemoryRegionSection,再按以下关系计算:

区域内偏移 = 访问地址
           - section.offset_within_address_space
           + section.offset_within_region

高端 RAM:section 指向 pc.ram
  客户机访问 GPA       = 0x100001000
  section 的 GPA 起点 = 0x100000000(4 GiB)
  区域起始偏移        = 0x80000000 (2 GiB)

  后备偏移 = 0x100001000 - 0x100000000 + 0x80000000
           = 0x80001000
  宿主 HVA = B + 0x80001000

软件访问普通 RAM 时,QEMU 取得 RAMBlock 对应的宿主指针并复制数据;软件写入还需维护脏页等状态。客户机 vCPU 在 KVM 中执行普通 RAM 读写时,则通过硬件地址转换访问宿主页,映射建立后不会逐次调用 QEMU 的 memcpy。两条路径使用一致的内存布局,但执行访问的方式不同。

用户态查找中的 address_space_lookup_region() 先检查最近使用的 section,未命中再查分发结构,并处理小于页面的 subpage 分段。这使访问路径可以使用已生成的视图,无需每次重新遍历设备拓扑。section 查找与偏移转换、RAM 与设备读写分支

六、MMIO 的宽度与访问条件

设备创建 MMIO MemoryRegion 时,通过 memory_region_init_io() 登记 MemoryRegionOps 和设备状态指针 opaque。设备 BAR 等映射确定后,访问相应 GPA 会解析到这个叶子区域。需要用户态模拟的访问经 KVM_EXIT_MMIO 回到 QEMU,再由 address_space_rw()、memory_region_dispatch_read/write() 进入设备回调;回调接收区域内偏移、访问宽度和设备状态。

MemoryRegionOps 除了读写函数,还区分设备允许的访问和实现支持的访问宽度。valid 描述客户机可执行的操作,impl 描述回调能够处理的宽度。两者混用可能使非法操作被拆分后错误接受,或让合法操作无法进入设备。

字段含义
valid.min_access_size / max_access_size设备接受的宽度范围;与对齐、accepts 回调一起做访问校验
impl.min_access_size / max_access_size实现处理的宽度范围;内存框架可据此调整或拆分调用
endianness设备寄存器的字节序,参与访问值转换
read_with_attrs / write_with_attrs接收 MemTxAttrs,并返回 MemTxResult,表达成功或访问错误

例如设备允许 4 字节读取,回调只实现 1 字节宽度,框架可分成四次调用。带有“读取后清零”等副作用的寄存器必须按设备语义设置这些字段,不能为了适配回调任意拆分。内核设备与 ioeventfd 等加速路径还可以在内核完成相应处理,减少返回 QEMU 的次数。MemoryRegionOps、校验与读取分发

七、局部映射示例:别名、覆盖与空洞

上面的 Q35 布局说明了主 RAM 如何初始化。下面用一个独立的 64 KiB 教学模型演示区域覆盖,便于逐项核对地址;这里的 system、bus 和设备均为简化对象,不对应 Q35 的完整布局,也与 x86 的 64 KiB 端口 I/O 空间无关。

模型的低 32 KiB 为 RAM;在 0x2000 处放置长度为 16 KiB 的别名 window,指向独立设备总线 bus 的一段。bus 的两个叶子区域都是 4 KiB MMIO,分别位于 0x1000 和 0x3000,其余位置是纯容器留下的空洞。

system:容器,大小 0x10000
  ram:RAM,addr = 0,size = 0x8000,priority = 0
  window:别名,addr = 0x2000,size = 0x4000,priority = 2
           alias = bus,alias_offset = 0x1000

bus:独立容器,大小 0x8000
  regs-a:MMIO,addr = 0x1000,size = 0x1000
  regs-b:MMIO,addr = 0x3000,size = 0x1000

对 0x2004 的访问进入 window 后,先扣除窗口起点,再加上 alias_offset,得到 bus 地址 0x1004;regs-a 的局部偏移就是 4。设备创建时登记的 ops 和 opaque 在此用于处理寄存器访问。两个设备之外的空洞没有终止区域承接,查找会回到外层,继续使用被窗口覆盖的 RAM。例如 0x3004 对应 bus 的 0x2004,这里没有设备,最终命中 ram 的偏移 0x3004。

优先级只在同一容器的子区域之间比较。window 的 priority = 2 让其可见叶子遮住 priority = 0 的 ram;regs-a 自己的优先级不会拿去与外层 ram 比较。关闭 window 后,原位置重新显露 RAM,RAM 内容仍在原来的后端。别名也会裁剪目标范围,窗口之外的设备不会自动出现在 system 中。

memory_region_add_subregion_overlap() 保存优先级,子区域链表按优先级排序。启用状态、区域边界、别名偏移与嵌套裁剪都在拓扑展开时参与计算。挂接与排序、别名和子区域展开

八、交互演示:窗口开关与地址解析

局部映射:别名窗口与地址查找

独立的 64 KiB 简化模型。切换设备窗口和映射偏移,再选择地址,观察区域图怎样展开为可见区间。

默认配置下,0x2004 经 window 映射到 bus 的 0x1004,命中 regs-a 的偏移 4。0x3004 对应 bus 空洞,回落到 ram 的偏移 0x3004。关闭窗口后,这两个地址都由 ram 处理。启用 JavaScript 后可切换布局和查询地址。

这是固定布局的地址计算模型,不执行真实设备读写。可见区间按本例 4 KiB 边界计算;完整 QEMU 实现还支持更细的区域、访问拆分、权限和 IOMMU 转换。关闭窗口不会清除被遮盖的 RAM。

演示固定为 4 字节对齐访问,两块示例设备均接受该宽度。每次修改布局后立即生成新视图,再查询选定地址。默认布局的主要 FlatRange 如下;没有映射的尾部不会成为普通 RAM 或 MMIO 的 FlatRange。

system 地址范围目标区域起始区域偏移
0x0000—0x1fffram0x0000
0x2000—0x2fffregs-a0x0000
0x3000—0x3fffram0x3000
0x4000—0x4fffregs-b0x0000
0x5000—0x7fffram0x5000

这里 ram 被切成三个可见区间,但仍是同一个 MemoryRegion 和同一份后端内存。FlatRange 的切分描述可见性,不会把 RAMBlock 复制成三份;迁移也需要按实际 RAMBlock 识别内存。

九、布局更新与 KVM 内存槽

区域修改、视图发布与 KVM 更新
外层事务提交时生成新视图,比较区间并通知监听器。KVM 在监听器提交阶段处理内存槽增删;旧视图通过引用计数与 RCU 延迟回收。
外层事务提交时生成新视图,比较区间并通知监听器。KVM 在监听器提交阶段处理内存槽增删;旧视图通过引用计数与 RCU 延迟回收。 打开原图
Mermaid 源码
flowchart TB
  accTitle: 区域修改、视图发布与 KVM 更新
  accDescr: 持有 BQL 时在内存事务中修改区域,最外层提交生成 FlatView 和 dispatch。框架比较新旧区间,通知监听器并发布 current_map,随后监听器 commit 更新 KVM 内存槽。读者通过 RCU 使用视图,旧视图在不再使用后回收。
  subgraph CONFIG["配置更新 · 持有 BQL"]
    edit["调整区域、别名或启用状态"] --> commit["最外层 transaction_commit"]
    commit --> build["展开可见区间<br/>生成 FlatView 与 dispatch"]
    build --> diff["比较新旧区间<br/>region_del / region_add 等通知"]
    diff --> publish["发布 AddressSpace.current_map"]
  end
  diff -.-> pending["KVM listener<br/>缓存删除和新增项"]
  publish --> listeners["监听器 commit"]
  pending --> listeners
  listeners --> slots["KVM:先移除旧槽,再加入新槽<br/>必要时协调加速器访问"]
  subgraph ACCESS["运行时访问"]
    reader["进入 RCU 读侧临界区"] --> view["读取 current_map<br/>按 dispatch 查找 section"]
    view --> access["RAM 访问或 MMIO 回调"]
    access --> leave["退出 RCU 临界区"]
  end
  publish -.-> view
  publish --> old["旧视图引用释放<br/>经 RCU 延迟销毁"]
  classDef layout fill:#e4f1ec,stroke:#438575,color:#173e34;
  classDef backend fill:#eaf0f6,stroke:#8198ac,color:#263c4c;
  classDef lifecycle fill:#fff4db,stroke:#b89a55,color:#59491e;
  class edit,commit,build,diff,publish layout;
  class pending,listeners,slots,reader,view,access,leave backend;
  class old lifecycle;

1. 事务提交与新视图发布

调整多个区域时,设备可用 memory_region_transaction_begin() 与 memory_region_transaction_commit() 合并修改。事务允许嵌套,最外层提交才处理待更新布局;提交要求持有 BQL。它合并内存拓扑的发布,并不提供数据库式的任意错误回滚。

框架比较旧、新 FlatView,先发出删除通知,再处理新增、保持不变和日志状态变化。address_space_set_flatview() 发布新的 current_map;全局监听器 commit 随后执行。KVM 的 region_add / region_del 先把更新排队,在 kvm_region_commit() 中先移除旧槽、再加入新槽,对需要保护的重叠变化协调加速器访问。不能把用户态视图指针的替换理解为所有内核状态同时完成切换。

2. 覆盖变化时同步 KVM 内存槽

设备窗口覆盖普通 RAM 后,QEMU 的可见区间和 KVM 的可直接访问范围必须一同调整。原来覆盖较大范围的 RAM 槽可能需要删除,再按剩余可见区间重新注册;否则客户机仍可能通过旧映射直接访问已被设备窗口占用的地址。窗口关闭时,底层 RAM 重新可见,也需要恢复相应登记。这项工作随布局变化发生,RAMBlock 中未被改写的数据仍保留。区间删除与登记、KVM 更新事务

3. 等待旧视图的读者退出

并发读取还需要保证旧视图和区域存活。访问端进入 RCU 读侧临界区,FlatView 引用归零后用 call_rcu() 延后销毁。RCU 保护这里的视图生命周期,设备寄存器的并发读写仍须由设备及其执行环境同步。视图回收、读侧访问

十、预先展开与视图共享的收益和成本

设备配置会改变可见布局,KVM、软件访存和后端又都需要了解变化。QEMU 逐步把局部区域描述、全局可见映射和观察者通知分离:2011 年的内存事务接口允许组合布局变更;同年的内存映射观察接口把变化通知交给监听器。这使设备模型可以描述自身映射,不必各自直接维护所有使用方的状态。

2012 年的 per-AddressSpace dispatch让不同访问入口拥有相应的查找结构。随后多个地址空间可能引用同一个有效根,重复维护视图与分发树又带来额外成本;2017 年的 FlatView 共享使经别名等关系归一后根指针相同的地址空间可以复用结果。

预先展开减少了运行时反复解释拓扑的工作,但映射变更需要重建、比较和发布视图,并协调监听器及旧对象的生命周期。热插拔、BAR 重定位主要承担更新成本;普通 MMIO 和用户态 DMA 频繁使用已经生成的查找结果。

可在目标 QEMU 的 HMP monitor 中执行以下只读命令。第一条同时显示区域 owner/parent,第二条显示展开后的 FlatView。对照区域名称、起止地址与偏移,可以检查 BAR 窗口或 RAM 别名;这些输出描述 QEMU 映射,不列出客户机页表或完整 EPT 内容。命令定义

info mtree -o
info mtree -f

对照本例,可以先在区域树中找到 pc.ram 及高低端别名,再在平坦视图中核对高端 0x100000000 起点与 0x80000000 后备偏移。这样从启动参数到实际地址的关系就连起来了:容量决定后备大小,机器决定 GPA 布局,FlatView 记录当前可见性,KVM 内存槽把 RAM 布局传给内核。修改其中一层时,需要沿这条关系检查其他层是否同步。

十一、参考资料