virt-report虚拟化社区动态

KVM 虚拟时钟与定时器

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

一、虚拟机时间读取与定时中断

客户机安排一个 1 ms 后执行的任务,需要先知道当前时间,再设置一个到期通知。虚拟化使这两步有了不同的困难:vCPU 可能被宿主抢占,恢复时应能读出已经经过的时间;定时事件即使已经到期,处理线程也可能还没有得到 CPU。迁移到另一台服务器后,物理计数器的起点和频率还可能改变。

Linux 将提供时间读数的机制称为 clocksource,将产生定时中断的机制称为 clockevent。本文以 x86 客户机的 KVM pvclock 和 Intel TSC-deadline 定时器为例,沿“读出当前时间—设置截止时刻—接收到期中断”说明两条路径。示例使用内核虚拟 LAPIC。

客户机 Linux 读取时钟、安排自己的任务,并处理虚拟定时中断;宿主 KVM 提供时间换算参数和虚拟 LAPIC 定时器,宿主 Linux 提供实际定时及线程调度。QEMU 主要在创建、暂停和迁移时协调这些状态,不参与客户机每一次 pvclock 读取。

先看最终要保证的行为:客户机在 5.002 s 安排一个 1 ms 后的任务,目标时刻就是 5.003 s。如果 vCPU 一直到 5.004 s 才获得运行机会,恢复后应能读到已经经过的时间,定时任务则已经迟到约 1 ms。下面分别解释读数怎样保持一致,以及到期事件怎样到达客户机。

源码版本:QEMU 10.2.0、Linux 6.18。

二、TSC 偏移与缩放建立客户机的计数坐标

TSC 是处理器提供的计数器,客户机可通过 RDTSC 等指令读取。具有 invariant TSC 能力的处理器使用稳定的计数频率;这个频率与程序当前执行指令的速度不同,不能用计数增量直接当作已执行的指令数。

物理 TSC 的起点属于宿主。KVM 为客户机配置偏移来选择起点,在支持硬件 TSC scaling 时还可配置比例来选择计数频率。Intel VMX 的比例采用 48 位小数,概念上的变换为:

guest_tsc = ((host_tsc × tsc_multiplier) >> 48) + tsc_offset

偏移可使迁移目的端从客户机原来的计数位置接续;比例可使不同物理 TSC 频率呈现同一个客户机频率。两者都不改变客户机获得多少 CPU 时间。常见的非嵌套 VMX 路径不拦截每次 RDTSC,处理器直接完成上述变换;配置更新才由 KVM 写入 VMCS。VMCS 字段、RDTSC 退出控制

三、从 5.002 秒安排一次 1 毫秒后的通知

设客户机 TSC 频率为 2 GHz。KVM 通过 pvclock 共享结构提供一对参考值:某个 TSC 读数对应多少纳秒;客户机再把后续计数增量加上去。先按一组稳定参数算一遍,下一节再看双方如何保证读取过程不会混用两次更新。

下面移位为 0、乘数为 231,每个 tick 对应 0.5 ns:

tsc_timestamp     = 20,000,000 ticks
system_time       = 5,000,000,000 ns
tsc_shift         = 0
tsc_to_system_mul = 2,147,483,648  // 2^31
current_tsc       = 24,000,000 ticks

delta_ns = ((24,000,000 - 20,000,000) × 2^31) >> 32
         = 2,000,000 ns
now_ns   = 5,000,000,000 + 2,000,000 = 5.002 s

一般情况下先按 tsc_shift 左移或右移 TSC 增量,再乘 tsc_to_system_mul 并右移 32 位。这里的定点运算和实际接口一致,不涉及浮点计算。结构与换算公式

若此时要安排 1 ms 后的事件,截止 TSC 为 24,000,000 + 2,000,000 = 26,000,000,对应同一时间轴上的 5.003 s。客户机将 LAPIC 定时器配置为 TSC-deadline 模式、指定中断向量,再向 IA32_TSC_DEADLINE 写入这个绝对计数。时钟读数回答“现在到哪里”,deadline 指定“何时通知”。

