执行记录与重放

核心概念

记录/重放用于确定性地重现 QEMU 执行。 记录阶段写入非确定性事件日志,之后可在任何位置不限次数地重放。 重放读取日志,复现外部输入、硬件时钟、中断等全部非确定性事件。

QEMU 多个部分加入了记录和重放调用。 从外部设备接收非确定性输入的模型,会立即把每次外部事件写入日志, 例如网络包到达虚拟网卡时就记录。

事件虽都来自这些设备,重放还必须知道它们发生的时刻。 通过统计相邻事件之间执行的指令数来标识时刻。

实现细节可参阅以下学术论文:

相关修改包括:

  • 封装时钟和时间函数,将返回值记录到日志;

  • 保存系统关闭等异步事件;

  • 同步 BH 执行;

  • 同步线程池线程;

  • 记录和重放鼠标、键盘、麦克风输入;

  • 添加内部检查点,同步 CPU 和 I/O;

  • 添加网络过滤器,记录和重放数据包;

  • 添加块驱动,使块层具有确定性;

  • 记录和重放串口输入;

  • 记录外部来源的随机数。

指令计数

使用此功能必须启用 icount。 它最初用于在没有外部输入时保证虚拟机执行确定, 现在也控制非确定性事件的发生时刻。 记录时写入自上次事件以来的执行指令数, 重放时根据计数预测何时注入事件。

锁与线程同步

过去主线程与 vCPU 通过 BQL 同步。 但系统一直在缩短持有 BQL 的时间,包括 TCG 系统模拟。 事件批次仍必须保持顺序,例如主线程的定时器到期和检查点, 与 vCPU 写入的指令检查点需要协调,因此引入另一把锁保证同步推进。

现在由 replay_mutex_lock 承担此职责。 它以前只在写单个事件时持有,现在覆盖整个执行时段, 使两个主要线程以确定顺序交替运行。

BQL 比 replay_lock 粒度更细,因此持有 BQL 时再获取 replay_mutex_lock 几乎肯定是缺陷,也会导致死锁,代码通过断言强制这一规则。 解锁通常反序,但非必需;可在仍持有 BQL 时释放 replay_lock, 无需执行复杂的 unlock_iothread/replay_unlock/lock_iothread 序列。

检查点

重放受到非确定性来源制约,包括时钟、外设输入和 QEMU 线程调度。 调度会影响定时器、异步 I/O 和 BH 的处理。

定时器调用伴随时钟读取及虚拟机状态变化。 读取主机时钟产生非确定性数据,状态变化又必须保持顺序, 重放时的回调相对顺序必须与记录时一致。 因此,记录模式处理某个时钟时会写入特殊检查点事件。 这里的检查点不是虚拟机快照,只是用于同步的记录/重放事件。

重放期间,QEMU 可能在任意实际时刻尝试处理定时器。 读取到对应检查点之前,不处理那组定时器, 由此同步 CPU 执行和定时器事件。

另外两个检查点控制虚拟时钟的跳进。 虚拟机空闲时,虚拟时钟按每实际纳秒增加 1 ns。 做法是在虚拟实时时钟设置 warp timer,使其在下一虚拟时钟期限触发; 定时器触发或 CPU 需要退出空闲时,立即推进虚拟时钟。 两个函数修改状态,都必须具有确定性,因此各创建一个检查点: icount_start_warp_timer 检查 CPU 是否空闲,是则开始累计实际时间; icount_account_warp_timer 在 CPU 收到中断或 warp timer 触发时调用, 将自 start 以来经过的实际时间加入虚拟时钟。

虚拟设备

通过 icount 启用的记录/重放要求虚拟设备满足: icount 执行期间,所有影响客户机状态的行为都必须确定。

定时器

定时器在指定时刻执行各子系统回调,有以下类别:

  • 实时时钟:基于主机时间,只用于不改变虚拟机状态的回调, 因此不影响确定性重放。

  • 虚拟时钟:只在模拟执行时运行,icount 模式根据指令计数计算, 完全确定,无需记录。

  • 主机时钟:供模拟真实时间来源的设备使用,例如 RTC 芯片。 它是非确定性来源,读取必须记录。

  • 虚拟实时时钟:类似实时时钟,但只用于虚拟机睡眠时推进虚拟时钟。 与主机时钟一样非确定,也需要记录。

改变客户机状态的定时器都应使用虚拟时钟,从而保证确定性。

不直接改变客户机状态的事件可以使用实时时钟。 若时钟速度应随虚拟机执行速度变化,则使用带 EXTERNAL 属性的虚拟时钟。 它不确定,但速度取决于客户机执行, 供位于重放客户机之外的虚拟设备使用,例如 slirp 路由设备。

