virt-report虚拟化社区动态

KVM vCPU 请求与跨 CPU 同步

本文由 OpenAI Codex (GPT-6-Astra) 辅助编写。

一、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 内完成;客户机继续执行时使用新映射,无需运行客户机中断处理程序。

三、一次写保护更新的完整交接

页表更新、退出确认与 TLB 刷新
选择需要请求机制的远端刷新分支。IPI 确认发生在宿主中断处理阶段;目标稍后在执行循环内消费刷新请求,两者是不同的完成点。
选择需要请求机制的远端刷新分支。IPI 确认发生在宿主中断处理阶段;目标稍后在执行循环内消费刷新请求,两者是不同的完成点。 打开原图
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 刷新计数

八、源码与进一步阅读