四、pvclock 用共享时间基准减少读取开销

客户机仍需知道计数怎样换成纳秒,并处理 vCPU 调度、宿主计数器条件和时钟基准变化。pvclock 让 KVM 把换算参数写入客户机内存;客户机读取共享数据和当前 TSC,自己计算时间。它不需要为了每次查询时间都向宿主发起调用。

共享时间基准与客户机的纳秒读数
实线表示一次读时钟的数据依赖,虚线表示 KVM 发布基准。TSC 的宿主到客户机缩放与 pvclock 的计数到纳秒换算是两组独立参数,图中没有定时中断。
实线表示一次读时钟的数据依赖,虚线表示 KVM 发布基准。TSC 的宿主到客户机缩放与 pvclock 的计数到纳秒换算是两组独立参数,图中没有定时中断。 打开原图
Mermaid 源码
flowchart TB
    accTitle: KVM pvclock 共享时间基准与客户机读时钟
    accDescr: 物理 TSC 经硬件比例与偏移得到客户机 TSC。KVM 将 version、tsc_timestamp、system_time、乘数、移位和 flags 发布到客户机共享结构。客户机验证版本,以当前 TSC 减去参考 TSC,换算纳秒并加参考时间。此图只有读取数据流,没有定时中断。
    H["宿主物理 TSC"] --> V["VMX 硬件变换<br/>比例 + 偏移"]
    V --> G["客户机读取 TSC<br/>current_tsc"]
    K["KVM 更新参考点<br/>奇数 version → 字段 → 偶数 version"] -. "发布到客户机 RAM" .-> P["pvclock_vcpu_time_info · 32 B<br/>version · tsc_timestamp · system_time<br/>tsc_to_system_mul · tsc_shift · flags"]
    G --> D["验证前后版本一致且为偶数<br/>delta = current_tsc − tsc_timestamp"]
    P --> D
    D --> N["delta 按 shift 移位<br/>乘以 mul,再右移 32 位"]
    N --> R["纳秒读数<br/>system_time + delta_ns"]
    classDef source fill:#e4f1ec,stroke:#438575,color:#173e34
    classDef data fill:#f4f3eb,stroke:#aaa98c,color:#3c4542
    classDef calc fill:#eef3f2,stroke:#738b87,color:#263c38
    class H,V,G source
    class K,P data
    class D,N,R calc

客户机为每个 vCPU 登记一份 32 字节的 pvclock_vcpu_time_info,通过 MSR_KVM_SYSTEM_TIME_NEW 提交其客户机物理地址并启用。KVM 更新的是参考点和换算关系,读取之间的时间增量由 TSC 提供,更新共享结构的频率不等于时钟的分辨率。

字段大小读取时的作用
version4 B检查本次读取是否跨过一次更新
tsc_timestamp8 B参考点的客户机 TSC
system_time8 B同一参考点的纳秒时间
tsc_to_system_mul、tsc_shift4 B、1 B将 TSC 增量换成纳秒
flags1 B稳定性、宿主暂停通知等标志

一致性问题出现在 KVM 正好更新参考点时。例如客户机读到旧 system_time,却用新 tsc_timestamp 计算增量,就会得到错误读数。版本号解决这一交错:写入方先发布奇数版本,更新字段后发布偶数版本;读取方在采样前后必须看到同一个偶数,否则重新读取。两侧内存屏障约束上述顺序。结构其余 6 字节为填充。KVM 发布协议、客户机读取协议

五、从定时截止时刻到客户机中断

KVM 保存客户机写入的 deadline,再选择当前可用的定时方式。先看软件定时分支:start_sw_tscdeadline() 读取当前客户机 TSC,将剩余 ticks 按客户机频率换成纳秒,以宿主 hrtimer 安排到期。先不计编程耗时与注入提前量,上述例子剩余 2,000,000 ticks,换算后是 1,000,000 ns;实际安排会根据采样时刻和提前量调整。

