在浏览器标签页里跑 Linux 内核:BrowserPod 的 WebAssembly 架构拆解

2026-05-22 23 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:12 分钟

Leaning Tech Labs 最近公开了 BrowserPod 的完整技术方案——把一个为 WebAssembly 定制的类 Linux 内核塞进浏览器标签页,多个 Linux 应用可以同时跑在里面,不需要远程服务器,不需要虚拟机镜像下载,打开网页就能用。

这听起来像玩具,但底层架构的选择值得认真看。它解决的核心矛盾是:浏览器只有 JavaScript 一个运行时,而 Linux 应用生态依赖的是 POSIX 系统调用和进程抽象。BrowserPod 用 WebAssembly 把这两端接上了。

从 JavaScript 单线程到 WASM 多进程

传统浏览器环境的限制不只是"不能跑 C",而是根本缺少进程、文件系统、信号这些操作系统概念。WebAssembly 本身也只是一个指令集,它不自带系统调用。

BrowserPod 的做法是:自己写一个内核。这个内核不是把 Linux 源码编译成 Wasm,而是针对 Wasm 的执行模型重新设计了进程调度、内存隔离和 I/O 投射。每个 Linux 应用编译成独立的 Wasm 模块,内核模块负责加载、调度和管理它们之间的通信。

关键设计决策有几个:

  • 进程即 Wasm 模块实例:每个应用是独立的模块实例,有自己的线性内存,内核通过 Wasm 的结构化机制保证隔离,而不是靠硬件 MMU。
  • 系统调用通过宿主函数(host function)桥接:应用 Wasm 模块导入内核提供的函数(类似 readwritefork 的语义),内核模块再通过 WASI 或自定义 ABI 与浏览器 API 对接。
  • 文件系统投射到浏览器存储:内核把 POSIX 文件操作映射到 IndexedDB 或 OPFS(Origin Private File System),不是在内存里模拟一个 tmpfs 就完事。

这意味着 BrowserPod 不是"浏览器里的终端模拟器连远程 SSH",而是真正的本地执行——计算发生在你的机器上,只是运行时从 x86 换成了 Wasm。

内核架构:不是移植,是重写

如果只是把 Linux 内核源码喂给 Emscripten 编译,产物会巨大且慢,因为 Linux 内核里有大量硬件驱动、中断控制器、MMU 管理等代码,在浏览器里毫无用处。BrowserPod 的内核是从 Wasm 的约束出发倒推设计的:

  • 调度器:Wasm 没有硬件中断概念,调度器基于协作式多任务 + 浏览器事件循环的抢占点实现。内核模块在每次宿主调用返回时检查是否需要切换进程。
  • 内存管理:Wasm 线性内存是连续字节数组,没有页表。内核用自己实现的 slab 分配器管理堆,进程间共享内存通过 Wasm 的共享线性内存提案(SharedArrayBuffer)实现。
  • 信号与管道:用 Wasm 模块间的直接函数调用模拟信号投递,管道用内核模块内部的环形缓冲区实现,不经过浏览器消息通道。

这套设计的结果是:内核本身很轻,启动快,但代价是兼容性——不是所有 POSIX 语义都能完美映射。比如 fork 的语义在 Wasm 里无法直接实现(没有地址空间的硬件级复制),BrowserPod 用一种"进程快照 + 重新初始化"的方式近似它,行为上类似 vfork 加 exec 的组合。

实际能跑什么

根据公开信息,BrowserPod 的目标场景不是跑完整 Debian 桌面,而是跑开发工具链和命令行应用:gcc、Python 解释器、Node.js、shell 工具、甚至轻量级数据库。这些应用 I/O 密集度低、计算路径可控,适合 Wasm 的执行模型。

下面用一个具体例子展示"把 C 程序编译成 Wasm 并通过 WASI 运行"的基础流程——这正是 BrowserPod 内核加载应用的方式的简化版。

动手试:编译一个 C 程序到 Wasm 并在浏览器沙箱里执行

BrowserPod 内核加载应用的底层机制,本质上就是"把 Linux 应用编译成 Wasm 模块,内核提供系统调用宿主函数"。我们可以用 WASI SDK 复现这个流程的最小版本。

1. 安装 WASI SDK 并编译 C 程序

# 下载 WASI SDK(以 Linux 为例)
curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz
tar xzf wasi-sdk-24.0-x86_64-linux.tar.gz

# 写一个简单的 C 程序
cat > hello.c << 'EOF'
#include <stdio.h>
#include <unistd.h>
#include <time.h>

int main() {
    time_t now = time(NULL);
    printf("Hello from WASI! Current time: %ld\n", now);
    // 模拟一个简单的文件操作
    FILE *f = fopen("/tmp/wasi-test.txt", "w");
    if (f) {
        fprintf(f, "Written by a Wasm module at %ld\n", now);
        fclose(f);
        printf("File written successfully.\n");
    } else {
        printf("Failed to open file.\n");
    }
    return 0;
}
EOF

