虚拟机中的 IPI 优化: IPIv

linux tag: v6.8-rc1

1. IPIv 介绍

patches:

Intel IPI Virtualization Ready For Linux 5.19

[PATCH v8 0/9] IPI virtualization support for VM - Zeng Guang

[PATCH v9 0/9] IPI virtualization support for VM - Zeng Guang

[PATCH v9 9/9] KVM: VMX: enable IPI virtualization - Zeng Guang

其他资料:

关于虚拟机中IPI中断的思考-腾讯云开发者社区-腾讯云

Hardware-assisted IPI Virtualization IPI虚拟化(IPI Virtualization)之一(基本原理)-天翼云开发者社区 - 天翼云

IPIv (IPI virtualization) 是 Intel 提供的一种硬件辅助的 IPI 虚拟化方案。通常 Guest 中虚拟 IPI 的传递需要两次 VM-exit:

  • 发送中断的 Core 写 ICR 时,需要通过 VM-exit 由 KVM 发送一个真实的 IPI。
    • 当 Local APIC 处于 xAPIC mode, 采用 memory-mapped 方式访问 APIC 并触发 VM-exit。
    • 当 Local APIC 处于 x2APIC mode, 采用 MSR-based 方式访问 APIC 并触发 VM-exit。
  • 目标 vCPU 接收 IPI 时,需要先 VM-exit 将中断注入到 Guest OS。

使用 IPI virtualization 解决发送端 vCPU 退出的问题, 使用 Posted Interrupt 解决接收端 vCPU 退出的问题。

具体来说,当 CPU 支持且开启了硬件辅助的 IPI 虚拟化 ( 即 IPIv ) 时,采用下面几种方式发送 IPI 并不会触发 VM-exit。

  • Memory-mapped ICR writes。xAPIC 模式下,写 APIC-access page 中偏移量为 300H,即虚拟 ICR 的低 32 位地址,触发 IPI。
  • MSR-mapped ICR writes。x2APIC 模式下,使用 WRMSR 0x83F,即通过写寄存器 ICR 的物理地址,触发 IPI。
  • 通过 SENDUIPI 发送一个 user IPI。

在支持 IPIv 的架构中,VMCS 中记录了 PID 指针列表的地址 PID-pointer table address ,用于找到 PID-pointer table。也记录了 PID-pointer table 中最后一个表项的索引号 Last PID-pointer index

上面的三种操作均会导致硬件产生一个 8 位的虚拟中断向量 (用 V 表示) 和 32 位的虚拟 APIC ID (用 T 表示),并使用 PID (posted-interrupt descriptor) 发送中断。

  • 如果 T 不超过 PID-pointer table 的最后一个表项索引号,则从表中找到一个对应的表项作为 PID 的地址。
  • 然后使用虚拟中断向量,将 PID: PIR(PID[255:0])V 对应的位置 1。
  • 最后将 PID: NDST(PID[319:288])PID: NV(PID[279:272]) 填入 ICR (xAPIC 和 x2APIC 有略微区别),以发送 IPI 通知。

由于 IPIv 大多是操作都是硬件帮忙做了,所以在 KVM 中的实现主要是做一些硬件配置的工作。

不过 IPIv 也有局限,要求中断类型是:physical mode, no shorthand (unicast), fixed, edge。 [PATCH v8 0/9] IPI virtualization support for VM - Zeng Guang

IPI virtualization is a new VT-x feature, targeting to eliminate VM-exits on source vCPUs when issuing unicast, physical-addressing IPIs.

1.1 与 pv-IPI 的区别

IPIv 和 pv-IPI (见 虚拟机中的 IPI 优化: pv-IPI) 的区别:

  • pv-IPI 是将 guest 中多个写 ICR 发送 IPI 的操作,使用一个 kvm_hypercall 完成 (multi-cast); 而 IPIv 是针对单个 ICR 写 (unicast)。
  • pv-IPI 仍然会有 VM-exit (EXIT_REASON_VMCALL) 和 VM-entry; 而 IPIv 不会。

理论上来说关闭 pv-IPI 可以更好的发挥 IPIv 的功能 (有时间再测试一下)。

