amd和intel在APIC timer虚拟化中的差异
这里有两篇博客写得很好:KVM APIC Timer 模拟详解-CSDN博客 KVM CPU虚拟化_vapic-CSDN博客
在amd机器和intel机器上测试虚拟机性能时发现一个奇怪的问题。由于x86机器上没有为虚拟化专门设置timer设备,虚拟机内的timer依然需要使用物理机上的clock event设备。timer中断的统计数据可以从/proc/interrupts里面的LOC一栏找到对应cpu的timer中断数量。因此,一个物理cpu上的时钟中断数量应该等于物理时钟中断数量加上虚拟机时钟中断数量。在amd机器上的确如此,但是在intel机器上却不是这样。
由于操作系统一般会动态调整时钟中断的频率,idle的时候可能处于nohz状态,有进程要运行的时候处于有时钟中断状态。在vcpu运行一个可以占满cpu‘的程序,然后在host上监控vcpu所在的cpu的时钟中断频率(这里最好将vcpu绑定到某个cpu上)发现intel机器的时钟中断频率只等于物理机时钟中断频率,缺少了虚拟机时钟中断的部分。
在虚拟机中观察v'cpu的时钟中断频率,发现intel的时钟中断频率波动很大,总体数量跟guest设置的相当。也就是说guest确实收到了timer中断。这就奇怪了,虚拟机中断有,却没有对应的物理时钟中断。
在找代码之前,我们先要了解一下x86设置timer的规则。根据intel或者amd的文档,timer一般是由APIC timer提供的。设置APIC timer需要设置3个寄存器:

The local APIC unit contains a 32-bit programmable timer that is available to software to time events or operations.
This timer is set up by programming four registers: the divide configuration register (see Figure 11-10), the initialcount
and current-count registers (see Figure 11-11), and the LVT timer register (see Figure 11-8).
LVT timer register:控制timer的模式,比如one-shot,periodic,tscdeadline(intel);
initial counter register:设置倒计时counter数值,timer开始后,每次减一,减到零时timer中断就触发了;
divide configuration register: 分频设置,APIC timer的频率除以分频值就得到了该timer运行时的频率,只有3个bit,从0-6对应7种不同的分频;
每次设置的时候先设置LVT和divide 寄存器,再设置initial counter寄存器。
了解了如何设置timer,我们来看看kvm是如何模拟APIC timer的。
当guest通过APIC寄存器来设置timer时会导致vm exit,从而trap到kvm,最终由kvm_lapic_reg_write来模拟。
static int kvm_lapic_reg_write(struct kvm_lapic *apic, u32 reg, u32 val) { ... case APIC_LVTT: if (!kvm_apic_sw_enabled(apic)) val |= APIC_LVT_MASKED; val &= (apic_lvt_mask[0] | apic->lapic_timer.timer_mode_mask); kvm_lapic_set_reg(apic, APIC_LVTT, val); apic_update_lvtt(apic); break; case APIC_TMICT: if (apic_lvtt_tscdeadline(apic)) break; cancel_apic_timer(apic); kvm_lapic_set_reg(apic, APIC_TMICT, val); start_apic_timer(apic); break; ... }
start_apic_timer会调用restart_apic_timer去实现timer设置。
static void restart_apic_timer(struct kvm_lapic *apic) { preempt_disable(); if (!apic_lvtt_period(apic) && atomic_read(&apic->lapic_timer.pending)) goto out; if (!start_hv_timer(apic)) start_sw_timer(apic); out: preempt_enable(); }
restart_apic_timer会先尝试start_hv_timer,如果失败再去调用start_sw_timer。amd和intel的差距就在这里。
static bool start_hv_timer(struct kvm_lapic *apic) { struct kvm_timer *ktimer = &apic->lapic_timer; struct kvm_vcpu *vcpu = apic->vcpu; bool expired; WARN_ON(preemptible()); if (!kvm_can_use_hv_timer(vcpu)) return false; if (!ktimer->tscdeadline) return false; if (static_call(kvm_x86_set_hv_timer)(vcpu, ktimer->tscdeadline, &expired)) return false; ktimer->hv_timer_in_use = true; hrtimer_cancel(&ktimer->timer); ... }
使用hv timer是有条件:
bool kvm_can_use_hv_timer(struct kvm_vcpu *vcpu) { return kvm_x86_ops.set_hv_timer && !(kvm_mwait_in_guest(vcpu->kvm) || kvm_can_post_timer_interrupt(vcpu)); }
要设置set_hv_timer回调,这一点amd就不满足,它没有这个回调,而intel是有的,其实这个hv timer确实是intel专门设置的。
我们可以看到start_hv_timer会关闭hrtimer,这样就不会有真是的物理中断出现在host的LOC上了。
那么hv timer是靠什么来实现timer中断的?答案是VMX preempt timer。这是intel独有的feature。它会在VMCS结构中设置倒计时counter。
static void vmx_update_hv_timer(struct kvm_vcpu *vcpu, bool force_immediate_exit) { struct vcpu_vmx *vmx = to_vmx(vcpu); u64 tscl; u32 delta_tsc; if (force_immediate_exit) { vmcs_write32(VMX_PREEMPTION_TIMER_VALUE, 0); vmx->loaded_vmcs->hv_timer_soft_disabled = false; } else if (vmx->hv_deadline_tsc != -1) { tscl = rdtsc(); if (vmx->hv_deadline_tsc > tscl) /* set_hv_timer ensures the delta fits in 32-bits */ delta_tsc = (u32)((vmx->hv_deadline_tsc - tscl) >> cpu_preemption_timer_multi); else delta_tsc = 0; vmcs_write32(VMX_PREEMPTION_TIMER_VALUE, delta_tsc); vmx->loaded_vmcs->hv_timer_soft_disabled = false; } else if (!vmx->loaded_vmcs->hv_timer_soft_disabled) { vmcs_write32(VMX_PREEMPTION_TIMER_VALUE, -1); vmx->loaded_vmcs->hv_timer_soft_disabled = true; } }
这样做的好处是可以省去物理timer中断host处理部分的开销。intel kvm代码中提供了一个针对因timer发生exit的fastpath。
static fastpath_t vmx_exit_handlers_fastpath(struct kvm_vcpu *vcpu, bool force_immediate_exit) { ... case EXIT_REASON_PREEMPTION_TIMER: return handle_fastpath_preemption_timer(vcpu, force_immediate_exit); ... }
sw timer的实现依赖hrtimer。
start_sw_timer ---> start_sw_period { if (!apic->lapic_timer.period) return; if (ktime_after(ktime_get(), apic->lapic_timer.target_expiration)) { apic_timer_expired(apic, false); if (apic_lvtt_oneshot(apic)) return; advance_periodic_target_expiration(apic); } hrtimer_start(&apic->lapic_timer.timer, apic->lapic_timer.target_expiration, HRTIMER_MODE_ABS_HARD);
}
所以我们可以在amd的机器上看到所有的虚拟timer中断。
至此文章开头的疑惑也就解开了。intel在timer虚拟化中,优先选择了hv timer的模式,而amd只支持sw timer模式。相对而言,intel的方案开销更小,但是会有注入的虚拟中不稳定的缺点(原因可能跟preemption timer有关)
浙公网安备 33010602011771号