virt-report虚拟化社区动态

QEMU 后拷贝迁移的缺页处理与页面传输

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

一、后拷贝迁移的切换与按需传页

预拷贝在源端持续运行虚拟机,同时反复发送被修改的内存页。写脏速度接近传输速度时,剩余内存可能长时间无法降到停机预算以内。后拷贝将执行切到目的端,再通过后台传输和缺页请求补齐内存,从而结束源端不断产生新脏页的循环。

本文分析 Linux 上普通 RAM 的后拷贝路径:切换时如何处理旧页面,目的端如何请求缺失页,以及接收线程怎样安装页面并恢复访问。先按单条页面传输通道展开,再讨论紧急通道、大页和故障恢复。

源码版本:QEMU 10.2.0、Linux 6.18。

预拷贝的迭代与停机条件见《QEMU 预拷贝迁移的内存迭代与停机控制》;记录格式见《QEMU 迁移流格式与状态恢复》。

二、目的端先执行,缺失页按需补齐

后拷贝开始后,源端 vCPU 停止执行,目的端恢复 CPU 和设备状态并开始运行。此时内存分为两部分:已经在目的端可访问的页面,以及仍需从源端取得的页面。目的端执行线程访问后一部分时,宿主内核暂时阻塞这次访问,由 QEMU 获取缺失内容。

这里有三组职责:目的端宿主内核捕获缺失映射并等待供页;目的端 QEMU 的缺页线程发出请求,接收线程安装页面;源端 QEMU 继续提供尚未交付的 RAM。源端客户机已经停止,源端 QEMU 进程仍然存活;目的端客户机已经开始执行,却仍依赖跨主机供页。

源端继续扫描待发送位图,并优先处理目的端请求的页面。随着页面到达,目的端可访问范围扩大,直到全部内存完成交接。客户机后续写入发生在目的端;源端保留的旧副本不能再覆盖已经交付的页面。

例如切换时 P0 已有最新副本,P1 从未传输,P2 只传过旧版本。目的端可立即访问 P0;P1 需要补齐;P2 必须先丢弃旧映射,再按缺失页处理。三种页面状态解释了后续流程的顺序:先确定哪些页有效,再开放执行,最后由缺页请求和后台传输完成其余交接。

三、切换与旧页面丢弃

1. 清理预拷贝留下的过期副本

假设 P2 在预拷贝时发送过 v1,之后源端又把它写成 v2。如果目的端带着 v1 开始执行,这次访问不会缺页,却会读到旧内容。因此,停住源端后还要做最终脏页同步,并通知目的端丢弃所有仍待发送的页,包括已传过但再次变脏的页和从未传过的页。

ram_postcopy_send_discard_bitmap() 先同步迁移位图,再按 RAMBlock 发送丢弃范围。如果 RAMBlock 的宿主页比迁移目标页大,同一宿主页中只要还有待传部分,就要把这一整组目标页统一标为待发送,目的端也按宿主页边界丢弃。这样后续可以一次安装完整宿主页。丢弃范围与宿主页对齐

2. 先让页面接收路径就绪,再恢复执行

  1. 停住源端停止客户机执行,完成非后拷贝迭代设备的收尾,最后同步 RAM 脏页。
  2. 发送 DISCARD目的端移除过期或尚未有效传输的页面映射,并清除相应接收位。
  3. 处理 LISTEN 与设备状态建立 userfaultfd 缺页处理和页面接收路径,再加载 CPU 与设备状态。
  4. 处理 RUN同步恢复的状态;通常在自动启动条件下恢复目的端 vCPU 和设备执行。

源端将 POSTCOPY_LISTEN → CPU / 设备状态 → POSTCOPY_RUN 放入 CMD_PACKAGED。目的端先把整个包读入内存,再解析其中内容;独立的接收线程此时可以继续从迁移通道取页。这个顺序同样服务于设备状态加载:设备的加载函数可能访问客户机 RAM,并在 vCPU 尚未运行时就触发缺页。切换与状态打包、LISTEN 与 RUN

四、一次缺页的往返

1. 从宿主地址定位到 RAMBlock