2. 如何测试 IPIv 性能

查看 host kernel 是否具备 IPIv feature, 缺省情况下 kvm 是 enable IPIv 的;上述参数返回 Y。

bash 复制代码
cat /sys/module/kvm_intel/parameters/enable_ipiv

如果要 disable IPIv :

bash 复制代码
modprobe -r kvm_intel && modprobe kvm_intel enable_ipiv=0

测试性能时,可以分别在关闭和开启 IPIv 的情况下,测量 guest 产生的 VM-exit 次数。或者借助 KVM-Unit-Test 中的测试子项 smptest。

bash 复制代码
# 在 disable IPIv 的情况下,执行:
perf kvm stat record -o perf.data.guest.noipiv ./tests/smptest
# 查看 apic_write/msr_write 的 VM-exit
perf kvm -i perf.data.guest.noipiv stat report

# enable IPIv,再重复一次 perf
# 如果看到 VM-exit 的数目少了很多,表明 IPIv 的功能是生效的

IPIv 的代码实现逻辑如下。

3. 开启硬件辅助 IPI 虚拟化

启动 guest 的 IPIv,需要配置 VMCS 的 Processor-Based VM-Execution Controls,该控制域包含:

  • the primary processor-based VM-execution controls (32 bits),
  • the secondary processor-based VM-execution controls (32 bits),
  • the tertiary processor-based VM-execution controls (64 bits).

需要将 tertiary processor-based VM-execution controls 的 bit4 置为 1。

linux 中定义了 TERTIARY_EXEC_IPI_VIRT

c 复制代码
// `TERTIARY_EXEC_IPI_VIRT` 为 Tertiary Processor-Based VM-Execution Controls 中的 bit4 
#define TERTIARY_EXEC_IPI_VIRT                  VMCS_CONTROL_BIT(IPI_VIRT)

// 对宏 `VMCS_CONTROL_BIT` 的定义
#define VMCS_CONTROL_BIT(x)     BIT(VMX_FEATURE_##x & 0x1f)

/* Tertiary Processor-Based VM-Execution Controls, word 3 */
#define VMX_FEATURE_IPI_VIRT            ( 3*32+  4) /* Enable IPI virtualization */

由于 tertiary processor-based VM-execution controls 并不是所有位都可以配置,它由寄存器 IA32_VMX_PROCBASED_CTLS3 (MSR 492H) 决定。

IA32_VMX_PROCBASED_CTLS3

  • "allowed 1-settings of the tertiary processor based VM-execution controls",即 VMCS 控域中允许置为 1 的位。
  • 比如 IA32_VMX_PROCBASED_CTLS3[4] 为 1,表示 tertiary processor-based VM-execution controls 中 bit4 允许置为 1,即允许开启 IPIv。

3.1 init_vmx_capabilities()

在 Linux 代码中,Host 上开始运行 kernel 时,会检测硬件平台支持的 VMX feature,然后记录到 boot_cpu_data.vmx_capability[] 中。比如读取 MSR_IA32_VMX_PROCBASED_CTLS3 的值。

c 复制代码
identify_boot_cpu
	identify_cpu(&boot_cpu_data);
identify_secondary_cpu
	identify_cpu(c);

static void identify_cpu(struct cpuinfo_x86 *c)
	this_cpu->c_init(c);
	==> init_intel
			init_ia32_feat_ctl
				init_vmx_capabilities(c);
				
#define MSR_IA32_VMX_PROCBASED_CTLS3    0x00000492
static void init_vmx_capabilities(struct cpuinfo_x86 *c)
{
	// ...
	/* All 64 bits of tertiary controls MSR are allowed-1 settings. */
	rdmsr_safe(MSR_IA32_VMX_PROCBASED_CTLS3, &low, &high);
	c->vmx_capability[TERTIARY_CTLS_LOW] = low;
	c->vmx_capability[TERTIARY_CTLS_HIGH] = high;
}

当打开文件 /proc/cpuinfo 时,会通过 show_cpuinfo() 获取平台的信息,包括支持的 VMX feature。

