KVM: User return MSRs

2023年 10月 10日 132.5k 0

学习 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

  • [PATCH 0/4] User return notifiers / just-in-time MSR switching for KVM - Avi Kivity (kernel.org)
  • [PATCH 21/35] KVM: x86 shared msr infrastructure - Avi Kivity (kernel.org)
  • [PATCH v2 01/15] KVM: x86: Rename "shared_msrs" to "user_return_msrs" - Sean Christopherson (kernel.org)
  • Related

    KVM shared MSRs – GeekBen (luo666.com)

    相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论