文章目录
-
- 前言
- [1. 整体架构:Firecracker、KVM、microVM 各管什么](#1. 整体架构:Firecracker、KVM、microVM 各管什么)
-
- [1.1 三个家伙不在一个层次](#1.1 三个家伙不在一个层次)
- [1.2 创建 VM 和运行应用是两条路](#1.2 创建 VM 和运行应用是两条路)
- [1.3 Firecracker 进程内部也有分工](#1.3 Firecracker 进程内部也有分工)
- [1.4 设备清单必须标明实现归属](#1.4 设备清单必须标明实现归属)
- [2. VirtIO:驱动、设备后端、共享队列怎么配合](#2. VirtIO:驱动、设备后端、共享队列怎么配合)
-
- [2.1 半虚拟化的核心是签合同](#2.1 半虚拟化的核心是签合同)
- [2.2 Virtqueue:一张双方共用的待办清单](#2.2 Virtqueue:一张双方共用的待办清单)
- [2.3 VirtIO 省的到底是哪种切换](#2.3 VirtIO 省的到底是哪种切换)
- [2.4 ioeventfd、irqfd 与事件循环怎么衔接](#2.4 ioeventfd、irqfd 与事件循环怎么衔接)
- [3. virtio-net:网络报文怎么离开 microVM](#3. virtio-net:网络报文怎么离开 microVM)
-
- [3.1 从一次 HTTP 请求追到宿主 TAP](#3.1 从一次 HTTP 请求追到宿主 TAP)
- [3.2 收包要 Guest 先备好盘子](#3.2 收包要 Guest 先备好盘子)
- [3.3 网卡、限流、授权是三件事](#3.3 网卡、限流、授权是三件事)
- [4. virtio-blk:文件操作怎么变成宿主磁盘 I/O](#4. virtio-blk:文件操作怎么变成宿主磁盘 I/O)
-
- [4.1 Guest 里的文件怎么存进镜像](#4.1 Guest 里的文件怎么存进镜像)
- [4.2 一个块请求包含什么](#4.2 一个块请求包含什么)
- [4.3 Sync、Async、vhost-user 是后端选择题](#4.3 Sync、Async、vhost-user 是后端选择题)
- [4.4 写成功 ≠ 持久化成功](#4.4 写成功 ≠ 持久化成功)
- [5. virtio-vsock:Guest 和宿主怎么绕过 IP 通信](#5. virtio-vsock:Guest 和宿主怎么绕过 IP 通信)
-
- [5.1 它解决的是本机宿主和 Guest 的通信](#5.1 它解决的是本机宿主和 Guest 的通信)
- [5.2 两个方向怎么建立连接](#5.2 两个方向怎么建立连接)
- [5.3 vsock、VirtIO、vhost、vhost-user 别搞混](#5.3 vsock、VirtIO、vhost、vhost-user 别搞混)
- [5.4 没有 IP 网络,照样有信任边界](#5.4 没有 IP 网络,照样有信任边界)
- [6. PIT:谁负责在指定时刻触发中断](#6. PIT:谁负责在指定时刻触发中断)
-
- [6.1 计时器负责产生事件](#6.1 计时器负责产生事件)
- [6.2 Firecracker 请求 KVM 创建 PIT](#6.2 Firecracker 请求 KVM 创建 PIT)
- [6.3 PIT 不是唯一的闹钟](#6.3 PIT 不是唯一的闹钟)
- [7. KVM 时钟:Guest 怎么知道时间过了多久](#7. KVM 时钟:Guest 怎么知道时间过了多久)
-
- [7.1 kvm-clock 解决高效读时间的问题](#7.1 kvm-clock 解决高效读时间的问题)
- [7.2 kvm-clock、TSC、PIT 怎么区分](#7.2 kvm-clock、TSC、PIT 怎么区分)
- [7.3 暂停恢复后,时间要单独处理](#7.3 暂停恢复后,时间要单独处理)
- [8. 串口控制台:没有图形设备怎么观察和操作系统](#8. 串口控制台:没有图形设备怎么观察和操作系统)
-
- [8.1 串口把 Guest 的字符输入输出接到宿主](#8.1 串口把 Guest 的字符输入输出接到宿主)
- [8.2 串口设备、内核控制台、登录服务是三件事](#8.2 串口设备、内核控制台、登录服务是三件事)
- [8.3 控制台该承担什么角色](#8.3 控制台该承担什么角色)
- [9. i8042:有限的键盘控制器怎么参与重置与退出](#9. i8042:有限的键盘控制器怎么参与重置与退出)
-
- [9.1 为什么精简虚拟机还留键盘控制器](#9.1 为什么精简虚拟机还留键盘控制器)
- [9.2 Guest 发重置命令后发生什么](#9.2 Guest 发重置命令后发生什么)
- [9.3 宿主发 Ctrl+Alt+Del 是另一个方向](#9.3 宿主发 Ctrl+Alt+Del 是另一个方向)
- [10. 设计取舍:这些机制对执行平台意味着什么](#10. 设计取舍:这些机制对执行平台意味着什么)
-
- [10.1 Firecracker 和 QEMU + KVM 差在哪](#10.1 Firecracker 和 QEMU + KVM 差在哪)
- [10.2 从设备能力推导平台责任](#10.2 从设备能力推导平台责任)
- [10.3 把整条路径连起来](#10.3 把整条路径连起来)
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/qq_34419312
前言
microVM 可以理解成一台精简到只剩刚需的虚拟电脑。VM 是虚拟机的缩写,micro 强调它很轻------轻到什么程度呢,配置表里连个正经显卡都懒得给你写。
你在里面跑 Python、下依赖、写文件、等超时、打日志,感觉跟在自己家一样自由。其实住的是别人的房子:房东叫 KVM,中介叫 Firecracker。它背后并没有一整套模拟出来的 PC 硬件,就像外卖厨房------菜是热的,但你找不到一个正经灶台。
Firecracker 负责创建和管理这种虚拟机,KVM 是 Linux 内核提供的底层虚拟化机制。两者配合,让虚拟机里的程序用真实机器的 CPU、内存和存储。搞懂分工,才能回答两个实际问题:应用每次操作经过哪里,这条路径的性能和安全边界在哪。
约定一下称呼:跑 Firecracker 的系统叫 Host(宿主) ,虚拟机里面的系统叫 Guest(客户机)。一台 Linux 服务器里启动了另一套虚拟 Linux,外面那套是 Host,里面那套是 Guest。就这么简单。
本文先建架构,再拆网络、磁盘、虚拟机通信、计时器、时钟、串口和键盘控制器。设备地址和传统计时器部分以 x86_64 + KVM 为主。
1. 整体架构:Firecracker、KVM、microVM 各管什么
1.1 三个家伙不在一个层次
先把 Host 和 Guest 放进一个具体例子:
Linux 服务器上的系统:Host / 宿主
└─ Firecracker 创建的 microVM:一台虚拟机
└─ 虚拟机里的 Linux:Guest / 客户机系统
└─ Agent、Python、Shell 等应用
**Guest 描述的是"位于虚拟机里面"这个位置,不是一个软件名,也不是登录用户。**Guest 内核就是里面那套 Linux 的内核,Guest 应用就是里面跑的程序。别把它当"请进来的客人",它是"住里面的房客"。
内核是操作系统里管 CPU、内存和设备的核心。Host 和 Guest 各有自己的内核;普通程序跑在用户态 ,内核跑在权限更高的内核态。注意:这两种状态在 Host 和 Guest 里都存在,不能把 Guest 等同于用户态、Host 等同于内核态。就像你不能因为人家住二楼,就说楼下的人全是物业。
| 对象 | 所在位置或形态 | 核心职责 |
|---|---|---|
| KVM | 宿主 Linux 内核中的虚拟化子系统 | 提供虚拟 CPU、内存映射和中断等机制,配合 CPU 硬件虚拟化运行 Guest |
| Firecracker | 宿主机用户态的 VMM 进程 | 创建配置 VM、装载 Guest 内核、提供设备模型,协调启动、暂停、状态保存 |
| microVM | 创建出来的轻量虚拟机实例 | 拥有自己的 Guest 内核、vCPU、内存和设备,跑应用 |
vCPU 就是虚拟 CPU,VMM 是 Virtual Machine Monitor(虚拟机监控器)的缩写,设备模型就是用代码实现的设备行为。要启动一台 1 个 vCPU、512 MiB 内存的 microVM,Firecracker 负责准备内存、内核、设备,再通过 KVM 让虚拟 CPU 跑起来。一句话:Firecracker 负责组织这台机器,KVM 提供底层运行能力,microVM 是最终跑起来的实例。
它们的接口是 /dev/kvm,Firecracker 用 ioctl 向 KVM 喊话:"给我创建 VM""让 vCPU 跑起来"。系统调用是程序向内核请求服务的入口,ioctl 是其中控制设备的一种。
microVM:Guest 执行环境
应用(Agent / Python / Shell)──系统调用── Guest Linux 内核
Guest 内核 ──虚拟设备请求── Firecracker
Linux 宿主机:
Firecracker:用户态 VMM ──/dev/kvm + ioctl── KVM
KVM ──配置与使用── CPU 硬件虚拟化
Firecracker ──设备后端 I/O── 宿主资源(虚拟网口、磁盘文件、本机通信接口)
Guest 用的还是宿主 CPU 和内存,只是虚拟化让它有了独立执行环境,不用每台 microVM 都配一套物理硬件。
这也解释了运行条件:这条路径依赖 Linux 和可用的 KVM。macOS 原生提供不了这个接口;想在 Linux 虚拟机里再开虚拟机,还得确认平台开放了嵌套虚拟化------相当于在民宿里再开民宿,房东不一定同意。光有一个 Linux 用户空间或容器镜像,不够。
1.2 创建 VM 和运行应用是两条路
启动分四件事:
- 创建 VM :打开
/dev/kvm,检查能力,调用KVM_CREATE_VM。 - 准备内存和内核:告诉 KVM 哪些宿主内存当 Guest 内存,再放入 Guest 内核和启动参数。
- 准备 CPU 与设备:创建 vCPU,设置启动状态,配置中断、磁盘、网卡。
- 开始执行 :vCPU 线程调用
KVM_RUN,让 CPU 进入 Guest 执行。
KVM_CREATE_VM 不会自动带来一套完整 Linux 系统,Guest 内核、内存布局、设备、启动状态都得自己准备。房东给了你钥匙,家具还得自己买。
跑起来之后,用三个动作区分执行路径:
| Guest 中的动作 | 主要由谁处理 |
|---|---|
| 程序做加减运算 | CPU 在硬件虚拟化机制下执行 Guest 指令 |
程序调用 write() |
先进 Guest 内核;只写缓存的话暂时不碰虚拟磁盘 |
| 驱动访问需要宿主处理的设备寄存器 | 进入虚拟化处理路径,KVM 处理,必要时交给 Firecracker |
**VM exit 是 CPU 暂时离开 Guest 去处理虚拟化事件,不是虚拟机退出,更不是关机。**别一看 exit 就以为它跑路了。有些事件 KVM 处理完就能继续,有些要返回 Firecracker。
需要返回 Firecracker 的包括部分 MMIO(通过特定内存地址访问设备寄存器)和 x86 端口 I/O。普通系统调用不等于 VM exit,VM exit 也不等于调用管理 API,三个概念别焊死。
1.3 Firecracker 进程内部也有分工
一个 Firecracker 进程封装一台 microVM,主要三个线程角色:
| 角色 | 处理什么 | 与应用执行的关系 |
|---|---|---|
| API 线程 | 接收管理请求 | 配置和控制 VM,不是普通 I/O 的转发入口 |
| VMM 线程 | 事件循环、设备事件、控制操作 | 处理设备请求队列的通知、网络数据到达等 |
| vCPU 线程 | 每个 vCPU 对应一个线程,进 KVM_RUN |
执行 Guest,处理需要返回用户态的退出事件 |
事件循环说白了就是"等着,有人按门铃再去开门",不是每件设备破事都得惊动它。
1.4 设备清单必须标明实现归属
Guest 能用一项设备功能,不代表它全由 Firecracker 实现。驱动可能在 Guest 内核,设备后端可能在 Firecracker,部分机制由 KVM 提供。
| 能力 | Guest 中的使用者 | 主要实现位置 |
|---|---|---|
| virtio-net / virtio-blk / virtio-vsock | Guest 内核的对应 VirtIO 驱动 | Firecracker 提供设备模型和后端连接 |
| PIT(传统可编程间隔计时器) | Guest 计时器相关代码 | x86 路径由 Firecracker 请求 KVM 创建内核态 PIT 模型 |
| kvm-clock | Guest 中与 KVM 协作读时间的代码 | KVM 与 Guest 协作,Firecracker 配置并协调状态保存恢复 |
| 串口控制台 | Guest 串口驱动、内核日志、终端程序 | Firecracker 的串口设备模型连接宿主输入输出 |
| 部分 i8042(传统键盘控制器) | Guest 重置路径、键盘驱动 | Firecracker 实现有限命令与按键注入,用于重置通知 |
源码里还有熵设备、balloon 设备等。快照是给虚拟机拍照片,恢复是从照片里活回来,但磁盘数据这种"身外之物"得配套另管。
2. VirtIO:驱动、设备后端、共享队列怎么配合
2.1 半虚拟化的核心是签合同
模拟真实网卡很累,要模仿寄存器、模仿各种硬件脾气。VirtIO 换了个活法:Guest 驱动和设备后端直接约定好,用统一格式提交"发这段数据""读这块磁盘"。
"半虚拟化"就是驱动知道自己在用专门为虚拟化设计的接口------明牌,不装。应用完全不用改:Python 照样读文件、发网络请求,底层交互由 Guest 内核里的驱动完成。它甚至不知道自己走了专用通道,跟坐地铁直达和打车绕路,终点一样,它只在乎结果。
准确分工是这样的:
- Guest 前端驱动:在 Guest Linux 内核里,提交和回收 I/O 请求。
- 设备模型与后端:主要在 Firecracker 用户态进程,处理队列,访问宿主资源。
- 传输机制:完成设备发现、配置、队列通知和中断,比如 MMIO 或 PCI。
所以"Firecracker 实现了 virtio-net 驱动"这种说法不准确------Guest 驱动和 Firecracker 的设备端得分开说。
2.2 Virtqueue:一张双方共用的待办清单
**Virtqueue(虚拟队列)**可以理解成双方共用的一份工作清单:Guest 填任务,设备后端处理,再留完成记录。本文实现用的是 split virtqueue,就是把描述符、待办信息、完成信息分开保存:
| 结构 | 谁主要写入 | 内容 |
|---|---|---|
| Descriptor table 描述符表 | Guest 驱动 | 任务材料在哪:缓冲区地址、长度、读写方向、链中下一项编号 |
| Available ring 可用环 | Guest 驱动 | 哪些任务待处理:准备好的描述符链编号 |
| Used ring 已用环 | 设备后端 | 哪些任务已完成:处理完的编号和完成信息 |
比如 Guest 要写 4 KiB 数据,准备好"请求头 → 数据 → 状态缓冲区"这条描述符链,链头编号是 7。它把 7 放进待处理列表;Firecracker 按编号找到请求和数据,处理完把 7 记入完成列表。Guest 看到完成记录,就能回收缓冲区。
注意:清单传的是编号,数据还在缓冲区里。"环"的意思是列表写到末尾可以绕回开头接着用------像食堂打菜循环播放,永远不用翻页。
Guest VirtIO 驱动 → 准备缓冲区与描述符,发布 available 索引
→ 按协商规则发送队列通知
Firecracker 设备后端 → 取出并校验描述符链 → 提交实际 I/O
宿主 I/O 资源 → I/O 完成
Firecracker 设备后端 → 写入结果,更新 used 索引 → 按需通知 Guest
Guest VirtIO 驱动 → 回收完成的缓冲区
数据在缓冲区里,通知只负责告诉对方"该检查队列了"。一次通知可能对应多个请求,能不能省通知、怎么批量处理,要看协商的特性和队列状态。
还有一点:这份清单来自不可信的 Guest,后端必须验货------编号、地址、长度、读写方向,不能照单全收。共享的是约定的 Guest 内存,不是随便一块宿主内存。
发布顺序也很关键:**先填好任务,再宣布"任务准备好了"。**不然后端可能读到半份内容,就像外卖还没出锅你就点了送达。底层的内存屏障就是用来保证这种顺序的。
2.3 VirtIO 省的到底是哪种切换
"VirtIO 减少用户态和内核态切换所以更快"只说对了一部分。虚拟化 I/O 至少涉及三种不同边界:
| 边界 | 例子 | VirtIO 带来的影响 |
|---|---|---|
| Guest 用户态 ↔ Guest 内核态 | 应用调用 write()、send() |
普通系统调用还在,VirtIO 不自动消除 |
| Guest 执行 ↔ 宿主虚拟化处理 | 设备通知或受拦截的寄存器访问 | 共享队列、批处理、通知抑制可减少部分交互成本 |
| 宿主用户态 ↔ 宿主内核态 | Firecracker 读写宿主虚拟网口、磁盘文件、本机通信接口 | 省不省取决于后端、批处理、异步机制 |
收益来自更简单的设备交互、共享队列、批量处理和通知优化,但它不自动消除系统调用、数据拷贝或所有 VM exit。翻译成人话:减肥药有效,但别指望躺着就瘦。
2.4 ioeventfd、irqfd 与事件循环怎么衔接
Virtqueue 是工作清单,通知机制就是门铃:清单里放任务,门铃只提醒对方来查看。Linux 的 eventfd 就是能被写入和监听的通知计数器(fd 是文件描述符,跟 VirtIO 里描述缓冲区的描述符不是一个概念)。
Guest 更新队列并写通知寄存器
→ KVM 的 ioeventfd 机制发出通知
→ Firecracker 事件循环处理对应队列
→ 后端完成请求,更新 used ring
→ 通过 irqfd 等中断机制通知 Guest
ioeventfd 负责"Guest 有请求了",irqfd 把 eventfd 和 Guest 中断关联,负责"干完了,通知 Guest 来检查"。这省掉了一部分"vCPU 专门跑回来,只为转交一个通知"的步骤。但硬件层面的 VM exit 仍可能发生,后端 I/O 也仍要执行。门铃优化的是喊人方式,不是替你干活。
3. virtio-net:网络报文怎么离开 microVM
3.1 从一次 HTTP 请求追到宿主 TAP
Guest 里执行 curl,应用和网络栈先把请求变成报文,virtio-net 负责收发。TX 表示发送,RX 表示接收,TAP 是宿主上的虚拟网络接口。
Guest 应用:curl / Agent
→ Guest socket 与 TCP/IP 协议栈
→ Guest virtio-net 驱动
→ TX Virtqueue:待发送报文
→ Firecracker 网络设备后端
→ 宿主 TAP
→ 宿主路由 / 网桥 / 网络策略
→ 目标服务
TAP 就是接进宿主网络的一个虚拟网口,交换的是以太网帧。Firecracker 只负责把报文交到 TAP,之后怎么路由、要不要 NAT、允许访问哪里,全是宿主网络的事。它就是个快递小哥,只负责送到楼下,不负责你楼上住的是谁。
Firecracker 还能处理特定的 MMDS 元数据请求(给 microVM 提供配置等元数据),但这条特殊路径不代表它会代理所有 HTTP 请求。
3.2 收包要 Guest 先备好盘子
接收时,Guest 驱动先预留内存,通过 RX 队列告诉后端"收到的报文放这里"。TAP 有数据可读后,Firecracker 填进这些缓冲区,记录完成结果,再通知 Guest。
没有可用接收缓冲区,后端不能把报文凭空塞进应用内存------先备好盘子再上菜,没盘子不能直接倒你兜里。队列容量、缓冲区供给、事件处理速度、宿主网络和 CPU 调度都会影响收发延迟。所以发现网络慢,只测物理网卡带宽根本定位不了问题。
3.3 网卡、限流、授权是三件事
virtio-net 解决"能不能收发报文";设备限流约束带宽和操作速率;但判断"这次请求有没有业务权限",限流不负责,DNS、网关、出站策略也得平台自己配。
举个例子:允许一个 Agent 下载依赖,要网络连通;禁止它访问宿主管理接口,要宿主防火墙或代理策略。Firecracker 官方把 Guest 出站流量视为不可信,过滤交给宿主层。
**给 Guest 一块网卡,只建立了通信能力;允许这块网卡连哪里,仍是平台的决定。**就像给你办了门禁卡,不等于所有房间你都能进。
4. virtio-blk:文件操作怎么变成宿主磁盘 I/O
4.1 Guest 里的文件怎么存进镜像
Guest 可以把 /dev/vda 当一块磁盘,在上面用 ext4 等文件系统。/dev/vda 是 Guest 里的磁盘设备名,不是宿主磁盘路径。
宿主侧,这块虚拟磁盘通常由一个镜像文件承载,比如 rootfs.ext4。磁盘镜像就是"用一个文件装下一块虚拟磁盘",这个承载文件也叫 backing file。rootfs 是根文件系统的简称,不等于宿主根目录。
Guest 应用:读取 /workspace/main.py
→ Guest 文件系统与内存缓存
→ 需要块设备 I/O 时,Guest virtio-blk 驱动
→ Virtqueue:操作类型、扇区、缓冲区
→ Firecracker 块设备后端
→ 宿主磁盘镜像文件 rootfs.ext4
→ 宿主文件系统、缓存与存储设备
应用要读 main.py,Guest 文件系统先查出数据在虚拟磁盘哪些位置,再发块读取请求。Firecracker 按位置读 rootfs.ext4,完全不用理解 main.py 的文件名和目录结构------它不认人名,只认门牌号。
内存缓存也会影响路径:Guest 内存里已有内容,读取可以完全不碰虚拟磁盘;写入可能先进缓存,稍后再向下提交。
4.2 一个块请求包含什么
块设备提供的是按位置读写的接口,不直接认识文件名。一个块请求说三件事:**读还是写、访问磁盘哪里、数据放在哪里。**Guest virtio-blk 驱动组织成请求,Firecracker 校验后访问 backing file,再把完成状态写回。
读请求、写请求、状态区域的访问方向不同。设备不能因为地址落在 Guest 内存里,就忽略描述符的读写约定------写请求不能往只读缓冲区里灌水。
这条路径提供的是块存储,不是把任意宿主目录直接挂进 Guest。镜像从哪来、哪些磁盘只读、每个任务的写入存哪、多个实例会不会错误共用同一份可写镜像,都是平台要拍板的事。
4.3 Sync、Async、vhost-user 是后端选择题
Sync 是同步,Async 是异步。区别一句话:**发起读取后是否在原处等着。**同步 I/O 等结果返回;异步先提交请求,完成后再取结果,期间干别的。异步只是给你更多安排并发的空间,不是每次物理读写都自动变快。
Guest 用同一种 virtio-blk 接口,宿主可以选不同后端:
| 后端路径 | 请求由谁执行 | 主要取舍 |
|---|---|---|
| 内置 Sync | Firecracker 用同步文件 I/O | 路径直接,但阻塞 I/O 可能影响处理延迟 |
| 内置 Async | Firecracker 用 io_uring 提交和回收 I/O |
多个请求可同时等待完成,收益取决于负载、存储与 CPU |
| vhost-user block | 外部用户态后端进程处理队列 | 可接不同存储实现,但要承担进程管理、共享内存、故障协调责任 |
在本文分析的源码版本里,Async 和 vhost-user block 文档还标着 Developer Preview(开发者预览)。它们也不是"开了就一定更快"的开关------初始化成本、小请求延迟、并发深度、实例密度都会影响结果。
Unix socket 用于同一操作系统内进程通信,不依赖 IP 路由。vhost-user block 用它交换配置、内存区域和通知 fd 等控制信息,块数据走共享内存里的队列和缓冲区。别把它跟后面 vsock 到 Unix socket 的桥接混为一谈------一个传的是块数据,一个传的是应用字节流。
4.4 写成功 ≠ 持久化成功
假设程序保存了 result.json,write() 也返回成功,然后宿主突然断电------文件可能还是丢了,因为数据可能只躺在某一层内存缓存里。
缓存像临时存放区,持久存储才负责断电后保留数据。要保证持久化的写入,得让数据经过 Guest 缓存、虚拟磁盘、宿主缓存,最终到达持久存储。fsync 是应用请求把文件修改同步到存储的系统调用;块设备 flush 是设备层的缓存刷新请求。两个层次不同,都在推动这条链路。
本文的块设备缓存策略有两种:
- Unsafe(默认):不向 Guest 宣告 VirtIO flush 功能,不能依赖这条刷新链获得持久化保证。
- Writeback :宣告 flush 功能;协商成功后,Guest 的刷新请求会由内置后端转成 backing file 的
fsync。
名字里的 Writeback 不意味着每次写都同步落盘,它只是给了你一条正确的刷新路径------就像给你一条安全通道,但你没走就没用。
5. virtio-vsock:Guest 和宿主怎么绕过 IP 通信
5.1 它解决的是本机宿主和 Guest 的通信
平台经常要给 Guest 里的执行服务发任务、收日志、拿退出码和健康状态。用 TCP 能实现,但要管 IP 地址、路由、端口,烦。
**vsock 是为虚拟机与宿主这类端点通信设计的 socket 机制。**它用 CID 和端口定位:CID 是通信端点的编号,端口标识端点上的服务------类似"找哪台机器,再找其中哪个服务"。注意,这是 vsock 自己的编号和端口,不是 IP 和 TCP 端口。
Guest 程序用 AF_VSOCK,Firecracker 在宿主侧连 AF_UNIX(同一宿主机内的 Unix socket)。这条路不需要给 Guest 配 IP 地址和路由------像公司内部对讲机,不用拨区号。
Guest 应用(AF_VSOCK)
→ Guest vsock 协议栈 / virtio-vsock 驱动
→ Virtqueue
→ Firecracker vsock 设备与连接管理
→ 宿主 Unix socket(AF_UNIX)
→ 宿主服务
5.2 两个方向怎么建立连接
假设设备配置里 uds_path 是 /run/task/vsock.sock,应用端口用 8000。UDS 是 Unix Domain Socket 的缩写。
| 连接发起方 | 建立过程 |
|---|---|
| 宿主 → Guest | Guest 服务监听 vsock 端口 8000;宿主连 /run/task/vsock.sock,发文本握手 CONNECT 8000\n,成功后收 OK <宿主侧端口>\n,再交换应用数据 |
| Guest → 宿主 | 宿主服务监听 /run/task/vsock.sock_8000;Guest 连 CID=2、端口 8000;Firecracker 转接到对应 Unix socket |
\n 是换行字节,CID=2 表示宿主端。实际路径要结合 jailer 和实例目录规划,别让不同实例抢同一个 socket 名。jailer 是给 Firecracker 设隔离环境、降运行权限的启动程序------相当于进小区前先搜身、换工牌。
比如宿主发 {"cmd":"pytest"},Guest 里的执行服务读懂了就跑测试,再返回结果。**vsock 负责送达消息,执行服务负责理解和执行消息。**就像外卖骑手只负责送到,怎么吃是你的事。Firecracker 管理 API 的 socket 是配置 VM 用的,跟这条应用通道分开。
5.3 vsock、VirtIO、vhost、vhost-user 别搞混
vsock 通道传递的是命令文本、日志这类应用数据。Firecracker 在用户态桥接两端 socket,这条实现路径绕过宿主 vhost 内核代码,也不向宿主传送或安装内核代码。
| 名称 | 表达的是什么 |
|---|---|
| VirtIO | 驱动与设备之间的标准接口和队列机制 |
| virtio-vsock | 基于 VirtIO 的一种设备类型,为 vsock 通信提供传输 |
| 内核 vhost(vhost-net、vhost-vsock) | 把 VirtIO 后端处理放进宿主内核的实现方式 |
| vhost-user | 通过协议把后端队列处理交给另一个用户态进程 |
用 VirtIO 不等于必须用内核 vhost。Firecracker 内置 vsock 后端 + 可选 vhost-user block,正好说明"设备接口"和"后端放哪"是两个独立决定------点菜和在哪吃是两件事。
5.4 没有 IP 网络,照样有信任边界
vsock 让通信不依赖 Guest 的 IP 配置,但不自动完成业务授权。宿主服务要根据可信的实例映射和任务身份决定权限,校验消息、参数、资源范围。不能因为消息来自 vsock 通道,就允许它执行任意宿主命令------通道干净不等于人可靠。
连接生命周期也要管:快照恢复不能保证旧 vsock 连接还在。Guest 已有监听 socket 能保留并接受新连接,但已建立的连接要重连。任务协议得能处理重连、重复请求和结果确认------电话挂了再打,你不能指望对方记得上一通说到哪。
6. PIT:谁负责在指定时刻触发中断
6.1 计时器负责产生事件
先用手表和闹钟区分:**时钟告诉你现在几点了,计时器负责到点吵你。**PIT 是 Programmable Interval Timer(可编程间隔计时器)的缩写,干的是后者:Guest 设置计数值和模式,到达条件后通过中断提醒 Guest 内核。
比如要每隔约 10 毫秒触发一次事件,就选周期模式、设计数值。"可编程"指参数可以调,不是让你把一段程序塞给它执行。
底层按时钟脉冲计数:传统 i8254 PIT 基准频率约 1.193182 MHz,每秒约 119 万次脉冲;周期计数值取约 11932,对应约 10 毫秒。它有三个 16 位计数通道,通道 0 连 IRQ 0。理解日常路径,抓住"设置计数,到点触发事件"就够了。
6.2 Firecracker 请求 KVM 创建 PIT
x86 源码里,KvmVm::setup_irqchip() 先创建内核中断控制器,再调 create_pit2()。所以 PIT 的设备模型主要由 KVM 在宿主内核提供,Firecracker 负责初始化和状态协调。
Firecracker 初始化 VM
→ 请求 KVM 创建中断控制器与 PIT
Guest 编程 PIT 的计数值与模式
→ KVM 维护虚拟 PIT 状态
→ 到期后通过虚拟中断机制通知 Guest
→ Guest 内核处理定时事件
源码里的 KVM_PIT_SPEAKER_DUMMY 让 KVM 对旧式扬声器端口做最低限度响应------就是"嗯嗯,知道了",没有真喇叭,也不提供完整音频功能。
6.3 PIT 不是唯一的闹钟
现代 x86 Guest 还可能有本地 APIC 定时器、TSC deadline 等机制。一次 sleep() 最后被哪个时钟事件设备叫醒,要看 Guest 内核、CPU 暴露能力和配置,不能直接说"Firecracker 的 PIT 每次唤醒应用"。ARM64 也有自己的体系结构定时器路径,套不了这套传统 PC 设备图。
还有,"到点提醒"不等于"应用立刻执行"。宿主 CPU 忙,vCPU 也得排队;Guest 内核收到提醒,应用还可能继续排队。定时 10 毫秒不保证应用恰好在第 10 毫秒继续跑------闹钟响了不代表你马上起床。
7. KVM 时钟:Guest 怎么知道时间过了多久
7.1 kvm-clock 解决高效读时间的问题
kvm-clock 是 x86 KVM 提供的半虚拟化时钟。思路是:KVM 给 Guest 一个时间基准和换算方法,Guest 自己根据 CPU 计数器的增量推算时间,不必每次都问设备"现在几点了"。
假设基准时刻是 10 秒,之后计数器增量换算成 2 毫秒,当前时间就约为 10.002 秒。换算关系简写:
估算时间 ≈ 基准时间 + 换算(当前 TSC − 基准 TSC)
TSC 是 CPU 时间戳计数器,换算要用 KVM 提供的乘数和移位参数。底层过程:
- Guest 通过 KVM 定义的 MSR 寄存器接口,登记存放时间信息的内存地址。
- KVM 写入时间基准、TSC 基准和换算参数。
- Guest 读当前计数,完成换算,检查版本字段。
查版本是为了防止混读新旧数据:KVM 正在更新基准时,你不能拿旧基准配新换算参数------就像记账记到一半你抄走了,数字是拧巴的。
7.2 kvm-clock、TSC、PIT 怎么区分
| 机制 | 主要作用 | 归属 |
|---|---|---|
| kvm-clock | 以半虚拟化方式提供时间信息 | KVM 与 Guest 时钟代码协作 |
| TSC | x86 CPU 时间戳计数器,可作时钟源基础 | 硬件与虚拟化机制支持,是否适用由 Guest 判断 |
| PIT / 其他时钟事件设备 | 在设定条件下产生定时事件 | 由相应硬件虚拟化或设备模型提供 |
Firecracker 在 x86_64 上暴露 kvm-clock 和 tsc。但"支持 kvm-clock"不等于"始终用 kvm-clock",用哪个时钟源看内核判断。ARM64 走 arch_sys_counter。
应用还会区分"现在是几点"和"这活干了多久":墙上时间 表示日期和时刻,可以因校时而调整;单调时间 按自己的基准向前推进,不会因为改日期而倒退,适合算耗时。Guest 内核在底层计数器之上维护这些时间,date 命令输出的不是原始计数值。
7.3 暂停恢复后,时间要单独处理
10:00 创建快照时,某个令牌还有 5 分钟有效期。10:10 恢复快照,内存里令牌还在,但外部服务已经认为它过期。恢复 VM 状态不会延长外部授权,快照不是时光机。
符合条件的 x86 kvm-clock 路径上,可以通过 clock_realtime 选项推进恢复后的时钟,但可能带来 Guest 观察到的时间跳变。实际使用要核对时钟源、宿主支持和恢复参数。
恢复任务时,要重新检查令牌、任务取消状态和租约(一段时间内有效的资源使用权),不能只看快照里的旧状态。
8. 串口控制台:没有图形设备怎么观察和操作系统
8.1 串口把 Guest 的字符输入输出接到宿主
串口是一条不依赖 Guest 网络的文字通道。Firecracker 提供 UART(通用异步收发器)设备模型。Guest 用串口驱动,典型 x86 配置把 ttyS0 当控制台,输出启动日志和错误信息。
Guest 内核日志 / 连接到串口的终端程序
→ Guest 串口驱动
→ 虚拟 UART 寄存器
→ Firecracker 串口模型
→ 宿主标准输出或配置的输出目标
x86 的 COM1(传统 PC 第一个串口)注册在 I/O 端口 0x3f8,串口中断用 IRQ 4。这些地址不能直接套 ARM64。串口像应急对讲机:网络还没起、登录服务还没起,它先能说话。
8.2 串口设备、内核控制台、登录服务是三件事
内核启动参数 console=ttyS0 选择串口控制台。但想要看到交互式登录提示,还得 Guest 用户空间启动相应的 getty 或其他终端服务。getty 是负责准备终端、显示登录提示、接入登录流程的程序。
所以,控制台能显示启动日志却没有 login: 提示,多半是 Guest 没启动串口登录服务,不是串口坏了。应用日志显不显示在这里,取决于标准输出和日志系统的配置。
宿主侧的输入也可以进入虚拟 UART,再由 Guest 读取。怎么接入日志收集,要结合进程启动方式和输出配置来确定。
8.3 控制台该承担什么角色
启动失败时,网络服务和 Guest 执行服务可能都不可用,串口却能提供更早的线索------这是精简执行环境还保留它的重要理由。
但长期的结构化任务通信,更适合走 vsock 或网络上的应用协议。串口输出是字符流,内核日志和命令输出混在一起,不好可靠判断请求归属和完成状态;高频串口输出还会消耗设备处理和日志资源。
串口负责让系统在早期和故障时仍可观察;任务协议负责可靠表达一次操作的输入、输出和结果。
9. i8042:有限的键盘控制器怎么参与重置与退出
9.1 为什么精简虚拟机还留键盘控制器
传统 x86 的 i8042 控制器除了键盘相关功能,还提供可触发 CPU 重置的接口。Guest 内核可以用这条传统路径表达"我要重启"。
Firecracker 保留了必要的控制寄存器、缓冲区、重置命令和有限按键注入能力。它不是面向桌面交互的完整键盘,"仅包含关机键的键盘"这种说法容易掩盖它的真正用途------它主业是重置,键盘只是兼职。
9.2 Guest 发重置命令后发生什么
这条路径里,Guest 表达"我要重启",Firecracker 的处理结果是结束当前 microVM。具体信号:Guest 向 i8042 命令端口 0x64 写重置命令 0xFE,设备模型触发退出事件。
Guest 内核 → 向命令端口写入重置命令
KVM → 将需要用户态处理的端口 I/O 返回 VMM
Firecracker i8042 模型 → 触发退出 eventfd
VMM 事件处理 → 结束当前 microVM 的运行
这也是官方入门示例里,配置了相应重启路径后,Guest 内的 reboot 能结束 Firecracker 的原因。"重启"得到的是"结束",不是"自动再来一台新的"。平台想要重启后获得新实例,得自己安排再次启动------Guest 的重置命令不等于 VMM 自动重建整套流程。
9.3 宿主发 Ctrl+Alt+Del 是另一个方向
x86 上的 SendCtrlAltDel 管理操作,通过 i8042 注入 Ctrl、Alt、Delete 的扫描码(表示按键按下或释放的编码),触发相应键盘中断,让 Guest 的驱动和用户空间处理。
宿主管理请求 SendCtrlAltDel
→ Firecracker 注入扫描码
→ Guest 键盘驱动接收
→ Guest 根据自身配置处理 Ctrl+Alt+Del
→ 如进入重启流程,再通过相应机制通知 VMM
这相当于给 Guest 递了个纸条:"请处理这组按键"。接下来做什么由 Guest 决定。如果 Guest 已经卡死、处理不了键盘事件,纸条也没用。
所以,正常退出、Guest 重置、强制终止宿主 VMM 进程要分别建模。正常退出给应用清理和刷盘的机会;强制终止就得接受未完成 I/O 和没打扫干净的现场。平台根据任务状态和超时策略决定走哪条路。
10. 设计取舍:这些机制对执行平台意味着什么
10.1 Firecracker 和 QEMU + KVM 差在哪
QEMU 是支持多种机器类型和设备的虚拟机软件,可以和 KVM 配合执行 Guest。它和 Firecracker 都能用 KVM 和 VirtIO,所以"用了 KVM"或"用了 VirtIO"本身解释不了 Firecracker 的价值。差异在于提供多大的设备与机器模型范围,以及围绕什么负载组织实现。
| 比较维度 | Firecracker | QEMU + KVM |
|---|---|---|
| 目标 | 面向轻量隔离执行环境的 microVM | 覆盖更广泛的虚拟机与硬件兼容需求 |
| 设备与机器模型 | 聚焦有限的设备集合,控制功能范围 | 提供丰富的机器类型、设备模型与后端配置 |
| I/O 基础 | 可采用 VirtIO 与 KVM 通知机制 | 同样支持 VirtIO,有多种后端与加速配置 |
| 使用前要确认 | 目标 Guest、设备能力、生命周期接口能否满足负载 | 所选机器类型、设备与配置是否符合负载和运维需求 |
精简设备模型减少需要实现、维护、暴露给 Guest 的接口,降低部分资源和安全审计负担。但实际性能仍受 Guest 启动服务、镜像、网络、存储、调度和配置影响。"功能更少"推不出"所有场景更快"------轻装不等于跑赢所有比赛。
10.2 从设备能力推导平台责任
回到那个要下载依赖、修改项目、跑测试的 Agent:
| 应用需要做的事 | 底层主要机制 | 平台还需要完成的部分 |
|---|---|---|
| 执行代码 | Guest 内核、vCPU、KVM 与硬件虚拟化 | 镜像准备、资源分配、身份与生命周期管理 |
| 访问外部服务 | virtio-net 与 TAP | 路由、DNS、出站策略与业务授权 |
| 保存项目文件 | Guest 文件系统、virtio-blk 与宿主存储 | 写入隔离、刷新策略、持久化与磁盘一致性 |
| 发送任务、返回结果 | virtio-vsock 与 Unix socket | Guest 执行服务、消息协议、鉴权、重连与去重 |
| 处理超时 | Guest 时间与定时事件机制 | 宿主侧截止时间、取消策略与恢复后状态核验 |
| 获取启动和故障线索 | 串口控制台 | 日志收集、限流、归属与访问控制 |
| 停止或重建环境 | Guest 退出路径、i8042 等通知与 VMM 生命周期 | 优雅退出期限、强制回收、存储清理与实例重建 |
这种分工也决定安全设计:Firecracker 的设备后端会读 Guest 提供的队列和数据,所以后端仍处在处理不可信输入的边界上。设备校验、宿主进程降权、seccomp 和 jailer 要一起工作。seccomp 是 Linux 用来限制进程能用哪些系统调用、怎么用的机制,和 jailer 设的隔离环境配合,约束 Firecracker 进程能碰哪些宿主能力。减少设备数量只是其中一项措施。
10.3 把整条路径连起来
理解 Firecracker 与 KVM,记住四组关系:
- 运行关系:KVM 配合硬件提供虚拟化机制,Firecracker 组织 VM 与设备,microVM 承载 Guest 内核和应用。
- I/O 关系:Guest 驱动提交请求,VirtIO 队列描述缓冲区,设备后端完成宿主 I/O,通知机制让双方继续推进。
- 时间与控制关系:时钟提供时间信息,定时器产生事件,串口提供字符通道,i8042 保留有限的重置和按键通知功能。
- 平台关系:设备提供执行与通信能力,平台负责权限、持久化、恢复和任务语义。
沿着"应用接口 → Guest 内核 → 虚拟设备 → Firecracker / KVM → 宿主资源"追一次操作,就能判断问题发生在哪一层,也能说清某项优化到底改了哪段路径、又保留了哪些边界。下次虚拟机卡了、网络慢了、时间飘了,你至少知道该去敲谁的房门。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/qq_34419312