# 用 WASI SDK 编译——产出的是独立的 Wasm 模块,自带 WASI 系统调用导入
WASI_SDK_PATH=./wasi-sdk-24.0
${WASI_SDK_PATH}/bin/clang \
    --target=wasm32-wasi \
    --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot \
    -O2 \
    -o hello.wasm \
    hello.c

# 验证产物
wasm-objdump -x hello.wasm | head -30

编译完成后 hello.wasm 是一个标准的 Wasm 模块,它导入了 wasi_snapshot_preview1 里的函数(fd_writepath_openclock_time_get 等)——这些就是"系统调用",需要宿主环境提供实现。

2. 在 Node.js 里用 WASI 运行这个模块

// run-wasi.js — 用 Node.js 的 WASI 实现运行 hello.wasm
const { WASI } = require('wasi');
const fs = require('fs');
const path = require('path');

async function run() {
  const wasmBytes = fs.readFileSync('./hello.wasm');
  const wasi = new WASI({
    version: 'snapshot_preview1',
    preopens: {
      '/tmp': '/tmp'  // 把宿主 /tmp 映射到 Wasm 的 /tmp
    },
    env: {},
    args: ['hello.wasm']
  });

  const { instance } = await WebAssembly.instantiate(wasmBytes, {
    wasi_snapshot_preview1: wasi.wasiImport
  });

  wasi.start(instance);
}

run().catch(console.error);
node run-wasi.js
# 输出:
# Hello from WASI! Current time: 1723456789
# File written successfully.

cat /tmp/wasi-test.txt
# Written by a Wasm module at 1723456789

这个例子展示了 BrowserPod 内核做的事情的简化版:应用编译成 Wasm 模块,内核(这里是 Node.js 的 WASI 实现)提供系统调用的宿主函数。BrowserPod 的内核比 WASI 更复杂——它要管理多个模块实例的调度和隔离,而 WASI 只服务单个模块。

3. 在浏览器里跑同样的模块

<!-- browser-wasi.html — 纯浏览器环境运行 hello.wasm -->
<!DOCTYPE html>
<html>
<body>
<pre id="output"></pre>
<script type="module">
import { WASI } from 'https://cdn.jsdelivr.net/npm/@bjorn3/wasi-browser@0.1.0/+esm';

const outputEl = document.getElementById('output');

async function run() {
  const resp = await fetch('./hello.wasm');
  const wasmBytes = await resp.arrayBuffer();

  // 浏览器里没有真正的文件系统,用 OPFS 或内存映射
  const wasi = new WASI({
    stdout: { write: (buf) => { outputEl.textContent += new TextDecoder().decode(buf); } },
    stderr: { write: (buf) => { outputEl.textContent += new TextDecoder().decode(buf); } },
    preopens: {}  // 浏览器环境暂不映射真实文件系统
  });

  const { instance } = await WebAssembly.instantiate(wasmBytes, {
    wasi_snapshot_preview1: wasi.wasiImport
  });

  wasi.start(instance);
}

run();
</script>
</body>
</html>

注意:浏览器环境里 fopen("/tmp/...") 会失败,因为没有预打开的文件系统目录。BrowserPod 的内核正是要解决这个问题——它把 POSIX 文件操作映射到 OPFS 或 IndexedDB,让应用觉得自己在操作真实文件。

代价与边界

BrowserPod 的方案不是银弹,有几个硬限制需要正视:

  • 性能差距:Wasm 的计算性能接近原生,但系统调用桥接有开销。每次 read/write 都要跨越 Wasm-宿主边界,密集 I/O 场景会明显慢于原生 Linux。
  • 兼容性不完整fork、信号、epoll 等机制只能近似实现。依赖这些语义的复杂应用(如 nginx、PostgreSQL)可能无法直接运行。
  • 调试困难:Wasm 模块内部的崩溃不像原生进程有 core dump,调试依赖浏览器 DevTools 的 Wasm 支持,目前还不够成熟。
  • 安全模型变化:浏览器沙箱比 Linux 用户空间隔离更严格,但也更死板。BrowserPod 内核必须在浏览器安全策略内操作,不能绕过 CORS 或 Same-Origin。

什么时候值得关注

BrowserPod 目前还在早期阶段,但它的架构指向了几个实际场景:

  • 在线 IDE 和开发环境:不需要远程容器,本地浏览器就能跑 gcc、Python、shell。对教育场景和轻量开发特别有用。
  • 安全沙箱执行:运行不可信代码时,Wasm + 浏览器沙箱的双重隔离比 Docker 容器更硬。
  • 离线工具链:把 CLI 工具编译成 Wasm 后,离线环境下也能在浏览器里使用,不需要本地安装。

如果你现在就想实验,路线是:

  1. 用 WASI SDK 把你的 C/Rust 命令行工具编译成 Wasm。
  2. 先在 Node.js WASI 环境验证功能。
  3. 再尝试在浏览器里用 WASI polyfill 运行,观察哪些系统调用需要适配。
  4. 关注 BrowserPod 的文件系统映射和进程调度 API——当它公开后,你的 Wasm 模块就是它的"应用包"。

这不是把桌面 Linux 搬进浏览器的故事,而是用 Wasm 的约束重新定义"操作系统内核"能是什么形状。值得跟踪。


相关推荐