c 复制代码
// arch/x86/kernel/cpu/proc.c
static int show_cpuinfo(struct seq_file *m, void *v)
{
	// ...
#ifdef CONFIG_X86_VMX_FEATURE_NAMES
	if (cpu_has(c, X86_FEATURE_VMX) && c->vmx_capability[0]) {
		seq_puts(m, "\nvmx flags\t:");
		for (i = 0; i < 32*NVMXINTS; i++) {
			if (test_bit(i, (unsigned long *)c->vmx_capability) &&
			    x86_vmx_flags[i] != NULL)
			    // 打印 vmx_capability[i] 对应的 feature。
				seq_printf(m, " %s", x86_vmx_flags[i]);
		}
	}
#endif
	// ...
}

const char * const x86_vmx_flags[NVMXINTS*32] = {
	[VMX_FEATURE_IPI_VIRT]		 = "ipi_virt",
};

比如支持 IPIv 的平台,可以在 host 看到 ipi_virt

bash 复制代码
[root@pc kvm]# cat /proc/cpuinfo | grep "vmx flags"
vmx flags       : vnmi preemption_timer posted_intr invvpid ept_x_only ept_ad ept_1gb flexpriority apicv tsc_offset vtpr mtf vapic ept vpid unrestricted_guest vapic_reg vid ple shadow_vmcs pml ept_mode_based_exec tsc_scaling usr_wait_pause notify_vm_exiting ipi_virt

3.2 setup_vmcs_config()

加载 KVM 模块时,将 CPU 支持的 VMX feature 记录到 vmcs_config

c 复制代码
#define KVM_OPTIONAL_VMX_TERTIARY_VM_EXEC_CONTROL			\
		(TERTIARY_EXEC_IPI_VIRT)
struct vmcs_config vmcs_config __ro_after_init;
struct vmx_capability vmx_capability __ro_after_init;

vmx_hardware_setup
	if (setup_vmcs_config(&vmcs_config, &vmx_capability) < 0)
		return -EIO;

static int setup_vmcs_config(struct vmcs_config *vmcs_conf,
			     struct vmx_capability *vmx_cap)
{
	u64 _cpu_based_3rd_exec_control = 0;
	
	if (_cpu_based_exec_control & CPU_BASED_ACTIVATE_TERTIARY_CONTROLS)
		_cpu_based_3rd_exec_control =
			// 根据寄存器的值,如果提供的 feature 支持,就返回相应的 bit
			adjust_vmx_controls64(KVM_OPTIONAL_VMX_TERTIARY_VM_EXEC_CONTROL,
					      MSR_IA32_VMX_PROCBASED_CTLS3);
	// ...
	vmcs_conf->cpu_based_3rd_exec_ctrl = _cpu_based_3rd_exec_control;
}

// 根据 MSR 读到的值,调整提供的控制域的配置,并作为返回值
static u64 adjust_vmx_controls64(u64 ctl_opt, u32 msr)
{
	u64 allowed;
	rdmsrl(msr, allowed);
	return  ctl_opt & allowed;
}

vmcs_config 数据结构

c 复制代码
struct vmcs_config {
	int size;
	u32 basic_cap;
	u32 revision_id;
	u32 pin_based_exec_ctrl;
	u32 cpu_based_exec_ctrl;
	u32 cpu_based_2nd_exec_ctrl;
	u64 cpu_based_3rd_exec_ctrl;
	u32 vmexit_ctrl;
	u32 vmentry_ctrl;
	u64 misc;
	struct nested_vmx_msrs nested;
};

3.3 APIC_WRITE cased VM-exit

没有 IPIv 时,guest 发送 IPI 可以通过触发 VM-exit 后由 KVM 模拟。 当 Guest 写 APIC 中的寄存器,通常是 ICR,会触发 VM-exit。读访问不会 VM-exit。

退出到 Host 之后,KVM 会模拟写寄存器的行为,即记录写入参数并发送 IPI。

c 复制代码
static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
	[EXIT_REASON_APIC_WRITE]              = handle_apic_write,
}

