学习 KVM 时,遇到了一些与 user return msr 有关的问题,因此写下这篇文章记录一下 uret msr 虚拟化方式,理解有误的地方,欢迎大家指出。
在对 x86 架构中的 MSR 寄存器进行虚拟化时,guest/host 可能对 MSR 设置不同的值。为解决这一问题,VMCS 中保存了一部分 MSR 的值,用于 vm-entry/vm-exit 过程中由硬件完成寄存器切换。但还有一部分 MSR 是由软件保存和切换,相较于硬件辅助,开销比较大。
这类软件维护的 MSR 中,有一部分只会在 user space 被使用,因此 Redhat 的 Avi 提交了一份 patch [1][2],使得它们不用在 vm-exit 时,切换回 host 设置的值,而只需要在返回 user space 时检查是否更新物理 MSR ,恢复 host MSR。Avi 在提交的 patch set 中将它们命名为 shared msrs,后来 Sean 为了避免歧义,改名为 user return msrs [2]。
结合代码 linux v6.1.56,uret MSR 的虚拟化实现中,引入了三个数组:
- kvm_uret_msrs_list[] 保存着 KVM 支持的 uret MSRs 地址
- user_return_msrs[] 保存每个 pCPU 中的 uret MSRs 的值
- vcpu_vmx->guest_uret_msrs[] 保存着每个 vCPU 中的 uret MSRs 的值
统计 uret MSRs
使用 kvm_uret_msrs_list 记录 KVM 支持的 user return MSR 的地址,kvm_nr_uret_msrs 则记录 MSR 的数量。也是初始化 kvm 内核模块时,调用函数 vmx_setup_user_return_msrs() 将 MSR 地址添加到 kvm_uret_msrs_list 中。
添加 MSR 地址时,根据给出的待选 MSRs (vmx_uret_msrs_list),会逐一对 host 上的寄存器进行检查,KVM 最终只会将能够正常读写的物理寄存器的地址添加到列表 kvm_uret_msrs_list 中,并记录数量。
u32 __read_mostly kvm_nr_uret_msrs;
static u32 __read_mostly kvm_uret_msrs_list[KVM_MAX_NR_USER_RETURN_MSRS];
vmx_init
kvm_x86_vendor_init
kvm_init
kvm_arch_hardware_setup
ops->hardware_setup();
=> hardware_setup
vmx_setup_user_return_msrs
kvm_add_user_return_msr
static __init void vmx_setup_user_return_msrs(void)
{
const u32 vmx_uret_msrs_list[] = {
#ifdef CONFIG_X86_64
MSR_SYSCALL_MASK, MSR_LSTAR, MSR_CSTAR,
#endif
MSR_EFER, MSR_TSC_AUX, MSR_STAR,
MSR_IA32_TSX_CTRL,
};
int i;
BUILD_BUG_ON(ARRAY_SIZE(vmx_uret_msrs_list) != MAX_NR_USER_RETURN_MSRS);
for (i = 0; i < ARRAY_SIZE(vmx_uret_msrs_list); ++i)
kvm_add_user_return_msr(vmx_uret_msrs_list[i]);
}
int kvm_add_user_return_msr(u32 msr)
{
if (kvm_probe_user_return_msr(msr))
return -1;
kvm_uret_msrs_list[kvm_nr_uret_msrs] = msr;
return kvm_nr_uret_msrs++;
}
记录 host MSR
KVM 通过一个 percpu 变量保存 host MSRs 。
vmx 初始化时,会创建 percpu 变量 user_return_msrs,用于保存 vCPU 切换到 guest 前,host 中 msr 的值。
static struct kvm_user_return_msrs __percpu *user_return_msrs;
struct kvm_user_return_msrs {
struct user_return_notifier urn; // notifier 保存回调函数
bool registered; // true, notifier 已经注册到通知链中
struct kvm_user_return_msr_values {
u64 host;
u64 curr;
} values[KVM_MAX_NR_USER_RETURN_MSRS];
};
vmx_init
kvm_x86_vendor_init
user_return_msrs = alloc_percpu(struct kvm_user_return_msrs);
然后是更新 percpu 变量。
kvm 初始化时定义了启动 vCPU 的回调函数:kvm_starting_cpu()
在启动 vCPU 时,就根据 kvm_uret_msrs_list[] 支持的 uret MSRs, 将寄存器在 host 中的值保存到 percpu 变量中。
kvm_init
kvm_arch_hardware_setup
r = cpuhp_setup_state_nocalls(CPUHP_AP_KVM_STARTING, "kvm/cpu:starting",
kvm_starting_cpu, kvm_dying_cpu);
kvm_starting_cpu
hardware_enable_nolock
kvm_arch_hardware_enable
kvm_user_return_msr_cpu_online
static void kvm_user_return_msr_cpu_online(void)
{
unsigned int cpu = smp_processor_id();
struct kvm_user_return_msrs *msrs = per_cpu_ptr(user_return_msrs, cpu);
u64 value;
int i;
for (i = 0; i values[i].host = value;
msrs->values[i].curr = value;
}
}
记录 guest MSR
KVM 通过 vcpu_vmx 结构体中的一个数组记录 guest MSRs 。
- vcpu_vmx->guest_uret_msrs[] 记录下 guest 设置的 MSR 的值。
- vcpu_vmx->guest_uret_msrs_loaded 表示 guest 的 uret MSRs 值是否已经载入到对应的物理寄存器。
- vmx_uret_msr->load_into_hardware 表示相关的值是否需要载入物理 MSR 。因为对于那些没有被激活的寄存器,不需要切换它们的寄存器值。
struct vcpu_vmx {
struct vmx_uret_msr guest_uret_msrs[MAX_NR_USER_RETURN_MSRS];
bool guest_uret_msrs_loaded;
};
struct vmx_uret_msr {
bool load_into_hardware;
u64 data;
u64 mask;
};
Guest VMM (如 QEMU) 通过 ioctl KVM_CREATE_VCPU 创建 vCPU 时,初始化 vmcs 的过程中就会将 guest 需要设置的 user return MSRs 的值保存在数组 vmx->guest_uret_msrs[] 中。
kvm_vm_ioctl
case KVM_CREATE_VCPU:
kvm_vm_ioctl_create_vcpu
kvm_arch_vcpu_create
kvm_vcpu_reset
vmx_vcpu_reset
__vmx_vcpu_reset
init_vmcs
vmx_setup_uret_msrs
static void vmx_setup_uret_msrs(struct vcpu_vmx *vmx)
{
vmx_setup_uret_msr(vmx, MSR_EFER, update_transition_efer(vmx));
...
vmx_setup_uret_msr(vmx, MSR_IA32_TSX_CTRL, boot_cpu_has(X86_FEATURE_RTM));
vmx->guest_uret_msrs_loaded = false;
}
static void vmx_setup_uret_msr(struct vcpu_vmx *vmx, unsigned int msr,
bool load_into_hardware)
{
struct vmx_uret_msr *uret_msr;
uret_msr = vmx_find_uret_msr(vmx, msr);
if (!uret_msr)
return;
uret_msr->load_into_hardware = load_into_hardware;
}
vmx_find_uret_msr() 用于从 KVM 支持的 uret MSRs 中查找寄存器编号。只有 KVM 支持保存的寄存器,才会将 guest 设置的值保存下来。
vmx_find_uret_msr
int i = kvm_find_user_return_msr(msr);
if (i >= 0)
return &vmx->guest_uret_msrs[i];
int kvm_find_user_return_msr(u32 msr)
{
int i;
for (i = 0; i < kvm_nr_uret_msrs; ++i) {
if (kvm_uret_msrs_list[i] == msr)
return i;
}
return -1;
}
vCPU 进入 guest
vcpu 切换到 guest 前,会将 host msr 保存到 percpu 变量 user_return_msrs 中。并把一个 notifier 注册到内核中,用于 vCPU 切换到用户态时的 MSR 切换。当 vCPU 进入 user space / qemu 时,调用 kvm_on_user_return 切换回 host 之前保存的 MSR。
vcpu_enter_guest
static_call(kvm_x86_prepare_switch_to_guest)(vcpu);
=> vmx_prepare_switch_to_guest
void vmx_prepare_switch_to_guest(struct kvm_vcpu *vcpu)
{
if (!vmx->guest_uret_msrs_loaded) {
vmx->guest_uret_msrs_loaded = true;
for (i = 0; i guest_uret_msrs[i].load_into_hardware)
continue;
kvm_set_user_return_msr(i,
vmx->guest_uret_msrs[i].data,
vmx->guest_uret_msrs[i].mask);
}
}
}
int kvm_set_user_return_msr(unsigned slot, u64 value, u64 mask)
{
struct kvm_user_return_msrs *msrs = per_cpu_ptr(user_return_msrs, cpu);
value = (value & mask) | (msrs->values[slot].host & ~mask);
if (value == msrs->values[slot].curr)
return 0;
err = wrmsrl_safe(kvm_uret_msrs_list[slot], value);
msrs->values[slot].curr = value;
if (!msrs->registered) {
msrs->urn.on_user_return = kvm_on_user_return;
user_return_notifier_register(&msrs->urn);
msrs->registered = true;
}
}
发生 vm-exit, vCPU 退出到 host 时,标志着 guest 的 uret MSRs 载入到物理寄存器的变量也会改为 false。至于保存 host uret MSRs 会不会写入到物理寄存器,得看 vCPU 是否会退出到 user space。
vmx_prepare_switch_to_host
vmx->guest_uret_msrs_loaded = false;
vCPU 返回用户空间
线程从用户空间 (user space) 切换到内核空间,有系统调用和中断两种方式。当线程返回用户空间时,就会通过 vCPU vm-entry 注册的 notifier, 触发寄存器的切换程序。
syscall_exit_to_user_mode_work
__syscall_exit_to_user_mode_work
exit_to_user_mode_prepare
irqentry_exit_to_user_mode
exit_to_user_mode_prepare
exit_to_user_mode_prepare
arch_exit_to_user_mode_prepare
fire_user_return_notifiers
void fire_user_return_notifiers(void)
{
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);
}
kvm_on_user_return() 在恢复物理 MSR 的同时,也把自己从通知链上注销了,因为一方面不需要别的进程在退出的时候也执行这个函数,另一方面在 vCPU enter guest 的时候还会再注册的。
static void kvm_on_user_return(struct user_return_notifier *urn)
{
if (msrs->registered) {
msrs->registered = false;
user_return_notifier_unregister(urn);
}
for (slot = 0; slot values[slot];
if (values->host != values->curr) {
wrmsrl(kvm_uret_msrs_list[slot], values->host);
values->curr = values->host;
}
}
}
到此,vCPU 就完成了 uret MSRs 的保存和切换。
Reference
Related
KVM shared MSRs – GeekBen (luo666.com)