块设备

blkreplay 在块驱动栈顶部拦截 bdrv 协程函数调用。

协程将所有块完成操作加入队列。 队列刷新时,已处理请求信息写入日志;重放时则与日志事件匹配。 因此块设备请求以确定顺序处理。

Bottom Half

影响客户机状态的 BH 回调应通过 replay_bh_schedule_event 或 replay_bh_schedule_oneshot_event 调用。 记录模式保存调用,重放模式按已有日志同步。

模型中的磁盘 I/O 事件完全确定,因为两种模式都从相同磁盘状态启动。 但虚拟磁盘控制器的读写回调在两种模式中可能发生于不同时刻。

CPU 线程创建读写请求,块层随后创建包含回调及参数的 BH, 主循环持有 BQL 时处理它们。 这些锁操作不与重放同步,因为主循环还处理监控器交互等不影响虚拟机状态的事件。

因此需要将 BH 回调的记录和重放与 CPU 执行同步。 回调即将执行时加入重放模块队列,实际执行时写入日志。 重放模式只有读到相应日志事件后才处理回调。

块层有时为内部用途使用异步回调,例如读写快照或镜像簇表。 这些 BH 不标记为可重放,也不写入日志。

保存与恢复虚拟机状态

记录/重放依赖完整、确定的状态保存和恢复。 设备状态结构全部字段,包括虚拟定时器,都应由 loadvm 恢复为 savevm 前的相同值。

不要访问其他设备状态,因为保存和恢复顺序没有定义。 例如不应在 post_load 中调用 update_irq。 应显式保存所有必要信息,避免依赖关系导致恢复非确定。

停止虚拟机

停止客户机不应改变其状态,网络连接可能因远端超时断开是例外。 用户可在重放任意时刻停止虚拟机,之后重新启动不应因不必要的状态变化破坏重放。

重放日志格式

日志由头部和执行事件序列组成。 头部包含 4 字节版本 ID 和 8 字节保留字段。 每次格式变化都更新版本,防止使用其他 QEMU 构建生成的日志。

事件序列描述虚拟机状态变化,包括全部非确定输入、同步标记, 以及在正确时刻注入输入所需的指令计数。

同步标记即检查点,用于协调操作虚拟硬件的 QEMU 线程。 这些操作可能修改寄存器或生成中断,因此必须与 CPU 线程同步。

每个事件包含 1 字节事件 ID 和可选参数。 数组参数存为 4 字节长度,后接相应字节数据。 事件列表如下:

  • EVENT_INSTRUCTION:自上次事件以来执行的指令,后接:

    • 4 字节指令数。

  • EVENT_INTERRUPT:同步中断处理。

  • EVENT_EXCEPTION:同步异常处理。

  • EVENT_ASYNC:异步事件组,生成后入队,在 icount_account_warp_timer() 中处理。 各子事件具有以下 ID:

    • REPLAY_ASYNC_EVENT_BH:BH 回调,同步通常异步调用但影响虚拟机状态的回调,后接:

      • 8 字节操作 ID。

    • REPLAY_ASYNC_EVENT_INPUT:输入事件,包含按键按下/释放、鼠标移动等参数,后接:

      • 按事件类型为 9~16 字节。

    • REPLAY_ASYNC_EVENT_INPUT_SYNC:内部输入同步事件。

    • REPLAY_ASYNC_EVENT_CHAR_READ:发送方发起的字符设备输入,例如串口,后接:

      • 1 字节字符设备 ID。

      • 已读取字节数组。

    • REPLAY_ASYNC_EVENT_BLOCK:块设备操作,同步磁盘、闪存操作与 CPU,后接:

      • 8 字节操作 ID。

    • REPLAY_ASYNC_EVENT_NET:收到网络包,后接:

      • 1 字节网卡 ID。

      • 4 字节包标志。

      • 数据包字节数组。

  • EVENT_SHUTDOWN:用户发送关闭事件,例如关闭窗口。

  • EVENT_CHAR_WRITE:同步字符输出,后接:

    • 4 字节输出函数返回值。

    • 4 字节输出数组偏移。

  • EVENT_CHAR_READ_ALL:同步 QEMU 发起的字符输入,后接:

    • 已读取字节数组。

  • EVENT_CHAR_READ_ALL_ERROR:QEMU 发起的字符输入失败,后接:

    • 4 字节错误码。

  • EVENT_CLOCK + clock_id:主机时钟读取事件组,后接:

    • 8 字节时钟值。

  • EVENT_CHECKPOINT + checkpoint_id:同步 CPU、内部线程和异步输入的检查点。

  • EVENT_END:日志最后一个事件。