虚拟机中的 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 许可协议。转载请注明出处!

相关推荐
程序猿阿越11 天前
Kafka源码(六)消费者消费
java·后端·源码阅读
漫谈网络12 天前
KVM创建的虚拟机,虚拟机的网卡是如何生成的
运维·服务器·网络·qemu·虚拟化·kvm
zh_xuan16 天前
Android android.util.LruCache源码阅读
android·源码阅读·lrucache
魏思凡19 天前
爆肝一万多字,我准备了寿司 kotlin 协程原理
kotlin·源码阅读
白鲸开源23 天前
一文掌握 Apache SeaTunnel 构建系统与分发基础架构
大数据·开源·源码阅读
Tans51 个月前
Androidx Fragment 源码阅读笔记(下)
android jetpack·源码阅读
Tans51 个月前
Androidx Fragment 源码阅读笔记(上)
android jetpack·源码阅读
Tans51 个月前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
凡小烦1 个月前
LeakCanary源码解析
源码阅读·leakcanary
程序猿阿越2 个月前
Kafka源码(四)发送消息-服务端
java·后端·源码阅读