Hypervisor as a Library

2025-05-20

在我们深入探讨这个话题之前,先介绍一下我的新朋友 catsay,这是一个简单的 Go 程序,可以读取标准输入并像猫一样说话:

catsay

可爱!... 但这不是我想讨论的重点。这张截图真正令人兴奋的是,它是在 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 仍然很有吸引力:

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