第5篇:半虚拟化 — pvclock、kvmclock、steal time 与 Hyper-V Enlightenment¶
源码:arch/x86/kvm/x86.c (14571行) | arch/x86/include/asm/pvclock-abi.h (48行) | arch/x86/include/asm/pvclock.h (108行) | arch/x86/include/asm/kvm_host.h (2547行) | Documentation/virt/kvm/x86/timekeeping.rst
系列目录:KVM 内核源码深度解析
一、半虚拟化概述¶
半虚拟化通过共享内存页实现 host-guest 高效通信,避免昂贵的 VM-exit。KVM 支持多种半虚拟化接口:
| 接口来源 | 核心功能 | 关键文件 |
|---|---|---|
| KVM pvclock | 精准时钟、steal time、PV TLB flush | x86.c, pvclock-abi.h |
| Hyper-V | Reference TSC 页、synthetic timers、EVMCS | hyperv.c (78KB) |
| Xen PV | Shared info 页、runstate、PV clock | xen.c (66KB) |
| ARM64 | Stolen time 结构 | arm64/kvm/pvtime.c |
本文聚焦 KVM pvclock 体系——所有 KVM guest 时钟基础设施的基石。
二、pvclock ABI:32 字节的精密协议¶
2.1 ABI 定义¶
// pvclock-abi.h:26-35
struct pvclock_vcpu_time_info {
u32 version; // seqlock 版本号
u32 pad0;
u64 tsc_timestamp; // 更新时的 TSC 值
u64 system_time; // 更新时的系统时间(纳秒)
u32 tsc_to_system_mul; // TSC→纳秒 转换乘数
s8 tsc_shift; // TSC→纳秒 转换位移
u8 flags; // 标志位
u8 pad[2];
} __attribute__((__packed__)); /* 32 bytes */
// pvclock-abi.h:37-41
struct pvclock_wall_clock {
u32 version;
u32 sec;
u32 nsec;
} __attribute__((__packed__));
// pvclock-abi.h:43-44
#define PVCLOCK_TSC_STABLE_BIT (1 << 0) // TSC 跨 vCPU 稳定
#define PVCLOCK_GUEST_STOPPED (1 << 1) // guest 被停止
2.2 Seqlock 读取协议¶
// pvclock.h:22-38
static __always_inline
unsigned pvclock_read_begin(const struct pvclock_vcpu_time_info *src)
{
unsigned version = src->version & ~1; // 屏蔽"更新中"标记
virt_rmb(); // version 必须在数据之前读取
return version;
}
static __always_inline
bool pvclock_read_retry(const struct pvclock_vcpu_time_info *src,
unsigned version)
{
virt_rmb(); // 数据必须在 version 之前读取
return unlikely(version != src->version);
}
Host 写入:version++(奇数)→ 写屏障 → 更新字段 → 写屏障 → version++(偶数)
三、pvclock 数学:TSC delta → 纳秒¶
3.1 核心公式¶
current_time = pvti->system_time
+ pvclock_scale_delta(rdtsc() - pvti->tsc_timestamp,
pvti->tsc_to_system_mul,
pvti->tsc_shift)
pvclock_scale_delta (pvclock.h:44-81) 使用优化的内联汇编:
static __always_inline u64 pvclock_scale_delta(u64 delta, u32 mul_frac, int shift)
{
u64 product;
if (shift < 0)
delta >>= -shift;
else
delta <<= shift;
__asm__ (
"mulq %[mul_frac] ; shrd $32, %[hi], %[lo]"
: [lo]"=a"(product),
[hi]"=d"(tmp)
: "0"(delta),
[mul_frac]"rm"((u64)mul_frac));
return product;
}
mulq 执行 64位×64位→128位乘法,shrd 取高 64 位(等效于除以 2^32)。实质:ns = delta * mul_frac * 2^(shift - 32)。
3.2 完整计算¶
// pvclock.h:83-90
static __always_inline
u64 __pvclock_read_cycles(const struct pvclock_vcpu_time_info *src, u64 tsc)
{
u64 delta = tsc - src->tsc_timestamp;
u64 offset = pvclock_scale_delta(delta, src->tsc_to_system_mul,
src->tsc_shift);
return src->system_time + offset;
}
四、kvmclock 实现¶
4.1 核心数据结构¶
// kvm_host.h:1482-1506
struct kvm_arch {
s64 kvmclock_offset; // 行1482: VM 全局时间偏移
raw_spinlock_t tsc_write_lock; // 行1488: TSC 写入保护
seqcount_raw_spinlock_t pvclock_sc; // 行1503: pvclock seqlock
bool use_master_clock; // 行1504: 主时钟模式
u64 master_kernel_ns; // 行1505: 参考内核时间
u64 master_cycle_now; // 行1506: 参考 TSC 周期
};
4.2 时间基准¶
// x86.c:2398-2401
static s64 get_kvmclock_base_ns(void)
{
/* Count up from boot time, but with the frequency of the raw clock. */
return ktime_to_ns(ktime_add(ktime_get_raw(), pvclock_gtod_data.offs_boot));
}
ktime_get_raw() 是单调原始时间(不受 NTP 调整影响),加上启动偏移后得到基于原始时钟的启动时间。
4.3 时钟读取¶
// x86.c:3247-3264
static void get_kvmclock(struct kvm *kvm, struct kvm_clock_data *data)
{
struct kvm_arch *ka = &kvm->arch;
unsigned seq;
do {
seq = read_seqcount_begin(&ka->pvclock_sc);
__get_kvmclock(kvm, data);
} while (read_seqcount_retry(&ka->pvclock_sc, seq));
}
u64 get_kvmclock_ns(struct kvm *kvm)
{
struct kvm_clock_data data;
get_kvmclock(kvm, &data);
return data.clock;
}
内层 __get_kvmclock (x86.c:3212-3245) 主时钟模式核心逻辑:
get_cpu(); // 确保连续操作在同一 CPU
if (ka->use_master_clock && CONSTANT_TSC) {
hv_clock.tsc_timestamp = ka->master_cycle_now;
hv_clock.system_time = ka->master_kernel_ns + ka->kvmclock_offset;
kvm_get_time_scale(NSEC_PER_SEC, get_cpu_tsc_khz() * 1000LL,
&hv_clock.tsc_shift, &hv_clock.tsc_to_system_mul);
data->clock = __pvclock_read_cycles(&hv_clock, data->host_tsc);
} else {
data->clock = get_kvmclock_base_ns() + ka->kvmclock_offset;
}
put_cpu();
主时钟模式下,guest 只需读自己的 TSC,代入 tsc_timestamp + system_time + 缩放参数,即得当前时间。
4.4 时钟更新同步¶
// x86.c:3163-3193
static void kvm_start_pvclock_update(struct kvm *kvm)
{
kvm_make_mclock_inprogress_request(kvm); // 阻止所有 vCPU 进入 guest
raw_spin_lock_irq(&kvm->arch.tsc_write_lock);
write_seqcount_begin(&kvm->arch.pvclock_sc);
}
static void kvm_end_pvclock_update(struct kvm *kvm)
{
write_seqcount_end(&ka->pvclock_sc);
raw_spin_unlock_irq(&ka->tsc_write_lock);
kvm_for_each_vcpu(i, vcpu, kvm)
kvm_make_request(KVM_REQ_CLOCK_UPDATE, vcpu); // 通知各 vCPU
kvm_for_each_vcpu(i, vcpu, kvm)
kvm_clear_request(KVM_REQ_MCLOCK_INPROGRESS, vcpu);
}
static void kvm_update_masterclock(struct kvm *kvm) // 外部触发入口
{
kvm_hv_request_tsc_page_update(kvm);
kvm_start_pvclock_update(kvm);
pvclock_update_vm_gtod_copy(kvm);
kvm_end_pvclock_update(kvm);
}
五、Steal Time¶
5.1 概念¶
Steal time 度量 guest vCPU 被 host 调度抢占的累计时间。Guest OS 将其报告为 /proc/stat 的 steal 字段,用于正确的 CPU 使用率计算。
5.2 record_steal_time¶
// x86.c:3748-3839
static void record_steal_time(struct kvm_vcpu *vcpu)
{
struct kvm_steal_time __user *st;
u64 steal;
u32 version;
if (kvm_xen_msr_enabled(vcpu->kvm)) {
kvm_xen_runstate_set_running(vcpu); // Xen: 使用 runstate 代替
return;
}
if (!(vcpu->arch.st.msr_val & KVM_MSR_ENABLED))
return;
// ... 确保 HVA cache 有效 ...
st = (struct kvm_steal_time __user *)ghc->hva;
// PV TLB flush 探测
if (guest_pv_has(vcpu, KVM_FEATURE_PV_TLB_FLUSH)) {
u8 st_preempted = 0;
asm volatile("1: xchgb %0, %2\n"
: "+q" (st_preempted), "+&r" (err), "+m" (st->preempted));
if (st_preempted & KVM_VCPU_FLUSH_TLB)
kvm_vcpu_flush_tlb_guest(vcpu);
}
// --- seqlock 写入 ---
unsafe_get_user(version, &st->version, out);
if (version & 1) version += 1;
version += 1;
unsafe_put_user(version, &st->version, out); // 奇数:更新中
smp_wmb();
steal += current->sched_info.run_delay - // 本次周期被偷走的时间
vcpu->arch.st.last_steal;
vcpu->arch.st.last_steal = current->sched_info.run_delay;
unsafe_put_user(steal, &st->steal, out);
version += 1;
unsafe_put_user(version, &st->version, out); // 偶数:完成
}
核心公式:steal += current_run_delay - last_recorded_run_delay
5.3 标记抢占¶
// x86.c:5240
static void kvm_steal_time_set_preempted(struct kvm_vcpu *vcpu)
{
// 在 guest 共享页设置 KVM_VCPU_PREEMPTED 标志
unsafe_put_user(KVM_VCPU_PREEMPTED, &st->preempted, out);
}
Guest 内核看到此标志后知道时间片被 host 打断,可优化锁等待(如 paravirt_spin_lock 的 pv_lock_ops)。
5.4 Steal Time 结构¶
Offset Size Field
────── ──── ────────────────
0 4 version (seqlock)
4 4 pad
8 8 steal (纳秒)
16 1 preempted
17 3 pad
5.5 调度钩子¶
vCPU 调度进/出时触发 KVM_REQ_STEAL_UPDATE(x86.c:5237),在下一轮 guest entry 时调用 record_steal_time。
六、vDSO 快速路径¶
KVM 将 per-vCPU pvclock 页映射到 guest vDSO,实现用户态零开销时钟读取:
Guest 用户态 clock_gettime()
│
▼
vDSO → vread_pvclock()
├─ pvclock_read_begin() 版本检查
├─ rdtsc() 当前 TSC
├─ pvclock_scale_delta() TSC→纳秒
├─ system_time + delta 最终时间
└─ pvclock_read_retry() 版本重试
⚡ 全程用户态,零 VM-Exit,零系统调用!
// pvclock.h:92-106
struct pvclock_vsyscall_time_info {
struct pvclock_vcpu_time_info pvti;
} __attribute__((__aligned__(SMP_CACHE_BYTES)));
#define PVTI_SIZE sizeof(struct pvclock_vsyscall_time_info)
七、Hyper-V Enlightenment¶
7.1 Hyper-V Reference TSC Page¶
与 pvclock 类似但格式不同,使用 (sequence, tsc_scale, tsc_offset) 三元组:
使用 128位×64位乘法 + 右移 64 位,无需 shift 参数。
7.2 Synthetic Timers¶
4 个 stimer(HV_X64_MSR_STIMER0-3_CONFIG),直接向 vCPU 投递中断而不经过 LAPIC timer。KVM 用 host hrtimer 实现。
7.3 Enlightened VMCS¶
hyperv_evmcs.c 实现 Enlightened VMCS:将 VMCS 字段放在共享 guest 物理页中,减少 L0→L1 的 VMREAD/VMWRITE VM-exit(嵌套虚拟化优化)。
7.4 其他 Enlightenments¶
- VP Assist Page:host 写入事件信息
- SynIC:虚拟设备中断轻量传递
- TSC Emulated Control:RDMSR 访问 TSC 频率无 VM-exit
- Reenlightenment Controls:guest TSC 频率迁移支持
八、Xen PV 时钟¶
8.1 架构¶
KVM 实现 Xen PV 接口的子集(arch/x86/kvm/xen.c, 66KB):
- Shared Info Page:vcpu_info 数组 + pvclock + event channels
- Runstate:各 vCPU 的状态时间统计
- Xen PV Clock:与 KVM pvclock 共享 ABI,调用 get_kvmclock_ns 作为时间源 (xen.c:272)
8.2 Runstate 结构¶
struct vcpu_runstate_info {
uint32_t state; // RUNNING / RUNNABLE / BLOCKED / OFFLINE
uint64_t state_entry_time;
uint64_t time[4]; // 各状态累计时间
};
九、ARM64 PV Time¶
arch/arm64/kvm/pvtime.c 实现了 ARM 的 stolen time 支持。结构与 x86 类似:
struct pvclock_vm_stolen_time {
uint32_t revision;
uint32_t attributes;
uint64_t stolen_time; // ns
};
ARM64 通过 SMCCC_ARCH_FEATURES 发现 PV time,通过 PV_TIME_ST IPA 映射共享页。
十、完整时钟架构图¶
╔══════════════════════════ Host (KVM) ══════════════════════════╗
║ ║
║ kvm_update_masterclock() (x86.c:3187) ║
║ ├─ kvm_start_pvclock_update() ║
║ │ ├─ KVM_REQ_MCLOCK_INPROGRESS (阻止 guest entry) ║
║ │ └─ write_seqcount_begin(&pvclock_sc) ║
║ ├─ pvclock_update_vm_gtod_copy() ║
║ │ ├─ ktime_get_raw() ─── 单调原始时间 ║
║ │ ├─ rdtsc() ─────────── TSC 快照 ║
║ │ └─ ktime_get_real_ns() ─ wall clock ║
║ └─ kvm_end_pvclock_update() ║
║ ├─ write_seqcount_end(&pvclock_sc) ║
║ └─ KVM_REQ_CLOCK_UPDATE (通知各 vCPU) ║
║ │ ║
║ ┌─────────────────┼─────────────────┐ ║
║ ▼ ▼ ▼ ║
║ KVM pvclock Hyper-V Ref Xen PV Clock ║
║ page (per vCPU) TSC page (shared info) ║
║ │ │ │ ║
╚═════╪═════════════════╪═════════════════╪═════════════════════╝
│ │ │
│ Guest Memory │ │
▼ ▼ ▼
╔══════════════════════════ Guest VM ═══════════════════════════╗
║ ║
║ ┌─ Clock Stack ──────────────────────────────────────────┐ ║
║ │ vDSO 快速路径 (用户态, 零 VM-Exit) │ ║
║ │ vread_pvclock() │ ║
║ │ ├─ seqlock begin │ ║
║ │ ├─ rdtsc() │ ║
║ │ ├─ pvclock_scale_delta() ── TSC→ns │ ║
║ │ ├─ system_time + delta │ ║
║ │ └─ seqlock retry │ ║
║ │ │ ║
║ │ 内核态路径 (需要时) │ ║
║ │ kvmclock 校准, wall clock 同步 │ ║
║ └────────────────────────────────────────────────────────┘ ║
║ ║
║ /proc/stat (guest): ║
║ user + nice + system + idle + iowait + irq + softirq ║
║ + steal ← 来自 KVM steal time 页 ║
╚════════════════════════════════════════════════════════════════╝
Steal Time 更新:
kvm_sched_out() → run_delay 记录 → KVM_REQ_STEAL_UPDATE
... vCPU 等待 CPU ...
kvm_sched_in() → KVM_REQ_STEAL_UPDATE
record_steal_time() (x86.c:3748)
steal += current_run_delay - last_steal
写入 shared page (seqlock 协议)
Guest 读取 → /proc/stat steal 字段
跨 vCPU TSC 同步 (Master Clock):
CONSTANT_TSC CPU → 所有 pCPU 的 TSC 同速率增长
master_cycle_now = rdtsc() @ t₀
master_kernel_ns = ktime_get_raw() + offs_boot @ t₀
Guest 读取:
my_tsc = rdtsc()
delta = my_tsc - master_cycle_now
now = master_kernel_ns + scale(delta)
→ 无论在哪颗 pCPU 上,时间一致
十一、关键文件和行号索引¶
| 文件 | 行号 | 功能 |
|---|---|---|
pvclock-abi.h:26 |
26 | struct pvclock_vcpu_time_info — 32B ABI |
pvclock-abi.h:37 |
37 | struct pvclock_wall_clock |
pvclock-abi.h:43 |
43 | PVCLOCK_TSC_STABLE_BIT, PVCLOCK_GUEST_STOPPED |
pvclock.h:23 |
23 | pvclock_read_begin() — seqlock 读取开始 |
pvclock.h:32 |
32 | pvclock_read_retry() — seqlock 重试 |
pvclock.h:44 |
44 | pvclock_scale_delta() — 核心缩放算法 |
pvclock.h:84 |
84 | __pvclock_read_cycles() — TSC→时间 |
pvclock.h:92 |
92 | struct pvclock_vsyscall_time_info — vDSO |
kvm_host.h:1482 |
1482 | kvm_arch.kvmclock_offset |
kvm_host.h:1503 |
1503 | kvm_arch.pvclock_sc — seqlock |
kvm_host.h:1504 |
1504 | use_master_clock |
x86.c:2398 |
2398 | get_kvmclock_base_ns() — 时间基准 |
x86.c:3163 |
3163 | kvm_start_pvclock_update() |
x86.c:3171 |
3171 | kvm_end_pvclock_update() |
x86.c:3187 |
3187 | kvm_update_masterclock() |
x86.c:3247 |
3247 | get_kvmclock() — seqlock 保护读取 |
x86.c:3258 |
3258 | get_kvmclock_ns() |
x86.c:3748 |
3748 | record_steal_time() — steal 更新 |
x86.c:5240 |
5240 | kvm_steal_time_set_preempted() |
系列结语¶
五篇文章从 KVM 核心(VM/vCPU 生命周期)→ x86 虚拟化(VMX/VMCS、SVM/VMCB)→ MMU(EPT/NPT、shadow/TDP)→ 设备直通(VFIO、IOMMU、posted interrupts)→ 半虚拟化(pvclock、kvmclock、steal time),覆盖了 KVM 的完整内核栈。
搭配之前六个专栏(nvidia-svm、soc-unified-memory、pcie-deep-dive、arm-smmu、intel-iommu、GPU buddy allocator),读者现在拥有从 PCIe 物理层到 KVM 虚拟化层的完整 Linux 内核知识图谱:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ PCIe 深度 │ │ GPU 内存 │ │ ARM SMMU │
│ 解析 │ │ 管理 │ │ 深度解析 │
└─────┬─────┘ └─────┬─────┘ └─────┬─────┘
└──────────────┼──────────────┘
│
▼
┌────────────────┬────────────────┐
│ Intel IOMMU │ SOC Unified │
│ 深度解析 │ Memory 解析 │
└───────┬────────┴───────┬────────┘
└───────┬────────┘
▼
┌───────────────────────────────┐
│ KVM 深度解析 │
│ 第1篇: VM/vCPU 生命周期 │
│ 第2篇: VMX/SVM 硬件虚拟化 │
│ 第3篇: EPT/NPT/TDP MMU │
│ 第4篇: VFIO/IOMMU/中断重映射 │
│ 第5篇: pvclock/steal/Hyper-V │
└───────────────────────────────┘
愿这套知识体系助你在 Linux 内核虚拟化领域深耕远行。