kvm_shared_msrs机制与分析
在做KVM模块热升级的过程中碰到了这个坑,通读代码时本来以为msr在vcpu load和vcpu exit中进行切换,便忽略了kvm_shared_msr_cpu_online,没想到它能直接重置了host,连投胎的过程都没有,直接没办法debug,还是要多亏这个问题在某种情况下不必现,chengwei才更快找到原因,顺便看了一下kvm_shared_msrs机制,理清楚了问题的触发逻辑,记录如下。 代码版本:https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git v5.1
1. kvm shared msr的作用
guest在发生VM-exit时会切换保存guest的寄存器值,加载host寄存器值,因为host侧可能会使用对应的寄存器的值。当再次进入VM即发生vcpu_load时,保存host寄存器值,加载guest的寄存器值。来回的save/load就是成本,而某些msr的值在某种情况是不会使用的,那边就无需进行save/load,这些msr如下:
/* x86-64 specific MSRs */#define MSR_EFER 0xc0000080 /* extended feature register */ #define MSR_STAR 0xc0000081 /* legacy mode SYSCALL target */ #define MSR_LSTAR 0xc0000082 /* long mode SYSCALL target */ #define MSR_CSTAR 0xc0000083 /* compat mode SYSCALL target */ #define MSR_SYSCALL_MASK 0xc0000084 /* EFLAGS mask for syscall */ #define MSR_FS_BASE 0xc0000100 /* 64bit FS base */ #define MSR_GS_BASE 0xc0000101 /* 64bit GS base */ #define MSR_KERNEL_GS_BASE 0xc0000102 /* SwapGS GS shadow */ #define MSR_TSC_AUX 0xc0000103 /* Auxiliary TSC */ const u32 vmx_msr_index[] = { #ifdef CONFIG_X86_64 MSR_SYSCALL_MASK, MSR_LSTAR, MSR_CSTAR, #endif MSR_EFER, MSR_TSC_AUX, MSR_STAR, };
这些msr只在userspace才会被linux OS使用,kernel模式下并不会被读取,具体msr作用见如上注释。那么当VM发生VM-exit时,此时无需load host msr值,只需要在VM退出到QEMU时再load host msr,因为很多VM-exit是hypervisor直接处理的,无需退出到QEMU,那么此处就有了一些优化。
2. kvm shared msr在KVM模块加载中的处理
kvm shared msr有两个变量,shared_msrs_global和shared_msrs,对应代码如下:
#define KVM_NR_SHARED_MSRS 16 //标记这有那些MSR需要被shared,具体msr index存储在msrs下 struct kvm_shared_msrs_global { int nr; u32 msrs[KVM_NR_SHARED_MSRS]; }; //这是per cpu变量,每个cpu下需要存储的msr值都在values中 struct kvm_shared_msrs { struct user_return_notifier urn; bool registered; struct kvm_shared_msr_values { //host上msr值save到此处 u64 host; //当前物理CPU上msr的值 u64 curr; } values[KVM_NR_SHARED_MSRS]; }; static struct kvm_shared_msrs_global __read_mostly shared_msrs_global; static struct kvm_shared_msrs __percpu *shared_msrs;
shared_msrs在kvm_arch_init下初始化:
shared_msrs = alloc_percpu(struct kvm_shared_msrs);
shared_msrs_global在hardware_setup下初始化,就是将vmx_msr_index的msr index填充到shared_msrs_global中:
void kvm_define_shared_msr(unsigned slot, u32 msr) { BUG_ON(slot >= KVM_NR_SHARED_MSRS); shared_msrs_global.msrs[slot] = msr; if (slot >= shared_msrs_global.nr) shared_msrs_global.nr = slot + 1; } static __init int hardware_setup(void) { for (i = 0; i < ARRAY_SIZE(vmx_msr_index); ++i) kvm_define_shared_msr(i, vmx_msr_index[i]); }
kvm_arch_init和hardware_setup都是在KVM模块加载过程中执行的.
3. kvm shared msr在VM创建中的处理
VM创建过程中执行kvm_arch_hardware_enable,继而kvm_shared_msr_cpu_online,shared_msr_update函数负责存储host的msr值
static void kvm_shared_msr_cpu_online(void) { unsigned i; for (i = 0; i < shared_msrs_global.nr; ++i) shared_msr_update(i, shared_msrs_global.msrs[i]); } static void shared_msr_update(unsigned slot, u32 msr) { u64 value; unsigned int cpu = smp_processor_id(); struct kvm_shared_msrs *smsr = per_cpu_ptr(shared_msrs, cpu); /* only read, and nobody should modify it at this time, * so don't need lock */ if (slot >= shared_msrs_global.nr) { printk(KERN_ERR kvm: invalid MSR slot!); return; } //VM-enter还没发生,此时msr是host值,而且host msr只获取这一次,VM生命周期内再也不会更新 rdmsrl_safe(msr, &value); smsr->values[slot].host = value; smsr->values[slot].curr = value; }
4. kvm shared msr在VM运行中的切换
前面提到host msr值已经保存在smsr->values[slot].host下,那么进入guest前则会执行vmx_prepare_switch_to_guest,通过kvm_set_shared_msr完成加载guest msr的动作。
/* * Note that guest MSRs to be saved/restored can also be changed * when guest state is loaded. This happens when guest transitions * to/from long-mode by setting MSR_EFER.LMA. */ if (!vmx->guest_msrs_ready) { vmx->guest_msrs_ready = true; for (i = 0; i < vmx->save_nmsrs; ++i) kvm_set_shared_msr(vmx->guest_msrs[i].index, vmx->guest_msrs[i].data, vmx->guest_msrs[i].mask); } if (vmx->guest_state_loaded) return;
另外因为guest可以也会设置guest msr而发生VM-exit,这一步则有vmx_set_msr来完成,此处可知,guest msr保存在vmx->guest_msrs中。 再说kvm_set_shared_msr,就是设置物理MSR值,同时将值保存在smsr->values[slot].curr。
int kvm_set_shared_msr(unsigned slot, u64 value, u64 mask) { unsigned int cpu = smp_processor_id(); struct kvm_shared_msrs *smsr = per_cpu_ptr(shared_msrs, cpu); int err; if (((value ^ smsr->values[slot].curr) & mask) == 0) return 0; smsr->values[slot].curr = value; err = wrmsrl_safe(shared_msrs_global.msrs[slot], value); if (err) return 1; if (!smsr->registered) { smsr->urn.on_user_return = kvm_on_user_return; user_return_notifier_register(&smsr->urn); smsr->registered = true; } return 0; }
5. user_return_notifier的作用
kvm_set_shared_msr末尾设置了smsr->urn.on_user_return为kvm_on_user_return,user_return_notifier_register将其注册到return_notifier_list,顾名思义,就是返回用户态时的通知链。 在do_syscall_64和do_fast_syscall_32都会处理到prepare_exit_to_usermode,在exit_to_usermode_loop中会执行fire_user_return_notifiers,将链表上的函数执行一遍。
void fire_user_return_notifiers(void) { struct user_return_notifier *urn; struct hlist_node *tmp2; struct hlist_head *head; head = &get_cpu_var(return_notifier_list); hlist_for_each_entry_safe(urn, tmp2, head, link) urn->on_user_return(urn); put_cpu_var(return_notifier_list); }
实际上此时使用user_return_notifier_register只有kvm_set_shared_msr,看一下回调函数kvm_on_user_return,就干了两件事,将smsr->values[slot].host写入到msr中,和取消kvm_on_user_return的注册。
static void kvm_on_user_return(struct user_return_notifier *urn) { unsigned slot; struct kvm_shared_msrs *locals = container_of(urn, struct kvm_shared_msrs, urn); struct kvm_shared_msr_values *values; unsigned long flags; /* * Disabling irqs at this point since the following code could be * interrupted and executed through kvm_arch_hardware_disable() */ local_irq_save(flags); if (locals->registered) { locals->registered = false; user_return_notifier_unregister(urn); } local_irq_restore(flags); for (slot = 0; slot < shared_msrs_global.nr; ++slot) { values = &locals->values[slot]; if (values->host != values->curr) { wrmsrl(shared_msrs_global.msrs[slot], values->host); values->curr = values->host; } } }
6. 问题出现的场景
当KVM模块A上的VM的某个VCPU运行在非用户态时,KVM模块B加载,因为kvm_shared_msr_cpu_online是通过kvm_arch_hardware_enable,hardware_enable_nolock在hardware_enable_all下的on_each_cpu函数上执行,即kvm_shared_msr_cpu_online被执行前,KVM模块A上的VCPU并没有发生从kernel->userspace,那么此时KVM模块B上kvm_shared_msrs.values[X].host存储的则是KVM模块A上VM的guest msr值,当KVM模块B上的VM切换到QEMU时,此刻host msr则被置为KVM模块A上VM的guest msr,于是就崩了。
kvm_shared_msrs机制与分析来自于OenHan
链接为:https://oenhan.com/kvm_shared_msrs