软件定时分支:到期、唤醒与客户机处理
图中省略注入提前量补偿。vCPU 设置截止时刻后进入空闲并阻塞;hrtimer 回调记录到期,唤醒使线程可运行,再由宿主调度、虚拟 LAPIC 投递及客户机处理依次衔接。
图中省略注入提前量补偿。vCPU 设置截止时刻后进入空闲并阻塞;hrtimer 回调记录到期,唤醒使线程可运行,再由宿主调度、虚拟 LAPIC 投递及客户机处理依次衔接。 打开原图
Mermaid 源码
sequenceDiagram
    accTitle: KVM 软件 deadline 定时器到期与客户机唤醒
    accDescr: 在 2GHz 客户机 TSC 的示例中,客户机当前计数 24000000,写入截止计数 26000000。省略编程耗时与注入提前量,KVM 将剩余 1ms 换算为宿主 hrtimer 期限。客户机随后空闲,vCPU 宿主线程阻塞。hrtimer 到期后,KVM 记录 pending 和 UNBLOCK 并唤醒线程;得到宿主调度后投递虚拟 LAPIC 中断,客户机允许中断时执行处理,再由客户机调度任务。本图限定普通软件定时分支。
    participant G as 客户机内核
    participant K as KVM / 虚拟 LAPIC
    participant T as 宿主 hrtimer
    participant S as 宿主调度器
    G->>K: 已设定时器向量,写 deadline = 26,000,000
    K->>K: 当前 TSC = 24,000,000<br/>2,000,000 ticks / 2 GHz = 1 ms
    K->>T: 目标间隔 1 ms<br/>省略编程耗时与提前量
    G->>K: 空闲,执行 HLT
    K->>S: vCPU 宿主线程阻塞
    T->>K: 到期回调
    K->>K: pending++,设置 UNBLOCK
    K->>S: kick 唤醒,使线程可运行
    S-->>K: 线程获得 CPU
    K->>K: 投递虚拟 LAPIC 定时器向量
    K-->>G: 恢复执行,允许时接受中断
    G->>G: 定时处理 → 唤醒任务 → 客户机调度

通用到期路径增加 lapic_timer.pending,设置 KVM_REQ_UNBLOCK,由定时器回调触发 kick。阻塞中的 vCPU 被唤醒;获得 CPU 后,KVM 将定时器向量交给虚拟 LAPIC。客户机允许中断、优先级满足条件时才接受它,随后执行定时处理并安排就绪任务。到期与软件 deadline

这条路径中有三个时间点:deadline、客户机进入中断处理程序、应用任务实际开始运行。它们之间可夹着宿主排队、客户机屏蔽中断以及客户机调度。时钟保持连续,只保证能够计量经过的时间;到期通知和任务执行仍可能迟到。

运行中的硬件定时与阻塞后的切换

支持且适用时,VMX 后端可用 VMX preemption timer 辅助实现虚拟 LAPIC 定时器。它按换算后的宿主计数安排 VM exit,再由 KVM 处理到期;客户机写入的 deadline 并非直接独占宿主物理 LAPIC 定时器。计数范围等条件不满足时会回退到 hrtimer。vCPU 将要阻塞时,KVM 把正在使用的硬件定时切换为软件定时,避免依赖只在 VMX non-root 环境中倒计时的机制。VMX 换算、阻塞前后的切换

APICv 与 posted timer interrupt 还可减少部分事件交接。后者有配置、APICv 和 IN_GUEST_MODE 等条件,不能据此推断宿主已经阻塞的 vCPU 总能直接收到硬件投递。投递条件

六、时间与定时器的状态归属

