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 代码
辅助 Vcpu 启动流程
辅助 Vcpu(Secondary Vcpu)不在 VM 启动时创建,而是由 Guest 操作系统通过 PSCI(Power State Coordination Interface)标准接口动态启动。这种按需启动的方式可以节省资源,也更符合真实硬件的行为。
下图展示了 Guest 内核启动额外 CPU 核心时的完整交互流程,包括 PSCI 调用、MPIDR 映射查找、Vcpu 状态检查和任务创建:
流程详解:
- PSCI 调用:Guest 内核执行 PSCI CPU_ON hypercall,传入目标 CPU 的 MPIDR(Multiprocessor Affinity Register)、入口地址和参数
- MPIDR 映射:系统需要将 Guest 的 MPIDR 值映射到实际的 Vcpu ID,这个映射在配置文件中定义
- 状态验证:检查目标 Vcpu 当前状态是否为 Free(未使用),避免重复启动
- Vcpu 配置:设置 Vcpu 的入口点和初始寄存器值,这些值由 Guest 内核提供
- 任务创建:分配并启动新的 Vcpu 任务,该任务立即开始执行 Guest 代码
- 返回结果:设置返回值 0 表示成功,Guest 内核据此判断 CPU 是否成功启动
CPU 亲和性设置
Vcpu 可以绑定到特定物理 CPU,这种绑定称为 CPU 亲和性(CPU Affinity)。下图展示了 CPU 亲和性的配置来源、设置流程和最终效果。通过在配置文件中指定 phys_cpu_ids,可以实现 Vcpu 与物理 CPU 的精确绑定,这对于性能调优和资源隔离非常有用:
CPU 亲和性的应用场景:
- 性能优化:将对延迟敏感的 Vcpu 绑定到特定核心,避免缓存失效和上下文切换开销
- 资源隔离:将不同 VM 的 Vcpu 绑定到不同物理 CPU,避免资源竞争
- 调试分析:固定 Vcpu 位置便于性能分析和问题复现
- NUMA 优化:在 NUMA 架构中,将 Vcpu 绑定到与其内存最近的 CPU,减少内存访问延迟
Vcpu 主循环
vcpu_run 完整流程
vcpu_run 是每个 Vcpu 任务的入口函数,包含了 Vcpu 整个运行期间的核心逻辑。下图展示了从任务启动到退出的完整流程,包括初始化、主循环、VMExit 处理、状态检查和退出清理等关键环节:
流程说明:
- 初始化阶段:获取 VM 和 Vcpu 的引用,设置启动延迟避免所有 VM 同时启动
- 等待启动:通过
wait_for阻塞等待 VM 状态变为 Running - 标记运行:增加运行计数器,表示 Vcpu 开始运行
- 主循环:反复执行
vm.run_vcpu进入 Guest 模式,直到发生 VMExit - 处理退出:根据 VMExit 原因执行相应处理逻辑
- 状态检查:检查 VM 是否被暂停或停止,决定是继续运行还是退出
- 清理退出:最后一个退出的 Vcpu 负责更新 VM 状态和通知等待者
错误处理
Vcpu 运行中的错误处理策略需要在系统稳定性和问题诊断之间取得平衡。不同类型的错误有不同的严重程度,因此采用不同的处理策略。
错误处理的设计原则:
-
分级响应:根据错误严重程度采取不同措施
- 致命错误(如硬件入口失败):关闭整个 VM
- 可恢复错误(如未知 VMExit):记录警告但继续运行
- 预期错误(如 Hypercall 失败):返回错误码给 Guest
-
最小影响范围:尽可能限制错误的影响范围
- 单个 Vcpu 的问题不应导致整个 VM 崩溃
- 单个 VM 的问题不应导致整个 Hypervisor 崩溃
- 但如果错误威胁到系统安全或数据完整性,必须及时终止
-
完善的日志记录:所有错误都会被记录
- 使用不同的日志级别(error、warn、info)
- 记录足够的上下文信息(VM ID、Vcpu ID、错误码)
- 便于问题复现和调试
下图展示了三种主要错误类型的处理流程和影响范围:
| 错误类型 | 处理方式 | 影响范围 |
|---|---|---|
run_vcpu 返回 Err | 关闭整个 VM | VM 级别 |
| FailEntry | 记录警告,继续运行 | 仅当前 Vcpu |
| 未知 VMExit 类型 | 记录警告,继续运行 | 仅当前 Vcpu |
| Hypercall 执行失败 | 返回 -1 给客户机 | 仅本次调用 |
VMExit 处理
VMExit 是虚拟化技术的核心概念,表示 Guest 代码的执行因某种原因陷入到 Hypervisor。理解 VMExit 的类型和处理流程对于理解整个虚拟化系统至关重要。
什么是 VMExit:
在 ARM 虚拟化中,Guest 代码运行在非特权模式(Non-secure EL1)。当 Guest 执行某些特权操作或发生特定事件时,硬件会自动切换到 Hypervisor 模式(EL2),这个过程就是 VMExit。类似于操作系统中的系统调用,但发生在虚拟化层面。
触发 VMExit 的常见原因:
-
主动请求:
- HVC 指令:Guest 主动请求 Hypervisor 服务(Hypercall)
- WFI/WFE 指令:Guest CPU 进入等待状态
-
硬件事件:
- 外部中断:物理中断需要 Hypervisor 处理或注入到 Guest
- 异常访问:访问需要模拟的设备寄存器(MMIO、SysReg)
-
状态变化:
- 系统电源管理:PSCI 调用(CPU 开关、系统关机等)
VMExit 处理的基本流程:
每个 VMExit 都携带退出原因和相关参数。Vcpu 任务的职责是:
- 识别退出类型
- 调用相应的处理函数
- 更新 Guest 状态(如设置返回值)
- 返回 Guest 继续执行
下图展示了 VMExit 的分类和分发逻辑,每种类型都有相应的处理路径: