Skip to the content.

async pf

当前协议状态见 机制状态核查:APF 机制仍在, page-not-present 仍可使用 synthetic #PF;旧的 page-ready #PF 通知已被 2635b5c4a0e4 替换为 LAPIC 中断,当前没有旧 ready 通知方式的回退分支。 因此旧文章里的两类 APF 都使用 #PF 的描述不能直接套用当前源码。

关键设计

因为 vCPU 发生 page fault 了,被切换走,然后 Host 从磁盘中请求内容,然后 host 就很尴尬:

基本的设计:

参考资料

MSR 位定义及 page-not-present / page-ready 协议见 Linux KVM MSR 文档。

async pf 的几个基本问题

arm 支持吗

不支持

最多支持多少个 guest thread 的 page fault 被 stall

答案一个 CPU 是 64 个

#define ASYNC_PF_PER_VCPU 64
bool kvm_setup_async_pf(struct kvm_vcpu *vcpu, ...)
 {
     if (vcpu->async_pf.queued >= ASYNC_PF_PER_VCPU)
         return false;  // 超过限制,转为同步处理
     ...
 }

需要 Guest 和 Host 的参与

  1. 为什么需要 Guest 感知到
    • guest 需要在触发 async page fault 的时候,将当前执行的 thread 挂起,
  2. 为什么 host 需要 worker 来参与
    • 总是需要一个等待者,要么当前的 vCPU thread ,显然 vCPU thread 需要做其他的事情,所以就让 worker 来等待

实现细节

触发

EPT/NPT violation(不一定意味着 backing page 不驻留)
  → kvm_mmu_page_fault()
  → kvm_mmu_do_page_fault()
  → kvm_tdp_page_fault() → kvm_tdp_mmu_page_fault() [启用 TDP MMU 时]
  → kvm_mmu_faultin_pfn() → __kvm_mmu_faultin_pfn()
      → __kvm_faultin_pfn(..., FOLL_NOWAIT, ...)
      → kvm_follow_pfn() → hva_to_pfn()
          ├─ 有效 PFN:外层继续 kvm_tdp_mmu_map()
          ├─ 其他错误编码:外层错误处理
          └─ KVM_PFN_ERR_NEEDS_IO
              ├─ prefetch / !kvm_can_do_async_pf():同步取页
              ├─ kvm_find_async_pf_gfn() 找到已有任务
              │    → KVM_REQ_APF_HALT,返回 RET_PF_RETRY
              └─ kvm_arch_setup_async_pf() → kvm_setup_async_pf()
                   ├─ 失败:同步取页
                   └─ 成功建立任务
                        → kvm_arch_async_page_not_present()
                            ├─ 可以通知:注入 synthetic #PF
                            └─ 不能通知:KVM_REQ_APF_HALT
                        → schedule_work(async_pf_execute)
                        → 返回 RET_PF_RETRY

同步取页:__kvm_faultin_pfn(..., 清除 FOLL_NOWAIT,添加 FOLL_INTERRUPTIBLE, ...)

非常清晰,注入中断就是 #PF

void kvm_inject_page_fault(struct kvm_vcpu *vcpu, struct x86_exception *fault,
			   bool from_hardware)
{
	++vcpu->stat.pf_guest;

	/*
	 * Async #PF in L2 is always forwarded to L1 as a VM-Exit regardless of
	 * whether or not L1 wants to intercept "regular" #PF.
	 */
	if (is_guest_mode(vcpu) && fault->async_page_fault)
		kvm_queue_exception_vmexit(vcpu, PF_VECTOR,
					   true, fault->error_code,
					   true, fault->address);
	else
		kvm_queue_exception_e_p(vcpu, PF_VECTOR, fault->error_code,
					fault->address);
}

KVM_PFN_ERR_NEEDS_IO 的原因,一定经典案例如下,其实就是 GUP 的时候没有

get_user_pages_unlocked()
  → ... → __get_user_pages()
      → follow_page_mask():当前还不能取得页面
      → faultin_page()
          → handle_mm_fault()
              → ... → do_swap_page()
                  → folio_lock_or_retry()
                      → folio_trylock() 失败
                      → __folio_lock_or_retry()
                          → 看到 RETRY_NOWAIT,返回 VM_FAULT_RETRY
          → 把 VM_FAULT_RETRY 转为 -EBUSY
      → 本次请求 1 页,却没有获得任何页,返回 0
  → hva_to_pfn_slow() 返回 0
  → hva_to_pfn():普通 VMA 有效 + FOLL_NOWAIT
      → KVM_PFN_ERR_NEEDS_IO