目的端为 RAM 映射注册 userfaultfd 的 missing 模式。执行线程访问缺失映射时,内核生成 uffd_msg,其 event 为 UFFD_EVENT_PAGEFAULT;arg.pagefault.address 给出目的端进程中的宿主虚拟地址,flags 区分读写等属性。这个地址不能直接发给源端使用,因为两端 QEMU 的虚拟地址布局可能不同。

postcopy_ram_fault_thread() 用 qemu_ram_block_from_host() 找到 RAMBlock 和块内偏移,再向下对齐到该块的宿主页大小。请求据此转换为“哪个 RAMBlock、从哪里开始、需要多长”。这里的缺页属于宿主内存交付过程,不要求把一个普通的客户机页错误注入给客户机操作系统。缺页地址转换

示例:P2 位于 RAMBlock "pc.ram" 的偏移 0x2000,宿主页 = 4096 B
目的端访问 address = 0x10002020,RAMBlock.host = 0x10000000
块内偏移 0x2020 → 对齐到 start = 0x2000,len = 4096

返回通道消息:type: BE16 | payload_length: BE16 | payload
REQ_PAGES_ID:start: BE64 | len: BE32 | id_length: u8 | "pc.ram"
REQ_PAGES:   start: BE64 | len: BE32
同一 RAMBlock 的后续请求可省略 id,复用返回通道上一次的块名。

前面已经丢弃 P2 的 v1 副本,这次访问等待的是源端保留的 v2。目的端先检查接收位图,再记录尚待解决的页面并发送请求;源端的返回通道线程校验 RAMBlock 和范围,随后交给页发送路径。单流模式优先取请求队列,队列为空时继续扫描后台页面。请求到达时,后台可能已经发送过该页;get_queued_page() 会检查发送位图并跳过这类请求。请求编码与已接收检查、源端请求去重

缺页请求与页面交付

先按“下一步”完成切换,再选择页面读写。可以让后台先发一页,在它到达前访问同一页,观察请求与传输的交错。

  1. 源端停机并同步脏页。目的端丢弃 P2、P5 的旧副本,以及 P3、P6、P7 的未传范围。
  2. LISTEN 建立缺页与接收路径,加载 CPU / 设备状态;RUN 恢复目的端执行。
  3. vCPU 访问缺失的 P2,产生 userfaultfd 事件并等待。QEMU 将宿主地址转为 pc.ram 中的偏移 0x2000。
  4. 返回通道请求 start = 0x2000、len = 4096。源端检查发送位图,优先发送 P2。
  5. 目的端收齐 P2,通过 UFFDIO_COPY 原子安装并唤醒;更新接收位图,访问继续。
  6. 后台补齐其他页。目的端对已交付页面的新写入不再被源端旧副本覆盖。

启用 JavaScript 后可逐步观察页面、位图和请求字段。

示例只取一个 RAMBlock 的 8 个 4 KiB 普通页,源端版本在切换后固定。一次“下一步”表示逻辑处理阶段;接收位图记账与内核唤醒合并显示。单 vCPU、单页面通道,不展开 preempt、大页、外部共享内存访问和故障恢复。版本号表示页内容变化,不表示耗时或带宽。

2. 完整页面安装后,访问才能继续

ram_load_postcopy() 按流格式读取页数据。如果宿主页包含多个迁移目标页,接收线程先在临时缓冲中收齐它们,再调用 postcopy_place_page()。宿主页与目标页相同的普通页面可以直接利用 QEMUFile 缓冲,仍要等整个页内容到齐后安装。

UFFDIO_COPY 将完整页原子地装入缺失映射:被阻塞的线程不会看到只填了一半的页。QEMU 在这条普通 RAM 路径中没有设置 DONTWAKE,因此内核同时唤醒等待访问;随后 QEMU 更新接收位图,移除待解决请求并结算阻塞时间。这里的“原子”指页面对访问者的可见性,并不表示网络传输、位图更新与线程调度发生在同一时刻。接收与整页组装、安装与完成记录

