KVM: User return MSRs

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