__kvm_mmu_faultin_pfn() 三种情况来处理 io

RET_PF_RETRY 是 KVM MMU 内部的处理结果,与 NEEDS_IO 不同: 它表示当前这次 fault 暂时没有建立最终映射,后续需要重试/重新处理;不会作为 errno 直接发给 guest。

Worker 代理执行

本次 x86 配置走非 CONFIG_KVM_ASYNC_PF_SYNC 分支,实际时序是:

host kworker:async_pf_execute()
  → get_user_pages_remote(kvm->mm, hva, 1, FOLL_WRITE, ...)
      [不带 FOLL_NOWAIT,允许等待]
  → work 加入 vcpu->async_pf.done
  → kvm_arch_async_page_present_queued()
      → KVM_REQ_APF_READY,必要时 kick vCPU
  → __kvm_vcpu_wake_up()

host vCPU:kvm_check_async_pf_completion()
  → kvm_arch_async_page_ready()
      → kvm_mmu_do_page_fault(..., prefetch=true, ...)
        [上下文仍匹配时,尝试预先建立映射]
  → kvm_arch_async_page_present()
      ├─ 先前确实注入过 not-present,且 guest APF 仍启用
      │   → apf_put_user_ready() 写 token
      │   → kvm_apic_set_irq() 投递 page-ready 中断
      └─ 清除 apf.halted,恢复 RUNNABLE

guest:sysvec_kvm_asyncpf_interrupt()
  → kvm_async_pf_task_wake(token)
  → 清除共享 token,写 MSR_KVM_ASYNC_PF_ACK
  → 原 task 可以再次获得调度并重试原指令

kvm_arch_async_page_ready()(MMU 预取)和 kvm_arch_async_page_present() (guest 通知/解除 halt)是两个不同函数,不能因名字接近而混用。 Worker 不向 vCPU 直接移交一个最终 PFN;vCPU 的 MMU 路径会再次检查映射。 而且 worker 取页失败也会通知完成,避免 vCPU 永远等待,后续 fault 再处理错误。 因此“completed/ready”也不能当作“这次底层 GUP 一定成功”的绝对证明。

void kvm_arch_async_page_present(struct kvm_vcpu *vcpu,
				 struct kvm_async_pf *work)
{
	struct kvm_lapic_irq irq = {
		.delivery_mode = APIC_DM_FIXED,
		// 这个就是 guest os 通过 wrmsrq(MSR_KVM_ASYNC_PF_INT, HYPERVISOR_CALLBACK_VECTOR); 来配置的
		.vector = vcpu->arch.apf.vec
	};

	if (work->wakeup_all)
		work->arch.token = ~0; /* broadcast wakeup */
	else
		kvm_del_async_pf_gfn(vcpu, work->arch.gfn);
	trace_kvm_async_pf_ready(work->arch.token, work->cr2_or_gpa);

	if ((work->wakeup_all || work->notpresent_injected) &&
	    kvm_pv_async_pf_enabled(vcpu) &&
	    !apf_put_user_ready(vcpu, work->arch.token)) {
		WRITE_ONCE(vcpu->arch.apf.pageready_pending, true);
		kvm_apic_set_irq(vcpu, &irq, NULL);
	}

	vcpu->arch.apf.halted = false;
	kvm_set_mp_state(vcpu, KVM_MP_STATE_RUNNABLE);
}

的角度

本来的 kvm 日志为:

arch/x86/include/asm/trapnr.h

#define X86_TRAP_PF		14	/* Page Fault */

现在我们仅仅考虑 KVM_FEATURE_ASYNC_PF_INT

触发 page fault

在 arch/x86/mm/fault.c 中标准 page fault 的入口:

DEFINE_IDTENTRY_RAW_ERRORCODE(exc_page_fault)

注意,这个还是复用了 #PF 的入口。

接受消息

host 处理完成之后,挂到:

/* Vector on which hypervisor callbacks will be delivered */
#define HYPERVISOR_CALLBACK_VECTOR	0xf3

