KVM VCPU 请求¶
概述¶
KVM 支持一个内部 API,使线程能够请求 VCPU 线程执行某些活动。例如,线程可以请求 VCPU 使用 VCPU 请求刷新其 TLB。该 API 由以下函数组成
/* Check if any requests are pending for VCPU @vcpu. */
bool kvm_request_pending(struct kvm_vcpu *vcpu);
/* Check if VCPU @vcpu has request @req pending. */
bool kvm_test_request(int req, struct kvm_vcpu *vcpu);
/* Clear request @req for VCPU @vcpu. */
void kvm_clear_request(int req, struct kvm_vcpu *vcpu);
/*
* Check if VCPU @vcpu has request @req pending. When the request is
* pending it will be cleared and a memory barrier, which pairs with
* another in kvm_make_request(), will be issued.
*/
bool kvm_check_request(int req, struct kvm_vcpu *vcpu);
/*
* Make request @req of VCPU @vcpu. Issues a memory barrier, which pairs
* with another in kvm_check_request(), prior to setting the request.
*/
void kvm_make_request(int req, struct kvm_vcpu *vcpu);
/* Make request @req of all VCPUs of the VM with struct kvm @kvm. */
bool kvm_make_all_cpus_request(struct kvm *kvm, unsigned int req);
通常,请求者希望 VCPU 在发出请求后尽快执行活动。这意味着大多数请求(kvm_make_request() 调用)之后都会调用 kvm_vcpu_kick(),而 kvm_make_all_cpus_request() 则内置了所有 VCPU 的唤醒操作。
VCPU 唤醒¶
VCPU 唤醒的目标是将 VCPU 线程从客户模式中唤醒,以便执行某些 KVM 维护。为此,会发送一个 IPI,强制退出客户模式。但是,VCPU 线程在唤醒时可能不在客户模式中。因此,根据 VCPU 线程的模式和状态,唤醒可能采取其他两种操作。下面列出了所有三种操作
发送 IPI。这会强制退出客户模式。
唤醒休眠的 VCPU。休眠的 VCPU 是在客户模式之外等待等待队列的 VCPU 线程。唤醒它们会从等待队列中移除线程,从而使线程可以再次运行。可以抑制此行为,请参见下面的 KVM_REQUEST_NO_WAKEUP。
无操作。当 VCPU 不在客户模式中,并且 VCPU 线程没有休眠时,则无需执行任何操作。
VCPU 模式¶
VCPU 具有一个模式状态 vcpu->mode
,用于跟踪客户机是否在客户模式下运行,以及某些特定的客户模式外状态。架构可以使用 vcpu->mode
来确保 VCPU 请求被 VCPU 看到(请参见“确保请求被看到”),并避免发送不必要的 IPI(请参见“IPI 减少”),甚至确保 IPI 确认被等待(请参见“等待确认”)。定义了以下模式
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
位图的位索引。这意味着也可以使用一般的位操作,例如 [atomic-ops] 中记录的那些操作,例如
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 在请求的发送者继续执行之前已退出客户模式。目标无需执行任何操作,因此实际上不会为目标记录任何请求。此请求类似于“唤醒”,但与唤醒不同,它保证 vCPU 实际上已退出客户模式。唤醒仅保证 vCPU 将在将来的某个时间点退出,例如,先前的唤醒可能已启动该过程,但不能保证要唤醒的 vCPU 已完全退出客户模式。
KVM_REQUEST_MASK¶
VCPU 请求在使用位操作之前应由 KVM_REQUEST_MASK 屏蔽。这是因为只有低 8 位用于表示请求的编号。高位用作标志。目前只定义了两个标志。
VCPU 请求标志¶
KVM_REQUEST_NO_WAKEUP
此标志应用于仅需要处于客户模式运行的 VCPU 立即注意的请求。也就是说,休眠的 VCPU 无需为这些请求唤醒。休眠的 VCPU 将在稍后因其他原因唤醒时处理这些请求。
KVM_REQUEST_WAIT
当使用 kvm_make_all_cpus_request() 发出带有此标志的请求时,调用者将等待每个 VCPU 确认其 IPI,然后再继续执行。此标志仅适用于将收到 IPI 的 VCPU。例如,如果 VCPU 正在休眠,因此不需要 IPI,则请求线程不会等待。这意味着可以安全地将此标志与 KVM_REQUEST_NO_WAKEUP 结合使用。有关带有 KVM_REQUEST_WAIT 的请求的更多信息,请参见“等待确认”。
具有关联状态的 VCPU 请求¶
希望接收 VCPU 处理新状态的请求者需要确保新写入的状态在接收 VCPU 线程观察到请求时,对于接收 VCPU 线程的 CPU 是可见的。这意味着在写入新状态之后,并且在设置 VCPU 请求位之前,必须插入一个写入内存屏障。此外,在接收 VCPU 线程端,在读取请求位之后,并且在继续读取与之关联的新状态之前,必须插入相应的读取屏障。请参阅 [lwn-mb] 的场景 3“消息和标志”,以及内核文档 [memory-barriers]。
函数对 kvm_check_request() 和 kvm_make_request() 提供了内存屏障,从而允许 API 在内部处理此要求。
确保请求被看到¶
在向 VCPU 发出请求时,我们希望避免接收 VCPU 在客户模式下任意长时间执行而不处理请求。我们可以确保这种情况不会发生,只要我们确保 VCPU 线程在进入客户模式之前检查 kvm_request_pending(),并且在必要时,唤醒将发送一个 IPI 以强制退出客户模式。必须格外小心地覆盖 VCPU 线程上次 kvm_request_pending() 检查之后和进入客户模式之前的这段时间,因为唤醒 IPI 仅会触发在客户模式下或至少已经禁用中断以准备进入客户模式的 VCPU 线程的客户模式退出。这意味着优化的实现(请参见“IPI 减少”)必须确定何时可以安全地不发送 IPI。除了 s390 之外,所有架构都应用的一种解决方案是
在禁用中断和上次 kvm_request_pending() 检查之间,将
vcpu->mode
设置为 IN_GUEST_MODE;在进入客户机时以原子方式启用中断。
此解决方案还要求将内存屏障谨慎地放置在请求线程和接收 VCPU 中。借助内存屏障,我们可以排除 VCPU 线程在其上次检查时观察到 !kvm_request_pending(),然后没有收到针对其发出的下一个请求的 IPI 的可能性,即使请求是在检查后立即发出的。这是通过 Dekker 内存屏障模式([lwn-mb] 的场景 10)来实现的。由于 Dekker 模式需要两个变量,因此此解决方案将 vcpu->mode
与 vcpu->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 线程有用。这就是为什么这个 Dekker 模式的特定情况已扩展为在将 vcpu->mode
设置为 IN_GUEST_MODE 之前禁用中断。WRITE_ONCE() 和 READ_ONCE() 用于严格实现内存屏障模式,从而保证编译器不会干扰 vcpu->mode
的精心计划的访问。
IPI 减少¶
由于只需要一个 IPI 即可让 VCPU 检查任何/所有请求,因此可以合并它们。这很容易通过让第一个发送的 IPI 也将 VCPU 模式更改为 !IN_GUEST_MODE 来实现。过渡状态 EXITING_GUEST_MODE 用于此目的。
等待确认¶
某些请求,即设置了 KVM_REQUEST_WAIT 标志的请求,需要发送 IPI,并等待确认,即使目标 VCPU 线程处于 IN_GUEST_MODE 之外的模式。例如,一种情况是当目标 VCPU 线程处于 READING_SHADOW_PAGE_TABLES 模式时,该模式是在禁用中断后设置的。为了支持这些情况,KVM_REQUEST_WAIT 标志将发送 IPI 的条件从检查 VCPU 是否处于 IN_GUEST_MODE 更改为检查它是否不处于 OUTSIDE_GUEST_MODE。
无请求的 VCPU 唤醒¶
由于是否发送 IPI 的决定取决于双变量 Dekker 内存屏障模式,因此很明显,无请求的 VCPU 唤醒几乎从来都不是正确的。如果没有保证非 IPI 生成的唤醒仍然会导致接收 VCPU 执行操作(就像最终的 kvm_request_pending() 检查对于附带请求的唤醒所做的那样),那么唤醒可能根本不会执行任何有用的操作。例如,如果对一个即将将其模式设置为 IN_GUEST_MODE 的 VCPU 发出无请求的唤醒,这意味着不会发送 IPI,那么 VCPU 线程可能会继续其入口,而实际上没有执行唤醒本应启动的任何操作。
一个例外是 x86 的发布中断机制。然而,在这种情况下,即使是无请求的 VCPU 唤醒也与上述相同的 local_irq_disable() + smp_mb() 模式耦合;发布中断描述符中的 ON 位(Outstanding Notification)承担了 vcpu->requests
的作用。发送发布中断时,在读取 vcpu->mode
之前设置 PIR.ON;相应地,在 VCPU 线程中,vmx_sync_pir_to_irr() 在将 vcpu->mode
设置为 IN_GUEST_MODE 后读取 PIR。
其他考虑事项¶
休眠的 VCPU¶
VCPU 线程可能需要在调用可能使它们进入休眠状态的函数(例如 kvm_vcpu_block())之前和/或之后考虑请求。它们是否这样做,以及如果这样做,哪些请求需要考虑,取决于架构。 kvm_vcpu_block() 调用 kvm_arch_vcpu_runnable() 来检查是否应该唤醒它。这样做的一个原因是为架构提供一个可以在必要时检查请求的函数。
参考资料¶
Documentation/atomic_bitops.txt 和 Documentation/atomic_t.txt
Documentation/memory-barriers.txt