VCpu 任务管理
本节介绍 AxVisor 如何管理 Vcpu 任务,包括任务创建、调度、生命周期管理,以及与其他模块的协作关系。
模块交互关系
Vcpu 任务管理涉及多个模块的协作。下图展示了 Vcpu 任务管理的分层架构,从上层的 Shell 命令到底层的架构特定实现,每一层都有明确的职责分工:
从图中可以看出,vcpus 模块处于核心位置,负责协调各层之间的交互。Shell 层的命令通过 vcpus 模块转化为具体的任务操作,vcpus 模块再调用调度器、等待队列、硬件抽象层等下层模块来实现 Vcpu 的生命周期管理。
模块职责
| 模块 | 职责 | 与 vcpus 的交互 |
|---|---|---|
| vm_list | VM 实例管理 | 提供 VM 引用,vcpus 持有 Weak 引用避免循环 |
| hvc | Hypercall 处理 | vcpus 分发 Hypercall,hvc 执行并返回结果 |
| timer | 定时器事件 | 外部中断后检查定时器事件 |
| ArceOS 调度器 | 任务调度 | vcpus 创建任务,调度器管理执行 |
| WaitQueue | 等待/唤醒 | vcpus 使用等待队列实现 Vcpu 阻塞 |
数据结构
VMVCpus 结构
每个 VM 有一个 VMVCpus 管理其所有 Vcpu:
字段说明:
wait_queue: 用于 Vcpu 等待(Halt、暂停、等待启动)vcpu_task_list: 该 VM 的所有 Vcpu 任务引用running_halting_vcpu_count: 跟踪活跃 Vcpu 数,用于判断 VM 是否完全停止
全局等待队列
全局等待队列使用 UnsafeCell 包装的 BTreeMap 实 现,为每个 VM 维护一个独立的等待队列。这种设计允许细粒度的 Vcpu 唤醒控制,不同 VM 的 Vcpu 可以独立地等待和唤醒,互不干扰。
下图展示了全局等待队列的数据结构和操作接口。UnsafeCell 提供了内部可变性,但需要调用者自己保证线程安全(通常通过禁用中断或使用锁来实现):
TaskExt 扩展
每个 Vcpu 任务携带扩展数据,这是 AxVisor 在通用任务系统之上添加虚拟化特定信息的机制。TaskExt 作为任务的附加数据,存储了 Vcpu 任务运行时必需的上下文信息。
TaskExt 的设计目的:
ArceOS 的任务系统是通用的,它不知道什么是虚拟机或 Vcpu。但 Vcpu 任务在执行过程中需要频繁访问它所属的 VM 和 Vcpu 对象。如果没有 TaskExt,每次需要这些信息时都要通过全局查找,既低效又容易出错。
TaskExt 解决了以下问题:
- 快速访问:任务可以通过
current_task().task_ext()直接获取所属的 VM 和 Vcpu 引用 - 类型安全:编译期保证只有 Vcpu 任务才有这些扩展数据
- 生命周期管理:通过
Weak<VM>和Arc<VCpu>正确管理对象生命周期
为什么 VM 使用 Weak 而 Vcpu 使用 Arc:
这是一个精心设计的决策,背后有深刻的内存管理考量:
-
VM 使用 Weak<VM> 的原因:
- 避免循环引用:VM 拥有 Vcpu 任务 → 任务持有 TaskExt → TaskExt 引用 VM
- 如果使用
Arc<VM>,形成循环,VM 永远无法释放 - 使用
Weak<VM>不增加强引用计数,允许 VM 在不再需要时被删除 - 每次访问前需要
upgrade()检查,这反而提供了额外的安全性:如果 VM 已删除,任务会自动退出
-
Vcpu 使用 Arc<VCpu> 的原因:
- Vcpu 对象比较小(主要是寄存器状态),不会造成显著的内存占用
- Vcpu 任务需要频繁访问 Vcpu 对象,每次都
upgrade()会有性能开销 - Vcpu 的生命周期与任务绑定:任务存在 → Vcpu 存在,任务退出 → Vcpu 可释放
- 不会形成循环引用,因为 Vcpu 不持有任务的引用
访问模式示例:
// 在 Vcpu 任务内部
let task_ext = current_task().task_ext();
// 访问 VM(需要 upgrade)
if let Some(vm) = task_ext.vm() { // vm() 内部调用 weak.upgrade()
// VM 仍存在,可以安全使用
vm.handle_exit();
} else {
// VM 已被删除,退出任务
return;
}
// 访问 VCcpu(直接获取)
let vcpu = &task_ext.vcpu; // 直接访问,无需检查
vcpu.get_regs();
下图展示了 TaskExt 的数据结构及其与 VM 和 Vcpu 的引用关系:
使用 Weak<VM> 的原因:避免 VM → Vcpu 任务 → VM 的循环引用导致内存泄漏。
Vcpu 任务生命周期
完整生命周期
Vcpu 任务从创建到退出经历多个状态转换。下图展示了一个 Vcpu 任务的完整生命周期,包括所有可能的状态和触发状态转换的条件。理解这个状态机对于调试 Vcpu 相关问题至关重要:
关键状态说明:
- Created:Vcpu 任务已分配但未加入调度器
- Blocked: 任务已加入调度器但处于阻塞状态,等待 VM 启动
- Waiting:等待 VM 进入 Running 状态
- Running:Vcpu 正常运行,执行 Guest 代码
- Halting:Vcpu 执行了 WFI(Wait For Interrupt)指令,暂时休眠等待中断
- Suspended:VM 被暂停,Vcpu 阻塞但保持状态
- Exiting:VM 正在关闭,Vcpu 准备退出
主 Vcpu 初始化流程
主 Vcpu(Primary Vcpu,通常是 Vcpu 0)是VM启动时第一个创建和运行的虚拟处理器,负责引导 Guest 操作系统。下图展示了主 Vcpu 从创建到启动的完整时序,包括任务分配、调度器注册、等待队列设置等关键步骤:
流程关键点:
- 任务创建:
alloc_vcpu_task创建 Vcpu 任务,设置入口函数为vcpu_run,分配 256KB 栈空间 - 初始阻塞:任务加入调度器时处于阻塞状态,不会立即运行
- 注册等待队列:将 VMVCpus 注册到全局等待队列,使其可以被唤醒
- 启动 VM:调用
vm.boot()设置 VM 状态为 Running - 唤醒 Vcpu:通过
notify_primary_vcpu唤醒主 Vcpu,开始执行 Guest 代码