KVM vCPU 请求与跨 CPU 同步
一、KVM 请求与跨 vCPU 状态同步
设一台双 vCPU 虚拟机正在运行,vCPU 1 反复写入客户机物理地址 0x4000 所在的 4 KiB 页。宿主开始新一轮写入跟踪,把相关二级页表项从可写改为只读;运行 vCPU 1 的物理 CPU 却可能仍缓存旧的可写转换。管理路径必须协调执行中的 vCPU,让旧权限失效。只修改内存中的页表项,无法消除硬件已经缓存的转换。
KVM 通过请求位图记录待办工作,再根据目标状态决定是否唤醒线程或发送 IPI。位图保存在宿主内核的 struct kvm_vcpu 中,接收方是运行该 vCPU 的 QEMU 线程在 KVM 内的执行循环,客户机内核不消费这些请求。本文以远端 TLB 刷新的通用请求路径为例,沿着“发布更新—促使退出—处理请求—再次进入”说明执行归属与同步边界。
写保护更新完成后,vCPU 1 需要按新权限访问 0x4000。目标可以先停在宿主中,稍后处理刷新,但旧转换必须在下一次执行客户机之前失效。请求发布、IPI 确认和操作生效分别对应这段交接中的不同位置。
源码版本:Linux 6.18。
二、用请求记录工作,用 kick 推动处理
1. 请求位图与控制标志
vcpu->requests 是待办类型的位图。某一位置位,表示该类工作需要处理;重复请求可以合并到同一位。它不保存消息队列、次数或每次操作的参数。需要随请求传递的状态,由相应子系统另外保存并同步。
| 对象或字段 | 用途 | 不能据此推断的内容 |
|---|---|---|
requests | 记录 TLB 刷新、MMU 同步、事件检查等待办类型 | 每次请求均有独立的一次处理或一次通知 |
mode | 让发送方判断进入、执行、退出及特殊读页表阶段 | 客户机的 RUNNING / PAUSED 管理状态 |
cpu | 目标 vCPU 所在的宿主 CPU,用于定向通知 | vCPU 永远绑定该物理 CPU |
wait | 阻塞 vCPU 的等待对象,用于唤醒 | 线程被唤醒就已执行客户机代码 |
通用请求包含 KVM_REQ_TLB_FLUSH;x86 还定义 KVM_REQ_MMU_SYNC、KVM_REQ_LOAD_MMU_PGD、KVM_REQ_EVENT 等。kvm_make_request() 只负责发布请求,并不自动通知远端 CPU;调用者按需要配合 kick,批量请求接口则封装了相应通知流程。请求接口
2. 三种容易混淆的通知
| 通知 | 处理方 | 作用 |
|---|---|---|
| 宿主 IPI | 目标物理 CPU 上的宿主内核 | 促使目标离开客户机,协调 KVM 的待办工作 |
| 客户机中断 | 客户机内核的中断处理程序 | 处理客户机可见的设备或定时器事件,需经过虚拟中断路由与交付 |
| QEMU 线程信号 | 宿主 Linux 信号机制与 QEMU 的信号处理路径 | 用于 QEMU 控制线程执行,例如让 vCPU 返回用户态处理工作 |
本例的 TLB 刷新可以由宿主 IPI 推动,并在 KVM 内完成;客户机继续执行时使用新映射,无需运行客户机中断处理程序。
三、一次写保护更新的完整交接
Mermaid 源码
sequenceDiagram
accTitle: KVM 远端 TLB 刷新的请求交接
accDescr: 发送方发布页表修改和刷新请求,并等待目标 CPU 的 IPI 确认。目标退出客户机后确认 IPI,随后才在进入循环中处理刷新请求,完成刷新后再次进入客户机。
participant S as 发送方 CPU
participant V as 目标 vCPU / 宿主 CPU
Note over V: 正在执行客户机<br/>可能缓存旧转换
S->>S: 在相应同步下修改页表项
S->>S: 发布 KVM_REQ_TLB_FLUSH 位
S->>V: 需要通用请求时发送 IPI<br/>WAIT · NO_WAKEUP
V->>V: 离开客户机,mode → OUTSIDE
V->>V: 允许并处理宿主中断
V-->>S: IPI 回调确认
Note over S,V: 退出确认已到达;请求处理尚可留待随后执行
V->>V: vcpu_enter_guest 检查并清除刷新位
V->>V: kvm_vcpu_flush_tlb_all
V->>V: 完成进入检查,重新运行客户机
Note over V: 访问按新权限处理
1. 先发布页表变化
写保护路径在相应 MMU 锁保护下修改 SPTE,并在要求的位置执行远端刷新。kvm_flush_remote_tlbs() 优先尝试架构提供的直接刷新;需要通用请求时,才调用 kvm_make_all_cpus_request(..., KVM_REQ_TLB_FLUSH)。因此不能把所有 TLB 失效都画成必经同一种 IPI 流程。远端刷新入口
2. 目标离开客户机后确认 IPI
发送方为目标设置刷新位,并按其执行模式选择通知。正在客户机中运行的远端 CPU 收到 IPI 后退出。目标离开客户机模式、重新允许宿主中断处理时,可以执行 ack_kick() 对应的回调,使等待 IPI 确认的发送方继续。
3. 目标在下一次进入前处理刷新
目标 vCPU 回到 vcpu_enter_guest(),通过 kvm_check_request(KVM_REQ_TLB_FLUSH, vcpu) 消费请求,再调用 kvm_vcpu_flush_tlb_all()。架构后端执行刷新,或设置下一次进入所需的失效控制,保证客户机继续执行前旧转换不可用。例如 SVM 可以设置 VMCB 的 tlb_ctl,在后续 VMRUN 时生效,也可以通过更新 ASID generation 强制分配新的 ASID。vCPU 1 随后写入 0x4000,会按新的写保护权限触发处理:写保护式跟踪路径可记录该页变脏,再允许客户机完成写入。SVM 刷新安排
这一过程可以留在一次 KVM_RUN 调用内部。宿主退出、IPI 确认、刷新处理和返回用户态是四种不同事件,排查耗时时应分别观察。
四、不同 vCPU 状态下的通知路径
| 目标状态 | 通常采取的动作 | 后续保证 |
|---|---|---|
| 等待中的线程 | 允许唤醒时解除等待,使线程可运行 | 获得 CPU 后再检查工作;唤醒本身不完成工作 |
IN_GUEST_MODE | 原子尝试改为 EXITING,并按需发送 IPI | 促使离开客户机;该模式也覆盖进入前的受保护窗口 |
OUTSIDE_GUEST_MODE | 通常无需额外 IPI | 待办请求仍在,进入前检查会阻止遗漏 |
EXITING_GUEST_MODE | 普通异步通知可合并;WAIT 请求按更严格条件处理 | 已有退出动作不等于已经退出 |
KVM_REQUEST_NO_WAKEUP 允许保留睡眠 vCPU 的请求,避免为只需进入前完成的工作专门唤醒它。通用 KVM_REQ_TLB_FLUSH 同时带有 WAIT 与 NO_WAKEUP,正在休眠的目标可以继续休眠;之后进入客户机前必须消费刷新请求。
批量请求会把需要通知的物理 CPU 收集到 cpumask,一并发送。单目标普通 kick 则可使用 reschedule IPI。CPU 调度迁移也会使目标先离开客户机,因此读取到先前的物理 CPU 并不自动意味着同步丢失;仍需结合执行模式和进入检查理解源码保证。批量请求、单目标 kick
五、两组内存顺序约束关闭竞争窗口
1. 看到请求时,也要看到新状态
发送方先更新关联状态,kvm_make_request() 内部通过 smp_wmb() 把这些写入排在请求位置位之前。接收方检查并清除请求后,kvm_check_request() 以 smp_mb__after_atomic() 约束后续读取。两端配对,使接收方在消费请求时能够看到已发布的状态;具体状态的多写者互斥和生命周期仍由其子系统负责。
2. 进入前检查与远端通知不能同时漏掉
考虑一个最危险的交错:目标刚判断“没有请求”,发送方又判断“目标尚未进入客户机”,于是两边都继续。若没有额外协调,目标可能长时间执行而不处理刚发布的工作。
Mermaid 源码
flowchart TB
accTitle: KVM 请求发布与客户机进入的双向检查
accDescr: 发送方先设置 requests 再检查 mode,目标先设置 mode 再检查 requests。两侧的配对屏障保证不会同时读到旧值,从而由取消进入或 IPI 退出推动请求处理。
subgraph S["发送方 CPU"]
S1["更新关联状态<br/>发布 requests 位"]
S2["屏障 + 原子模式检查<br/>IN → EXITING"]
S3["需要时发送 IPI"]
S1 --> S2 --> S3
end
subgraph V["目标 vCPU"]
V1["关闭本地中断<br/>发布 IN_GUEST_MODE"]
V2["配对全屏障<br/>检查 requests / 退出条件"]
V3{"是否存在退出条件?"}
V1 --> V2 --> V3
end
S1 -.->|目标检查待办| V2
V1 -.->|发送方检查模式| S2
S3 --> R["目标退出或取消进入<br/>回到请求处理路径"]
V3 -->|有| R
V3 -->|无| G["硬件进入客户机"]
G -.->|随后 IPI 到来时退出| R
classDef actor fill:#e4f1ec,stroke:#438575,color:#173e34
classDef barrier fill:#f4f3eb,stroke:#aaa98c,color:#3c4542
classDef result fill:#eef3f2,stroke:#738b87,color:#263c38
class S1,V1 actor
class S2,V2 barrier
class S3,V3,G,R result
x86 进入路径先关闭本地中断,发布 IN_GUEST_MODE,退出 SRCU 读侧后执行相应全屏障,再通过 kvm_vcpu_exit_request() 检查请求和其他退出条件。发送方发布请求后,kvm_vcpu_exiting_guest_mode() 以屏障和 cmpxchg() 读取、更新模式。这组顺序排除了双方都观察到旧状态的结果。
IN_GUEST_MODE 在真正的硬件进入指令之前就已发布,保证那段窗口同样受通知保护。只发一个缺少持久待办状态的 kick,无法普遍替代上述协议。Posted interrupt 使用自己的待处理中断状态配合进入检查,属于另外一条有对应同步的路径。进入前检查
六、等待退出确认与等待操作完成的区别
KVM_REQUEST_WAIT 在批量请求路径中要求等待必要的 IPI 确认。确认表示目标已越过相应执行区间,不能把它当作任意请求处理函数的完成通知。上面的例子中,发送方可以先收到 IPI 确认,目标随后才在进入循环中执行 TLB 刷新;待处理位确保这项工作不会跨过下一次客户机进入。
WAIT 请求判断通知条件时使用“模式不是 OUTSIDE”,也覆盖 READING_SHADOW_PAGE_TABLES。这是部分非 TDP MMU 的无锁读页表区间:读者关闭中断并声明该模式,发送方需要等它结束相应读取后,才能越过同步点。当前 TDP MMU 的无锁遍历使用 RCU,不能把这套特殊模式套到全部 EPT 读路径。无锁页表遍历边界
KVM_REQ_OUTSIDE_GUEST_MODE 还有 NO_ACTION 标志,只用于确保目标离开客户机模式,不向 requests 留下待处理位。需要完整操作结果的调用者,仍须采用该操作定义的锁、完成量或其他同步协议。
七、同步开销与性能观测
请求位图把同类工作合并,EXITING 状态减少重复 kick,NO_WAKEUP 减少无收益唤醒,批量 CPU 掩码减少通知组织成本。进一步的优化方向包括批量修改后集中失效、按地址范围刷新,以及使用架构提供的直接远端刷新能力;具体收益取决于映射更新频率、vCPU 数量与硬件支持。
代价也有两面:合并降低通知次数,却使请求计数不能直接代表实际处理次数;更大的失效范围实现简单,却可能带来更多 TLB 重填;更长的批处理窗口可以提高吞吐,但会延后管理操作生效。
在宿主侧读取目标 VM 的 KVM 统计:remote_tlb_flush_requests 记录进入通用远端刷新入口的次数;该入口直接刷新成功,或批量请求实际发出远端通知时,remote_tlb_flush 才增加;每 vCPU 的 tlb_flush 则统计相应 vCPU 刷新入口的调用。再结合宿主内核 kvm_exit / kvm_entry 事件、调度唤醒与运行时序,定位退出等待还是后续处理耗时。这些计数受请求合并、架构直接刷新和 vCPU 状态影响,并不具有逐次一一对应关系;需要用同一时间窗口内的增量作比较。远端计数更新、vCPU 刷新计数