内核虚拟化: VCPU requests

KVM VCPU requests(vcpu请求)

通过翻译内核文档和源码学习vcpu requests

kernel v6.2

文章目录

  • KVM VCPU requests(vcpu请求)
    • VCPU Kicks(vcpu激活)
    • VCPU Mode
    • VCPU请求内部结构
      • 与体系结构无关的请求
      • VCPU请求掩码
        • VCPU请求标志
    • 具有关联状态的VCPU请求
    • 确保请求可见
      • 减少IPI
      • 等待确认
      • 无请求的 VCPU Kicks
    • VCPU睡眠
    • vcpu->requests 处理
    • 参考

KVM支持一个内部API,允许线程请求VCPU线程执行某些活动。例如,线程可能请求一个VCPU刷新其TLB通过VCPU requests。该API包括以下函数:

/* 检查是否有待处理的请求针对VCPU @vcpu。 */
bool kvm_request_pending(struct kvm_vcpu *vcpu);

/* 检查VCPU @vcpu是否有请求@req待处理。 */
bool kvm_test_request(int req, struct kvm_vcpu *vcpu);

/* 清除VCPU @vcpu的请求@req。 */
void kvm_clear_request(int req, struct kvm_vcpu *vcpu);

/*
 * 检查VCPU @vcpu是否有请求@req待处理。当请求待处理时,它将被清除,并发出一个内存屏障,与kvm_make_request()中的内存屏障配对。
 */
bool kvm_check_request(int req, struct kvm_vcpu *vcpu);

/*
 * 使VCPU @vcpu的请求@req。在设置请求之前,发出一个内存屏障,与kvm_check_request()中的内存屏障配对。
 */
void kvm_make_request(int req, struct kvm_vcpu *vcpu);

/* 使结构体kvm @kvm的所有VCPU请求@req。 */
bool kvm_make_all_cpus_request(struct kvm *kvm, unsigned int req);

通常,请求方希望VCPU尽快在请求后执行活动。这意味着大多数请求(kvm_make_request()调用)后面会调用kvm_vcpu_kick(),

如下:

case APIC_DM_STARTUP:
    result = 1;
    // 将SIPi(Startup Inter Processor interrupt)的向量设置为传入的vector
    apic->sipi_vector = vector;
    // 确保sipi_vector对接收方可见
    smp_wmb();
    // 设置KVM_APIC_SIPI位,表示有SIPi事件等待处理
    set_bit(KVM_APIC_SIPI, &apic->pending_events);
    // 发送KVM_REQ_EVENT请求,通知VCPU有新事件需要处理
    kvm_make_request(KVM_REQ_EVENT, vcpu);
    // 唤醒VCPU以处理事件
    kvm_vcpu_kick(vcpu);
    break;
 

 /*
  * 唤醒一个处于睡眠状态的vcpu的线程,或者将处于guest模式的vcpu切换到主机的内核模式。
  */
 void kvm_vcpu_kick(struct kvm_vcpu *vcpu)   

而kvm_make_all_cpus_request()将所有VCPU的触发内置其中。

bool kvm_make_all_cpus_request_except(struct kvm *kvm, unsigned int req,
                                      struct kvm_vcpu *except)
{
    struct kvm_vcpu *vcpu;
    struct cpumask *cpus;
    unsigned long i;
    bool called;
    int me;

    // 获取当前CPU编号
    me = get_cpu();

    // 获取本地CPU掩码
    cpus = this_cpu_cpumask_var_ptr(cpu_kick_mask);
    cpumask_clear(cpus);

    // 遍历KVM中的每个VCPU
    kvm_for_each_vcpu(i, vcpu, kvm) {
        // 如果VCPU是要排除的VCPU,则跳过
        if (vcpu == except)
            continue;
        // 向该VCPU发出请求,并更新本地CPU掩码
        kvm_make_vcpu_request(vcpu, req, cpus, me);
    }

    // 调用kvm_kick_many_cpus来唤醒多个CPU,根据req中是否包含KVM_REQUEST_WAIT来决定是否等待
    called = kvm_kick_many_cpus(cpus, !!(req & KVM_REQUEST_WAIT));
    
    // 释放当前CPU编号
    put_cpu();

    return called;
}


bool kvm_make_all_cpus_request(struct kvm *kvm, unsigned int req)
{
	return kvm_make_all_cpus_request_except(kvm, req, NULL);
}

VCPU Kicks(vcpu激活)

