将 Hypervisor 作为 Library 使用
Hypervisor as a Library
2025-05-20
在我们深入探讨这个话题之前,先介绍一下我的新朋友 catsay
,这是一个简单的 Go 程序,可以读取标准输入并像猫一样说话:
可爱!... 但这不是我想讨论的重点。这张截图真正令人兴奋的是,它是在 Starina operating system 上运行的 Linux 轻量级虚拟机!
也就是说,这篇文章不是关于编写 Hypervisor 有多么困难(参见 我之前的文章)。事实上,它并没有那么难。最难的部分是设计你如何与 Hypervisor 交互。换句话说,设计 Hypervisor API。Starina 需要与 Linux 进行有吸引力的集成。
在这篇文章中,我将分享一种设计模式:Hypervisor as a Library。
今天我们如何在 Linux 中运行 Linux 应用?
首先,如果你要编写一个使用 catsay
的 Rust 应用程序,你该如何集成?
在 Rust 中,你会使用 std::process::Command
(https://doc.rust-lang.org/std/process/struct.Command.html) 来运行 catsay
:
Command::new("/bin/catsay")
.stdin(stdin)
.spawn()
如果想传递环境变量,只需添加一行:
Command::new("/bin/catsay")
.stdin(stdin)
.env("CATSAY_MODE", "dog")
.spawn()
很好。如果你对它的输出感兴趣,添加另一个参数 .stdout
:
let child = Command::new("/bin/catsay")
.stdin(stdin)
.env("CATSAY_MODE", "dog")
.stdout(Stdio::piped())
.spawn()
这非常明显,对吧?然而,在另一个操作系统(如 Starina)上,如何提供 Linux 环境或 Linux 兼容性并不明显。
Linux 兼容性
提供 Linux 兼容性是一项具有挑战性的任务,有很多方法可以实现它。一种流行的方法是将 Linux 二进制文件作为一种 in native 方式运行:hook 系统调用并模拟它们。Windows Subsystem for Linux (WSL 1) 和 FreeBSD Linuxulator 就是例子。
在 Starina 中,我采取了一种不同的方法:在轻量级虚拟机中运行真正的 Linux kernel。
这听起来可能很极端,但 WSL2 已经证明了它的实用性。在云计算环境中,它有一个巨大的缺点,即在廉价的基于 VM 的实例中,硬件辅助虚拟化不可用(或速度很慢),但它对 Starina 仍然很有吸引力:
- Linux 和 Starina 之间的接口更清晰:Virtio virtual devices。
- 与 FreeBSD 不同,Starina 与 Linux 没有那么相似。特别是,如果没有向 microkernel 添加专门的逻辑,我没有一个好主意来有效地实现
fork(2)
。 - 它是真正的 Linux kernel。无需逐个实现 Linux 系统调用,也无需发现每个鲜为人知的功能,例如 auxiliary vector。
std::process::Command
用于 Linux VM
所以,在轻量级 VM 中运行真正的 Linux kernel 在技术上听起来是可行的。但是,我们如何真正使用它呢?我的答案是,遵循 std::process::Command
模式!
这是你在 Starina 中如何运行 catsay
的方法:
use core::str::from_utf8;
use starina::prelude::*;
use starina_linux::BufferedStdin;
use starina_linux::BufferedStdout;
pub fn catsay(text: &str) {
let stdin = BufferedStdin::new(text);
let stdout = BufferedStdout::new();
starina_linux::Command::new("/bin/catsay")
.stdin(stdin)
.stdout(stdout.clone())
.spawn()
.expect("failed to execute process");
info!("{}", from_utf8(&stdout.buffer()).unwrap());
}
看起来是不是很眼熟?
Hypervisor 如何工作
在底层,starina_linux::Command
使用 Starina 的 HvSpace
(guest-physical address space) 和 vCPU
(virtual CPU) API 创建一个轻量级 VM。
这是一个简化版本的 starina_linux::Command
:
let mut virtio_fs = VirtioFs::new();
virtio_fs.add_file("/stdin", stdin);
virtio_fs.add_file("/stdout", stdout);
let ram = Folio::allocate(MEMORY_SIZE); // folio = a memory region
install_device_tree(&mut ram);
extract_linux_image(&mut ram, include_bytes!("linux.bin"));
let hvspace = HvSpace::new();
let vcpu = Vcpu::new(&hvspace);
loop {
let exit = vcpu.run();
match exit {
Reboot => {
break;
}
PageFault { gpaddr, data } if gpaddr.is_in_mmio() => {
virtio_fs.handle_mmio_access(gpaddr, data);
}
_ => {
panic!("unexpected VM exit reason: {:?}", exit);
}
}
}
简而言之,创建一个 guest-physical address space,将 Linux kernel 安装到其中,进入 VM 世界,并处理 MMIO accesses。
是不是出奇的简单?Linux KVM API 也以类似的方式工作。Hypervisor 通常是一个非常简单的程序(否则我们不会信任它作为安全边界)。
你可能会注意到一个神秘的 virtio_fs
对象。顾名思义,它是一个虚拟文件系统,用于提供与 Starina 的无缝集成。在 Linux 中,你提供的标准输入可以作为 /virtfs/stdin
访问,并且我们的自定义 init 将其连接到 catsay
。
Hypervisor as a Library
Hypervisor,更具体地说,虚拟机监视器(VMM),例如 QEMU 和 Firecracker,通常用作一个单独的进程。你的控制平面程序通过 IPC 机制与它们通信。拥有进程隔离对安全有好处,但它限制了集成的灵活性和性能。
Starina 采取了一种不同的方法:starina_linux::Command
Hypervisor 作为 library 提供。这听起来很奇怪,但使用起来非常令人满意:
struct YourVirtualFile;
impl FileLike for YourVirtualFile {
// Similar to std::io::Read, but with an offset and
// a zero-copy writer.
fn read_at(
&self,
offset: usize,
size: usize,
completer: ReadCompleter,
) -> ReadResult {
// This writes into the virtio's buffer directly!
completer.complete(b"Hello, world!")
}
}
starina_linux::Command::new("/bin/catsay")
.stdin(Arc::new(YourVirtualFile))
.spawn();
在此代码段中,YourVirtualFile
是一个类似文件的对象,用作 catsay
的标准输入。它看起来是否类似于 std::io::Read
trait?
由于 Hypervisor-as-a-Library 的设计,你可以透明地将 Rust 对象(YourVirtualFile
)传递给 catsay
,更重要的是,你可以安全地直接写入 guest memory!
将 Linux VM 嵌入到应用程序中是个好主意吗?
虚拟机通常被认为是缓慢和繁重的,但它们不一定是这样。guest Linux 在大多数情况下在没有 VMM 干预的情况下本地运行。VMM 的工作是模拟几个 virtio devices,而且它仍然非常简单:virtio 只是一个带有中断的命令队列。
Starina 的 Linux image 暂时嵌入到每个应用程序中,但只有 6.7 MB(64-bit RISC-V)。所需的最小内存空间约为 32MB。在 QEMU 软件模拟中需要 2 秒才能启动。
公平地说,我还没有投入时间来优化它。我很确定它可以优化到小于 100 毫秒(在 QEMU 中),甚至小于 10 毫秒(通过 VM snapshotting,同样在 QEMU 的软件模拟中!)。
你可能想知道为什么它不跨多个应用程序共享单个 VM 实例。这对我来说当然是有意义的,但是 Hypervisor-as-a-Library 有可能比本地 Linux 环境更快地启动你的 Linux 应用程序(通过 VM snapshotting),与 Checkpoint/Restore In Userspace 相比,这将更容易开发,这要归功于清晰的 VMM 接口。
我还没有实现低延迟(名言),但我对其可行性非常乐观。所以答案是,是的,我认为这是个好主意!
接下来是什么?
starina_linux::Command
是一个非常简单的 API,用于在 Starina 中提供 Linux 兼容性。但是,它仍然不完整。它缺少网络(virtio-net)、持久存储等等。
我的梦想是在 Starina 上拥有更像容器的体验。它可能是这样的:
starina_linux::Command::new("node")
.arg("/app/server.js")
.env("NODE_ENV", "production")
// What if Starina supports the container images out of the box?
.image("docker.io/library/node:24-alpine")
// Use VM state caching to start faster (if it's safe to do so).
.snapshot(true)
// Pass a channel connected to Starina FS server.
.mount("/app", dir_ch)
// Export a TCP port to the Starina TCP/IP server.
.expose(Export {
host: tcp_listen_ch,
guest: 80,
})
.spawn();
Linux 兼容性不仅意味着运行 Linux 二进制文件。它也意味着它解锁了 Starina 上的 Linux device drivers,这要归功于它的 microkernel 设计。我还没有研究过它,但这是一个非常有趣的想法。
如果你有兴趣开发一个新的 microkernel 操作系统,请查看 GitHub。请随时提问或分享你的想法 :)
— Written by Seiya NutaCC BY 4.0