后拷贝缺页处理的线程协作
图示普通单流、4 KiB 宿主页路径。缺页线程发送请求,接收线程安装完整页;内核唤醒访问线程之后,QEMU 完成接收位图与请求记录的更新。
图示普通单流、4 KiB 宿主页路径。缺页线程发送请求,接收线程安装完整页;内核唤醒访问线程之后,QEMU 完成接收位图与请求记录的更新。 打开原图
Mermaid 源码
sequenceDiagram
    accTitle: 后拷贝缺页处理的线程协作
    accDescr: 目的端访问缺页后由内核阻塞,缺页线程将宿主地址转换为 RAMBlock 与页偏移并请求源端。源端在单流路径优先处理未发送的请求页;目的端接收线程收齐宿主页后通过 UFFDIO_COPY 安装,内核唤醒访问线程,QEMU 随后更新接收位图并删除请求记录。
    participant V as 目的端访问线程
    participant K as 目的端内核
    participant F as 目的端缺页线程
    participant S as 源端 QEMU
    participant R as 目的端接收线程
    Note over V,R: 后拷贝已开始;源端 vCPU 停止,目的端运行
    V->>K: 访问缺失页 P2
    Note over V: 访问被阻塞
    K-->>F: userfaultfd:PAGEFAULT 与宿主地址
    F->>F: 转换为 RAMBlock 与对齐偏移<br/>检查 receivedmap,登记 page_requested
    F->>S: 返回通道 REQ_PAGES_ID<br/>pc.ram,start = 0x2000,len = 4096
    S->>S: 请求入队,优先选择 bmap 中未发页<br/>已由后台安排发送的页不重复发送
    S-)R: 页面传输通道:P2 的页内容
    Note over F,R: 请求与页面接收由不同线程推进
    R->>R: 收齐完整宿主页
    R->>K: UFFDIO_COPY<br/>完整页原子安装,未设置 DONTWAKE
    K-->>V: 唤醒等待访问
    K-->>R: ioctl 成功返回
    par 访问线程获调度
        V->>V: 继续读写已安装页面
    and 接收线程完成记账
        R->>R: receivedmap 置位<br/>删除 page_requested,结算阻塞时间
    end

全零页可以使用内核支持的 UFFDIO_ZEROPAGE;不支持该方式的 RAMBlock 则通过零缓冲完成整页安装。共享内存的外部访问者还涉及各自的 userfaultfd 和唤醒协作,后端必须具备相应支持。

五、源码:丢弃、请求与原子装页

上面的 P2 从缺失到可用,要经过请求登记、源端调度、网络传输和目的端安装。两端分别记录各自进度,下面四类对象支撑这一过程:

结构由谁维护一个比特或条目表示什么
RAMBlock.bmap源端 RAM 迁移线程;preempt 路径还与请求线程协作置位表示该页仍需发送。后拷贝期间源端不再运行客户机,成功安排传输后逐步清除。
RAMBlock.receivedmap目的端页面接收路径记录已接收的页面;安装成功后更新,可避免对已到达页继续请求,并用于恢复时同步进度。
MigrationIncomingState.page_requested目的端缺页与接收线程,在互斥锁下交接按对齐的宿主地址记录尚待解决的页面,安装完成后删除。
RAMSrcPageRequest 队列源端返回通道线程入队,RAM 迁移线程消费单流模式下的请求范围,以 RAMBlock 和块内偏移定位。

源端“已发送”和目的端“已安装”是不同进度:两者之间还有网络、接收缓冲和安装操作。故障恢复需要核对目的端实际收到的页面,不能只依赖源端已经清掉的位。

环节入口与读写的状态
准备切换postcopy_start():停源、发送 discard,打包 LISTEN、设备状态与 RUN。
丢弃旧映射ram_postcopy_send_discard_bitmap() / ram_discard_range():发送最后待传范围,清除目的端接收位与旧映射。
处理缺页事件postcopy_ram_fault_thread():读取 uffd_msg,将宿主地址换成块内页偏移。
请求页面migrate_send_rp_req_pages():检查 receivedmap、维护 page_requested,编码返回通道请求。
调度发送ram_save_queue_pages()、ram_find_and_save_block():处理紧急请求或排队,结合 bmap 选择未发页。
安装与唤醒ram_load_postcopy()、postcopy_place_page():收齐宿主页,原子安装,更新接收与请求状态。