VCPU kick的目标是使VCPU线程退出客户模式,以执行一些KVM维护工作。为此,将发送一个核间中断处理请求(IPI),强制进行客户模式退出。然而,在激活时,VCPU线程可能并未处于客户模式。因此,根据VCPU线程的模式和状态,激活可能采取另外两种操作。以下列出了这三种操作:

  1. 发送一个IPI。这将强制进行客户模式退出。
  2. 唤醒处于休眠状态的VCPU。休眠的VCPU是在等待队列上等待的处于客户模式之外的VCPU线程。唤醒它们将线程从等待队列中移除,允许线程再次运行。可以通过KVM_REQUEST_NO_WAKEUP来抑制此行为。
  3. 什么也不做。当VCPU不处于客户模式且VCPU线程未处于休眠状态时,则无需执行任何操作。

VCPU Mode

VCPU具有一个模式状态字段,即 vcpu->mode,用于跟踪客户是否在客户模式下运行以及一些特定于客户模式之外的状态。

  • OUTSIDE_GUEST_MODE

    VCPU线程处于客户模式之外。

  • IN_GUEST_MODE

    VCPU线程处于客户模式中。

  • EXITING_GUEST_MODE

    VCPU线程正在从IN_GUEST_MODE过渡到OUTSIDE_GUEST_MODE。

  • READING_SHADOW_PAGE_TABLES

    VCPU线程处于客户模式之外,但希望发送某些VCPU请求(即KVM_REQ_TLB_FLUSH)的发送者等待,直到VCPU线程完成读取页表。

VCPU请求内部结构

VCPU请求简单地是 vcpu->requests 位图的位索引。如:

clear_bit(KVM_REQ_UNBLOCK & KVM_REQUEST_MASK, &vcpu->requests);

然而,VCPU请求的使用者应该避免这样做,因为这将破坏抽象。前8位保留给与体系结构无关的请求,所有附加位可用于与体系结构相关的请求。

与体系结构无关的请求

  • KVM_REQ_TLB_FLUSH

    KVM的通用MMU通知程序可能需要刷新客户的所有TLB项,调用 kvm_flush_remote_tlbs() 来完成。选择使用通用 kvm_flush_remote_tlbs() 实现的架构将需要处理这个VCPU请求。

  • KVM_REQ_VM_DEAD

    此请求通知所有VCPU,VM已死亡且不可用,例如由于致命错误或因为VM的状态已被有意销毁。

  • KVM_REQ_UNBLOCK

    此请求通知VCPU退出 kvm_vcpu_block。例如,它可以由在主机上代表VCPU运行的定时器处理程序使用,或者确保唤醒vcpu为了设备来更新中断路由信息。

  • KVM_REQ_OUTSIDE_GUEST_MODE

    这个 “请求” 确保目标VCPU在请求发送者继续之前已经退出了客户模式。目标不需要执行任何操作,因此实际上不会为目标记录任何请求。这个请求类似于 “kick”,但与 “kick” 不同,它保证VCPU实际上已经退出了客户模式。Kick仅保证将来某个时候VCPU将退出,例如,先前的kick可能已经启动了这个过程,但不能保证即将被kick的VCPU已完全退出了客户模式。

VCPU请求掩码

在使用位操作之前,应通过 KVM_REQUEST_MASK 对VCPU请求进行掩码。这是因为只有低8位用于表示请求的编号。高位用作标志。目前只定义了两个标志。

VCPU请求标志
  • KVM_REQUEST_NO_WAKEUP

    此标志应用于仅需要处于客户模式的VCPUs立即注意的请求,即,休眠的VCPU不需要为处理这些请求而唤醒。当休眠的VCPU稍后因某种原因被唤醒时,它们将处理这些请求。

  • KVM_REQUEST_WAIT

    当使用 kvm_make_all_cpus_request() 发出带有此标志的请求时,调用者将等待每个VCPU在继续之前确认其IPI。此标志仅适用于将接收IPI的VCPU。例如,如果VCPU正在休眠,因此不需要IPI,则请求线程不等待。这意味着此标志可以安全地与 KVM_REQUEST_NO_WAKEUP 结合使用。

具有关联状态的VCPU请求

希望接收的VCPU处理新状态的请求方需要确保新写入的状态在接收VCPU线程的CPU观察到请求时是可观察的。这意味着在写入新状态后并在设置VCPU请求位之前,必须插入写内存屏障。此外,在接收VCPU线程方面,在读取请求位后并在继续读取与之关联的新状态之前,必须插入相应的读屏障。

函数对kvm_check_request()和kvm_make_request()提供了内存屏障,允许API在内部处理此要求。

确保请求可见

在向VCPUs发出请求时,我们希望确保接收的VCPU在没有处理请求的情况下不会在客户模式下执行太长时间。只要我们确保VCPU线程在进入客户模式之前检查 kvm_request_pending(),并且在必要时进行kick将发送一个IPI以强制从客户模式退出,我们就可以确保这种情况不会发生。需要特别注意VCPU线程最后一次检查 kvm_request_pending() 和进入客户模式之间的时间段,因为kick IPIs仅会触发处于客户模式的VCPU线程的退出,或者至少已经禁用中断以准备进入客户模式。

一个解决方案,除了s390外,所有体系结构都适用,即:

  • 在禁用中断和最后一次 kvm_request_pending() 检查之间,将 vcpu->mode 设置为 IN_GUEST_MODE
  • 在进入客户模式时以原子方式启用中断。

这个解决方案还要求在请求线程和接收VCPU线程中都要小心地放置内存屏障。通过内存屏障,我们可以排除VCPU线程在最后一次检查时观察到 !kvm_request_pending(),然后在接收到下一个请求时没有收到IPI的可能性,即使请求在检查后立即发出。这是通过Dekker内存屏障模式来完成的。由于Dekker模式需要两个变量,此解决方案将 vcpu->modevcpu->requests 配对。将它们代入模式得到:

  CPU1                                    CPU2
  =================                       =================
  local_irq_disable();
  WRITE_ONCE(vcpu->mode, IN_GUEST_MODE);  kvm_make_request(REQ, vcpu);
  smp_mb();                               smp_mb();
  if (kvm_request_pending(vcpu)) {        if (READ_ONCE(vcpu->mode) ==
                                              IN_GUEST_MODE) {
      ...abort guest entry...                 ...send IPI...
  }                                       }

正如前文所述,IPI仅对处于客户模式的VCPU线程或已禁用中断的线程有用。这就是为什么在设置 vcpu->modeIN_GUEST_MODE 之前,这个特定情况的Dekker模式被扩展为禁用中断的原因。使用 WRITE_ONCE()READ_ONCE() 是为了严格实施内存屏障模式,确保编译器不会干扰对 vcpu->mode 的精心规划的访问。

简而言之,这段代码中的设计目的是为了确保在进行一系列操作(如设置 vcpu->mode 和发送请求)时,不会在多处理器环境中发生竞态条件或不一致。通过禁用中断、插入内存屏障以及使用 WRITE_ONCE()READ_ONCE(),可以确保对共享变量 vcpu->mode 的访问在执行期间按照预期进行,防止编译器进行优化干扰。这有助于确保同步操作的正确性和可预测性。

减少IPI

由于只需要一个 IPI 来让一个 VCPU 检查任何/所有请求,它们可以被合并。这可以通过在第一个发送 kick 的 IPI 中同时将 VCPU 模式更改为非 IN_GUEST_MODE 来轻松实现。过渡状态 EXITING_GUEST_MODE 就是为此目的而使用的。

等待确认

那些设置了 KVM_REQUEST_WAIT 标志的请求,即使目标 VCPU 线程处于非 IN_GUEST_MODE 模式时,也需要发送 IPI,并等待确认。例如,当目标 VCPU 线程处于 READING_SHADOW_PAGE_TABLES 模式时,该模式在禁用中断后设置。为了支持这些情况,KVM_REQUEST_WAIT 标志将发送 IPI 的条件从检查 VCPU 是否处于 IN_GUEST_MODE 模式更改为检查它是否不处于 OUTSIDE_GUEST_MODE 模式。

无请求的 VCPU Kicks

由于是否发送 IPI 的决定依赖于两个变量的 Dekker 内存屏障模式,因此可以明确指出,无请求的 VCPU Kick 几乎总是不正确的。没有确保无需生成 IPI 的 Kick 仍会导致接收 VCPU 执行操作的保证,就像请求伴随的 Kick 的最终kvm_request_pending()检查一样,那么 Kick 可能根本不会产生任何有用的效果。例如,如果向即将将其模式设置为 IN_GUEST_MODE 的 VCPU 发出了无请求的 Kick,这意味着不会发送 IPI,那么 VCPU 线程可能会继续进行其进入客户模式,而实际上没有执行 Kick 的目的,也就是说,并没有因为 Kick 而启动任何操作。这可能导致不一致性或错误,因为 Kick 的意图可能是在 VCUP 进入客户模式之前触发某种处理或操作,但由于没有相应的请求,实际上并未完成这些操作。

唯一的例外是 x86 的 posted interrupt 机制。然而,在这种情况下,即使是无请求的 VCPU Kick 也与上文描述的相同的 local_irq_disable() + smp_mb() 模式紧密关联;Posted interrupt 描述符中的 ON 位(Outstanding Notification)扮演了 vcpu->requests 的角色。在发送Posted interrupt时,PIR.ON 在读取 vcpu->mode 之前被设置;同样,在 VCPU 线程中,vmx_sync_pir_to_irr() 在将 vcpu->mode 设置为 IN_GUEST_MODE 后读取 PIR。

VCPU睡眠

VCPU 线程在调用可能使其进入休眠状态的函数之前和/或之后,例如 kvm_vcpu_block(),可能需要考虑请求。它们是否这样做,以及如果这样做,哪些请求需要被考虑,取决于架构。kvm_vcpu_block() 调用了 kvm_arch_vcpu_runnable() 来检查是否应唤醒。这样做的一个原因是为了为不同的架构提供一个函数,在该函数中可以在必要时检查请求。

vcpu->requests 处理

上次VM-Exit时可能调用kvm_make_request设置不同的request下次准备VM-Entry时需要处理这些request.

img

img

VM-Entry最终的实现方法是vcpu_enter_guest:

该函数开始时会坚持vcpu上是否有未处理的requests,如果有未处理的requests,则调用对应的处理函数或者退出到用户空间qemu进行处理:

static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
{
	...
        
    if (kvm_request_pending(vcpu)) {
        // 检查并处理 VM 是否处于死亡状态的请求
        if (kvm_check_request(KVM_REQ_VM_DEAD, vcpu)) {
            r = -EIO;
            goto out;
        }

        // 检查并处理脏环检查的请求
        if (kvm_dirty_ring_check_request(vcpu)) {
            r = 0;
            goto out;
        }

        // 检查并处理获取嵌套状态页面的请求
        if (kvm_check_request(KVM_REQ_GET_NESTED_STATE_PAGES, vcpu)) {
            if (unlikely(!kvm_x86_ops.nested_ops->get_nested_state_pages(vcpu))) {
                r = 0;
                goto out;
            }
        }
        // 处理其他请求,包括时钟更新、迁移定时器、全局时钟更新等
		if (kvm_check_request(KVM_REQ_MMU_FREE_OBSOLETE_ROOTS, vcpu))
			kvm_mmu_free_obsolete_roots(vcpu);
		if (kvm_check_request(KVM_REQ_MIGRATE_TIMER, vcpu))
			__kvm_migrate_timers(vcpu);
		if (kvm_check_request(KVM_REQ_MASTERCLOCK_UPDATE, vcpu))
			kvm_update_masterclock(vcpu->kvm);
		if (kvm_check_request(KVM_REQ_GLOBAL_CLOCK_UPDATE, vcpu))
			kvm_gen_kvmclock_update(vcpu);
		if (kvm_check_request(KVM_REQ_CLOCK_UPDATE, vcpu)) {
			r = kvm_guest_time_update(vcpu);
			if (unlikely(r))
				goto out;
		}
		if (kvm_check_request(KVM_REQ_MMU_SYNC, vcpu))
			kvm_mmu_sync_roots(vcpu);
		if (kvm_check_request(KVM_REQ_LOAD_MMU_PGD, vcpu))
			kvm_mmu_load_pgd(vcpu);
        // 检查并处理 TLB 刷新的请求
        if (kvm_check_request(KVM_REQ_TLB_FLUSH, vcpu))
            kvm_vcpu_flush_tlb_all(vcpu);

        // 服务本地 TLB 刷新请求
        kvm_service_local_tlb_flush_requests(vcpu);

        // 在 Hyper-V 的精确刷新失败时,回退到"full"客户机刷新
        if (kvm_check_request(KVM_REQ_HV_TLB_FLUSH, vcpu) &&
            kvm_hv_vcpu_flush_tlb(vcpu))
            kvm_vcpu_flush_tlb_guest(vcpu);

        // 处理其他请求,如 TPR 访问报告、三重故障、APF 停机、IOAPIC EOI 退出等
		if (kvm_check_request(KVM_REQ_REPORT_TPR_ACCESS, vcpu)) {
			vcpu->run->exit_reason = KVM_EXIT_TPR_ACCESS;
			r = 0;
			goto out;
		}
		if (kvm_test_request(KVM_REQ_TRIPLE_FAULT, vcpu)) {
			if (is_guest_mode(vcpu))
				kvm_x86_ops.nested_ops->triple_fault(vcpu);

			if (kvm_check_request(KVM_REQ_TRIPLE_FAULT, vcpu)) {
				vcpu->run->exit_reason = KVM_EXIT_SHUTDOWN;
				vcpu->mmio_needed = 0;
				r = 0;
				goto out;
			}
		}
		if (kvm_check_request(KVM_REQ_APF_HALT, vcpu)) {
			/* Page is swapped out. Do synthetic halt */
			vcpu->arch.apf.halted = true;
			r = 1;
			goto out;
		}
		if (kvm_check_request(KVM_REQ_STEAL_UPDATE, vcpu))
			record_steal_time(vcpu);
#ifdef CONFIG_KVM_SMM
		if (kvm_check_request(KVM_REQ_SMI, vcpu))
			process_smi(vcpu);`
#endif
		if (kvm_check_request(KVM_REQ_NMI, vcpu))
			process_nmi(vcpu);
		if (kvm_check_request(KVM_REQ_PMU, vcpu))
			kvm_pmu_handle_event(vcpu);
		if (kvm_check_request(KVM_REQ_PMI, vcpu))
			kvm_pmu_deliver_pmi(vcpu);
		if (kvm_check_request(KVM_REQ_IOAPIC_EOI_EXIT, vcpu)) {
			BUG_ON(vcpu->arch.pending_ioapic_eoi > 255);
			if (test_bit(vcpu->arch.pending_ioapic_eoi,
				     vcpu->arch.ioapic_handled_vectors)) {
				vcpu->run->exit_reason = KVM_EXIT_IOAPIC_EOI;
				vcpu->run->eoi.vector =
						vcpu->arch.pending_ioapic_eoi;
				r = 0;
				goto out;
			}
		}
		if (kvm_check_request(KVM_REQ_SCAN_IOAPIC, vcpu))
			vcpu_scan_ioapic(vcpu);
		if (kvm_check_request(KVM_REQ_LOAD_EOI_EXITMAP, vcpu))
			vcpu_load_eoi_exitmap(vcpu);
		if (kvm_check_request(KVM_REQ_APIC_PAGE_RELOAD, vcpu))
			kvm_vcpu_reload_apic_access_page(vcpu);
		if (kvm_check_request(KVM_REQ_HV_CRASH, vcpu)) {
			vcpu->run->exit_reason = KVM_EXIT_SYSTEM_EVENT;
			vcpu->run->system_event.type = KVM_SYSTEM_EVENT_CRASH;
			vcpu->run->system_event.ndata = 0;
			r = 0;
			goto out;
		}
		if (kvm_check_request(KVM_REQ_HV_RESET, vcpu)) {
			vcpu->run->exit_reason = KVM_EXIT_SYSTEM_EVENT;
			vcpu->run->system_event.type = KVM_SYSTEM_EVENT_RESET;
			vcpu->run->system_event.ndata = 0;
			r = 0;
			goto out;
		}
		if (kvm_check_request(KVM_REQ_HV_EXIT, vcpu)) {
			struct kvm_vcpu_hv *hv_vcpu = to_hv_vcpu(vcpu);

			vcpu->run->exit_reason = KVM_EXIT_HYPERV;
			vcpu->run->hyperv = hv_vcpu->exit;
			r = 0;
			goto out;
		}

        // 处理 Hyper-V 定时器等
        if (kvm_check_request(KVM_REQ_HV_STIMER, vcpu))
            kvm_hv_process_stimers(vcpu);
        if (kvm_check_request(KVM_REQ_APICV_UPDATE, vcpu))
            kvm_vcpu_update_apicv(vcpu);
        if (kvm_check_request(KVM_REQ_APF_READY, vcpu))
            kvm_check_async_pf_completion(vcpu);
        if (kvm_check_request(KVM_REQ_MSR_FILTER_CHANGED, vcpu))
            static_call(kvm_x86_msr_filter_changed)(vcpu);

        // 处理更新 CPU 脏日志记录等请求
        if (kvm_check_request(KVM_REQ_UPDATE_CPU_DIRTY_LOGGING, vcpu))
            static_call(kvm_x86_update_cpu_dirty_logging)(vcpu);
    }
	...

参考

https://docs.kernel.org/virt/kvm/vcpu-requests.html

https://cloud.tencent.com/developer/article/1069832