前言
从Windows 7开始,微软为了使Windows操作系统能够在自研的Hyper-V平台得到更好性能,在Windows操作系统内嵌了许多半虚拟化接口,这些半虚拟化接口通过TLFS规范对外公开。假设其他虚拟化平台(KVM、Xen、VMware)实现了和Hyper-V一样的接口暴露给Guest,那么Windows内部的半虚拟化特性也会被激活,此时这些虚拟化平台被称为Hyper-V兼容平台。KVM就是一个Hyper-V兼容平台,KVM提供了部分关键的兼容接口,本文将详细描述这些接口。
CPUID
在使用Hyper-V半虚拟化接口之前,GuestOS应该先探测:
运行在物理机上还是运行在虚拟机上?
运行在微软的Hyper-V还是开源的QEMU/KVM还是VMware的vSphere?
当前的Hypervisor有提供哪个功能和特性?
Guest通过CPUID指令实现这个探测的过程,基本过程如下:
执行指令#cpuid 0x00000001,指令返回后,通用寄存器EAX、EBX、EXC、EDX存放返回值,如果EAX的bit31位为0 表示运行在物理机平台,如果EAX的bit31位为1 表示运行在虚拟化平台。对于虚拟化平台,这意味着GuestOS能继续使用CPUID指令获取虚拟化特性,X86规定0x40000000~0x400000FF共256个LEAF(参数)专用于虚拟化,所有虚拟化平台必须实现0x40000000和0x40000001两个LEAF,其他254个可选择性提供,当CPU执行#cpuid 0x40000000~0x400000FF时,物理CPU不认识这个参数引发#GP异常,陷入Hyper-visor处理。
如果是虚拟化平台,继续执行指令#cpuid 0x40000000,返回EAX = 0x40000005,返回EBX|ECX|EDX = "Microsoft Hv"。其中EAX值表示GuestOS能传递给cpuid指令的最大LEAF值,如果GuestOS发起#cpuid 0x40000006将引发#GP异常,EBX|ECX|EDX三个寄存器用于存放Hypervisor签名。规范要求这个签名仅用于展示作用, 但是很多程序仍然使用它来做控制判断。
如果存在0x40000001,执行指令#cpuid 0x40000001,返回EAX = "Hv#1"。其中EAX值为接口ID,目前固定为"Hv#1",是它决定了如何解释0x40000002~0x400000FF。各个位而不是签名。目前接口ID只有一种"Hv#1"。
如果接口ID为"Hv#1",执行指令#cpuid 0x40000003。指令返回后EAX和EBX存放了当前运行的Hyper-V兼容平台支持的特性,通过位表示。
* 如下EAX暴露的特性均通过MSRs实现(这里先不关心何为MSRs以及各位语义)
+ EAX-bit-00: AccessVpRunTimeReg
+ EAX-bit-01: AccessPartitionReferenceCounter
+ EAX-bit-02: AccessSynicRegs
+ EAX-bit-03: AccessSyntheticTimerRegs
+ EAX-bit-04: AccessIntrCtrlRegs
+ EAX-bit-05: AccessHypercallMsrs
+ EAX-bit-06: AccessVpIndex
+ EAX-bit-07: AccessResetReg
+ EAX-bit-08: AccessStatsReg
+ EAX-bit-09: AccessPartitionReferenceTsc
+ EAX-bit-10: AccessFrequencyRegs
+ EAX-bit-11: AccessDebugRegs
+ EAX-bit-12: AccessReenlightenmentControls
+ EAX-other-bits(13~31): Reserved
* 如下EBX暴露的特性均通过Hypercall实现(这里先不关心何为Hypercall以及各位语义)
+ EBX-bit-00: CreatePartitions
+ EBX-bit-01: AccessPartitionId
+ EBX-bit-02: AccessMemoryPool
+ EBX-bit-04: PostMessages
+ EBX-bit-05: SignalEvents
+ EBX-bit-06: CreatePort
+ EBX-bit-07: ConnectPort
+ EBX-bit-08: AccessStats
+ EBX-bit-11: Debugging
+ EBX-bit-12: CpuManagement
+ EBX-bit-16: AccessVSM
+ EBX-bit-17: AccessVpRegisters
+ EBX-bit-20: EnableExtendedHypercalls
+ EBX-bit-21: StartVirtualProcessor
+ EBX-other-bits(3,9-10,13-15,18-19,22~31): Reserved
最后,如果存在0x40000004,执行指令#cpuid 0x40000004,指令#cpuid 0x40000003能获取到Hypervisor支持的所有半虚拟化特性,Guest使用这些特性是否能提升性能,这取决于Hypervisor实现,不意味着半虚拟化性能一定很好。为此,需要GuestOS执行指令#cpuid 0x40000004 返回的EAX、EBX、ECX、EDX值各个位是Hypervisor推荐(或不推荐)GuestOS使用哪些特性。
Hypercall
在开始hypercall之前,需要介绍一下syscall(系统调用)实现,例
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
如上是系统调用read的声明,当用户态程序尝试调用read时,大概会执行如下指令:
配置寄存器EAX=3,3为read的编号,每个syscall都有一个唯一的编号,如下:
#cat /usr/include/asm/unistd_32.h
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
...
配置寄存器EBX=fd 参数1
配置寄存器ECX=buf 参数2
配置寄存器EDX=count 参数3
执行陷阱指令#int 0x80,触发中断,CPU特权级别改变,陷入内核态的中断回调函数处理,内核通过寄存器EAX值获取请求号, 然后调用相应的内核处理函数。
指令返回后,系统调用函数返回值存放在寄存器EAX。
用户态发起syscall请求,调用陷阱指令(i386为int指令)陷入内核态执行syscall,CPU特权级别变更,每个系统调用函数都有一个唯一的ID,内核态通过这个ID区别不通的系统调用请求。linux提供了大约300个系统调用。
hypercall和syscall原理上没有什么两样,hypercall也可以视为函数,pv-guest发起hypercall请求时把hypercall编号和参数写到通用寄存器,然后触调用陷阱指令(x86为vmcall指令)陷入ROOT模式,处于ROOT模式的hypervior处理相应的hypercall请求。hypercall能大大降低vmexit次数,例:SMP处理器某个CPU有时需要给一组CPU发送IPI中断。引入hypercall前,guest通过I/O端口配置LAPIC,期间会读写几十次I/O port,每次读写都会触发vmexit。引入hypercall后,hypervisor类似声明了如下函数供guest使用:
int hc_send_ipi((unsigned long)cpu_mask);
pv-guest调用此hypercall只触发一次vmexit(vmcall引发的),每个Hypervisor提供的hypercall均不相同,各个Hypervisor提供的hypercall如下:
标准Hyper-V(约提供200个hypercall调用):
编号 Hypercall
0x0001 HvSwitchVirtualAddressSpace()
0x0002 HvFlushVirtualAddressSpace()
0x0003 HvFlushVirtualAddressList()
...(具体参数和返回值见TLFS v6.0规范Appendix A: Hypercall Code Reference)
KVM(只提供8个hypercall调用):
编号 Hypercall
0x0001 KVM_HC_VAPIC_POLL_IRQ()
0x0002 KVM_HC_MMU_OP()
0x0003 KVM_HC_FEATURES()
0x0004 KVM_HC_PPC_MAP_MAGIC_PAGE()
0x0005 KVM_HC_KICK_CPU()
0x0006 KVM_HC_CLOCK_PAIRING()
0x0007 KVM_HC_SEND_IPI()
0x0008 KVM_HC_SCHED_YIELD()
(具体参数和返回值参考# cat Documentation/virtual/kvm/hypercalls.txt)
需要注意的是,GuestOS可能运行在不同类型的Hypervisor上,不能假定所有Hypervisor都有提供如上Hypercall接口,因此新增的Hypercall接口需要通过cpuid特性暴露给Guest使用。
MSRs
操作系统可以通过rdmsr/wrmsr两个处理器指令读或写目标MSR寄存器的值。MSR寄存器可以分为三类:
- Architectural MSRs(处理器架构定义,例如X86)
MSR-Number MSR-Name
0x010 X64_MSR_TIME_STAMP_COUNTER
0x01B X64_MSR_APIC_BASE
0x0FE X64_MSR_MTRRCAP
...(详细参考X86 CPU手册)
- Vendor-Specific MSRs(处理器厂商自定义,例如AMD或INTEL)
MSR-Number MSR-Name
0xC0010010 AMD_MSR_SYSCFG
0xC001001F AMD_MSR_NB_CFG
...(以上AMD CPU提供, 详细参考AMD X86 CPU手册)
0x006 INTEL_MSR_MONITOR_FILTER_SIZE
0x017 INTEL_MSR_PLATFORM_ID
...(以上INTEL CPU提供, 详细参考INTEL X86 CPU手册)
- Hypervisor Synthetic MSRs(Hypervisor定义)
MSR-Number MSR-Name
0x40000000 HV_X64_MSR_GUEST_OS_ID
0x40000001 HV_X64_MSR_HYPERCALL
0x40000002 HV_X64_MSR_VP_INDEX
0x40000003 HV_X64_MSR_RESET
0x40000010 HV_X64_MSR_VP_RUNTIME
0x40000020 HV_X64_MSR_TIME_REF_COUNT
...(以上Hyper-V提供, 数量较多详细参考HYPER-V TLFS v4.0手册)
guest执行指令#rdmsr HV_X64_MSR_RESET,如果目标MSR属于CPU内部提供,那么将返回物理MSR寄存器的值,如果目标MSR不属于CPU内部提供,将导致#GP异常,陷入ROOT模式由Hypervisor处理后返回。需要注意的是,GuestOS可能运行在不同类型的Hypervisor上,不能假定所有Hypervisor都有提供目标MSR,因此新增的MSR需要通过cpuid特性暴露给Guest使用。
注意:MSRs和CPUID区别:CPUID是单向的,只适用于从底层获取信息,无法用CPUID把信息传递给Hypervisor,而MSR是有提供rdmsr和wrmsr指令,Guest可以把少量信息写入MSR,传递给Hypervisor。
QEMU/KVM实现
hv-relaxed
用例: #qemu -cpu host,hv-relaxed ...
是否配置hv-relaxed决定Guest执行#cpuid 0x40000004返回的寄存器EAX中的bit5。#cpuid 0x40000004返回值各个位是Hypervisor推荐GuestOS使用哪些半虚拟化功能bit5表示,Hypervisor建议GuestOS关闭中断和看门狗定时器超时机制,在虚拟化环境,因vCPU可能被抢占,因此很有可能出现超时,导致windows蓝屏,并非硬件异常了。配置hv-relaxed后,vCPU被长时间抢占不会导致WindowsOS蓝屏,建议所有Windows虚机都打开。
hv-vapic
用例: #qemu -cpu host,hv-vapic ...
KVM提供了如下三个MSR(半虚拟化的APIC),用于pv-guest一次性提交中断请求或中断应答:
HV_X64_MSR_EOI
HV_X64_MSR_ICR
HV_X64_MSR_TPR
是否配置hv-vapic决定Guest执行#cpuid 0x40000003返回的寄存器EAX中的bit4,同时也决定Guest执行#cpuid 0x40000004返回的寄存器EAX的bit3。#cpuid 0x40000003返回值各个位表示Hypervisor具备哪些特性。#cpuid 0x40000004返回值各个位表示Hypervisor推荐Guest使用哪些特性。EAX.bit4(HV_SYNTIMERS_AVAILABLE)为1时指示Guest当前Hypervisor有提供如上三个MSRs。guest可以通过rdmsr/wrmsr指令直接提交中断请求或中断应答,相比全模拟的APCI,HV-VAPIC大量减少vmexit。倘若EAX.bit4(HV_SYNTIMERS_AVAILABLE)为0,Guest执行#wrmsr HV_X64_MSR_EOI将引发异常。
hv-spinlocks
用例: #qemu -cpu host,hv-spinlocks=0x10000 ...
GuestOS执行spinlock期间,其实是可以转让CPU给其他vCPU调度的。短时间的spinlock可以节省vCPU调度开销,长时间的spinlock会浪费CPU资源。为此,参数用于让guest重试"hv-spinlocks=number"次无果后通告hypervisor,主动转让CPU。
hv-spinlocks=0 表示不尝试(一旦guest调用spinlock,立刻退出到hypervisor转让CPU)
hv-spinlocks=0xFFFFFFFF(x86虚机缺省值)任其guest一直执行spinlock。
配置hv-spinlocks决定#cpuid 0x40000004返回后的整个EBX寄存器值。
hv-vpindex
用例: #qemu -cpu host,hv-vpindex ...
Virtual-Processor的编号其实就是LAPIC的编号,编号ID在一台机器内唯一。完全虚拟化环境,程序要获取当前运行CPU的编号,需配置LAPIC寄存器,引发多个vmexit。为此,KVM实现了如下MSR
HV_X64_MSR_VP_INDEX
pv-guest只需要调用#rdmsr HV_X64_MSR_VP_INDEX,一次vmexit就能获取到VP_INDEX。配置hv-vpindex决定Guest执行#cpuid 0x40000003返回的寄存器EAX中的bit6,bit6(HV_VP_INDEX_AVAILABLE)表示运行环境的Hypervisor有提供HV_X64_MSR_VP_INDEX供Guest用。
hv-runtime
用例: #qemu -cpu host,hv-runtime ...
KVM实现了如下MSR,存放vCPU实际运行时间(单位100ns),这可以让GuestOS知道被'stolen'(被抢占时间,#top可以查看)的时间。
HV_X64_MSR_VP_RUNTIME
配置hv-vapic决定Guest执行#cpuid 0x40000003返回的寄存器EAX中的bit0,bit0(HV_VP_RUNTIME_AVAILABLE)表示运行环境的Hypervisor有提供HV_X64_MSR_VP_RUNTIME供Guest用。
hv-crash
用例: #qemu -cpu host,hv-crash ...
KVM实现了如下MSR,当Guest发生CRASH时,会将crash信息写入到上面MSR寄存器,QEMU日志会记录这些信息。
注:写入CRASH信息后会触发Windows关机,此时windows不会再有crashdump生成。配置hv-crash决定Guest执行#cpuid 0x40000003返回的寄存器EAX中的bit10,bit10表示运行环境的Hypervisor有提供如上MSRs供Guest用。
- HV_X64_MSR_CRASH_P0
- HV_X64_MSR_CRASH_P1
- HV_X64_MSR_CRASH_P2
- HV_X64_MSR_CRASH_P3
- HV_X64_MSR_CRASH_P4
- HV_X64_MSR_CRASH_P5
- HV_X64_MSR_CRASH_CTL
hv-synic
用例: #qemu -cpu host,hv-synic ...
KVM实现了如下MSR,Synthetic-interrupt-controller(SynIC,是LAPIC的功能扩展)SynIC是一个半虚拟化中断控制器提供向Guest发送中断机制(VMBus Message),guest通过如下MSR接口控制,VMBus-devices和Hyper-V-synthetic-timers依赖此特性,QEMU目前尚未有VMBus-devices设备。
- HV_X64_MSR_SCONTROL..HV_X64_MSR_EOM (0x40000080..0x40000084)
- HV_X64_MSR_SINT0..HV_X64_MSR_SINT15 (0x40000090..0x4000009F)
配置hv-time决定Guest执行#cpuid 0x40000003返回的寄存器EAX中的bit2,bit2表示运行环境的Hypervisor有提供如上MSRs供Guest用。
hv-stimer
用例: #qemu -cpu host,hv-stimer ...
KVM实现了如下MSR为每个vCPU提供四路独立的定时器,通过MSR提供给Guest:不使用此半虚拟化时钟的windows CPU在空闲时也消耗大量的CPU资源,因为其他非半虚拟化的时钟都会产生周期性的中断,进而导致频繁的vm-exit。
HV_X64_MSR_STIMER0_CONFIG..HV_X64_MSR_STIMER3_COUNT(0x400000B0..0x400000B7)
配置hv-stimer决定Guest执行#cpuid 0x40000003返回的寄存器EAX中的bit3,bit3(HV_SYNTIMERS_AVAILABLE)表示运行环境的Hypervisor有提供如上MSRs供Guest用。
hv-tlbflush
用例: #qemu -cpu host,hv-tlbflush ...
TLB(translation lookaside buffer)缓存:
完整的虚拟地址和物理地址映射信息存放在页表(内存)上,TLB缓存了最频繁用的部分信息。当一个CPU变更了一条virtual--physical-mapping时,需要通过IPI(x86)中断让其他CPU执行TLBflush(清空)这个过程被称为“TLB shoot-down”。对于虚机,目标vCPU可能都还没被调度等,因此hypervisor可以为其实现“TLB shoot-down”优化配置此参数后,会推荐Guest使用Hypercall的方式实现TLBFlush而是不是IPI中断。配置hv-tlbflush决定Guest执行#cpuid 0x40000004返回的寄存器EAX中的bit2,bit2(HV_REMOTE_TLB_FLUSH_RECOMMENDED)推荐Guest使用hyper_call_tlbflush()半虚拟化接口。
hv-ipi
用例: #qemu -cpu host,hv-ipi ...
IPI表示由一个处理器发通告给一组处理器(可能包含自身)。
完全虚拟化IPI通过写APIC寄存器(虽然是KVM模拟的,但是对guest是完全虚拟化)pv-guest可以很轻易的把IPI描述到MSR上,然后通知hypervisor处理。激活该配置后,推荐Guest使用半虚拟化hypercall来实现IPI。配置hv-tlbflush决定Guest执行#cpuid 0x40000004返回的寄存器EAX中的bit10,bit10(HV_CLUSTER_IPI_RECOMMENDED)推荐Guest使用hyper_call_send_ipi()半虚拟化接口。
hv-vendor-id
用例: #qemu -cpu host,hv-vendor-id="KVMKVMKVM" ...
guest执行 #cpuid 0x40000000,后可能返回:
- EAX|EBX|ECX|EDX = "Microsoft Hv"
- EAX|EBX|ECX|EDX = "KVMKVMKVM"
QEMU如果有配置任何hv特性,那么返回"Microsoft Hv",此配置用于配置Hypervisor签名。
hv-reset
用例: #qemu -cpu host,hv-reset ...
KVM实现了如下MSR,Guest可通过写HV_X64_MSR_RESET来执行RESET操作。
- HV_X64_MSR_RESET
不推荐使用!!!,即使在#cpuid 0x40000004中推荐Guest用,windows也可能不使用。
hv-frequencies
用例: #qemu -cpu host,hv-frequencies ...
KVM-Hyper-V提供了如下MSR,Guest无需测量计算可直接从MSR中读取TSC/APCI频率:
- HV_X64_MSR_TSC_FREQUENCY 用于提供TSC频率
- HV_X64_MSR_APIC_FREQUENCY 用于APIC频率
对于物理机,在使用TSC时钟前需要通过rdtsc指令获取TSC中的当前值,通过两次计算算出具体频率。激活后,TSC频率将由KVM显式告诉Guest TSC和APIC时钟频率,ACPI频率计算也是类似的。