DEFINE_IDTENTRY_SYSVEC(sysvec_kvm_asyncpf_interrupt)
$ cat /proc/interrupts
 HYP:          1          1          1  Hypervisor callback interrupts

嵌套的 async pf 存在 bug

2026-09-15 不确定当前是否还存在

bogon login: [    8.974820] mount.nfs (3059) used greatest stack depth: 10416 bytes left
[  340.176347] Kernel panic - not syncing: Host injected async #PF in kernel mode
[  340.176843] CPU: 25 UID: 1000 PID: 6896 Comm: zsh Tainted: G           O       6.11.0 #136
[  340.177374] Tainted: [O]=OOT_MODULE
[  340.177614] Hardware name: Martins3 Inc Hacking Alpine, BIOS 12 2022-2-2
[  340.178059] Call Trace:
[  340.178244]  <TASK>
[  340.178406]  dump_stack_lvl+0x86/0xc0
[  340.178657]  panic+0x12f/0x310
[  340.178868]  __kvm_handle_async_pf+0xa3/0xb0
[  340.179145]  exc_page_fault+0x10c/0x1f0
[  340.179416]  asm_exc_page_fault+0x26/0x30
[  340.179678] RIP: 0010:__put_user_4+0x11/0x20
[  340.179957] Code: 1f 84 00 00 00 00 00 66 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 f3 0f 1e fa 48 89 cb 48 c1 fb 3f 48 09 d9 0f 01 cb <89> 01 31 c9 0f 01 ca c3
 cc cc cc cc 0f 1f 00 90 90 90 90 90 90 90
