问题现象
虚机内使用PoleFS(共享文件系统),在对PoleFS进行性能压测的过程中,我们观察到一个反直觉现象:相同配置的虚机,仅仅因为是否暴露Hypervisor CPUID,IOPS表现出现明显差异(暴露后的顺序读写IOPS是暴露前的两倍)。整个过程中,虚机IO路径、网络链路、存储后端均无变化,这说明真正的瓶颈,很可能藏在虚机系统默认行为中(Hypervisor CPUID的影响)。
结论先行
经过完整的调用链追踪与KVM侧验证,我们最终确认:性能差异的根因不在存储,而在CPU Idle策略。更准确地说,是这条路径:Hypervisor CPUID->haltpoll governor启用->Busy Poll替代HLT->VMEXIT显著减少->IPI唤醒延迟下降->IOPS提升,这条链路揭示了一个经常被忽视的事实:在虚拟化环境中,CPU调度策略对性能的影响,可能远超IO路径本身。
问题定位
热点分析
分别对未暴露和暴露Hypervisor指令的场景进行Perf分析,结果如下:

上图为未Hypervisor指令集时的perf数据,热点集中在__rawspin_unlock_irqrestore,且占比异常偏高。

上图为暴露Hypervisor指令集时的perf数据,__rawspin_unlock_irqrestore函数的热点明显下降,这非常关键,这个现象表明,锁本身没有变,变的是“等待锁的CPU行为”。
内核链路
我们继续沿调用链深入,最终定位到:kvm_para_available(),该函数用于判断CPU是否暴露Hypervisor特征位。一旦成立,Linux将启用:haltpoll cpuidle driver、haltpoll governor。
static void __wake_up_common_lock(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
unsigned long flags;
wait_queue_entry_t bookmark;
bookmark.flags = 0;
bookmark.private = NULL;
bookmark.func = NULL;
INIT_LIST_HEAD(&bookmark.entry);
do {
spin_lock_irqsave(&wq_head->lock, flags);
nr_exclusive = __wake_up_common(wq_head, mode, nr_exclusive,
wake_flags, key, &bookmark);
spin_unlock_irqrestore(&wq_head->lock, flags);
} while (bookmark.flags & WQ_FLAG_BOOKMARK);
}
#define raw_spin_unlock_irqrestore(lock, flags) \
do { \
typecheck(unsigned long, flags); \
_raw_spin_unlock_irqrestore(lock, flags); \
} while (0)通过分析linux kernel代码,kvm_para_available的核心引用在drivers/cpuidle/cpuidle-haltpoll.c:113和drivers/cpuidle/governors/haltpoll.c:143两处,代码如下:
kvm_para_available:
bool kvm_para_available(void)
{
return kvm_cpuid_base() != 0;
}
static inline uint32_t kvm_cpuid_base(void)
{
static int kvm_cpuid_base = -1;
if (kvm_cpuid_base == -1)
kvm_cpuid_base = __kvm_cpuid_base();
return kvm_cpuid_base;
}
static noinline uint32_t __kvm_cpuid_base(void)
{
if (boot_cpu_data.cpuid_level < 0)
return 0; /* So we don't blow up on old processors */
if (boot_cpu_has(X86_FEATURE_HYPERVISOR))
return hypervisor_cpuid_base("KVMKVMKVM\0\0\0", 0);
return 0;
}drivers/cpuidle/cpuidle-haltpoll.c:113引用如下
static int __init haltpoll_init(void)
{
int ret;
struct cpuidle_driver *drv = &haltpoll_driver;
/* Do not load haltpoll if idle= is passed */
if (boot_option_idle_override != IDLE_NO_OVERRIDE)
return -ENODEV;
cpuidle_poll_state_init(drv);
if (!kvm_para_available() || !haltpoll_want())
return -ENODEV;
ret = cpuidle_register_driver(drv);
if (ret < 0)
return ret;
haltpoll_cpuidle_devices = alloc_percpu(struct cpuidle_device);
if (haltpoll_cpuidle_devices == NULL) {
cpuidle_unregister_driver(drv);
return -ENOMEM;
}
ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "cpuidle/haltpoll:online",
haltpoll_cpu_online, haltpoll_cpu_offline);
if (ret < 0) {
haltpoll_uninit();
} else {
haltpoll_hp_state = ret;
ret = 0;
}
return ret;
}drivers/cpuidle/governors/haltpoll.c:143引用如下:
static struct cpuidle_governor haltpoll_governor = {
.name = "haltpoll",
.rating = 9,
.enable = haltpoll_enable_device,
.select = haltpoll_select,
.reflect = haltpoll_reflect,
};
static int __init init_haltpoll(void)
{
if (kvm_para_available())
return cpuidle_register_governor(&haltpoll_governor);
return 0;
}HLT和BusyPoll,这两段内核逻辑实际上在决定一件极其重要的事情:当VCPU短暂空闲时,是“睡眠”,还是“等待”?
HLT模式:看似省电,实则昂贵。当haltpoll未启用时:VCPU执行HLT->触发VMEXIT->控制权回到宿主机,随后若锁被释放:宿主机注入中断->VMENTRY恢复执行,整个链路包含多次特权切换,这些切换带来的影响就是CPU执行延迟。
BusyPoll模式:用CPU换延迟。当haltpoll启用时:1.VCPU保持运行态;2.主动轮询IPI pending位;3.避免频繁VM切换。为防止无限自旋,KVM引入:PLE(Pause Loop Exiting),超过阈值才触发VMEXIT。这是一种非常经典的系统设计哲学:用可控的CPU消耗,换取确定性的低延迟。
实验证明
为了排除偶然性,我们进行了一个“带有攻击性”的实验,在未暴露hypervisor的情况下,强制启用haltpoll。将drivers/cpuidle/cpuidle-haltpoll.c:113和drivers/cpuidle/governors/haltpoll.c:143两处代码调整为如下:
将cpuidle-haltpoll.c:113调整: 原逻辑: if (!kvm_para_available() || !haltpoll_want()) 调整后: if (kvm_para_available() || !haltpoll_want()) 将haltpoll.c:143调整: 原逻辑: if (kvm_para_available()) 调整后: if (!kvm_para_available())
重新编译内核后,fio测试Polefs的IOPS与有hypervisor指令集时基本一致,perf数据如下:

