Skip to the content.

qemu 中关于 page size 问题合集

  1. TARGET_PAGE_BITS 是如何确定的?
  2. 虚拟机和物理机的页大小不同 (虚拟机是 16k ,物理机是 4k 的页面)
  3. 虚拟机中使用大页,但是物理机中不是
  4. 哪些问题是二进制翻译特有的,哪些问题是 KVM 特有的?
    • kvm 的 stage 2 page table 还有自己独特的 tlb size 的
  5. pss_host_page_prepare 中,为什么热迁移需要考虑这个问题,还是说,这个是给 loadvm 用的

包括,需要对比一下,ram_save_host_page 和 ram_save_target_page 的区别是什么?

非当 tcg 模式下,还存在这个问题吗,也就是同构场景有没有这个问题、

  1. 似乎非常接近热迁移了,但是我来理解一下,为什么会有这种情况
    • dirty bitmap 为什么需要是热迁移的情况
  2. 需要注意,在热迁移的时候,自动转换为 4k 页面的

为什么 qemu 需要关系 page size

  1. migration 中

一共存在那些 page size

RAMBlock::page_size

如果启动 qemu 的时候,后端使用的是 HugeTLB ,可以发现其结果 PSize 就是 2MiB 的

(qemu) info ramblock
              Block Name    PSize              Offset               Used              Total                HVA  RO
                    mem0    2 MiB  0x0000000000000000 0x0000000300000000 0x0000000300000000 0x00007fe9c7c00000  rw
 0000:00:0d.0/gpu-fb-mem    4 KiB  0x0000000300100000 0x0000000001000000 0x0000000001000000 0x00007fe9aac00000  rw
    /rom@etc/acpi/tables    4 KiB  0x0000000301100000 0x0000000000020000 0x0000000000200000 0x00007fe9b4400000  ro
                 pc.bios    4 KiB  0x0000000300000000 0x0000000000040000 0x0000000000040000 0x00007fecc7e00000  ro
0000:00:05.0/virtio-net-pci.rom    4 KiB  0x0000000300080000 0x0000000000040000 0x0000000000040000 0x00007fe9c5600000  ro
0000:00:06.0/virtio-net-pci.rom    4 KiB  0x00000003000c0000 0x0000000000040000 0x0000000000040000 0x00007fe9c5400000  ro
                  pc.rom    4 KiB  0x0000000300040000 0x0000000000020000 0x0000000000020000 0x00007fe9c6200000  ro
   /rom@etc/table-loader    4 KiB  0x0000000301300000 0x0000000000001000 0x0000000000010000 0x00007fe9b4200000  ro
      /rom@etc/acpi/rsdp    4 KiB  0x0000000301340000 0x0000000000001000 0x0000000000001000 0x00007fe9abe00000  ro

显然,

  - qemu_fd_getpagesize() 对 hugetlbfs 返回 statfs.f_bsize,否则返回普通 host page size,见 util/mmap-alloc.c:60
  - 建立 RAMBlock 时做正确的对齐和大小检查。QEMU 要保证 align、offset、memory size 都是 page_size 的整数倍,否则映射本身就不合法,
  - 做 discard / balloon / poisoned-page recovery 时按正确粒度操作。比如 system/physmem.c:4094 强制要求地址和长度按 rb->page_size 对齐,而且会根据
    rb->page_size == qemu_real_host_page_size() 决定能不能走 madvise,hugetlb 场景更多要走 fallocate
  - 做单页 remap 时必须尊重 RAMBlock 的 backing page size。QEMU 在 system/physmem.c:2656 里先取 qemu_ram_pagesize(block),再按这个粒度向下对齐和重
    建映射
  - virtio-balloon/virtio-mem 这类按页回收、热插拔的设备逻辑,需要知道 host 侧真实粒度。比如 balloon 遇到 rb_page_size > 4K 时,要先累计完整一个
    host page 才能 discard,见 hw/virtio/virtio-balloon.c:92。