[  340.181079] RSP: 0018:ffffc900185d3f18 EFLAGS: 00050202
[  340.181430] RAX: 0000000000001af0 RBX: 0000000000000000 RCX: 00007fc378019510
[  340.181877] RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffff8881047e2400
[  340.182356] RBP: 0000000000000000 R08: ffff8881047e2400 R09: ffffffff84076c00
[  340.182818] R10: 0000000000000001 R11: 0000004f341718de R12: 0000000000000000
[  340.183296] R13: 0000000000000000 R14: ffffffff81113ddc R15: 0000000000000000
[  340.183763]  ? ret_from_fork+0x1c/0x50
[  340.184033]  schedule_tail+0x8a/0xa0
[  340.184309]  ret_from_fork+0x1c/0x50
[  340.184568]  ret_from_fork_asm+0x1a/0x30
[  340.184848]  </TASK>
[  340.185227] Kernel Offset: disabled
[  340.185493] ---[ end Kernel panic - not syncing: Host injected async #PF in kernel mode ]---

启动嵌套的时候有这个错误。

  1. 思考下,为什么 host 中不可以使用 async pf 啊

所以,我猜测,是本来应该注入给 L2 的 async pf 注入到 L1 中了,导致 L1 crash 了。

并不是,L1 用文件,L2 用普通的 memory ,还是有问题

测试一些程序的时候,似乎启动嵌套,其中 l2 使用的是普通内存,也会有这个问题: 也就是相当于 L1 是文件,或者是 memfd 就会有问题

[  245.347107][ T2261] Kernel panic - not syncing: Host injected async #PF in kernel mode
[  245.426245][ T2261] CPU: 33 PID: 2261 Comm: dockerd Not tainted 6.6.0-28.0.0.34.oe2403.x86_64 #1
[  245.437923][ T2261] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.3-32-g9029a010ec41 04/01/2014
[  245.456334][ T2261] Call Trace:
[  245.462508][ T2261]  <TASK>
[  245.467151][ T2261]  dump_stack_lvl+0x32/0x50
[  245.476485][ T2261]  panic+0x304/0x340
[  245.482298][ T2261]  ? restore_regs_and_return_to_kernel+0x22/0x22
[  245.490174][ T2261]  __kvm_handle_async_pf+0x98/0xb0
[  245.496845][ T2261]  exc_page_fault+0x358/0x780
[  245.502580][ T2261]  ? __futex_unqueue+0x25/0x40
[  245.508663][ T2261]  ? futex_unqueue+0x38/0x60
[  245.514184][ T2261]  asm_exc_page_fault+0x22/0x30
[  245.520170][ T2261] RIP: 0010:__get_user_8+0xd/0x20
[  245.541414][ T2261] Code: ca c3 cc cc cc cc 0f 1f 80 00 00 00 00 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 48 89 c2 48 c1 fa 3f 48 09 d0 0f 01 cb <48> 8b 10 31 c0 0f 01 ca c3 cc cc cc cc 66 0f 1f 44 00 00 90 90 90
[  245.618930][ T2261] RSP: 0018:ffffc900024fbe50 EFLAGS: 00050206
[  245.692158][ T2261] RAX: 00007f479a69cfe8 RBX: ffff8880208d4f80 RCX: 000000000001a408
[  245.701688][ T2261] RDX: 0000000000000000 RSI: ffffc900024fbe80 RDI: ffff8880208d4f80
[  245.711109][ T2261] RBP: ffffc900024fbee8 R08: ffff88805a0b5488 R09: 0000000000000000
[  245.720641][ T2261] R10: 0000000000000000 R11: 000000000080d6fe R12: ffffc900024fbf58
[  245.729971][ T2261] R13: 000055f1299597a3 R14: ffff8880208d4f80 R15: 0000000000000000
[  245.739432][ T2261]  rseq_get_rseq_cs+0x1d/0x270
[  245.746024][ T2261]  rseq_ip_fixup+0x46/0x190
[  245.751546][ T2261]  ? do_futex+0x106/0x1b0
[  245.756816][ T2261]  ? __se_sys_futex+0x6d/0x1b0
[  245.762258][ T2261]  __rseq_handle_notify_resume+0x26/0x50
[  245.768925][ T2261]  syscall_exit_to_user_mode+0xa9/0x1e0
[  245.775506][ T2261]  do_syscall_64+0x62/0x100
[  245.780837][ T2261]  entry_SYSCALL_64_after_hwframe+0x78/0xe2
[  245.788095][ T2261] RIP: 0033:0x55f1299597a3
[  245.793544][ T2261] Code: 24 20 c3 cc cc cc cc 48 8b 7c 24 08 8b 74 24 10 8b 54 24 14 4c 8b 54 24 18 4c 8b 44 24 20 44 8b 4c 24 28 b8 ca 00 00 00 0f 05 <89> 44 24 30 c3 cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc cc
[  245.816822][ T2261] RSP: 002b:00007f479a69bc48 EFLAGS: 00000202 ORIG_RAX: 00000000000000ca
[  245.826639][ T2261] RAX: ffffffffffffff92 RBX: 0000000000000000 RCX: 000055f1299597a3
[  245.835797][ T2261] RDX: 0000000000000000 RSI: 0000000000000080 RDI: 000055f12d56c780
[  245.845079][ T2261] RBP: 00007f479a69bc90 R08: 0000000000000000 R09: 0000000000000000
[  245.853816][ T2261] R10: 00007f479a69bc80 R11: 0000000000000202 R12: 00007f479a69bc80
[  245.863244][ T2261] R13: 0000000000000016 R14: 000000c0000069c0 R15: 00007f4799e9c000
[  245.872866][ T2261]  </TASK>
[  245.881821][ T2261] Kernel Offset: disabled
[  245.886099][ T2261] ---[ end Kernel panic - not syncing: Host injected async #PF in kernel mode ]---

基本执行流程

┌──────────────────────────────────────────────────────────────────────────────┐
│                        APF 完整工作流程                                       │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   PHASE 1: 页错误检测与异步判定                                               │
│   ─────────────────────────────────────                                     │
│                                                                              │
│   Guest: 访问虚拟地址 VA → EPT Violation → VM Exit                           │
│                                                                              │
│   Host Handler: kvm_mmu_page_fault()                                         │
│        ↓                                                                     │
│   kvm_tdp_page_fault() / paging64_page_fault()                               │
│        ↓                                                                     │
│   kvm_faultin_pfn()                                                          │
│        ↓                                                                     │
│   ┌────────────────────────────────────────────────────────────────┐        │
│   │ kvm_mmu_faultin_pfn -> __kvm_mmu_faultin_pfn():                                               │        │
│   │   1. 调用 gfn_to_pfn_prot() 尝试获取页面                        │        │
│   │   2. 如果页面不在内存中(需要 I/O):                           │        │
│   │      - 检查 kvm_find_async_pf_gfn() 是否已有相同请求            │        │
│   │      - 没有则调用 kvm_arch_setup_async_pf() 创建异步请求        │        │
│   └────────────────────────────────────────────────────────────────┘        │
│        ↓                                                                     │
│   kvm_setup_async_pf() ──────→ 创建 async_pf 结构                           │
│        ↓                                                                     │
│   INIT_WORK(&apf->work, async_pf_execute)                                    │
│   queue_work(system_unbound_wq, &apf->work)                                  │
│        ↓                                                                     │
│   kvm_arch_async_page_not_present() ──→ 向 Guest 注入 "Page Not Present"    │
│                                                                              │
│   ═══════════════════════════════════════════════════════════════════════   │
│                                                                              │
│   PHASE 2: Guest 侧处理                                                       │
│   ─────────────────────────────────────                                     │
│                                                                              │
│   Guest 收到特殊 #PF(带有 PV 标志)                                          │
│        ↓                                                                     │
│   exc_page_fault() → kvm_handle_async_pf()                                  │
│        ↓                                                                     │
│   __kvm_handle_async_pf():                                                   │
│       - 识别这是 Host 注入的异步页错误                                        │
│       - 记录 apf_id 到 per-cpu 变量                                           │
│       - 将当前 task 加入 async_pf 等待队列                                    │
│       - 调用 kvm_async_pf_task_wait_schedule()                                │
│        ↓                                                                     │
│   schedule() ──────→ 当前线程睡眠,让出 CPU                                   │
│                                                                              │
│   ═══════════════════════════════════════════════════════════════════════   │
│                                                                              │
│   PHASE 3: 异步页面加载                                                       │
│   ─────────────────────────────────────                                     │
│                                                                              │
│   Worker Thread (async_pf_execute):                                          │
│        ↓                                                                     │
│   1. 调用 gup 获取页面(可能睡眠等待 I/O)                                    │
│   2. 页面就绪后,调用 kvm_arch_async_page_present()                           │
│        ↓                                                                     │
│   ┌────────────────────────────────────────────────────────────────┐        │
│   │ 两种通知方式(取决于 KVM_FEATURE_ASYNC_PF_INT):                │        │
│   │                                                                │        │
│   │  Legacy (异常方式):                                            │        │
│   │    - 向 Guest 注入特殊 #PF (Page Ready)                        │        │
│   │    - 风险:可能与 Guest 自身的 #PF 冲突                        │        │
│   │                                                                │        │
│   │  Modern (中断方式, KVM_FEATURE_ASYNC_PF_INT):                  │        │
│   │    - 向 Guest 注入 KVM_ASYNC_PF_VECTOR 中断                    │        │
│   │    - 更安全,不会与正常 #PF 冲突                               │        │
│   └────────────────────────────────────────────────────────────────┘        │
│        ↓                                                                     │
│   list_add_tail(&apf->link, &vcpu->async_pf.done);                          │
│                                                                              │
│   ═══════════════════════════════════════════════════════════════════════   │
│                                                                              │
│   PHASE 4: Guest 恢复执行                                                     │
│   ─────────────────────────────────────                                     │
│                                                                              │
│   Guest 收到 "Page Ready" 通知:                                             │
│        ↓                                                                     │
│   Legacy: exc_page_fault() → kvm_handle_async_pf()                           │
│   Modern: sysvec_kvm_asyncpf_interrupt()                                     │
│        ↓                                                                     │
│   kvm_async_pf_task_wake():                                                  │
│       - 根据 apf_id 找到等待的 task                                           │
│       - 唤醒之前睡眠的线程                                                    │
│        ↓                                                                     │
│   被唤醒的线程重新执行缺页指令 → 页面已在内存中 → 正常执行                     │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
kvm:kvm_try_async_get_page                         [Tracepoint event]
kvm:kvm_async_pf_completed                         [Tracepoint event]
kvm:kvm_async_pf_not_present                       [Tracepoint event]
kvm:kvm_async_pf_ready                             [Tracepoint event]
kvm:kvm_async_pf_repeated_fault                    [Tracepoint event]
  1. 需要修改内核 kvm 外面的代码 ? 不然怎么来识别从 host inject 的
  2. 内核如何调度 host 的另一个 task 过来运行的

本站所有文章转发 CSDN 将按侵权追究法律责任,其它情况随意。