文档目录架构 · 18 篇

时钟与时间

规定 NVX 时间 ABI:客户机可见的时钟源、定时器速率、时间采样、停机时间与恢复后的时间连续性契约。

译自microsoft/nvx · doc/design/3.2-clock-and-time.md。技术细节以英文原文为准。

架构文档 > 3. 虚拟设备

NVX 时间 ABI 是每个后端和每条启动路径上唯一的、客户机可见的时间、定时器与时钟契约。它独立于机器 ABI 进行版本管理。每一条规则都是硬性检查:无法满足契约的宿主或快照会被拒绝并给出稳定的错误码,观察到违规的客户机会快速失败,没有任何规则会退回到另一种行为。

时间 ABI 落实了架构目标中“使客户机可见的机器独立于所选 hypervisor”这一目标,并将其应用于时钟和定时器。它提供:

  • 在每个后端和每条启动路径上相同的 CPUID 身份、合成 MSR、CPU 时间位、时钟源、滴答、定时器和恢复语义。
  • 不需要客户机内核补丁,也不需要在内核命令行上传递时钟参数。
  • 在同一后端内、跨同一 CPU 世代和 CPU 配置文件且 TSC 速率在较小容差内匹配的宿主之间进行恢复,包括宿主重启之后。
  • 随快照停机时间推进的客户机单调时间,以及每次恢复都会设置并随后校正的挂钟时间。
  • 跨捕获与恢复从不触发的内核停转、锁死、时钟源和 TSC 跃迁检测器。

客户机看到:

  • 一个在每种处理器电源与频率状态下都以恒定速率计数的 TSC(不变 TSC),并把它作为唯一的时钟源。
  • 通过最小化的合成 hypervisor 身份提供声明的 TSC 与 LAPIC 定时器速率。
  • 一个以后端固定速率运行的单次触发 LAPIC 定时器。
  • 一个由客户机在启动时禁用的 PIT。
  • 一个锚定到 UTC 的可编程 RTC。

不变 TSC 保持某个给定宿主上的单一速率;它并不让每个宿主都有相同的速率。客户机 TSC 以其宿主的原生速率运行,该速率在同一 CPU 世代的不同宿主之间可能不同,也不属于 CPU 配置文件的一部分,因此客户机从一个频率 MSR 读取声明速率,而不是从 CPUID 推导它。冷启动声明其宿主的速率,而快照只能恢复到一个速率落在该声明的 250 ppm 以内的宿主上;参见速率。

表 3.4 列出了时间 ABI 所使用的记号。

表 3.4. 时间 ABI 记号。

符号 含义
F 声明的客户机 TSC 速率,单位 Hz
F_s 快照中记录的声明速率
F_d 目标分区的原生 TSC 速率,单位 Hz
L LAPIC 定时器速率,单位 Hz
C 机器的处理器容量
D 快照停机时间,单位纳秒
T_c 捕获锚点处第一个处理器的客户机 TSC
Q 从静默时的虚拟机时间切断点到捕获锚点的宿主单调时间
g 虚拟机进程的恢复世代计数器

3.2.1 Hypervisor 身份

如表 3.5 所示,每个处理器都看到一个最小化的 Hyper-V 频率与不变 TSC 身份,它在每个后端上都以相同方式配置。每一行列出了 CPUID 通过四个通用寄存器(EAX、EBX、ECX 和 EDX)报告结果时某个叶所返回的值。

表 3.5. Hypervisor 身份叶。

叶 EAX EBX ECX EDX
0x40000000 0x40000005(最大叶) 0x7263694d("Micr") 0x666f736f("osof") 0x76482074("t Hv")
0x40000001 0x31237648("Hv#1") 0 0 0
0x40000002 0x0058564e("NVX") 0x00010000(时间 ABI 1.0) 0 0
0x40000003 0x00008860 0 0 0x00000100
0x40000004 0 0xffffffff 0 0
0x40000005 C C 0 0

叶 0x40000003 恰好授予 Linux 检测平台所需的权限、hypercall MSR 与虚拟处理器编号(VP-index)访问权限,再加上频率寄存器与不变 TSC 控制访问权限,并且只公布频率寄存器。