handle_apic_write
	/* emulate APIC access in a trap manner */
	void kvm_apic_write_nodecode(struct kvm_vcpu *vcpu, u32 offset)
	{
		struct kvm_lapic *apic = vcpu->arch.apic;
		if (apic_x2apic_mode(apic) && offset == APIC_ICR)
			// KVM 发送 IPI,并将写入的 ICR 值记录到 KVM 中
			kvm_x2apic_icr_write(apic, kvm_lapic_get_reg64(apic, APIC_ICR));
		else
			kvm_lapic_reg_write(apic, offset, kvm_lapic_get_reg(apic, offset));
	}

若有了 IPIv, 就直接有硬件帮忙完成,可以避免很多 root mode 与 non-root mode 之间切换的开销。

3.4 KVM 中 IPIv 相关的几个检测

c 复制代码
// 加载 KVM
vt_init
	kvm_x86_vendor_init
		__kvm_x86_vendor_init
			vt_hardware_setup
				vmx_hardware_setup
module_init(vt_init);

// 检查 CPU 是否可以开启 IPIv 硬件支持。开启 IPIv 需满足:
	// 开启了 APICv
	// 硬件支持 IPIv
__init int vmx_hardware_setup(void)
{
	// ...
	if (!enable_apicv || !cpu_has_vmx_ipiv())
		enable_ipiv = false;
	// ...
}

// 检查 CPU 是否有 IPIv 的硬件支持
static inline bool cpu_has_vmx_ipiv(void)
{
	return vmcs_config.cpu_based_3rd_exec_ctrl & TERTIARY_EXEC_IPI_VIRT;
}

// 检查是否可以使用 IPIv
	// LAPIC 在 KVM 中模拟
	// 开启了 IPIv 硬件支持
static inline bool vmx_can_use_ipiv(struct kvm_vcpu *vcpu)
{
	return  lapic_in_kernel(vcpu) && enable_ipiv;
}

4. KVM_CAP_MAX_VCPU_ID

KVM_CAP_MAX_VCPU_ID,KVM 提供给 userspace VMM 的接口, 用于设置 VM 的最大 vCPU id。

相应的,KVM 中新加了一个专用于 x86 架构的参数 max_vcpu_ids,默认值是 KVM_MAX_VCPU_IDS。 userspace 可以也使用 KVM_ENABLE_CAP ioctl 的 KVM_CAP_MAX_VCPU_ID ,设置最大的 vCPU id。以便在开启 VMX IPI 后,创建合适大小的 PID-pointer table。

c 复制代码
#define KVM_CAP_MAX_VCPU_ID 128