相关逻辑
cpuidle-haltpoll.c和haltpoll.c两处相关逻辑决定了CPU Idle时,是进入HLT还是BusyPoll。在HLT模式下,VCPU VMEXIT到宿主机,执行__rawspin_unlock_irqrestore后,通过IPI(核间中断)触发其他等待lock的VCPU,再次产生VMEXIT,由宿主机VMM触发其他VCPU VMENTRY唤醒。在BusyPoll模式下,VCPU一直处于自旋状态,为避免长时忙等浪费CPU资源,KVM层面设置PLE限制,自旋一定时间后触发限制,然后VMEXIT到宿主机。BusyPoll模式下,由于等锁的VCPU处于运行态,直接检查对应IPI Pending寄存器,没有VMEXIT\VMENTRY,所以延迟较低。

上图为没有hypervisor指令集时,宿主机的KVM Perf数据,VMEXIT中,HLT占比 22.59%,APIC_WRITE占比76.93%

上图为有hypervisor指令集时,宿主机的KVM Perf数据,HLT占比14.67%(-8%),PAUSE_INSTRUCTION占比15.07%,EXTERNAL_INTERRUPT占比8.93%,MSR_WRITE占比60.94%(非hypervisor指令集下使用APIC_WRITE,是老旧CPU架构下的xAPIC中断模式,使用mmio方式,MSR_WRITE是新架构下的x2APIC中断模式,使用MSR寄存器,x2没有APIC和mmio的数量限制)。EXTERNAL_INTERRUPT是BusyPoll模式下,触发PLE的操作。

上图是没有hypervisor指令集,修改Linux Kernel cpuidle-haltpoll.c:113和haltpoll.c:143两处代码后的宿主机KVM Perf数据,HLT占比14.86%,PAUSE_INSTRUCTION占比14.42%,EXTERNAL_INTERRUPT占比8.60%,APIC_WRITE占比62.00%,与有hypervisor指令集的数据基本一致。
综合以上宿主机层面的KVM Perf数据,hypervisor指令集之所以能够影响__rawspin_unlock_irqrestore的性能,核心在于CPU Idle场景下,目标VCPU的行为,HLT带来的VMEXIT\VMENTRY提升了IPI的延迟,Hypervisor指令将VCPU从“休眠 + 中断唤醒”转向“忙等 + 快速响应”。
针对这类性能问题,我们的常规视角是:应用->IO->网络->存储,但在虚拟化背景下,真实路径是:应用->锁竞争->CPU Idle策略->VMEXIT->调度延迟->底层IO,在真正的IO之前,隐藏着更复杂的链路,这正是现代系统性能分析最容易被误判的地方。
HaltPoll的本质:虚拟化时代的“自旋锁哲学”。haltpoll其实代表一种重要思想:短Idle ≠ 值得睡眠,尤其在NVMe、分布式存储、高QPS数据库,在这些场景中,HLT反而是一种错误优化。
优化建议
优化点:
1.默认开启暴露Hypervisor CPUID,尤其适用于:高IOPS虚机、数据库、低延迟服务等。
2.建立VMEXIT观测体系,在宿主机侧进行VMEXIT观测,分析虚拟化层VMEXIT原因并进行优化
通过本次链路分析,会发现瓶颈越来越多出现在“调度与虚拟化边界”。未来的高性能优化也将包含:Guest-Hypervisor、调度-中断等这些“看不见”的地方。