执行记录与重放
核心概念
记录/重放用于确定性地重现 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:日志最后一个事件。