从一个动态库到完整的跨平台 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() 函数按严格顺序执行每个阶段:
- 分配 guest 物理内存(
GuestMemoryMmap,基于mmap或 Windows 上的VirtualAlloc) - 创建 Hypervisor 分区(KVM fd / WHPX partition / HVF partition)
- 加载内核(ELF loader 解析节头,写入 guest 内存)
- 配置引导参数(x86_64 零页,包含内存图、cmdline 指针、initrd 位置)
- 注册 IO/MMIO 总线设备
- 创建并启动 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_LOOKUP、FUSE_READ、FUSE_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) |
| i8042 | 0x60-0x64 | 键盘/鼠标控制器 |
| CMOS (RTC) | 0x70-0x77 | 实时时钟,内存大小存储 |
| PIT 8254 | 0x40-0x43 | 可编程定时器,IRQ 0 时钟源 |
| PIC 8259A | 0x20-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 位置 - cmdline(
CMDLINE_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/
支持多种内核格式:
| 格式 | 常量 | 使用场景 |
|---|---|---|
| Raw | KRUN_KERNEL_FORMAT_RAW | libkrunfw 提供的内存映射内核 |
| ELF | KRUN_KERNEL_FORMAT_ELF | 调试用 vmlinux(未压缩) |
| PeGz | KRUN_KERNEL_FORMAT_PEGZ | Windows/UEFI 内核(PE 格式+gzip) |
| ImageBz2 | KRUN_KERNEL_FORMAT_IMAGE_BZ2 | ARM64 常用压缩格式 |
| ImageGz | KRUN_KERNEL_FORMAT_IMAGE_GZ | x86_64 常见压缩格式 |
| ImageZstd | KRUN_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 需要正确模拟这些响应,原因:
- 隐藏宿主机上不应暴露给 guest 的特性(如 SGX)
- 为虚拟 CPU 设置一致的特性集
- 设置
HYPERVISORbit,让 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,包含各种寄存器类型(Reg64、Fp、Reg128 等)。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 时钟频率。校准流程:
- 对 PIT counter 2(端口 0x42)编程
- 读取计数器值,同时记录 TSC 周期数
- 根据 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。主要难点:
- 符号链接:Windows 符号链接需要管理员权限或开发者模式,且有
FILE_FLAG_OPEN_REPARSE_POINT等特殊标志 - 文件同步:
FUSE_FSYNC映射到FlushFileBuffers() - 目录遍历:
FindFirstFileW/FindNextFileW替代readdir - 权限模型:Windows ACL 与 Unix 权限位之间的映射
- 磁盘空间:
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 1:
socket_wrapper.rs——Windows Socket API(WSA)的 Rust 抽象,处理WSAStartup/WSACleanup生命周期 - Phase 2:
stream_proxy.rs——TCP STREAM 代理,处理 connect/accept/send/recv 操作 - Phase 3:
dgram_proxy.rs——UDP DGRAM 代理,处理 sendto/recvfrom - Phase 4:
pipe_proxy.rs——Named Pipe 代理,作为 AF_UNIX 的替代(Windows 不支持 Unix 套接字) - Phase 5:
muxer.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):
- 加载 Linux 5.10 vmlinux ELF(20.3 MB)到 256MB guest 内存
- 配置 x86_64 引导状态(零页 + 长模式寄存器)
- 内核命令行:
earlycon=uart8250,io,0x3f8将早期日志重定向到 COM1 - COM1 捕获设备记录所有 guest 输出
- 断言 90 秒内出现
"Linux version"banner
测试输出:
test windows::vstate::tests::test_whpx_real_kernel_e2e ... ok
finished in 90.02s
Linux 内核在 WHPX 上成功启动,打印版本信息。
六、当前状态与展望
6.1 功能完整度
| 功能 | Linux/macOS | Windows |
|---|---|---|
| 硬件虚拟化 | ✅ 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 APIvmm(VMM 核心)管理 VM 生命周期,对不同 hypervisor 后端提供统一抽象devices(设备层)实现所有 virtio 设备,通过BusDevicetrait 插入 IO/MMIO 总线arch(架构层)封装 x86_64/aarch64/RISC-V 的引导差异- 各 hypervisor 后端(KVM/HVF/WHPX)只需实现 vCPU 运行和内存映射
Windows 后端的实现,特别是 PIT/PIC 模拟、中断注入架构、TSI 的 Windows 适配,证明了这套分层设计的延展性——在不修改任何上层接口的情况下,添加了全新的平台支持,并通过 Linux 5.10 的端到端启动测试验证了功能正确性。