- virtio-mem 也要求 block size 不能小于 qemu_ram_pagesize(rb),见 hw/virtio/virtio-

  一个关键区分是:
  - TARGET_PAGE_SIZE:guest/target 架构页大小,见 include/exec/target_page.h:42
  - RAMBlock::page_size:某个具体 RAMBlock 的 backing page size,可能是 4K,也可能是 hugetlb 的 2M/1G

qemu_real_host_page_size()

和操作系统有关:

/* Using intptr_t ensures that qemu_*_page_mask is sign-extended even
 * when intptr_t is 32-bit and we are aligning a long long.
 */
static inline uintptr_t qemu_real_host_page_size(void)
{
    return getpagesize();
}

TARGET_PAGE_SIZE

TARGET_PAGE_SIZE 不是总动态的。也就是基本上是静态的,我的天啊

它的典型用途是:

原来 TARGET_PAGE_SIZE 是写死的,既然如此,那么我就真的感觉到很奇怪了, 那么岂不是可以无视 guest 内核的 page size ?

源码分析

基于 qemu master (05e27e70df42) 的源码结论,对应回答上面的问题。

TARGET_PAGE_BITS 是如何确定的

固定值来自各 target 的 cpu-param.h,不是 meson 配置。meson 会把 configs/targets/*.mak 的键值写进 <target>-config-target.h(meson.build:3365),但 .mak 里只有 TARGET_ARCH 等,没有 page bits。per-target 编译单元定义 -DCOMPILING_PER_TARGET(meson.build:3950)后,include/exec/target_page.h:21-24 直接包含 cpu-param.h 取编译期常量:

可变架构的运行时机制:

只有 arm/aarch64(softmmu + user)真正可变;alpha/ppc/loongarch 仅 user 模式可变,softmmu 固定 12。

可变架构的热迁移兼容:migration/savevm.c:460-478 有 configuration/target-page-bits 小节,加载端比对(savevm.c:393)。

TARGET_PAGE_SIZE 是写死的,那岂不是可以无视 guest 内核的 page size?

是的,而且这正是设计:

所以 “TARGET_PAGE_SIZE 无视 guest 内核 page size” 不是缺陷,是 QEMU 内部管理粒度与 guest 页表语义的解耦。

qemu_real_host_page_size()

RAMBlock::page_size 的来源链

虚拟机 16k 页 / 物理机 4k 页,怎么兼容

QEMU 侧没有 guest/host page size 一致性检查。KVM 注册内存时 userspace_addr 直接传给 KVM_SET_USER_MEMORY_REGION(kvm-all.c:388),section 对齐只用 host page(kvm_align_section,kvm-all.c:328)。16k guest 页拆成 4 个 4k host 页映射完全在内核 KVM stage2 里完成,target/arm/kvm.c 里没有任何 granule 处理。唯一的 QEMU 侧约束是断言 TARGET_PAGE_SIZE <= qemu_real_host_page_size()(kvm-all.c:2920)。

虚拟机用大页,物理机不是(THP)

TCG 特有 vs KVM 特有的问题

TCG 特有:

KVM 特有:

两者共用的换算点:KVM 按 host 页粒度上报 dirty bitmap 后,QEMU 用 hpratio = host/target 展开进 TARGET_PAGE_SIZE 粒度的 dirty bitmap(system/physmem.c:1221,hpratio==1 走整块 OR 快路径,否则逐位展开)。

同构场景(host 4k + guest 4k,x86 on x86)这些问题基本退化:hpratio==1,host page == target page,ram_save_host_page 的循环一次只发一页。page size 问题本质上是 arm(可变 granule / 64k host 页)和大页后端引入的。

热迁移中的 host page 与 target page

回答 “ram_save_host_page 和 ram_save_target_page 的区别”:

“On systems where host-page-size > target-page-size it will send all the pages in a host page that are dirty”。 根本原因是 postcopy:目标端必须原子地放置整个 host page(可能是大页), 所以源端必须把一个 host page 的所有 target page 连续发送(目标端注释 ram.c:3860)

回答 “pss_host_page_prepare 是热迁移还是 loadvm”:

回答 “热迁移时自动转换为 4k 页面”:

postcopy 的 page size 校验(两端必须一致):

目标端恢复逻辑(ram_load_postcopy,ram.c:3809-3961):

dirty ring:ring 项 offset 按 host page 解释(kvm-all.c:953-972),汇总时同样走 hpratio 展开。

总结:一共哪些 page size

  1. TARGET_PAGE_SIZE:QEMU 内部管理粒度(软 TLB、dirty bitmap、迁移协议),编译期确定,arm 可运行时调小。与 guest 内核实际页大小无关
  2. guest translation granule:arm 的 4k/16k/64k,guest 自己选,TCG 在 page walk 里感知,KVM 下完全不可见
  3. qemu_real_host_page_size():host 普通页,KVM dirty log 粒度、对齐断言的基准
  4. RAMBlock::page_size:该块的 host backing 粒度(大页时 2M/1G),决定 discard/balloon/postcopy 放置的原子单位
  5. multifd 包、XBZRLE、zero page 检测全部回到 TARGET_PAGE_SIZE 粒度

host page size vs target page size

看了相关代码,核心原因是:脏页跟踪、零页检测、发送协议都是按 target page(4K)粒度工作的, 按整个 host page(比如 2M 大页)发送会浪费大量带宽。具体来说:

那为什么还要以 host page 为单位迭代(而不是纯 4K 自由扫描)?因为 host page 边界仍然有意义:

一句话:以 host page 为扫描单位是为了内存布局和写保护的正确性,以 4K 为发送单位是为了带宽效率和协议兼容。

不是真的

当前代码直接说明了这一点:

hpratio = qemu_real_host_page_size() / TARGET_PAGE_SIZE;

如果 host page 比 target page 大,一个 KVM dirty bit 会展开成多个 QEMU dirty bits:

KVM bitmap: 1 bit / host page │ ▼ QEMU bitmap: hpratio bits / host page 1 bit / TARGET_PAGE_SIZE

例如 ARM64 64K host 上运行 TARGET_PAGE_BITS=12 的 QEMU:

在常见的 x86 4K host 上,host PAGE_SIZE == TARGET_PAGE_SIZE == 4K,两者碰巧一致,所以容易产 生“KVM 都按 TARGET_PAGE_BITS 记录”的印象。结论是:

KVM 侧按 host page 粒度记录;QEMU 内部按 TARGET_PAGE_BITS 保存,二者在同步时转换。

有点这个意思,但是不是完全对的

• 仅针对 KVM dirty-log buffer,确实应该直接使用 qemu_real_host_page_size()——而 QEMU 当前就是这么做的。

TARGET_PAGE_SIZE 是 QEMU 公共内存层的逻辑粒度,不能换成 host page size,因为两者解决的问题不同。

概念 含义 例子 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━ TARGET_PAGE_SIZE QEMU 为目标架构选择的最小内存管理粒度 ARMv7 通常 4 KiB ──────────────────────────── ──────────────────────────────────────── ────────────────────────── guest OS page size Guest 当前实际配置的页大小 AArch64 可为 4K/16K/64K ──────────────────────────── ──────────────────────────────────────── ────────────────────────── qemu_real_host_page_size() QEMU 进程所在宿主 OS 的 base page size ARM64 host 可能是 64 KiB ──────────────────────────── ──────────────────────────────────────── ────────────────────────── RAMBlock page size RAM backend 的实际映射粒度 2 MiB hugepage

注意,TARGET_PAGE_SIZE 也不一定等于 guest OS 当前使用的 page size;它一般是目标架构需要支持的最小粒度。

### 为什么公共层不能直接用 host page

假设:

TARGET_PAGE_SIZE = 4 KiB host PAGE_SIZE = 64 KiB

KVM 只能报告:

这个 64 KiB host page 脏了

QEMU 将它转换成:

16 个连续的 4 KiB target pages 都脏了

即:

hpratio = qemu_real_host_page_size() / TARGET_PAGE_SIZE;

对应 system/physmem.c:1221。

这样虽然会多迁移一些数据,但不会漏掉脏页。反过来,如果 QEMU 公共 dirty bitmap 也只有 64 KiB 粒度,那么设备模拟、TCG 等能 够精确发现的 4 KiB 写入也只能被放大为 64 KiB。

### TARGET_PAGE_SIZE 还有很多非 KVM 用途

它不仅服务 dirty logging,还决定:

如果这些都使用 host page size,那么同一虚拟机在 4K host 和 64K host 上会具有不同的:

这会让虚拟机模型被宿主机实现细节污染。

### 为什么选择较小的公共粒度

QEMU 采用的是“公共层使用 target 最小粒度,各数据源向它转换”:

KVM dirty log(host page) │ 展开 ▼ QEMU dirty bitmap(target page) ▲ │ 直接标记 TCG / DMA / 设备模拟

粗粒度信息可以安全地展开成多个细粒度 dirty bits;细粒度信息一旦存进粗粒度 bitmap,就无法恢复。

所以可以概括为:

qemu_real_host_page_size() 是宿主资源和 KVM ABI 粒度;TARGET_PAGE_SIZE 是 QEMU 虚拟机模型的公共、可迁移粒度。KVM 层使 用前者,进入 QEMU 公共内存层后转换成后者。

再一次分析下

vhost dirty log 的粒度固定为 4 KiB,它和 host PAGE_SIZE、guest 页表大小是相互独立 的。兼容策略是:在合并到 QEMU dirty bitmap 时,按 QEMU 的页粒度向上合并,允许多标 脏,但绝不能漏标。

QEMU 中:

#define VHOST_LOG_PAGE 0x1000

见 include/hw/virtio/vhost.h:43。

内核 vhost 写 dirty log 时,本质是:

bit = guest_physical_address / 4096

一个 bit 始终代表一段 4 KiB 的 GPA。

QEMU 同步 vhost log 时,把每个置位 bit 转换为:

page_addr = addr + bit * VHOST_LOG_PAGE; memory_region_set_dirty(mr, mr_offset, VHOST_LOG_PAGE);

见 hw/virtio/vhost.c:115。

假设 QEMU dirty bitmap 的粒度真是 64 KiB:

一个 QEMU dirty bit ┌────────────────────────── 64 KiB ──────────────────────────┐ │ vhost bit0 │ bit1 │ bit2 │ … │ bit15 │ │ 4K │ 4K │ 4K │ │ 4K │ └────────────────────────────────────────────────────────────┘

只要这 16 个 vhost bit 中任意一个被置位,QEMU 就会把对应的整个 64 KiB 页标脏。实 现依据是:

end = TARGET_PAGE_ALIGN(start + length) » TARGET_PAGE_BITS; page = start » TARGET_PAGE_BITS;

见 system/physmem.c:1011。

例如:

vhost 报告 GPA 0x3000~0x3fff 被写 TARGET_PAGE_SIZE = 64 KiB

start page = 0x3000 » 16 = 0 end page = ALIGN_UP(0x4000, 64K) » 16 = 1

结果:QEMU dirty bit 0 被置位 代表整个 0x0000~0xffff 都需要重新迁移

这是一种保守合并:

不过,“x86 使用 64 KiB 页面”需要区分具体指什么:

另一个容易混淆的反向情况是 64 KiB host 上的 KVM dirty log。KVM dirty log 的 bit 粒度是 host page,即 64 KiB;QEMU 会通过:

hpratio = host_page_size / TARGET_PAGE_SIZE;

把一个 64 KiB KVM dirty bit 扩展为 16 个 4 KiB QEMU dirty bits,见 system/ physmem.c:1211。

所以可以概括为:

vhost dirty:固定 4 KiB → 必要时合并到更大的 QEMU 页 KVM dirty:host PAGE_SIZE → 必要时拆分成更小的 QEMU target 页

两条路径都采取保守标脏,因此最多影响迁移效率,不影响正确性。

等等

cat /sys/kernel/mm/transparent_hugepage/enabled cat /sys/kernel/mm/transparent_hugepage/shmem_enabled

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。