状态维护位置作用
TSC offset / scaling ratio宿主内核的 KVM vCPU 状态与硬件 VMCS维持客户机计数的起点和频率
pvclock 参考点与换算参数客户机 RAM 中的共享结构;KVM 写入,客户机内核读取让客户机从 TSC 推导纳秒时间
deadline、pending、hv_timer_in_use宿主 KVM 的 kvm_lapic.lapic_timer保存目标、待处理事件及定时方式
kvmclock 的保存值宿主用户态 QEMU 的 KVMClockState协调暂停、恢复和迁移

kvm_guest_time_update() 从一组宿主计数与时间样本生成 pvclock 参考点;kvm_set_lapic_tscdeadline_msr() 更新事件目标。它们服务于不同对象,因此修改读时钟参数与重新安排定时器须按各自恢复路径处理。更新时间基准、定时器状态、deadline 写入

七、暂停与迁移中的时间衔接

从源端搬到目的端,不能直接沿用宿主 TSC 原值。QEMU 保存客户机 TSC、频率、deadline 等状态,在目的端恢复;KVM 根据新宿主建立偏移和可支持的比例。QEMU 通过 KVM_GET_CLOCK / KVM_SET_CLOCK 保存、重建 kvmclock 参考点,跨宿主频率不兼容时还需检查 TSC 控制能力。客户机频率设置

具体停顿怎样计入取决于恢复策略。QEMU 10.2 的 kvmclock 设备在停止时保存读数,恢复时用保存值重新设置基准;迁移运行中的 VM 时,pre_save 会再取一次读数,把停止到保存之间的末批传输时间计入。已经显式暂停的 VM 则保留之前的基准。这能减少迁移丢失的时间,但不意味着所有停机区间都会自动补全。状态变化、迁移保存

还要区分经过时间与日期。pvclock 的纳秒读数配合 wallclock 基准可提供日期参考;客户机操作系统在其上维护 CLOCK_MONOTONIC、CLOCK_REALTIME 等接口,后者还受设置日期和 NTP 校时影响。MONOTONIC 不随手工设置日期直接跳变,也不等于 vCPU 累计执行时长。宿主暂停、客户机自行挂起和迁移是不同事件,不能仅凭某个时钟名称推断它们都采用同一种停顿语义。

PVCLOCK_GUEST_STOPPED 用于告知客户机发生过宿主暂停,让看门狗避免据此误判锁死;这个标志本身不负责补齐墙钟。暂停通知接口

八、从模拟计时设备到协作与硬件辅助

需要解决的成本或限制改进方向剩余代价
PIT、HPET 等设备访问及周期事件带来模拟工作高分辨率计数源、按需的单次定时兼容客户机仍可能使用传统设备
TSC 起点、频率及跨 CPU 条件不同硬件 offset / scaling,稳定性能力检查迁移须匹配能力,稳定频率不等于共享同一起点
客户机反复查询宿主时间成本高pvclock 发布参考点,客户机本地换算需要客户机支持、一致读取与更新协议
定时到期后的退出、注入和调度增加迟延VMX 定时辅助、APICv、按条件 posted 投递阻塞目标仍需唤醒;调度和客户机屏蔽仍影响尾延迟

这些改进分别作用于计数、换算和通知,可以共同使用。KVM 还会按条件提前唤醒定时路径、在真正截止时刻前等待,以补偿注入开销;这以一定 CPU 消耗换取较低迟延。评价时应把时钟漂移、跨 vCPU 后退、定时迟到和应用调度延迟分开测量,避免只看平均值。提前量与等待处理

九、总结:时间读数、到期事件与执行机会

回到 5.003 s 到期的任务,客户机内可记录设定的截止时刻与处理程序实际执行时刻,得到可见的迟到量;宿主侧再对应定时器到期、vCPU 唤醒和运行事件,定位迟到发生在哪段。两侧时间戳来自不同时间基准,跨端相减前需要建立对应关系。pvclock 读数连续、定时中断及时、应用按时运行是三个可以分别验证的结果。

十、参考资料与延伸阅读