VM 命令实现
本节深入介绍 AxVisor Shell 中虚拟机管理命令的内部实现机制,包括状态管理、数据流和资源清理等核心设计。
该部分内容目前处于测试开发阶段,实现细节可能会随着项目迭代而发生变化。请在使用或参考此处内容时注意其暂时性,并以实际代码仓库中的最新实现为准。如果你在使用过程 中发现问题或有改进建议,欢迎通过 GitHub Issues 反馈。
VM 状态机设计
VM 命令的实现基于一个严格的状态机模型。虚拟机在其生命周期中会经历多个状态,每个命令通过状态转换来控制 VM 的行为。理解状态机是理解命令实现的基础。
状态说明:
- Loading:VM 正在创建和初始化,配置加载中
- Loaded:VM 已完成配置,但尚未启动,处于待命状态
- Running:VM 正在运行,所有 Vcpu 正常执行
- Suspended:VM 已暂停,Vcpu 被阻塞但状态保留
- Stopping:VM 正在关闭,等待所有 Vcpu 退出
- Stopped:VM 已停止,可以重启或删除
下图展示了 VM 状态之间的转换关系及触发命令:
状态转换规则:
- 单向转换:某些状态转换是单向的,如 Stopping → Stopped,无法逆转
- 条件转换:某些转换需要满足条件,如 Running → Stopped 必须先经过 Stopping 状态
- 命令限制:某些命令只能在特定状态下执行,如 suspend 只能作用于 Running 状态的 VM
常见操作路径:
- 正常启动:Loading → Loaded → Running
- 正常关闭:Running → Stopping → Stopped → 删除
- 暂 停恢复:Running ⇄ Suspended(可反复切换)
- 重启:Running → Stopping → Stopped → Running
命令实现概览
下表列出了各个命令的实现特点和依赖:
| 命令 | 主要实现机制 | 关键依赖 |
|---|---|---|
vm list | 全局列表快照 + 状态聚合 | VM 全局列表 |
vm show <id> | 状态读取 + 格式化输出 | VM 引用 |
vm create <config> | TOML 解析 + VM 初始化 | 文件系统 (fs feature) |
vm start [id...] | 状态验证 + Vcpu 任务创建 | 调度器 (fs feature) |
vm stop <id...> | 停止信号 + 异步等待 | Vcpu 任务协作 |
vm suspend <id> | 状态标志 + 轮询验证 | Vcpu 主循环检测 |
vm resume <id> | 状态切换 + Vcpu 通知 | 等待队列机制 |
vm restart <id> | 同步停止 + 重新启动 | stop + start 组合 |
vm delete <id> | 资源清理 + 引用计数验证 | Arc 引用管理 |
vm list 实现
vm list 命令展示了查询类命令的典型实现模式:快照读取 + 状态聚合 + 格式化输出。
核心实现机制
数据源:
- 调用
get_vm_list()从全局VM_LIST获取所有 VM 的Arc<VM>引用 - 这是一个快照操作,使用 RwLock 保护的 Vec 克隆
- 返回的列表不受后续 VM 创建/删除影响,避免了迭代期间的并发修改问题
状态聚合算法(table 格式):
- 对每个 VM,遍历其
vcpus: Vec<Arc<Vcpu>>列表 - 使用
vcpu.state()读取每个 Vcpu 的当前状态 - 统计 Running、Blocked、Free 三种状态的数量
- 格式化为 "Run:N, Blk:M, Free:K" 形式
输出格式化:
--format table:使用prettytable-rscrate 生成对齐的表格--format json:使用serde_json序列化为 JSON 对象
处理流程
vm list 命令的处理流程展示了典型的查询操作模式,从数据获取到格式化输出。理解这个流程有助于理解其他查询类命令的实现。
处理步骤解析:
-
获取数据源:
- 调用
get_vm_list()从全局 VM 列表获取所有 VM 的 Arc 引用 - 这是一个快照操作,返回的列表不会受到后续 VM 创建/删除 的影响
- 获取的是引用克隆,不会复制 VM 对象本身
- 调用
-
空列表处理:
- 如果系统中没有任何 VM,显示友好的提示信息而非空输出
- 这符合用户体验最佳实践,避免用户困惑
-
格式选择:
- 根据
--format选项决定输出格式 - table 格式:适合人类阅读,包含对齐、分隔线等视觉元素
- json 格式:适合程序处理,可以被脚本解析和处理
- 根据
-
数据聚合(仅 table 格式):
- 遍历每个 VM 的所有 Vcpu
- 统计各个状态(Running、Blocked、Free)的 Vcpu 数量
- 汇总显示在 "VCPU STATE" 列,如 "Run:2, Blk:1"
为什么需要状态统计:
Vcpu 状态统计提供了 VM 运行状态的快速概览:
- Run: N:有 N 个 Vcpu 正在执行 Guest 代码
- Blk: N:有 N 个 Vcpu 处于阻塞状态(等待中断、暂停等)
- Free: N:有 N 个 Vcpu 尚未启动
这些信息帮助用户快速判断:
- VM 是否真正在运行(Run > 0)
- VM 是否处于暂停状态(所有 Vcpu 都是 Blk)
- VM 是否刚创建未启动(所有 Vcpu 都是 Free)
下图展示了 vm list 命令的内部处理逻辑,包括如何获取数据、处理空列表情况以及根据格式选项生成不同的输出:
vm show 实现
vm show 命令实现了渐进式信息披露机制,通过标志位控制输出详细度。
核心实现机制
VM 查找:
- 调用
get_vm_by_id(id)从全局列表获取 VM 的Arc<VM>引用 - 如果 VM 不存在,返回
VmNotFound错误 - 使用 Arc 引用避免复制整个 VM 对象
详细度控制:
- 使用布尔标志
show_full、show_config、show_stats控制输出内容 - 不同标志组合触发不同的数据读取路径
- 基本模式只访问 VM 的基本字段,避免不必要的计算
输出层次:
pub fn show_vm(vm_id: usize, show_full: bool, show_config: bool, show_stats: bool) {
let vm = get_vm_by_id(vm_id)?;
// 基本信息(总是显示)
print_basic_info(&vm);
// 配置信息(--config)
if show_config || show_full {
print_config(&vm);
}
// 设备统计(--stats)
if show_stats || show_full {
print_device_stats(&vm);
}
// 完整详情(--full)
if show_full {
print_vcpu_details(&vm);
print_memory_details(&vm);
print_device_details(&vm);
}
}
状态感知提示实现:
通过模式匹配 VM 状态提供上下文相关的操作建议:
match vm.status() {
VmStatus::Suspended => println!("Use 'vm resume {}' to continue.", vm_id),
VmStatus::Stopped => println!("Use 'vm delete {}' to clean up.", vm_id),
VmStatus::Loaded => println!("Use 'vm start {}' to boot.", vm_id),
_ => {}
}
vm create 实现
vm create 命令实现了从配置文件到 VM 实例的完整转换流程,涉及文件 I/O、TOML 解析、配置验证和对象初始化。
核心实现机制
文件读取:
- 依赖
fsfeature,使用fs::read_to_string()或类似接口 - 读取整个配置文件内容到字符串
- 处理文件不存在、权限不足等 I/O 错误
TOML 解析:
- 使用
tomlcrate 将原始字符串解析为结构化配置 - 调用
init_guest_vm(raw_cfg: &str)进行解析和初始化 - 解析失败会返回详细的错误位置和原因
VM 初始化:
- 根据配置创建
VM对象(包括内存区域、设备、Vcpu 等) - 分配唯一的 VM ID(使用全局计数器或 ID 池)
- 初始状态设置为
Loading,初始化完成后转为Loaded
全局注册:
- 将新创建的
Arc<VM>加入全局VM_LIST - 使用
RwLock::write()保护并发访问 - 注册成功后返回 VM ID
创建流程
下图展示了从用户输入命令到 VM 创建完成的完整时序。流程包括文件读取、TOML 解析、配置验证、VM 实例化和注册等步骤:
批量创建的错误隔离:
批量创建使用 for 循环顺序处理每个配置文件:
for config_path in config_paths {
match create_single_vm(config_path) {
Ok(vm_id) => println!("✓ Successfully created VM[{}]", vm_id),
Err(e) => eprintln!("✗ Failed to create VM from {}: {}", config_path, e),
}
}
单个失败不会 panic 或中断循环,确保其他配置文件仍能被处理。
vm start 实现
vm start 命令是最复杂的命 令之一,涉及状态验证、Vcpu 任务创建、调度和同步等多个环节。
核心实现机制
状态验证 (can_start_vm):
fn can_start_vm(vm: &VM) -> Result<()> {
match vm.status() {
VmStatus::Loaded | VmStatus::Stopped => Ok(()),
VmStatus::Running => Err("VM is already running"),
VmStatus::Suspended => Err("VM is suspended, use 'vm resume' instead"),
VmStatus::Stopping => Err("VM is stopping, please wait"),
VmStatus::Loading => Err("VM is still loading"),
}
}
Vcpu 任务创建:
- 调用
setup_vm_primary_vcpu(vm)为 BSP (Bootstrap Processor) 创建任务 - 对于多核 VM,可能需要为每个 AP (Application Processor) 创建任务
- 每个 Vcpu 任务绑定到特定的物理 CPU 核心(通过 CPU affinity)
VM 启动序列:
vm.boot()- 设置 VM 状态为Runningnotify_primary_vcpu()- 唤醒 BSP Vcpu 任务开始执行- 增加全局运行计数器(用于跟踪活跃 VM 数量)
--detach 实现:
- 不使用
--detach:主线程等待 VM 退出(阻塞) - 使用
--detach:创建任务后立即返回(非阻塞)
启动流程
下图展示了 VM 启动的决策树和执行流程。系统会检查每个 VM 的状态,跳过已经在运行的 VM,对可启动的 VM 执行完整的启动序列:
状态验证
启动命令只能在特定状态下执行。下图清楚地列出了允许启动和拒绝启动的状态,以及拒绝的原因:
vm stop 实现
vm stop 命令展示了异步停止机制,通过状态标志和 Vcpu 主循环协作实现优雅关闭。
注意:该功能仍在完善中。
核心实现机制
停止信号:
pub fn shutdown(vm: &VM) {
vm.set_status(VmStatus::Stopping);
// 不直接杀死 Vcpu 任务,而是设置标志让它们自行退出
}
Vcpu 主循环检测:
// 在 Vcpu 主循环中
loop {
if vm.stopping() {
break; // 退出循环
}
// 执行 Guest 代码
run_guest_code();
}
// 最后一个退出的 Vcpu 设置 VM 状态为 Stopped
--force 实现:
- 优雅停止:设置标志 + 等待 Vcpu 任务自行退出
- 强制停止:直接调用
task.cancel()或task.kill()终止任务
批量停止处理:
pub fn stop_multiple(vm_ids: &[usize], force: bool) {
for &id in vm_ids {
if let Some(vm) = get_vm_by_id(id) {
vm.shutdown();
}
}
// 并行等待所有 VM 停止(如果不是 force 模式)
if !force {
wait_for_vms_stopped(vm_ids, Duration::from_secs(5));
}
}
停止模式对比
优雅停止和强制停止有本质的区别。优雅停止给予 VM 时间完成清理工作(如同步磁盘、关闭网络连接等),而强制停止会立即终止执行,可能导致数据丢失:
停止信号传播
下图展示了停止信号如何从 Shell 命令传播到各个 Vcpu。所有 Vcpu 会并行检测停止信号并退出,最后一个退出的 Vcpu 负责将 VM 状态设置为 Stopped:
vm suspend / resume 实现
暂停和恢复命令展示了状态标志 + 等待队列的协作机制。
注意:该功能目前并未真正实现,仍在完善中。
核心实现机制
暂停实现:
pub fn suspend(vm: &VM) -> Result<()> {
vm.set_status(VmStatus::Suspended);
// 轮询等待所有 Vcpu 进入 Blocked 状态
for _ in 0..10 { // 最多等待 1 秒
if vm.all_vcpus_blocked() {
return Ok(());
}
sleep(Duration::from_millis(100));
}
Err("Suspend timeout")
}
恢复实现:
pub fn resume(vm: &VM) -> Result<()> {
vm.set_status(VmStatus::Running);
vm.notify_all_vcpus(); // 唤醒所有等待的 Vcpu
Ok(())
}
Vcpu 响应机制:
// Vcpu 主循环中
loop {
if vm.suspending() {
vm.vcpu_wait_queue.wait_for(|| !vm.suspending());
}
// 执行 Guest 代码
}
暂停/恢复流程
暂停和恢复是一对互补的操作。暂停通过状态标志让 Vcpu 主动进入等待,恢复则通过通知机制唤醒它们:
暂停等待机制
暂停操作是异步的——vm suspend 命令只是设置了 VM 的暂停状态标志,但 Vcpu 任务需要一些时间才能检测到这个标志并进入阻塞状态。为了确保暂停真正完成,命令需要等待并验证所有 Vcpu 都已停止执行。
为什么需要等待机制:
如果不等待验证,可能出现以下问题:
- 用户以为 VM 已暂停,但实际上 Vcpu 还在运行
- 在 VM 未完全暂停时执行其他操作(如快照),导致状态不一致
- 无法给用户明确的反馈(操作成功还是失败?)
轮询等待的实现细节:
采用轮询方式而非阻塞等待的原因:
- 超时保护:避免无限等待,如果 10 秒内未完成则报告超时
- 状态检查:每次循环都检查所有 Vcpu 状态,提供进度反馈
- 非阻塞:允许在等待期间响 应其他事件(虽然当前实现是同步的)
轮询参数的选择:
-
最多 10 次:每次 100ms,总计 1 秒超时
- 1 秒对于暂停操作来说是合理的等待时间
- 正常情况下,Vcpu 应该在几十毫秒内响应
- 如果超过 1 秒未完成,很可能出现了异常
-
间隔 100ms:
- 足够短,用户不会感觉到明显的延迟
- 足够长,避免过于频繁的轮询消耗 CPU
- 给 Vcpu 任务足够的时间完成状态转换
状态检查的逻辑:
每次轮询检查所有 Vcpu 的状态:
- 如果全部都是 Blocked:暂停成功完成
- 如果仍有 Running 或其他状态:继续等待
- 如果达到最大次数:报告超时错误
这种"全部完成才算成功"的语义确保了 VM 处于完全一致的暂停状态。
下图展示了暂停命令如何通过轮询机制等待所有 Vcpu 进入阻塞状态:
vm restart 实现
vm restart 命令是 vm stop 和 vm start 的同步组合,展示了命令之间的复用。
注意:该功能目前并未真正实现,仍在完善中。
核心实现机制
同步停止 + 启动:
pub fn restart(vm_id: usize, force: bool) -> Result<()> {
let vm = get_vm_by_id(vm_id)?;
// 1. 停止 VM
match vm.status() {
VmStatus::Running | VmStatus::Suspended => {
stop(vm_id, force)?;
// 2. 轮询等待停止完成
for _ in 0..50 { // 最多等待 5 秒
if vm.status() == VmStatus::Stopped {
break;
}
sleep(Duration::from_millis(100));
}
if vm.status() != VmStatus::Stopped {
return Err("Stop timeout");
}
}
VmStatus::Stopped | VmStatus::Loaded => {
// 已经停止,直接启动
}
_ => return Err("Cannot restart VM in current state"),
}
// 3. 重新启动
start(vm_id)?;
Ok(())
}
命令复用设计:
vm restart不是独立实现,而是stop + start的组合- 复用现有命令逻辑,减少代码重复
- 通过同步等待确保停止完成后再启动
状态优化:
- 如果 VM 已经是 Stopped 或 Loaded 状态,直接调用
start() - 避免不必要的停止操作,提高效率
- 对于 Running 或 Suspended 状态,必须先完整停止