可以沿 P2 追踪两端位图:切换后源端 bmap[P2] 为 1、目的端 receivedmap[P2] 为 0;源端发送后前者清零,但后者仍需等安装完成才置位。目的端之后对 P2 的写入留在当地,源端已清除的待发送位保证正常后拷贝扫描不会再次发送旧 P2。

六、收敛收益、缺页延迟与故障代价

1. 停机结束之后仍可能等待内存

后拷贝将“所有剩余内存复制完毕”从恢复执行的前提中移走,但 CPU 与设备状态切换仍需时间。恢复之后的缺页等待则包含事件处理、请求往返、源端排队、页数据传输及安装。这类延迟直接影响访问该页的执行线程,不能只看迁移停机时间就判断应用影响。

运行指标应同时观察切换停机时间、总迁移时间、请求数、应用尾延迟,以及启用 postcopy-blocktime 后的阻塞统计。postcopy-vcpu-blocktime 给出各 vCPU 的累计阻塞,整体 postcopy-blocktime 统计所有 vCPU 同时因缺页受阻的重叠时间;这些数值与客户端看到的业务停顿并不等价。

2. 大页与通道排队

请求和原子安装采用 RAMBlock 宿主页粒度。一个 2 MiB 大页中的少量访问,可能需要等待整页到齐;页越大,传输和组装的等待越明显。减少缺页数与缩短单次缺页等待之间存在取舍,需结合内存后端和负载局部性判断。

单流模式中,请求页即使被优先选中,也不能越过已经排入同一通道的数据。postcopy-preempt 为紧急页面提供独立通道,源端返回路径线程可直接发送请求页,减少在后台数据后的排队;网络带宽、CPU 和目的端安装开销仍然共享。

QEMU 10.2 可以同时启用 MultiFD 与后拷贝,但 MultiFD 在这里加速的是前面的预拷贝阶段。切换前 ram_save_postcopy_prepare() 会 flush 并同步 MultiFD,确保目的端运行前已完成这些非原子的页加载;后拷贝页由主迁移通道或 preempt 紧急通道处理。MultiFD 切换屏障。多通道同步本身见《QEMU MultiFD 的任务分发与跨通道同步》。

3. 中断恢复与状态丢失

连接中断时,只要两端进程和内存仍完整,QEMU 可以进入 POSTCOPY_PAUSED,重新建立通道、核对接收位图并继续迁移。典型管理流程是在目的端用 migrate-recover 准备连接,再由源端执行带 resume=true 的 migrate。尚未安装的在途页面需要重新传送。

POSTCOPY_PAUSED 描述迁移状态,不能直接理解为整台虚拟机已经停止。目的端对已到达页的访问仍可能继续,访问缺失页的线程则等待恢复。若源端丢失仍未交付的内存,或目的端丢失已经产生的新执行状态,单靠重连无法还原这些内容。切到后拷贝之后,也不能像普通预拷贝失败那样直接回到源端继续运行。

七、后拷贝路径的演进

1. 2015:缺页通知与按需供页

QEMU 引入 userfaultfd 事件处理、反向页面请求和独立接收线程,把目的端的缺失映射转为源端可服务的页面需求。这个设计结束了必须等待预拷贝收敛才恢复执行的限制,也把部分性能压力转移到缺页延迟,并使迁移未完成时的状态依赖两端。缺页处理提交、返回路径请求提交

2. 2022:分离紧急页面与后台传输

独立的 preempt 通道将缺页响应与后台流分开;随后请求页发送移入返回路径线程,降低与后台扫描的耦合。多个发送上下文共享迁移位图,因此还需要互斥与去重,保证页面交付后不再被旧副本覆盖。preempt 能力、请求线程直接发送

3. 2024—2025:恢复状态与预拷贝并行化

2024 年增加源端 POSTCOPY_RECOVER_SETUP,让管理层区分“正在重建连接”与“已进入恢复握手”,也能获得建连失败的状态事件。2025 年允许 MultiFD 与后拷贝共同启用,并在切换边界同步所有先前的页传输。前者改善故障处理的可观察性,后者缩短前半程传输;目的端缺页仍遵循完整页原子安装的约束。恢复建连状态、MultiFD 与后拷贝协作

八、参考资料