static long kvm_vm_ioctl(struct file *filp,
			   unsigned int ioctl, unsigned long arg)
{
	struct kvm *kvm = filp->private_data;
	void __user *argp = (void __user *)arg;

	switch (ioctl) {
	case KVM_ENABLE_CAP: {
		struct kvm_enable_cap cap;
		r = -EFAULT;
		if (copy_from_user(&cap, argp, sizeof(cap)))
			goto out;
		// 使用 userspace 传入的 `cap`, 配置 KVM
		r = kvm_vm_ioctl_enable_cap_generic(kvm, &cap);
			=> kvm_vm_ioctl_enable_cap(kvm, cap);
		break;
	}
	// ...
}

int kvm_vm_ioctl_enable_cap(struct kvm *kvm,
			    struct kvm_enable_cap *cap)
{
	int r;
	
	switch (cap->cap) {
	// ...
	case KVM_CAP_MAX_VCPU_ID:
		r = -EINVAL;
		if (cap->args[0] > KVM_MAX_VCPU_IDS)
			break;

		mutex_lock(&kvm->lock);
		if (kvm->arch.max_vcpu_ids == cap->args[0]) {
			r = 0;
		// 只有当 KVM 的最大 vCPU id 没有设置,才使用 userspace 传入的值
		} else if (!kvm->arch.max_vcpu_ids) {
			kvm->arch.max_vcpu_ids = cap->args[0];
			r = 0;
		}
		mutex_unlock(&kvm->lock);
		break;
	// ...
	}
}

kvm_vm_ioctl_check_extension
	case KVM_CAP_MAX_VCPU_ID:
		r = KVM_MAX_VCPU_IDS;
		break;

5. KVM_CREATE_VCPU

KVM_CREATE_VCPU,KVM 提供给 userspace VMM 的接口, 用于创建 VM 的 vCPU。

创建 vCPU 时,会:

  • kvm_arch_vcpu_precreate() 时检查 kvm->arch.max_vcpu_ids
  • 创建 PID-pointer table。
  • 初始化 VMCS。包括开启 IPIv,将 MSR-bitmap 地址,PID 的地址写入到 VMCS。
  • 响应 KVM_REQ_APICV_UPDATE 请求,更新 APICV 相关的 VMCS,和关闭 APIC_WRITE cased VM-exit。响应请求的操作在 VM-entry 前一步进行。
c 复制代码
kvm_vm_ioctl
	// 创建 vCPU 的 API 对应的处理逻辑
	case KVM_CREATE_VCPU:
	kvm_vm_ioctl_create_vcpu
		kvm_arch_vcpu_precreate // 检查 `max_vcpu_ids`
			return static_call(kvm_x86_vcpu_precreate)(kvm);
				vmx_vcpu_precreate
					vmx_alloc_ipiv_pid_table // 创建 pid table
		kvm_arch_vcpu_create
			kvm_vcpu_reset
			=> {
				// 发出请求 KVM_REQ_APICV_UPDATE
				if (irqchip_in_kernel(vcpu->kvm))
					if (enable_apicv) {
						vcpu->arch.apic->apicv_active = true;
						kvm_make_request(KVM_REQ_APICV_UPDATE, vcpu);
					}
				// ...
				// 复位 vCPU
				vmx_vcpu_reset
					__vmx_vcpu_reset
						init_vmcs // 初始化 VMCS
			}

5.1 kvm_arch_vcpu_precreate()

c 复制代码
// 设置 `kvm->arch.max_vcpu_ids`。
// 会检查是否已经通过 ioctl `KVM_CAP_MAX_VCPU_ID` 赋值,若没有,则设为默认值 `KVM_MAX_VCPU_IDS`
// 也会检查 vCPU id 是否超上限。
int kvm_arch_vcpu_precreate(struct kvm *kvm, unsigned int id)
{
	if (!kvm->arch.max_vcpu_ids)
		kvm->arch.max_vcpu_ids = KVM_MAX_VCPU_IDS;
	// 若 vCPU id 超上限,创建 vCPU 失败
	if (id >= kvm->arch.max_vcpu_ids)
		return -EINVAL;
	// 创建 PID-pointer table
	return static_call(kvm_x86_vcpu_precreate)(kvm);
	==> vmx_vcpu_precreate
			vmx_alloc_ipiv_pid_table
}

5.2 vmx_alloc_ipiv_pid_table()

创建 PID-pointer table, 并将地址记录在 kvm_vmx->pid_table。仅在开启了 IPIv 时创建,且只创建一次。

c 复制代码
struct kvm_vmx {
	struct kvm kvm;

	unsigned int tss_addr;
	bool ept_identity_pagetable_done;
	gpa_t ept_identity_map_addr;
	/* Posted Interrupt Descriptor (PID) table for IPI virtualization */
	u64 *pid_table; // PID-pointer table
};

// 创建 PID-pointer table
static int vmx_alloc_ipiv_pid_table(struct kvm *kvm)
{
	struct page *pages;
	struct kvm_vmx *kvm_vmx = to_kvm_vmx(kvm);
	// 若 LAPIC 不在 KVM 中模拟, 或, 没有开启 IPIv, 直接返回
	if (!irqchip_in_kernel(kvm) || !enable_ipiv)
		return 0;
	// 若 table 已存在,直接返回
	if (kvm_vmx->pid_table)
		return 0;
	// 分配一块内存用于存放 PID-pointer table
	pages = alloc_pages(GFP_KERNEL_ACCOUNT | __GFP_ZERO,
			    vmx_get_pid_table_order(kvm));
	if (!pages)
		return -ENOMEM;

	kvm_vmx->pid_table = (void *)page_address(pages);
	return 0;
}

// 根据最大 vCPU id,计算需要分配的内存 order。
static inline int vmx_get_pid_table_order(struct kvm *kvm)
{
	return get_order(kvm->arch.max_vcpu_ids * sizeof(*to_kvm_vmx(kvm)->pid_table));
}

5.3 init_vmcs()

为 vCPU 初始化 VMCS。对于 IPIv,主要涉及下面三个方面。

c 复制代码
static void init_vmcs(struct vcpu_vmx *vmx)
{
	// 将 MSR-bitmap 地址,写入 VMCS 的 MSR_BITMAP 域
	if (cpu_has_vmx_msr_bitmap())
		vmcs_write64(MSR_BITMAP, __pa(vmx->vmcs01.msr_bitmap));
	// 修改 VMCS 中的控制域 tertiary processor-based VM-execution controls。
	if (cpu_has_tertiary_exec_ctrls())
			tertiary_exec_controls_set(vmx, vmx_tertiary_exec_control(vmx));
	// 填写 VMCS 中 PID_POINTER_TABLE 和 LAST_PID_POINTER_INDEX
	if (vmx_can_use_ipiv(&vmx->vcpu)) {
		vmcs_write64(PID_POINTER_TABLE, __pa(kvm_vmx->pid_table));
		vmcs_write16(LAST_PID_POINTER_INDEX, kvm->arch.max_vcpu_ids - 1);
	}
}

5.4 KVM_REQ_APICV_UPDATE

vCPU 发生 VM-entry 前,检查各类待处理的请求。若检查到了 KVM_REQ_APICV_UPDATE,更新 APICv 有关的 VMCS 的配置。

c 复制代码
vcpu_enter_guest
	if (kvm_request_pending(vcpu)) {
		// ...
		if (kvm_check_request(KVM_REQ_APICV_UPDATE, vcpu))
			kvm_vcpu_update_apicv(vcpu);
	}
	// exit_fastpath = static_call(kvm_x86_vcpu_run)(vcpu); // VM-entry

kvm_vcpu_update_apicv
	__kvm_vcpu_update_apicv
		static_call(kvm_x86_refresh_apicv_exec_ctrl)(vcpu);
		==> vt_refresh_apicv_exec_ctrl
				vmx_refresh_apicv_exec_ctrl

void vmx_refresh_apicv_exec_ctrl(struct kvm_vcpu *vcpu)
{
	struct vcpu_vmx *vmx = to_vmx(vcpu);
	
	if (kvm_vcpu_apicv_active(vcpu)) {
		// ...
		if (enable_ipiv)
			tertiary_exec_controls_setbit(vmx, TERTIARY_EXEC_IPI_VIRT);
	} else {
		// ...
		if (enable_ipiv)
			tertiary_exec_controls_clearbit(vmx, TERTIARY_EXEC_IPI_VIRT);
	}

	vmx_update_msr_bitmap_x2apic(vcpu);
}

5.4.1 vmx_update_msr_bitmap_x2apic()

配置 VM-Execution Controls 中的 MSR-Bitmap 时,x2apic 模式下可能会将读写 ICR 触发 VM-exit 关闭。 若关闭 ICR caused VM-exit,后续 guest 读写 ICR 发送 IPI,就直接有硬件帮忙完成。

c 复制代码
static void vmx_update_msr_bitmap_x2apic(struct kvm_vcpu *vcpu)
{
	u8 mode;
	// 检查是否开启了 VMCS 的 MSR-Bitmap
	if (cpu_has_secondary_exec_ctrls() &&
	    (secondary_exec_controls_get(vmx) &
	     SECONDARY_EXEC_VIRTUALIZE_X2APIC_MODE)) {
		mode = MSR_BITMAP_MODE_X2APIC;
		// 检查是否开启了 APIC 虚拟化
		if (enable_apicv && kvm_vcpu_apicv_active(vcpu))
			mode |= MSR_BITMAP_MODE_X2APIC_APICV;
	} else {
		mode = 0;
	}

	if (mode == vmx->x2apic_msr_bitmap_mode)
		return;

	vmx->x2apic_msr_bitmap_mode = mode;
	// 如果开启了 APIC 虚拟化,和 IPI 虚拟化,则关闭读写 ICR 的 VM-exit
	if (mode & MSR_BITMAP_MODE_X2APIC_APICV) {
		// ...
		if (enable_ipiv)
			vmx_disable_intercept_for_msr(vcpu, X2APIC_MSR(APIC_ICR), MSR_TYPE_RW);
	}
}

5.4 2 其他 KVM_REQ_APICV_UPDATE

其他还有情况下也会产生 APICV 更新请求 KVM_REQ_APICV_UPDATE,比如:

当设置 APIC 基地址时,会发出更新 APIC 虚拟化的配置请求 KVM_REQ_APICV_UPDATE。然后在 vCPU 发生 VM-entry 之前,修改 VMCS 中的控制域 tertiary processor-based VM-execution controls。

c 复制代码
kvm_lapic_set_base
	static_call_cond(kvm_x86_set_virtual_apic_mode)(vcpu);
		
void kvm_lapic_set_base(struct kvm_vcpu *vcpu, u64 value)
{
	u64 old_value = vcpu->arch.apic_base;
	vcpu->arch.apic_base = value;
	
	// 设置 APIC 基地址时,会发出更新 APIC 虚拟化的配置请求 `KVM_REQ_APICV_UPDATE`
	if ((old_value ^ value) & (MSR_IA32_APICBASE_ENABLE | X2APIC_ENABLE)) {
	
		// 响应 `KVM_REQ_APICV_UPDATE` 时会调用 vmx_update_msr_bitmap_x2apic
		kvm_make_request(KVM_REQ_APICV_UPDATE, vcpu);
		--> vmx_refresh_apicv_exec_ctrl
				vmx_update_msr_bitmap_x2apic
		
		static_call_cond(kvm_x86_set_virtual_apic_mode)(vcpu);
		==> vmx_set_virtual_apic_mode
				vmx_update_msr_bitmap_x2apic // 更新 MSR bitmap
	}
}

还有 kvm_set_or_clear_apicv_inhibit() 会向所有 vCPU 设置请求 KVM_REQ_APICV_UPDATE

c 复制代码
kvm_set_or_clear_apicv_inhibit
	__kvm_set_or_clear_apicv_inhibit
		kvm_make_all_cpus_request(kvm, KVM_REQ_APICV_UPDATE);

6. 修改 VMCS 的函数实现

内存中保存 VMCS 的数据结构,每个 vCPU 都会有一份。

c 复制代码
struct vmcs_controls_shadow {
	u32 vm_entry;
	u32 vm_exit;
	u32 pin;
	u32 exec;
	u32 secondary_exec;
	u64 tertiary_exec;
};

struct loaded_vmcs {
	struct vmcs_controls_shadow controls_shadow;
};

struct vcpu_vmx {
	struct kvm_vcpu       vcpu;
	struct loaded_vmcs   *loaded_vmcs;
}

访问 VMCS 控制域的值,比如 tertiary_exec_controls_clearbit(),通过宏定义实现多个功能函数。

c 复制代码
BUILD_CONTROLS_SHADOW(vm_entry, VM_ENTRY_CONTROLS, 32)
BUILD_CONTROLS_SHADOW(vm_exit, VM_EXIT_CONTROLS, 32)
BUILD_CONTROLS_SHADOW(pin, PIN_BASED_VM_EXEC_CONTROL, 32)
BUILD_CONTROLS_SHADOW(exec, CPU_BASED_VM_EXEC_CONTROL, 32)
BUILD_CONTROLS_SHADOW(secondary_exec, SECONDARY_VM_EXEC_CONTROL, 32)
BUILD_CONTROLS_SHADOW(tertiary_exec, TERTIARY_VM_EXEC_CONTROL, 64)

写入 VMCS 时:

  • 会调用 vmwrite 将值写入指定控制域,如 TERTIARY_VM_EXEC_CONTROLSECONDARY_VM_EXEC_CONTROL 等。
  • 同时将写入值存入 vcpu_vmx->loaded_vmcs->controls_shadow.<lname>

读取时,直接从 vcpu_vmx->loaded_vmcs->controls_shadow.<lname> 获取。

修改 VMCS 的功能函数分为两类

  • lname##_controls_set() / lname##_controls_get() 直接修改/读取整个控制域。
  • lname##_controls_clearbit() / lname##_controls_setbit() 只修改指定位。

例如, tertiary_exec_controls_clearbit (vmx, TERTIARY_EXEC_IPI_VIRT); 只修改 controls_shadow.tertiary_exec 中的第 4 位。

c 复制代码
#define BUILD_CONTROLS_SHADOW(lname, uname, bits)						\

// lname##_controls_set
static inline void lname##_controls_set(struct vcpu_vmx *vmx, u##bits val)			\
{												\
	if (vmx->loaded_vmcs->controls_shadow.lname != val) {					\
		vmcs_write##bits(uname, val);							\
		vmx->loaded_vmcs->controls_shadow.lname = val;					\
	}											\
}												\
// __##lname##_controls_get
static inline u##bits __##lname##_controls_get(struct loaded_vmcs *vmcs)			\
{												\
	return vmcs->controls_shadow.lname;							\
}												\
// lname##_controls_get
static inline u##bits lname##_controls_get(struct vcpu_vmx *vmx)				\
{												\
	return __##lname##_controls_get(vmx->loaded_vmcs);					\
}												\

// lname##_controls_setbit
static __always_inline void lname##_controls_setbit(struct vcpu_vmx *vmx, u##bits val)		\
{												\
	BUILD_BUG_ON(!(val & (KVM_REQUIRED_VMX_##uname | KVM_OPTIONAL_VMX_##uname)));		\
	lname##_controls_set(vmx, lname##_controls_get(vmx) | val);				\
}												\
// lname##_controls_clearbit
static __always_inline void lname##_controls_clearbit(struct vcpu_vmx *vmx, u##bits val)	\
{												\
	BUILD_BUG_ON(!(val & (KVM_REQUIRED_VMX_##uname | KVM_OPTIONAL_VMX_##uname)));		\
	lname##_controls_set(vmx, lname##_controls_get(vmx) & ~val);				\
}

写 VMCS 的最终函数,调用指令 vmwrite

c 复制代码
static __always_inline void vmcs_write64(unsigned long field, u64 value)
{
	vmcs_check64(field);
	// 嵌套虚拟化时的 VMCS 修改
	if (kvm_is_using_evmcs())
		return evmcs_write64(field, value);

	// 对于非嵌套的虚拟化,使用指令 `vmwrite` 为 VMCS 指定区域写入值
	__vmcs_writel(field, value);
	// 写入 VMCS 的值是 64 位时,且不系统不支持 x86_64 时,则分两次写入
	// `long` 在 x86 机器上长度为32位,x86_64 机器上为64位。
#ifndef CONFIG_X86_64
	__vmcs_writel(field+1, value >> 32);
#endif
}

static __always_inline void __vmcs_writel(unsigned long field, unsigned long value)
{
	vmx_asm2(vmwrite, "r"(field), "rm"(value), field, value);
}

本文作者:文七安

本文链接:juejin.cn/post/732843...

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

相关推荐
Tans59 天前
LeakCanary 源码阅读笔记(四)
源码阅读·leakcanary
灵感__idea10 天前
Vuejs技术内幕:组件渲染
前端·vue.js·源码阅读
Sword9919 天前
【ThreeJs原理解析】第4期 | 向量
前端·three.js·源码阅读
biubiubiu王大锤25 天前
Nacos源码分析-永久实例健康检查机制
java·源码阅读
Sword991 个月前
【ThreeJs原理解析】第3期 | 射线检测Raycaster实现原理
前端·three.js·源码阅读
欧阳码农1 个月前
看不懂来打我!Vue3的watch是如何实现数据监听的
vue.js·源码·源码阅读
biubiubiu王大锤1 个月前
nacos源码分析-客户端启动与配置动态更新的实现细节
后端·源码阅读
Sword991 个月前
【ThreeJs原理解析】第2期 | 旋转、平移、缩放实现原理
前端·three.js·源码阅读
侠客行03172 个月前
Eureka Client的初始化
java·架构·源码阅读
web_code2 个月前
webpack源码快速分析
前端·webpack·源码阅读