叶 0x40000004 不推荐任何特性,也从不请求自旋锁通知。叶 0x40000006..0x4000000f 和 0x40000080..0x40000082 读取为显式的零。0x40000006..0x400000ff 中的其他任何叶都返回零或厂商的架构性越界结果,而从 0x40000100 到 0x4000ff00 的任何基址都不携带 hypervisor 签名,因此 Linux 只选择 Hyper-V 平台。除表 3.5 中的叶之外,没有任何叶返回 Hyper-V 特性数据,叶 0x40000081 也不携带 VS#1 虚拟化栈接口签名。

该身份有意偏离 Hyper-V 规范:

  • hypercall MSR 会引发一般保护故障(#GP),不存在 hypercall 页,hypercall 指令不在契约范围内。
  • TSC 频率 MSR 返回声明速率 F,在恢复之后它与物理速率的差异最多可达速率容差。
  • 不变 TSC 控制位在置位之后仍可被清除。
  • 不变 TSC 的 CPUID 位从启动起即被置位,而不是在写入控制位之后才置位。
  • 叶 0x40000002 携带 NVX 签名和时间 ABI 版本,而不是构建号。
  • 叶 0x40000005 报告处理器容量。

客户机唯一的副作用是选择 Hyper-V 平台。任何后端上都不存在半虚拟化时钟、中断、TLB、自旋锁、偷取时间或缺页特性,调度器时钟和 vDSO 直接使用原始 TSC。

3.2.2 MSR

如表 3.6 所示,VMM 在每个后端上都以相同方式提供身份 MSR 范围 0x40000000..0x400001ff。

表 3.6. 身份 MSR。

MSR 名称 读 写
0x40000002 VP 索引 处理器索引 #GP
0x40000022 TSC 频率 F #GP
0x40000023 APIC 频率 L #GP
0x40000118 TSC 不变控制 最后写入的值;复位后为 0 接受 0 或 1;其他任何值触发 #GP
该范围内的其他任何 MSR #GP #GP

TSC 不变控制是整个分区共有的单一值。它属于客户机状态,随虚拟机一起保存和恢复。Linux 在启动时写入一次 1 且不做故障处理,因此该写入在任何后端上都绝不能触发故障。

保存要加到处理器 TSC 上的偏移的 MSR(IA32_TSC_ADJUST,0x3b),以及保存 LAPIC 定时器在 TSC-deadline 模式下触发时的 TSC 值的 MSR(IA32_TSC_DEADLINE,0x6e0),在任何后端上都会引发 #GP;英特尔 P6 处理器世代的缓存控制寄存器(旧版 P6 L2 缓存 MSR)也是如此。

如表 3.7 所示,在 AMD CPU 配置文件下,VMM 还以固定值提供两个配置 MSR。

表 3.7. AMD 配置 MSR。

MSR 名称 读 写
0xc0010015 硬件配置(HWCR) 0x1000000:仅包含使 TSC 以固定频率计数的那个位(TscFreqSel) 读取该值不改变任何东西;其他任何值触发 #GP
0xc0011029 解码配置(DE_CFG) 0x2:仅包含使 load-fence 指令把后续指令保持到先前指令完成之后的那个位(LFENCE 串行化) 读取该值不改变任何东西;其他任何值触发 #GP

3.2.3 CPU 时间位

表 3.8 列出了每个 CPU 配置文件都固定的位。违反这些位的 CPU 配置文件会被拒绝。

表 3.8. CPU 时间位。

叶 寄存器与位 值
0x1 ECX[31] hypervisor 存在 1
0x1 ECX[24] TSC-deadline 定时器 0
0x1 ECX[15] 性能与调试能力 MSR(PDCM) 0
0x1 EDX[4] TSC 1
0x6 EAX、EBX、ECX、EDX 0x00000004(仅始终运行的 APIC 定时器,ARAT)、0、0、0
0x7.0 EBX[1] IA32_TSC_ADJUST 0
0xa EAX、EBX、ECX、EDX 0(无 PMU)
0x15、0x16 EAX、EBX、ECX、EDX 0,在最大基本叶范围之内时
0x80000001 EDX[27] 同时返回处理器标识符的 RDTSC 变体(RDTSCP) 1
0x80000007 EAX、EBX、ECX、EDX 0、0、0、0x00000100(仅不变 TSC)

叶 0x15 和 0x16 被清零而不是被合成。Linux 从频率 MSR 取得两个速率,这让 CPU 配置文件的 CPUID 与任何宿主的速率无关。不变 TSC 位是一项策略而不是被指纹识别的特性:每个 CPU 配置文件都会设置它,每个后端都会呈现它。

3.2.4 速率

客户机 TSC 以目标的原生速率 F_d 运行,绝不做缩放。声明速率 F 在一个快照血缘的整个生命周期内固定:冷启动声明其原生速率,每个被恢复的进程都保持 F = F_s。内核只计算一次 TSC 频率,绝不重新校准。

在恢复时,原生速率必须落在声明速率的 250 ppm 以内,该比较使用至少 128 位的整数运算计算:

accept  iff  |F_d - F_s| * 1_000_000  <=  250 * F_s

边界值被接受。清单记录该容差,任何其他记录值都被拒绝。在容差范围内,TSC 既不缩放也不被陷入,频率 MSR 继续返回 F_s,客户机时钟最多按该偏差走快或走慢。恢复数据包以内核的频率调整单位(按 2^16 缩放的 ppm)报告该偏差,以便客户机进行补偿:

rate_deviation = round((F_d - F_s) * 1_000_000 * 65_536 / F_s)

每个速率还必须是合理的,即 500 MHz <= F <= 10 GHz。无法报告其原生速率的后端在冷启动、捕获和恢复时一律被拒绝;不存在校准回退。

LAPIC 定时器速率 L 是后端常量:KVM 上为 1 GHz,MSHV 和 WHP 上为 200 MHz。内核根据 APIC 频率 MSR 预设其定时器周期,绝不对 LAPIC 做校准。在恢复时,目标的速率必须与记录的速率完全相等。

3.2.5 时钟源、滴答与定时器

在这一身份之下,客户机内核从频率 MSR 取得 TSC 速率,把 TSC 视为可靠,这会禁用时钟源看门狗和 CPU 上线跃迁测试,预设 LAPIC 定时器周期,并在启动时禁用 PIT。此时已启动的客户机必须处于以下状态:

  • 每个 CPU 都报告恒定、不停、频率已知且可靠的 TSC,并具备 RDTSCP 和 ARAT,且没有任何 CPU 报告 TSC-deadline 定时器或 TSC_ADJUST。
  • TSC 是当前的时钟源,并且在每个 CPU 第一次滴答之后是唯一可用的时钟源。
  • 滴答采用无滴答空闲(tickless-idle)并配合高精度定时器,每个在线 CPU 的滴答设备都是处于单次触发模式的 LAPIC 定时器。
  • 不存在 PIT、高精度事件定时器(HPET)或广播时钟事件设备。
  • 启动之后 LAPIC 定时器从不处于周期模式,也从不处于 TSC-deadline 模式。
  • 启动之后 PIT 通道 0 从不计数,IRQ 0 也没有定时器处理程序。

捕获和恢复都会拒绝已装载的周期模式或 TSC-deadline 模式的 LAPIC 定时器,以及处于周期计数状态的 PIT 通道 0。在停机时间 D 期间,处于计数状态的单次触发 LAPIC 定时器恰好前进:

ticks = floor(D * L / 1_000_000_000 / divide)

其中 divide 是定时器的分频配置。若 ticks 达到当前计数值,则计数值归零,并且定时器中断会被排队,除非配置它的 LAPIC 寄存器(其 LVT 条目)被屏蔽;否则计数值减少 ticks。已排队或待处理的中断向量必须在处理器被放行时送达该处理器,即使它处于停止状态。

3.2.6 后端义务

每个后端在同一个 VMM 接口之后实现相同的义务。客户机可见的结果完全相同,只有机制不同。每个后端都必须:

  • 恰好呈现身份叶及其显式零叶,不带任何其他 hypervisor 签名。
  • 呈现 CPU 配置文件的生效 CPUID,包括 CPU 时间位。
  • 把身份 MSR(以及在 AMD CPU 配置文件下的两个配置 MSR)路由到 VMM。
  • 报告原生 TSC 速率,绝不缩放 TSC。
  • 以后端的固定速率运行 LAPIC 定时器。
  • 在 LAPIC 状态写入之后交付待处理的 LAPIC 中断向量,即使目标是已停止的处理器。
  • 隐藏 TSC-deadline 定时器和 TSC_ADJUST,并让这两个 MSR 触发故障。
  • 公开不变 TSC。
  • 不提供任何半虚拟化或合成特性,绝不启用 hypervisor 专用的模拟器或其已保存状态。
  • 为捕获锚点在 100 µs 内把第一个处理器的 TSC 与一个宿主时间采样配对,并保留至多 64 个采样中最紧的那个。
  • 在同一个宿主时刻设置每个已实例化处理器的 TSC,并在任何处理器运行之前通过回读进行校验。
  • 在放行之后把活跃的跨处理器偏差保持在 1 µs 以内。

3.2.7 跨处理器一致性

在每一个客户机可见的时刻,任意两个在线处理器的 TSC 相差至多 1 µs,也就是 F_s / 1,000,000 个周期。客户机内核不再检查 TSC 一致性,因此该界限在客户机之外强制执行。冷启动同时启动每个处理器的 TSC。恢复在同一个宿主时刻把同一个目标写入每个已实例化的处理器并通过回读进行校验,因此 VMM 自身不会引入偏差。放行之后,每个后端把活跃偏差保持在界限之内,无法维持该界限的宿主不符合契约要求。

3.2.8 停机时间与连续性

客户机单调时间前进停机时间 D,即捕获锚点与恢复锚点之间流逝的宿主时间,再加上从恢复锚点到第一条被恢复指令之间至多恢复延迟 R。客户机在快照请求与第一条被恢复指令之间不会观察到任何其他不连续。

捕获锚点记录:

  • T_c,第一个处理器的客户机 TSC。
  • 自 Unix 纪元以来的宿主 UTC 纳秒数。
  • 包含挂起时间的宿主单调时间。
  • 宿主身份。
  • 宿主启动身份。

无法提供全部这些项的宿主会拒绝捕获和恢复。如表 3.9 所示,恢复在自己的锚点处取同样的采样,并恰好选择一个停机时间来源。

表 3.9. 停机时间来源选择。

条件 来源 D
相同的宿主身份、启动身份和宿主时钟种类 宿主单调时间 monotonic_now - monotonic_capture
其他任何情况 宿主 UTC utc_now - utc_capture

D 必须满足 0 <= D <= 30 days。在单调时间路径上,与 D 相差超过一秒的 UTC 增量会被报告为宿主挂钟跳变,并且不改变 D。恢复数据包会报告所选来源。

虚拟机时间、VMM 模拟设备的时钟以及 RTC 在静默时、也就是捕获锚点之前停止。捕获因此记录 Q,即从该切断点到锚点的宿主单调时间间隔,而恢复会让虚拟机时间和 RTC 前进 D + Q,使它们与贯穿 Q 持续运行的客户机 TSC 同步恢复。TSC、LAPIC 定时器和恢复数据包使用 D。D + Q 不得超过 30 days。Q 是尽力而为的:缺少它的快照以 Q = 0 恢复并附带一条警告。

恢复锚点是同步设置 TSC 的时刻,客户机时间在每个后端上都从该时刻开始运行;没有后端把它推迟到第一个处理器运行。同样也没有后端为捕获而停止客户机时间,因此一次回滚的捕获让客户机的时钟保持连续,客户机把静默看作流逝的时间,就像一次长时间的抢占。

3.2.9 时间采样

图 3.1 展示了向字节通道选择器写入 0xa7 时锁存到时间窗口中的 16 字节采样的布局。

offset size field
     0    1 version = 1
     1    1 flags                第 3 位镜像恢复数据包的覆盖标志;其他位为零
     2    2 reserved = 0
     4    4 generation           u32,小端序,此虚拟机进程的 g
     8    8 utc_ns               u64,小端序,锁存时刻的宿主 UTC

图 3.1. 时间采样布局。

VMM 无法在每个后端上观察运行中处理器的 TSC,因此客户机把宿主 UTC 与自己的时钟配对。它在写入选择器之前立即把实时时钟读作 t0,在写入之后立即读作 t1。宿主时刻位于 [t0, t1] 之内,因此:

theta = utc_ns - (t0 + t1) / 2      uncertainty epsilon = (t1 - t0) / 2

同样的区间也适用于恢复数据包中的 UTC 采样。如表 3.10 所示,客户机只有在 epsilon 落在其用途对应的界限之内时才接受该配对。

表 3.10. 时间配对的不确定度界限。

用途 界限 尝试次数 当每次尝试都超出界限时
恢复修复 1 ms 3 修复失败,客户机关机
启动时的初始同步 1 ms 3 一致性检查失败,客户机关机
挂钟校正 50 µs 3 跳过该次轮询;绝不致命

3.2.10 客户机义务

客户机代理在每种客户机模式下执行一致性检查、监视违规、校正挂钟并执行快照的时间步骤。生产路径保持控制台安静;状态写入一个状态文件,工作负载可以通过只读挂载读取它。该状态文件报告各阶段检查的结果、声明的速率、世代、最近一次停机时间及其来源,以及挂钟的不连续、同步、偏移、不确定度和频率修正。

一致性检查。 在就绪之前,客户机只在界限之内取一次时间采样,并把时钟阶跃到宿主 UTC,使工作负载从宿主时间开始。它在就绪之后异步运行其余所有启动检查,从空闲优先级开始,因此这些检查绝不延迟就绪,也绝不占用工作负载的处理器。检查涵盖身份叶与显式零叶、每个 CPU 的 CPU 时间位、身份 MSR、内核的平台/时钟源/LAPIC 定时器选择、CPU 标志、滴答设备、禁用时钟参数的缺失、停转检测器设置、正在运行的违规监视器以及内核 W+X 完整性。捕获会重新运行时钟源、滴答设备和监视器检查。恢复也会重新运行它们,再次检查频率 MSR 中的声明速率,并检查它上线的每个处理器的身份叶、CPU 时间位和 CPU 标志。每项检查都是快速失败的:一旦失败,即使工作负载正在运行也会让客户机关机。

违规监视器。 监视器从启动检查之前就开始跟踪内核日志,因此不会遗漏任何记录。TSC 不稳定;时钟源不稳定、切换或偏移;TSC 跃迁;TSC_ADJUST 故障;RCU 停转或饿死;软锁死或硬锁死;挂起任务;未检查的 MSR 访问;APIC 身份不匹配;以及丢失的日志记录,都属于违规。监视器还轮询内核的 RCU 停转计数,因此即使某次停转的记录丢失也能被捕获。违规会以一条有界的事件行报告到内核日志、字节通道和控制台,随后客户机通过关机端口断电。

停转抑制。 在停机之后恢复会使内核时间跳变,因此客户机在捕获请求之前抑制 RCU 停转检测以及调试用停转检测器。恢复之后,只有当捕获时正在进行的宽限期结束时才解除抑制,从而先强制经历一个完整的正常宽限期。如果该宽限期仍卡住并超过停转超时,客户机仍会解除抑制,以便内核报告真实的停转。

挂钟校正。 客户机通过内核的锁相环把实时时钟保持在宿主 UTC 上。启动或恢复之后的第一次轮询后,它在前四次轮询中每 16 秒轮询一次,之后每 64 秒一次,并且每次轮询最多取三个采样。至少 128 ms 的偏移会被阶跃修正并计为一次不连续;更小的偏移则以由该偏移及其不确定度导出的误差界限进行渐变调整,这会让应用认为时钟已同步。内核把频率修正限制在 ±500 ppm,其中速率容差最多占用 250 ppm。世代与所记录世代不同的采样会被丢弃,因为发生过恢复,而恢复修复会重置校正状态。校正会跟随宿主的挂钟跳变,绝不让客户机关机。恢复修复和校正都通过内核阶跃时钟,因此工作负载可以通过内核的设钟通知观察到每一次阶跃,例如以 TFD_TIMER_CANCEL_ON_SET 装载的 timerfd。

快照协议和客户机修复定义了快照的时间步骤。