libkrun 深度解析:架构设计、模块实现与 Windows WHPX 后端

15 阅读20分钟

从一个动态库到完整的跨平台 VMM:libkrun 如何在保持极简 API 的同时,支撑起 Linux、macOS 和 Windows 三大平台的虚拟化能力


一、为什么需要 libkrun?

1.1 容器安全的困境

过去十年,容器技术(Docker、containerd、CRI-O)彻底改变了软件交付方式。但容器的核心机制——Linux namespace 和 cgroup——本质上是对同一个宿主机内核的切片,而非真正的隔离。一旦宿主机内核存在漏洞,容器边界便形同虚设。Dirty COW(CVE-2016-5195)、runc 漏洞(CVE-2019-5736)等真实攻击案例一再证明了这一点。

传统虚拟机(QEMU/KVM)提供了硬件级的强隔离,但代价是:

  • 启动慢:完整系统镜像加载 + BIOS/UEFI + 内核初始化,通常需要数秒到数十秒
  • 资源重:每个 VM 独占完整内存,内核栈本身就消耗数百 MB
  • 使用复杂:需要管理磁盘镜像、网络配置、快照等大量基础设施

libkrun 的核心洞察是:绝大多数容器工作负载只需要运行一个进程。既然如此,VMM 不需要模拟完整 PC,只需要模拟"恰好够用"的硬件——一个能运行 Linux 进程的最小化虚拟机。

1.2 libkrun 的定位

传统容器(namespace)  ←── 隔离性 ──→  传统虚拟机(QEMU)
    弱隔离 / 快启动                    强隔离 / 慢启动

              libkrun
          硬件隔离 + 毫秒启动

libkrun 是一个以动态库形式交付的轻量级 VMM(Virtual Machine Monitor)。应用程序链接它,就像链接 libc 一样——不需要守护进程、不需要特权进程、不需要套接字通信。整个虚拟化栈在调用方进程内运行。


二、整体架构

2.1 分层结构

┌──────────────────────────────────────────────────────────────────────────────┐
│                        宿主应用  (C / Rust)                                  │
│              crun · krunkit · muvm · a3s box · 自定义程序                    │
└────────────────────────────────┬─────────────────────────────────────────────┘
                                 │  include/libkrun.h  (稳定 C API)
┌────────────────────────────────▼─────────────────────────────────────────────┐
│                    src/libkrun  ·  公共 C API 层                             │
│   krun_create_ctx · krun_set_vm_config · krun_set_root · krun_set_kernel    │
│   krun_add_virtiofs · krun_add_disk · krun_add_net · krun_start_enter …     │
└──────┬──────────────────┬──────────────────┬──────────────┬──────────────────┘
       │                  │                  │              │
┌──────▼──────┐  ┌────────▼────────┐  ┌─────▼──────┐  ┌───▼──────────────────┐
│  src/vmm    │  │  src/devices    │  │  src/arch  │  │  src/kernel           │
│             │  │                 │  │            │  │                        │
│ VM/vCPU     │  │ virtio-console  │  │ x86_64     │  │ ELF/Image/PeGz 加载器 │
│ 生命周期    │  │ virtio-block    │  │ aarch64    │  │                        │
│             │  │ virtio-fs       │  │ riscv64    │  │ 内核命令行构建         │
│ 内存管理    │  │ virtio-net      │  │            │  └────────────────────────┘
│             │  │ virtio-vsock    │  │ 引导状态   │
│ IRQ 芯片    │  │   └─ TSI 代理   │  │ 内存布局   │  ┌────────────────────────┐
│ IO/MMIO 总线│  │ virtio-gpu      │  │ configure_ │  │  src/cpuid             │
│             │  │ virtio-balloon  │  │ system()   │  │  CPUID 叶子模拟        │
│ vCPU 事件  │  │ virtio-rng      │  └────────────┘  └────────────────────────┘
│ 循环        │  │ virtio-snd      │
│             │  │                 │                  ┌────────────────────────┐
└──────┬──────┘  │ 遗留设备:       │                  │  src/polly             │
       │         │  8250 串口      │                  │  epoll 事件管理器      │
       │         │  i8042 键盘     │                  └────────────────────────┘
       │         │  CMOS (RTC)    │
       │         │  PIT 8254      │                  ┌────────────────────────┐
       │         │  PIC 8259A     │                  │  src/utils             │
       │         └─────────────────┘                  │  EventFd · epoll       │
       │                                              │  时间戳 · 字节工具     │
       │                                              └────────────────────────┘
