Minos 虚拟化: 启动流程
本文介绍 Minos Hypervisor 启动流程。
0. 全景图
1 | 上电 (QEMU 把控制权交给 _start,EL2) |
1. 镜像布局
这是”地图”,先知道镜像长什么样、各段放哪。
关键点:
1.1 入口与首段
1 | ENTRY(_start) // 入口 = _start(boot.S) |
- QEMU 平台
CONFIG_MINOS_ENTRY_ADDRESS=0x40000000(qemu_arm64_defconfig:108)。 - 首段固定地址,因为上电时 MMU 还没开,CPU 只能从物理地址执行。
CONFIG_PTOV_MASK=0x0(qemu defconfig:11)→ 虚拟地址 = 物理地址(identity map,后面 boot.S 里是关键)。
1.2 核心段
| 段 | 作用 | 关键符号 |
|---|---|---|
.vectors |
启动汇编 + 向量表 | __code_start / __code_end |
.text |
所有 C 函数代码 | |
.__init_func_0..9 |
初始化函数表(按 initcall 优先级分 10 档) | __init_func_*_start/end |
.__init_data_section |
初始化用数据 | |
.__init_text |
初始化完可释放的代码 | __init_text_start/end |
.stage1_page_table |
hypervisor 自身页表(1 页 4K) | __stage1_page_table |
.data |
数据 | |
.smp_affinity_id |
每核 affinity 数组 | __smp_affinity_id |
.__percpu |
percpu 变量,NR_CPUS 份拷贝 | __percpu_start/end |
.bss |
零初始化数据 | __bss_start/end |
.__vmodule |
vCPU 模块表 | __vmodule_start/end |
.__platform / .__irqchip / .__virqchip / .__vdev / .__console / .__smc_handler / .__hvc_handler / .__shell_command |
各种链接器表(注册机制的数据基础) | 各 __xxx_start/end |
1.3 三个最重要的”表段”理解
minos 大量用”把结构体放进专用 section,启动时遍历”的注册方式:
1 | // 例:vmodule(vtimer.c:288 register_vcpu_vmodule) |
.__init_func_N:初始化函数按优先级分档,对应core/init.h的 initcall 宏
(early_initcall/arch_initcall/subsys_initcall/module_initcall/device_initcall)。.__irqchip:IRQCHIP_DECLARE注册的中断控制器驱动。.__platform:PLATFORM_DECLARE注册的平台。
为什么用段不用链表? 因为很多注册发生在静态数据里,链接器表比运行时数组更省代码、且顺序确定(按链接顺序)。
一个易混点:
__init_func_N:初始化函数(initcall,call 优先级)__init_text:初始化代码段(启动后整个释放)
两者是不同概念,别混淆。
2. 汇编启动核心
这是 Phase 2 的重头戏。逐段精读。
2.1 _start
源码位置:
boot.S:55-105
1 | _start: |
要点:
- minos 虚拟化模式强制要求从 EL2 上电(QEMU
virtualization=on,u-boot 直接起在 EL2)。 - 一开始就把
HCR_EL2清零——干净起点,后续各阶段再逐步设。
2.2 EL2 环境初始化
源码位置:
boot.S:108-123
1 | mrs x0, midr_el1 // 把真实 MIDR/MPIDR 存入 |
要点:
vpidr/vmpidr:EL2 的虚拟化寄存器,guest 读到的是 hypervisor 伪装的 CPU 信息。
先填真实值,创建 VM 时(arch_virt.c:216-219)再改伪造值。CPTR_EL2=0x300000:关键位是 TFP(bit10)=0 → 不 trap EL1 的 FP/SIMD(浮点放行);
bit20(TCPAC)=1 trap CPACR_EL1 访问、bit21(TTA)=1 trap trace,用于控制 guest 浮点配置。VTTBR_EL2=0:清空 Stage2 页表基址——尚无 guest 映射,任何 guest 内存访问都 fault(安全起点)。dsb sy + isb:改完系统寄存器必须等待生效(dsb 等完成,isb 刷流水线)。
2.3 降级到 EL1 分支
CONFIG_VIRT 下直接 b minos_os_start,这段不执行。知道存在即可。
2.4 minos_os_start
这一段是开 MMU 前最后、也是最长的汇编段(约 160 行),它把
“栈、异常向量表、cache/TLB、Stage1 页表、MAIR、TCR、bss、percpu、idle task”
全部手工搭好。为什么这么长? 因为还没进 C,没有函数库,一切靠裸汇编。
整体分 7 步,下面逐段精读。
2.4.1 入口检查 + 记录镜像基址
源码位置:
boot.S:155-165
1 | minos_os_start: |
- 为什么要求 4K 对齐? 后面要把镜像当页表/段映射,页表要求 4K 对齐。
- 为什么再 2M 对齐存 base? 镜像起始地址记录成 2M 对齐的值,配合大页映射(Phase 4 学过的 PMD 2M 块)效率最高。注释也说”minos_start 到 __code_start 是保留内存,必须 2M 对齐”。
2.4.2 拿 cpuid、选栈、装向量表、开 Abort
源码位置:
boot.S:167-178
1 | bl arch_raw_smp_processor_id // 从 MPIDR 算出当前核编号 |
spsel #1:AArch64 每个 EL 有两套 SP(SP_EL0/SP_ELx),选 SP_ELx 用专用栈。VBAR_EL2:Phase 1 学过的向量表基址,此处指向entry.S的elx_vectors。
2.4.3 刷 dcache + TLB
源码位置:
boot.S:180-191
1 | bl inv_dcache_all // 逐级逐路失效数据缓存(Cache.S:69) |
- 上电后 cache/TLB 内容不可信(可能残留旧数据),先全部失效,建立干净基础。
inv_dcache_all用 set/way 方式(MMU 未开、无法按地址操作)。
2.4.4 设 SCTLR 初值
源码位置:
boot.S:193-196
1 | ldr x1, =ARM64_SCTLR_VALUE |
- 此时只设非 MMU 相关位(对齐检查/异常行为等),MMU 的
M位还没置(最后一步才置)。 nsh= non-shareable,单核配置阶段足够。
2.4.5 建 Stage1 页表四件套
源码位置:
boot.S:198-239
1 | // (a) 页表基址 |
MAIR 值 0xbbff440c0400 分解(每 8bit 一个 AttrIdx,对应 stage1.h 的 MT_*):
1 | AttrIdx0=0x00 Device-nGnRnE AttrIdx4=0x44 Normal WB, Inner/Outer |
为什么要动态读 PaRange? 不同 CPU 支持的最大物理地址位数不同(36/40/44/48…)。
TCR.IPS 必须≤硬件能力,硬编码可能让 MMU 配置非法。从 ID_AA64MMFR0_EL1 读出来动态填。
2.4.6 设 SPSR + 分叉 BSP / secondary
源码位置:
boot.S:241-272
1 | mov x1, #ARM64_SPSR_VALUE // 0x1c9(EL2h,中断屏蔽) |
- BSP 负责一次性规划内存:镜像末尾→ bootmem 基址 → 各核栈区(每核一块
CONFIG_TASK_STACK_SIZE)。 - x19(cpuid)在这里二次使用:给本核挑自己那块栈。
- secondary 分支(boot.S:329)也会设自己的 sp,但不做 bootmem 规划(那是 BSP 的活)。
全局内存布局(真实链接地址,QEMU 平台,镜像入口 0x40000000):
布局要点:
- 镜像区(0x40000000 ~ 0x4004e3b9):lds 排定,含代码/数据/页表/注册表。
- 对齐边界(0x4004e3b9 → 0x4004f000):镜像末尾向上取整到 4K。
- 启动规划区(0x4004f000 ~ 0x40057000):BSP 现场规划,4 个核各 8K 栈。
- 每核栈范围与 sp:
sp_cpuN = minos_stack_top - N × CONFIG_TASK_STACK_SIZE,
sp 位于各核栈区顶端(栈向下增长),CPU0 在最高处 0x40057000,CPU3 在最低处 0x40051000。
对齐是如何做的(boot.S:254-258):
1 | add x0, x0, #4095 // 先加 (4K-1) |
等价于 bootmem_base = (__minos_end + 0xfff) & ~0xfff,即”向上取整到 4K 边界”。
例:0x4004e3b9 + 0xfff = 0x4004f3b8,& ~0xfff → 0x4004f000。
- 为什么加
4095再and ~4095?加满一个 4K 让非对齐部分进位,再清零低 12 位得到下一个 4K 边界(若本身已对齐,加完无进位,值不变)。 - 这个”加(2^n-1)再按 ~(2^n-1) 清零”是向上取整的标准位运算技巧。
2.4.7 清 bss / percpu、映射启动内存、设 idle & percpu
源码位置:
boot.S:274-301
1 | // (a) 清 bss |
**
map_boot_mem**:真正往__stage1_page_table里填映射。到这一步页表内容才齐备。实现细节(分代码段/数据段/RO段/IO段,按属性建立 Stage1 映射)属于 Phase 4 内存虚拟化,
本阶段只需知道”它在此处被 BSP 调用、一次性把镜像与启动内存映射好”即可。x18 / TPIDR 是 minos 的两个全局约定(Phase 2 要掌握):
x18永远指向当前任务(task)TPIDR_EL2永远指向本核的 percpu 数据(vector.S 里异常入口靠它找当前任务/栈)
2.4.8 开 MMU 的”瞬间”
源码位置:
boot.S:302-315
1 | ldr x26, =mmu_on // ★ 提前把目标地址放 x26 |
为什么 br x26 而不是直接 b mmu_on? 开 MMU 那一刻起,PC 变成”虚拟地址”执行。mmu_on 这个标签在编译时被赋了虚拟地址,直接 b 也是 OK 的——但代码刻意先 ldr x26,=mmu_on
再用 br,是把”开关 MMU”和”跳转到新地址空间”两步明确分开,防止编译器在 msr SCTLR
和跳转之间插入依赖旧地址空间的指令。
四个”瞬间”节点(地址翻译相关的寄存器必须在 MMU 开之前设好):
- 页表:TTBR0/1_EL2 =
__stage1_page_table(lds 里预留的一页 4K)。 - 属性:MAIR_EL2 = 0xbbff440c0400(8 个 memory type 槽)。
- 控制:TCR_EL2 = T0SZ(39) | 4K granule | Inner/Outer WBWA | Inner Shareable。
- 开关:SCTLR_EL2 置 M/C/I 位 → MMU 生效 →
br x26跳到已映射的地址继续。
为什么开 MMU 前必须 identity map? 开 MMU 的瞬间,PC 还在物理地址执行。页表必须把”当前 PC 所在的物理地址”映射到”相同的虚拟地址”(虚拟=物理),br x26 才不跳飞。此平台 PTOV_MASK=0,虚拟=物理,天然满足。
2.4.9 多核启动路径全景图
总结 2.4 节的执行流:所有核都走同一段公共代码,到
cbnz x19, secondary_start_up
(boot.S:245)按 cpuid 分叉。绿色 = 每核各做各的,红色 = 只有 BSP 做一次。
图上要点:
- 红色路径(只有 CPU0 做一次):清 bss/percpu、
map_boot_mem填共享页表、内存规划。
这些是全局资源,BSP 干完,其他核直接复用。 - 绿色路径(每核各做各的):设 SP、设 TTBR/MAIR/TCR、设 idle/percpu、开 MMU。
这些是每核私有的寄存器/状态,谁也替不了谁。 - secondary 进 C 之前必须等 BSP(
boot_secondary里while(!is_cpus_all_up()) mb();,
minos.c:113),否则可能用 BSP 还没建好的数据结构。 - 分叉依据就是 x19(cpuid),所以
_start开头bl arch_raw_smp_processor_id那一步至关重要。
2.5 mmu_on 与 secondary core
源码位置:
boot.S:317-381
1 | mmu_on: |
clear_ttbr0:CONFIG_VIRT 下执行tlbi alle2(刷全部 EL2 TLB)。- secondary core 走
secondary_start_up(boot.S:329),各自设栈/页表,最后bl boot_secondary。
2.6 关键宏
源码位置:
asm_marco.S:11-25
1 | .macro asm_vtop reg // 虚拟→物理:and reg, #CONFIG_VTOP_MASK |
- 本平台两个 mask 都是 0 → 均为 no-op(虚拟地址 == 物理地址)。
- 理解:这是为支持”高地址映射”平台预留的(如 FVP,PTOV_MASK 非零时 PT→VT 要 orr 一个偏移)。
2.7 本节小结
本节完成了什么:汇编启动段(boot.S)把 hypervisor 从”裸机上电”带到了”能进 C 世界”。核心成果:
| # | 完成的工作 | 关键代码 |
|---|---|---|
| 1 | 确认运行在 EL2(虚拟化前提) | b.ne minos_panic |
| 2 | 初始化 EL2 环境(清 HCR、设 vpidr/vmpidr/CPTR/VTTBR) | boot.S:108-123 |
| 3 | 装异常向量表 + 清 cache/TLB(干净基础) | msr VBAR_EL2 / inv_dcache_all |
| 4 | 搭好 Stage1 地址翻译四件套(TTBR/MAIR/TCR) | boot.S:198-239 |
| 5 | 规划每核栈区(bootmem 基址 + 4 核各 8K 栈) | boot.S:247-272 |
| 6 | 清 bss/percpu + 填页表(map_boot_mem) | boot.S:274-301 |
| 7 | 开 MMU,跳转 C 世界 | SCTLR.M + br x26 → arch_main |
核心知识点(要记住的 4 件事):
- 强制 EL2:虚拟化版启动绝不停留 EL1,
b.ne minos_panic兜底。 - 开 MMU 四件套顺序:TTBR(页表)→ MAIR(属性)→ TCR(控制)→ 最后 SCTLR.M。地址翻译相关的必须先设好再点火。
- identity map(pa=va):开 MMU 瞬间 PC 不能变,QEMU 因 PTOV_MASK=0 天然满足。
- 多核分叉:公共段所有核跑,
cbnz x19后 CPU0 干全局活(清 bss/map_boot_mem/内存规划),secondary 直接复用并各自设栈/开 MMU。
下一步是什么:arch_main(第 3 节 3.1)——汇编到 C 的桥,处理 dtb 落地和串口初始化,然后进入 boot_main(第 3 节 3.2)按顺序跑各 init 阶段。
3. C 世界入口:arch_main → boot_main
汇编开完 MMU 后进入 C 世界。先由
arch_main处理 dtb 和串口,再进boot_main
按顺序跑各 init 阶段。本节省略为”三步”:arch_main(桥)→ boot_main(主流程)→ cpu_idle(归宿)。
3.1 arch_main:汇编 → C 的桥
源码位置:
arch.c:343-366
1 | void arch_main(void *dtb) |
要点:汇编开完 MMU 后,先解决两件事——dtb 落地 + 串口能打印(后续 log 都靠它)。
3.2 boot_main:C 世界初始化主流程
源码位置:
minos.c:45-96
1 | void boot_main(void) |
3.3 init 的优先级梯队
early_init → arch_init → platform_init → irq_init → ... → device_init
全部由 core/init.h 的宏声明,编译期进 .init_func_N 段,启动期由 subsys_init() 等统一遍历调用。这是 minos 扩展性的基础:新组件用对宏,自动进启动序列。
3.4 与本主题(虚拟化)最相关的两条
module_init()→ vmodule(vmodule.c:158subsys_initcall(vmodules_init))会创建 vcpu 的模块表,为后续 vCPU 上下文切换打基础。virt_init()(vm.c:1052)→vmm_init()建内存池、vm_daemon_init()建守护线程、parse_and_create_vms()从 dts 解析 VM → 创建 host VM。虚拟化从这一步真正开始。
3.4.1 启动分界线:guest 相关的动作全从 virt_init() 开始
1 | boot_main() |
结论:第 2-4 节是”hypervisor 把自己跑起来”;”让 guest 跑起来”是 virt_init() 及之后的事(Phase 4-9 内容)。
3.4.2 启动早期的 3 处”虚拟化痕迹”
不能说完完全全与普通 OS 相同,启动过程有三处虚拟化独有的动作:
| 位置 | 动作 | 为什么是虚拟化独有 |
|---|---|---|
| boot.S:90-91 | 强制 EL2 启动(b.ne minos_panic) |
普通 OS 会 eret 降级到 EL1,hypervisor 必须留 EL2 |
| arch.c:227 | `HCR_EL2 | = IMO|FMO|AMO` |
| minos.c:93 | virt_init() 是 boot_main 最后一步 |
自身跑完才启动 guest,顺序依赖 |
这三处是”为虚拟化铺路”,但真正的 guest 启动动作(建 Stage2、vCPU、注入中断)都不在这一阶段。
3.5 secondary core
- 等待所有核起来(
is_cpus_all_up)。 - 各自执行
early_init_percpu → arch_init_percpu → irq_secondary_init → ... → create_idle_task → cpu_idle。 - 只跑 percpu 部分,全局初始化只做一次(BSP)。
4. 架构层 init
4.1 aarch64_init_percpu
源码位置:
arch.c:213-240
1 | static int __init_text aarch64_init_percpu(void) |
要点(虚拟化关键):设 HCR_EL2.IMO/FMO/AMO —— 让物理 IRQ/FIQ/SError 直接路由到 EL2。不设的话,物理中断根本送不进 EL2,hypervisor 无法接管(这是虚拟化能工作的前提之一)。
4.2 arch_early_init / __arch_init
arch_early_init:of_setup_platform(),从 dtb 识别平台。__arch_init:of_parse_host_device_tree(),解析 host dtb。
4.3 affinity 转换
cpuid_to_affinity / affinity_to_cpuid:逻辑 CPU 号 ↔ MPIDR affinity 值互换。SGI/跨核通信会用到(Phase 6 见过 cpuid_to_affinity)。
5. minos 与 Linux 的对比
为什么 minos 的启动流程看起来和 Linux 多核启动”长得很像”?因为两者都要解决
同一组问题(识别核号、搭地址翻译、开 MMU、BSP 干全局活、secondary 等待、进 idle)。
但目标完全不同——一个是启动一个 OS,一个是启动一个 hypervisor。
5.1 启动流程对比
5.1.1 骨架相同
| 阶段 | Linux 多核启动 | Minos 多核启动 |
|---|---|---|
| 入口检查 | _start → 查 EL / 对齐 |
_start → 查 EL、4K 对齐 |
| 每核自识别 | mrs MPIDR → cpuid |
arch_raw_smp_processor_id → x19 |
| 建页表 | __create_page_tables identity map |
map_boot_mem identity map |
| 开 MMU | __enable_mmu + br |
SCTLR.M + br x26 |
| BSP/secondary 分叉 | secondary_holding_pen / __cpu_up |
cbnz x19, secondary_start_up |
| 汇合等待 | secondary_start_kernel 等 BSP |
boot_secondary 等 is_cpus_all_up |
| 最终进 idle | cpu_startup_entry → cpu_idle |
cpu_idle() |
为什么必然像:任何”多核从裸机上电到 idle”都要解决同一组问题,这是 ARM 多核启动的标准流程。
5.1.2 本质区别
| Linux(非虚拟化) | Minos(虚拟化) | |
|---|---|---|
| 启动后运行层级 | EL1 | EL2 |
| EL2 的角色 | 路过(eret 降级到 EL1) |
就是家(b.ne minos_panic 强制 EL2) |
| 给谁启动 | 给自己(唯一 OS) | 给自己 + 之后创建 guest VM |
| 地址翻译 | 一套 Stage1(自用) | Stage1(自用)+ 为 guest 建 Stage2 |
| 配置的寄存器 | SCTLR/TTBR/TCR/MAIR | 同上 + HCR_EL2 / VTCR_EL2 / VTTBR_EL2 / vPIDR/vMPIDR |
| 异常处理 | 自己处理 | 接管 guest 异常(trap.c,Phase 3) |
| 中断 | 自己用物理 GIC | 物理 GIC + vGIC / LR 注入(Phase 5) |
5.1.3 代码里的关键证据
Linux 式(降级到 EL1)——boot.S:125-153:
1 | drop_to_el1: |
Minos 式(强制留 EL2)——boot.S:90-91:
1 | #ifdef CONFIG_VIRT |
且 arch_vcpu_state_init(arch_virt.c:187)配置 HCR_EL2.VM | TWI | IMO | FMO | ...,
为 guest 准备 Stage2 翻译和 trap 机制。
5.1.4 一句话总结
启动的”手脚架”一样,但”终点”不同:Linux 启动完是一个 OS 跑在 EL1;
minos 启动完是一个 EL2 hypervisor,它启动自己只是第一步——紧接着 virt_init() 创建 host VM、
为 guest 建 Stage2、接管中断与 trap(Phase 3-9 的全部内容)。
所以 Linux 启动经验能完全迁移,但虚拟化的新东西全在”启动完成之后”。
5.2 内核定位对比
minos 到底是什么?一句话:**带微内核骨架的 Type-1 hypervisor,是”操作系统的操作系统”**。
它不是通用操作系统,但确实长着操作系统的样子——这是和 Linux 对比理解的钥匙。
5.2.1 一句话定位
1 | 传统 Linux: 硬件 ← 管理 → 应用(进程) |
**关键在”服务对象”**:
- Linux 的”用户”是进程(EL0)
- Minos 的”用户”是其他操作系统(EL1)
所以 minos 本质是管理操作系统的操作系统——它的”应用”不是程序,而是一个个 guest OS。
5.2.2 为什么它”像”操作系统
minos 内核里有完整的微内核组件,这也是能和 Linux 类比的基础:
| 组件 | Linux | Minos | 用途 |
|---|---|---|---|
| 任务/调度 | task_struct / CFS |
task / sched.c |
调度执行单元 |
| 内存管理 | mm / 伙伴系统 |
mm.c / vmm.c |
分配物理内存 |
| 中断 | GIC 驱动 | gicv3.c |
处理硬件中断 |
| 定时器 | hrtimer | timer.c |
定时调度 |
| 异常处理 | 向量表 → do_IRQ 等 | vector.S → trap.c |
处理异常 |
为什么必须有这些? 因为它要自举(启动流程)、要调度 vCPU(每个 vCPU 是一个 task)、
要管理自己的内存和中断。没有内核骨架,hypervisor 自己都跑不起来。
5.2.3 本质差异
| 维度 | Linux | Minos |
|---|---|---|
| 运行层级 | EL1 | EL2 |
| 服务对象 | 进程 | guest OS |
| 虚拟化 | 不支持(或作为 KVM 宿主) | 核心功能 |
| 页表 | 只管 Stage1 | Stage1 + 为每个 guest 建 Stage2 |
| 调度对象 | 进程/线程 | vCPU(一个 vCPU 就是一个 task) |
| 中断 | 自己处理 | 接管 + 用 LR 注入虚拟中断 |
| 上下文切换 | 保存/恢复寄存器 | 保存/恢复寄存器 + vmodule 保存 guest 全套虚拟化状态 |
| 内存隔离 | 进程地址空间 | VMID 硬件隔离 |
5.2.4 类比
- Linux = 一栋楼的物业(管理水电/门禁,服务住客=进程)
- Minos = 一栋楼的房东公司(自己也要懂物业管理,但真正服务的是”租下整层的公司”= guest OS)
- 房东公司既要懂物业管理(像内核),又要懂出租整层(虚拟化),所以它两者都沾。
5.2.5 回到本项目代码
这正好解释了启动流程的两面性:
- 第 2 节 boot.S 建页表/开 MMU → 微内核部分(像 Linux)
- 3.4 节
virt_init()→ hypervisor 部分(Linux 没有) - 强制 EL2、HCR_EL2 中断路由 → 虚拟化独有(层级不同导致)
一句话总结:minos 是”拥有微内核骨架的 hypervisor”——内核组件是它的”手”,虚拟化是它的”目的”。
对比理解时,凡”管硬件、调度、分配内存”的部分和 Linux 同构;凡”为 guest 建 Stage2、注入中断、保存 vCPU 状态”的部分是 Linux 没有的。
6. 术语速查
| 术语 | 含义 |
|---|---|
| identity map | 虚拟地址 == 物理地址的映射,开 MMU 瞬间必需 |
| initcall | 编译期注册、启动期按优先级调用的初始化函数 |
| percpu | 每 CPU 一份的变量(lds 里 NR_CPUS 份拷贝) |
| PTOV_MASK / VTOP_MASK | 物理↔虚拟地址换算掩码(QEMU 平台为 0) |
| Stage1 / Stage2 | hypervisor 自身地址翻译(1) / guest 地址翻译(2) |