┌──────▼──────────────────────────────────────────────────────────────────────┐
│                         Hypervisor 后端                                      │
│  ┌───────────────┐  ┌───────────────┐  ┌───────────────┐  ┌──────────────┐ │
│  │  KVM          │  │  HVF          │  │  WHPX         │  │  Nitro       │ │
│  │  Linux        │  │  macOS/ARM64  │  │  Windows      │  │  AWS Enclave │ │
│  └───────────────┘  └───────────────┘  └───────────────┘  └──────────────┘ │
└──────────────────────────────────────────────────────────────────────────────┘

2.2 数据流:从 API 调用到 VM 运行

krun_start_enter(ctx_id)
    │
    ├─ build_microvm(vm_resources)               [vmm/builder.rs]
    │      ├─ choose_payload()                    → libkrunfw 或 ExternalKernel
    │      ├─ create_guest_memory()              → GuestMemoryMmap
    │      ├─ Vm::new()                          → WHvCreatePartition / KVM_CREATE_VM
    │      ├─ load_payload()                     → 内核加载到 guest 物理内存
    │      ├─ attach_legacy_devices()            → PIT/PIC/串口注册到 IO 总线
    │      ├─ attach_virtio_devices()            → virtio-fs/block/net/vsock 注册
    │      ├─ create_vcpus_x86_64()             → WHvCreateVirtualProcessor
    │      └─ Vmm::run_control()                → 启动 vCPU 线程
    │
    └─ vCPU 线程循环
           ├─ configure_x86_64()               → 设置 GDT/IDT/页表/寄存器
           └─ loop { self.run() }
                  ├─ WHvRunVirtualProcessor()  → 执行 guest 指令
                  ├─ IoPortWrite(0x3f8, 'H')  → io_bus.write() → 串口设备
                  ├─ MmioRead(0xfec00000, 4)  → mmio_bus.read() → APIC
                  └─ Halted                   → 等待中断 → 重新进入

三、各模块深度解析

3.1 libkrun — 公共 C API 层

位置src/libkrun/src/lib.rs

这是整个库的入口点。它的职责是将 C 语言调用翻译成 Rust 内部的 VmResources 配置结构,然后驱动 VMM 启动。

核心设计:上下文映射

每个 krun_create_ctx() 调用返回一个整数 ID,对应一个 VmResources 实例存储在全局 HashMap 中:

static CTX_MAP: Mutex<HashMap<u32, CtxCfg>> = Mutex::new(HashMap::new());

pub extern "C" fn krun_create_ctx() -> i32 {
    let ctx_id = NEXT_CTX_ID.fetch_add(1, Ordering::Relaxed);
    CTX_MAP.lock().unwrap().insert(ctx_id, CtxCfg::default());
    ctx_id as i32
}

这种设计使得多个 VM 上下文可以并发存在,互不干扰。每个上下文在 krun_start_enter() 时被消耗,转化为实际的 VM。

平台差异抹平

同一个 krun_add_net_unixstream() API 在 Linux 上通过 Unix 套接字连接 passt,在 Windows 上通过 TcpStream 连接网络后端:

#[cfg(not(target_os = "windows"))]
pub unsafe extern "C" fn krun_add_net_unixstream(...) { /* Unix 路径 */ }

#[cfg(target_os = "windows")]
pub unsafe extern "C" fn krun_add_net(...) { /* TCP 地址 */ }

3.2 vmm — VMM 核心

位置src/vmm/

vmm 是 libkrun 的心脏,负责 VM 的完整生命周期。它源自 AWS Firecracker,经过大量修改以支持多平台和 libkrun 的特定需求。

关键子模块:

builder.rs:VM 组装流水线。build_microvm() 函数按严格顺序执行每个阶段:

  1. 分配 guest 物理内存(GuestMemoryMmap,基于 mmap 或 Windows 上的 VirtualAlloc
  2. 创建 Hypervisor 分区(KVM fd / WHPX partition / HVF partition)
  3. 加载内核(ELF loader 解析节头,写入 guest 内存)
  4. 配置引导参数(x86_64 零页,包含内存图、cmdline 指针、initrd 位置)
  5. 注册 IO/MMIO 总线设备
  6. 创建并启动 vCPU 线程

vstate.rs(x86_64):vCPU 状态机。configure_x86_64() 设置完整的 x86_64 长模式引导状态:

// 初始化关键寄存器
// CR0: 保护模式 + 分页使能
// CR3: PML4 页表地址
// CR4: PAE 使能
// EFER: IA-32e 模式 + 长模式使能
// RIP: 内核入口点
// RSI: 零页地址(Linux 引导协议)
fn configure_x86_64(&mut self, guest_mem: &GuestMemoryMmap, entry: GuestAddress) {
    // 写入 GDT(4 个段描述符:null, 代码, 数据, TSS)
    // 写入 IDT(空,CPU 异常在 guest 内核初始化时配置)
    // 写入 PML4/PDPTE/PDE(映射前 4GB guest 物理地址)
    // 通过 WHvSetVirtualProcessorRegisters 写入所有寄存器
}

device_manager/:设备总线管理。Bus 是一个基于 BTreeMap 的地址空间路由器:给定地址,查找拥有该地址范围的设备并调用其 read()/write()

windows/whpx_vcpu.rs:WHPX vCPU 实现(见第五节详述)。

3.3 devices — 设备实现

位置src/devices/src/

所有模拟设备的实现。每个设备实现 BusDevice trait:

pub trait BusDevice: AsAny + Send {
    fn read(&mut self, vcpuid: u64, offset: u64, data: &mut [u8]) {}
    fn write(&mut self, vcpuid: u64, offset: u64, data: &[u8]) {}
}

设备通过 Arc<Mutex<dyn BusDevice>> 注册到 IO/MMIO 总线,允许多线程安全访问。

virtio 设备通用框架(virtio/mmio.rs

所有 virtio 设备遵循 MMIO 传输协议。Guest 驱动通过特定 MMIO 地址与设备通信:

Guest 驱动写入:    VIRTIO_MMIO_QUEUE_NOTIFY  通知设备有新请求
Guest 驱动写入:    VIRTIO_MMIO_DRIVER_FEATURES  协商功能集
设备读取 MMIO:     VIRTIO_MMIO_CONFIG  读取设备配置(如网卡 MAC)

**描述符链(Descriptor Chain)**是 virtio 的核心数据结构:

    ┌────────────┐     ┌────────────┐     ┌────────────┐
     Desc[0]    │────▶│ Desc[1]    │────▶│ Desc[2]    
     addr: 0x..       addr: 0x..       addr: 0x.. 
     len:  512        len:  16         len:  1    
     flags: W         flags: R         flags: W   
    └────────────┘     └────────────┘     └────────────┘
    Guest 内存中的分散-聚集 I/O 列表

VMM 遍历这个链,读写 guest 内存中的数据,完成后将 used ring 中的条目标记为已用,并通过 EventFd 通知 guest。

主要设备实现:

virtio-console:最简单的设备之一。TX 队列(guest→host):VMM 读取描述符链中的字节,写入宿主 stdout/文件。RX 队列(host→guest):VMM 读取 stdin,填充描述符链,通知 guest。Windows 版本使用后台线程读取 stdin 到环形缓冲区(WindowsStdinInput),因为 Windows 不支持非阻塞 stdin。

virtio-block:实现 VIRTIO_BLK_T_IN(读)和 VIRTIO_BLK_T_OUT(写)请求。Linux 版本通过 preadv/pwritev 实现向量 I/O;Windows 版本使用 std::fs::File + Seek

virtio-fs(virtiofs):最复杂的设备。它在 VMM 侧实现了 FUSE 服务器。Guest 内核的 FUSE 客户端通过 virtio 队列发送 FUSE 请求(FUSE_LOOKUPFUSE_READFUSE_WRITE 等),VMM 翻译成宿主文件系统调用。Windows 版本(fs/windows/passthrough.rs)直接调用 Win32 API,支持符号链接(需开发者模式)、fsync、稀疏文件等特性。

virtio-net:实现标准以太网帧传输。Linux/macOS 通过 Unix 套接字连接 passt/gvproxy;Windows 通过 TcpStream 后端,并实现了校验和卸载(checksum offload)和 TSO(TCP Segmentation Offload),减少 CPU 开销。

virtio-vsock + TSI

TSI(Transparent Socket Impersonation,透明套接字替换)是 libkrun 最创新的功能。Guest 内核的 TSI 补丁将 socket 系统调用拦截后转为 vsock 消息发送给 VMM。VMM 侧的 TSI 代理接收这些消息,在宿主机上代为执行真正的 socket 操作:

Guest 进程 connect("8.8.8.8:53")
    │
    ▼ (TSI 内核补丁拦截)
virtio-vsock 消息 →→→→→→→→→→→→→→→→→→→→→→→→→ VMM TSI 代理
                                                    │
                                                    ▼
                                          宿主机 connect("8.8.8.8:53")
                                                    │
                                                    ▼
                                             真实网络

Windows 实现分为 5 个阶段(约 2,100 行代码),完整支持 TCP/UDP/Named Pipe(AF_UNIX 替代)。

遗留设备(legacy/

设备端口用途
8250 串口0x3F8, 0x2F8, 0x3E8, 0x2E8内核早期控制台输出(earlycon)
i80420x60-0x64键盘/鼠标控制器
CMOS (RTC)0x70-0x77实时时钟,内存大小存储
PIT 82540x40-0x43可编程定时器,IRQ 0 时钟源
PIC 8259A0x20-0x21, 0xA0-0xA1遗留中断控制器

3.4 arch — 架构抽象层

位置src/arch/src/

每个目标架构都有独立的引导协议:

x86_64:遵循 Linux x86_64 引导协议(Documentation/x86/boot.rst):

  • 零页ZERO_PAGE_START = 0x7000):boot_params 结构,包含内存图(e820 entries)、cmdline 指针、initrd 位置
  • cmdlineCMDLINE_START = 0x20000):内核命令行字符串
  • GDT/IDT:长模式所需的段描述符表,位于 0x500/0x520
  • 页表:PML4→PDPTE→PDE 三级结构,恒等映射前 4GB

aarch64:遵循 Linux ARM64 引导协议,使用 FDT(Flattened Device Tree)描述硬件拓扑。

RISC-V:遵循 RISC-V Linux 引导约定,使用 OpenSBI 作为 M 模式固件。

configure_system() 函数将所有这些结构写入 guest 物理内存,这是内核启动前的最后一步。

3.5 kernel — 内核加载器

位置src/kernel/src/

支持多种内核格式:

格式常量使用场景
RawKRUN_KERNEL_FORMAT_RAWlibkrunfw 提供的内存映射内核
ELFKRUN_KERNEL_FORMAT_ELF调试用 vmlinux(未压缩)
PeGzKRUN_KERNEL_FORMAT_PEGZWindows/UEFI 内核(PE 格式+gzip)
ImageBz2KRUN_KERNEL_FORMAT_IMAGE_BZ2ARM64 常用压缩格式
ImageGzKRUN_KERNEL_FORMAT_IMAGE_GZx86_64 常见压缩格式
ImageZstdKRUN_KERNEL_FORMAT_IMAGE_ZSTD现代压缩格式,速度更快

ELF 加载器(基于 linux-loader crate)解析 ELF 节头,将每个 PT_LOAD 段复制到对应的 guest 物理地址:

let load_result = Elf::load(&guest_mem, None, &mut kernel_file, None)?;
let kernel_entry = load_result.kernel_load; // ELF 入口点 GPA

3.6 cpuid — CPUID 模拟

位置src/cpuid/src/

x86 guest 通过 CPUID 指令查询 CPU 特性。libkrun 需要正确模拟这些响应,原因:

  1. 隐藏宿主机上不应暴露给 guest 的特性(如 SGX)
  2. 为虚拟 CPU 设置一致的特性集
  3. 设置 HYPERVISOR bit,让 guest 知道自己在 VM 中运行
// CPUID leaf 1, ECX bit 31 = 超级管理程序存在标志
fn filter_cpuid(cpuid: &mut CpuId) {
    for entry in cpuid.as_mut_slice() {
        if entry.function == 1 {
            entry.ecx |= 1 << 31; // 设置 hypervisor present bit
        }
    }
}

3.7 polly — 事件管理器

位置src/polly/src/

基于 epoll(Linux/macOS)或 Windows 事件对象的异步事件多路复用器。virtio 设备后端(如 console 的 stdin 读取、vsock 的套接字 I/O)通过向 EventManager 注册感兴趣的文件描述符,实现非阻塞 I/O。

Subscriber trait 定义了设备如何参与事件循环:

pub trait Subscriber: Send {
    fn process(&mut self, event: &EpollEvent, evmgr: &mut EventManager);
    fn interest_list(&self) -> Vec<EpollEvent>;
}

Windows 版本将 EventFd 映射到 Win32 事件对象(HANDLE),通过 WaitForMultipleObjects 实现等效的多路复用。

3.8 utils — 跨平台工具集

位置src/utils/src/

EventFd:Linux 上封装 eventfd(2) 系统调用,用于线程间信号传递。Windows 上使用 Win32 手动重置事件对象(CreateEventW),并通过全局注册表将整数 ID 映射到 HANDLE,模拟文件描述符语义:

// Windows EventFd 实现
pub fn write(&self, v: u64) -> io::Result<()> {
    let mut state = self.shared.state.lock().unwrap();
    state.value = state.value.saturating_add(v);
    unsafe { SetEvent(self.shared.event_handle); } // 通知等待者
    Ok(())
}

pub fn read(&self) -> io::Result<u64> {
    // 如果 value > 0,消耗并返回
    // 否则 WaitForSingleObject(INFINITE) 等待信号
}

epoll(Windows 适配):Windows 没有 epoll,src/utils/src/windows/epoll.rs 通过 WaitForMultipleObjects 实现等效功能,仅支持 EventFd 类型的文件描述符(通过注册表查找对应 HANDLE)。

3.9 hvf — macOS Hypervisor.framework 绑定

位置src/hvf/src/

Apple Silicon(M 系列)使用 Hypervisor.framework 作为底层虚拟化 API,提供与 KVM 类似的能力但接口完全不同。hvf crate 提供 Rust 安全封装。

3.10 rutabaga_gfx — GPU 虚拟化

位置src/rutabaga_gfx/

虚拟 GPU 的核心库,支持两种后端:

  • Venus:Vulkan-over-virtio,将 guest Vulkan API 调用转发给宿主 GPU
  • Native Context:直接将宿主 GPU 上下文暴露给 guest,用于游戏(muvm)场景

这是 libkrun 中最复杂的可选组件,仅在 Linux 和 macOS 上支持。

3.11 smbios — SMBIOS 表构建

位置src/smbios/

SMBIOS(System Management BIOS)是固件向操作系统报告硬件信息的标准。libkrun 生成最小化的 SMBIOS 3.0 表,让 guest 内核能够正确识别自己运行在虚拟机中,并获取基本的"硬件"描述。


四、libkrunfw:内核即动态库

4.1 设计哲学

libkrunfw 解决了一个根本性问题:libkrun 需要一个内核,但不能假设磁盘上有内核文件。解决方案是把内核变成动态库的一部分。

libkrunfw.so.5 内部结构:
┌──────────────────────────────────┐
│  ELF 头 + 动态链接信息           │
├──────────────────────────────────┤
│  .text 段:krunfw_get_kernel()   │ ← 返回内核指针的函数
│             krunfw_get_initrd()  │
├──────────────────────────────────┤
│  .data 段:vmlinux 二进制        │ ← 完整 Linux 内核镜像(约 20MB)
│            initrd.img            │ ← 极小的 initrd(TEE 变体)
└──────────────────────────────────┘

当操作系统的动态链接器加载 libkrunfw.so.5 时,内核镜像直接 mmap 到进程地址空间。libkrun 拿到指针,通过 GuestMemoryMmap::write() 把这段内存复制到 guest 物理内存——零文件 I/O,零磁盘延迟。

4.2 TSI 内核补丁

libkrunfw 中的内核包含多个关键补丁:

TSI 补丁修改了 Linux socket 系统调用路径。当 guest 进程调用 connect()bind()sendto() 等 socket 系统调用时,TSI 内核代码检查目标地址族(AF_INET、AF_INET6、AF_UNIX),如果匹配,将请求序列化为 TSI 协议消息,通过 /dev/vsock 发送给 VMM 侧的 TSI 代理,再由代理在宿主机上执行真正的 socket 操作:

Guest: connect(fd, {AF_INET, 8.8.8.8, 53}, len)
    ↓ TSI 内核拦截
vsock 消息: {op: CONNECT, addr: 8.8.8.8:53}
    ↓ VMM TSI 代理接收
host: real_connect(8.8.8.8:53)
    ↓ 返回结果
vsock 消息: {op: CONNECT_REPLY, errno: 0}
    ↓ TSI 内核将结果返回给 guest 进程
Guest: connect() 返回 0(成功)

从 guest 进程的视角,它完全感知不到这个代理过程——透明性是 TSI 的核心价值。

4.3 多变体体系

变体功能内核特殊配置
标准版通用虚拟化TSI + 最小化
SEV 版AMD 内存加密SEV/SEV-ES/SEV-SNP 支持
TDX 版Intel 可信域TDX 客户机支持,单 vCPU,≤3072MB
EFI 版UEFI 引导捆绑 OVMF/EDK2 固件(仅 macOS)

五、Windows WHPX 后端:从零到 Linux 启动

Windows WHPX 后端是 libkrun 最晚实现的平台支持,也是工程挑战最大的部分。本节详细记录我们在实现过程中解决的每一个关键问题。

5.1 WHPX API 概述

Windows Hypervisor Platform(WHPX)是 Hyper-V 的用户态接口,自 Windows 10 2004 起通过 WinHvPlatform.dll 提供。与 KVM(内核 ioctl)不同,WHPX 是纯用户态 API,调用层次更高,抽象程度更强。

应用程序(libkrun)
    ↓ C API
WinHvPlatform.dll(用户态)
    ↓ IOCTL
hvax64.sys(Hyper-V 组件)
    ↓ VMX/SVM
硬件(Intel VT-x / AMD-V)

核心 WHPX 对象:

  • Partition:对应一个 VM,包含 guest 内存映射和 vCPU 集合
  • vCPU:虚拟处理器,有自己的寄存器状态
  • GPA Range:Guest Physical Address 范围,映射到 host 虚拟内存

5.2 WHV_REGISTER_VALUE 初始化陷阱

实现过程中遇到的第一个严重 bug:ACCESS_VIOLATION(访问违规崩溃)。

根本原因WHV_REGISTER_VALUE 是一个 16 字节的 union,包含各种寄存器类型(Reg64FpReg128 等)。Rust 的 WHV_REGISTER_VALUE { Reg64: val } 写法只初始化低 8 字节,高 8 字节是未定义内存。WHvSetVirtualProcessorRegisters 读取完整的 16 字节,垃圾数据导致 WHPX 内部状态损坏。

修复

// 错误写法(高 8 字节未初始化)
let value = WHV_REGISTER_VALUE { Reg64: rip_value };

// 正确写法
let mut value: WHV_REGISTER_VALUE = unsafe { std::mem::zeroed() };
unsafe { value.Reg64 = rip_value; }

5.3 IO 指令模拟:InstructionByteCount = 0 问题

WHPX 处理 IO 端口访问(OUT/IN 指令)时,有时将 InstructionByteCount 设置为 0,表示需要软件模拟。在这种模式下,直接通过 WHvSetVirtualProcessorRegisters 修改 RIP(指令指针)会被 WHPX 静默忽略,导致 vCPU 永远重新执行同一条指令,陷入无限循环。

修复:使用 WHvEmulatorTryIoEmulation。WHPX 提供了一套软件仿真器 API,专门处理这种情况:

// 对于 InstructionByteCount == 0 的 IO 退出,使用仿真器
unsafe {
    WHvEmulatorTryIoEmulation(
        self.emulator,
        context_ptr,           // 传递给回调的上下文
        &exit_context.VpContext,
        &io_port_context,
    )?;
}
// 仿真器通过回调读取指令字节、执行 IO、并更新 RIP
// 这个路径下 WHvSetVirtualProcessorRegisters(RIP) 会被正确执行

仿真器需要 5 个回调函数(IO 处理、内存读写、寄存器读写),通过 WHvEmulatorCreateEmulator 注册。

5.4 中断注入与 HLT 空闲循环

Linux 内核在没有任务可运行时执行 HLT 指令让 CPU 睡眠,等待中断唤醒。WHPX 将 HLT 作为 VM exit 返回给 VMM。

问题在于:从 WHPX 的角度,vCPU 已经回到用户态(WHvRunVirtualProcessor 已返回)。此时即使设备线程调用 WHvRequestInterrupt 注入中断,WHPX 也无法自动"唤醒"这个 vCPU——因为它根本不在运行。

解决方案:共享的 EventFd + 超时等待:

// builder.rs:WhpxIrqChip
fn set_irq(&self, irq_line: Option<u32>, ...) {
    // 1. 通过 WHPX 虚拟 APIC 注入中断
    WHvRequestInterrupt(self.partition, &interrupt, size)?;
    // 2. 通知 vCPU 线程退出 HLT 等待,重新进入 WHvRunVirtualProcessor
    let _ = self.irq_pending_evt.write(1);
}

// vstate.rs:vCPU 线程
Ok(VcpuEmulation::Halted) => {
    if let Some(ref evt) = self.irq_pending_evt {
        // 等待最多 5ms,减少忙等 CPU 开销
        evt.wait_timeout(5);
        // 重新进入 WHvRunVirtualProcessor,WHPX 会投递虚拟 APIC 中的中断
        continue;
    }
}

EventFd::wait_timeout() 使用 WaitForSingleObject(handle, 5) 实现,既避免了忙等浪费 CPU,又保证了中断延迟不超过 5ms。

5.5 PIT 8254 与 TSC 校准

Linux 内核在早期启动阶段用 PIT 8254 定时器校准 TSC 时钟频率。校准流程:

  1. 对 PIT counter 2(端口 0x42)编程
  2. 读取计数器值,同时记录 TSC 周期数
  3. 根据 PIT 的已知频率(1.193182 MHz)计算 TSC 频率

如果 PIT 端口(0x40-0x43)没有设备响应,校准失败:

[0.000000] tsc: Fast TSC calibration failed
[0.000000] [Firmware Bug]: TSC doesn't count with P0 frequency!
← 内核在此挂起,等待 IRQ 0(PIT 时钟中断)驱动 jiffies 前进

实现src/devices/src/legacy/x86_64/pit.rs

PIT 模拟基于墙钟时间(wall clock)计算计数器值:

fn current_count(&self) -> u16 {
    let reload = if self.reload == 0 { 65536u64 } else { self.reload as u64 };
    let ticks = self.start.elapsed().as_micros() as u64 * PIT_CLOCK_HZ / 1_000_000;
    ((reload - (ticks % reload)) & 0xffff) as u16
}

同时,在 attach_legacy_devices() 中启动后台定时器线程,以 100 Hz 频率注入 IRQ 0:

std::thread::Builder::new()
    .name("pit-timer".into())
    .spawn(move || {
        loop {
            std::thread::sleep(Duration::from_millis(10)); // 100 Hz
            intc.lock().unwrap().set_irq(Some(0), None)?;
        }
    })?;

5.6 8259A PIC stub

Linux 内核在切换到 APIC 模式之前会初始化 8259A PIC(端口 0x20-0x21 主 PIC,0xA0-0xA1 从 PIC)。没有对应设备会导致内核的 ICW(初始化控制字)写入被忽略,某些内核版本会在此出现异常行为。

实现非常简单(src/devices/src/legacy/x86_64/pic.rs):

impl BusDevice for Pic {
    fn read(&mut self, _: u64, offset: u64, data: &mut [u8]) {
        // 返回 0(无中断等待)
        if offset < 2 { data[0] = self.regs[offset as usize]; }
    }
    fn write(&mut self, _: u64, offset: u64, data: &[u8]) {
        // 静默吸收所有 ICW/OCW 初始化写入
        if offset < 2 { self.regs[offset as usize] = data[0]; }
    }
}

5.7 APIC Trap 处理

WHPX 在某些配置下会将 APIC 寄存器写操作作为 VM exit 上报(WHvRunVpExitReasonX64ApicWriteTrap)。这些 exit 是通知性的——虚拟 APIC 已经完成了写操作,VMM 无需额外处理。

原始实现将这些 exit 处理为致命错误导致 VM 停止。修复后改为 no-op:

reason if reason == WHvRunVpExitReasonX64ApicWriteTrap => {
    // 虚拟 APIC 已处理写操作,VMM 无需介入,继续执行
}

5.8 ACPI 关机

Linux poweroff 命令通过向 PM1a_CNT 寄存器(端口 0x604)写入特定值触发 ACPI 关机。检测到 SLP_EN bit(bit 13)后返回 VcpuEmulation::Stopped

VcpuExit::IoPortWrite(port, data) => {
    // ...
    let acpi_shutdown = port == 0x604
        && data.len() >= 2
        && (u16::from_le_bytes([data[0], data[1]]) & 0x2000) != 0;
    if let Ok(()) = self.whpx_vcpu.complete_io_write() {
        if acpi_shutdown {
            info!("Guest requested ACPI shutdown");
            return VcpuEmulation::Stopped; // 干净退出
        }
    }
}

5.9 Windows 上的 virtio-fs(virtiofs)

virtiofs 在 Windows 上的挑战是将 FUSE 协议操作映射到 Win32 文件 API。主要难点:

  1. 符号链接:Windows 符号链接需要管理员权限或开发者模式,且有 FILE_FLAG_OPEN_REPARSE_POINT 等特殊标志
  2. 文件同步FUSE_FSYNC 映射到 FlushFileBuffers()
  3. 目录遍历FindFirstFileW/FindNextFileW 替代 readdir
  4. 权限模型:Windows ACL 与 Unix 权限位之间的映射
  5. 磁盘空间GetDiskFreeSpaceExW 替代 statvfs

完整实现(src/devices/src/virtio/fs/windows/passthrough.rs,约 2,500 行)支持所有核心 FUSE 操作,包括读写、目录操作、符号链接、fsync、fallocate(稀疏文件)。

5.10 Windows 上的 TSI

TSI 的 Windows 实现分 5 个阶段(约 2,100 行代码):

  • Phase 1socket_wrapper.rs——Windows Socket API(WSA)的 Rust 抽象,处理 WSAStartup/WSACleanup 生命周期
  • Phase 2stream_proxy.rs——TCP STREAM 代理,处理 connect/accept/send/recv 操作
  • Phase 3dgram_proxy.rs——UDP DGRAM 代理,处理 sendto/recvfrom
  • Phase 4pipe_proxy.rs——Named Pipe 代理,作为 AF_UNIX 的替代(Windows 不支持 Unix 套接字)
  • Phase 5muxer.rs 集成——将上述代理集成到 vsock 多路复用器中

信用流控(credit-based flow control)防止 guest 发送过快导致 VMM 缓冲区溢出:

struct ConnectionState {
    tx_credit: u32,      // 可发送字节数(guest→host)
    rx_credit: u32,      // 可接收字节数(host→guest)
}
// guest 每发送 N 字节,从 tx_credit 减去 N
// host 处理完后发送 CREDIT_UPDATE 消息补充 credit

5.11 端到端验证

所有组件就绪后,Linux 5.10 内核成功在 WHPX 上启动到用户空间。验证测试(test_whpx_real_kernel_e2e):

  1. 加载 Linux 5.10 vmlinux ELF(20.3 MB)到 256MB guest 内存
  2. 配置 x86_64 引导状态(零页 + 长模式寄存器)
  3. 内核命令行:earlycon=uart8250,io,0x3f8 将早期日志重定向到 COM1
  4. COM1 捕获设备记录所有 guest 输出
  5. 断言 90 秒内出现 "Linux version" banner
测试输出:
test windows::vstate::tests::test_whpx_real_kernel_e2e ... ok
finished in 90.02s

Linux 内核在 WHPX 上成功启动,打印版本信息。


六、当前状态与展望

6.1 功能完整度

功能Linux/macOSWindows
硬件虚拟化✅ KVM/HVF✅ WHPX
virtio-console
virtio-block
virtio-fs
virtio-net✅(TcpStream 后端)
virtio-vsock + TSI✅(TCP/UDP/Named Pipe)
virtio-balloon✅(free-page + page-hinting)
virtio-rng✅(BCryptGenRandom)
virtio-snd⚠️(NullBackend)
virtio-gpu
中断注入
Linux 启动验证✅(5.10 已验证)

6.2 Roadmap

近期

  • Windows 多 vCPU(SMP 引导协议 INIT/SIPI)
  • libkrunfw-windows(内置内核,消除用户手动提供内核的需求)
  • ACPI 表生成(MADT/FADT),改善内核 APIC 初始化兼容性

中期

  • Windows virtio-gpu(WGPU 或 D3D12 后端)
  • Windows virtio-snd(WASAPI 后端)
  • Windows ARM64(等待 Microsoft 开放 WHPX ARM64 分区类型)

长期

  • SEV-SNP 热迁移
  • 机密容器(Confidential Container)深度集成

七、总结

libkrun 的优雅之处在于它的分层设计:每一层只做好一件事,层与层之间通过清晰的接口解耦。

  • libkrun(API 层)屏蔽所有平台差异,给上层一个统一的 C API
  • vmm(VMM 核心)管理 VM 生命周期,对不同 hypervisor 后端提供统一抽象
  • devices(设备层)实现所有 virtio 设备,通过 BusDevice trait 插入 IO/MMIO 总线
  • arch(架构层)封装 x86_64/aarch64/RISC-V 的引导差异
  • 各 hypervisor 后端(KVM/HVF/WHPX)只需实现 vCPU 运行和内存映射

Windows 后端的实现,特别是 PIT/PIC 模拟、中断注入架构、TSI 的 Windows 适配,证明了这套分层设计的延展性——在不修改任何上层接口的情况下,添加了全新的平台支持,并通过 Linux 5.10 的端到端启动测试验证了功能正确性。