Minos 虚拟化: 启动流程

本文介绍 Minos Hypervisor 启动流程。

0. 全景图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
上电 (QEMU 把控制权交给 _start,EL2)


boot.S:_start ──► 检查 CurrentEL ──► 初始化 EL2 环境 ──► 建 Stage1 页表 ──► 开 MMU
│ │
│ (identity map, 虚拟=物理, 无 PTOV) │
▼ ▼
arch_main(dtb) ◄──────────────────────────────────────────────────── 跳转 mmu_on
│ → relocate dtb / of_init / console_init

boot_main() ← C 世界入口
│ 按顺序执行各 init 阶段

cpu_idle() ← 启动完成,BSP 进入 idle 等调度

1. 镜像布局

这是”地图”,先知道镜像长什么样、各段放哪。

关键点:

1.1 入口与首段

1
2
3
4
5
6
7
ENTRY(_start)                          // 入口 = _start(boot.S)
.vectors CONFIG_MINOS_ENTRY_ADDRESS + CONFIG_PTOV_MASK:
{
__code_start = .;
KEEP(*(__start_up)) // boot.S 的启动代码段
KEEP(*(__elx_vectors ...)) // 异常向量表等 asm
}
  • 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
2
3
// 例:vmodule(vtimer.c:288 register_vcpu_vmodule)
MINOS_MODULE_DECLARE(...) // 宏会把结构体塞进 .__vmodule 段
// 启动时 vmodules_init() 遍历 __vmodule_start~__vmodule_end 逐个创建
  • .__init_func_N:初始化函数按优先级分档,对应 core/init.h 的 initcall 宏
    early_initcall / arch_initcall / subsys_initcall / module_initcall / device_initcall)。
  • .__irqchipIRQCHIP_DECLARE 注册的中断控制器驱动。
  • .__platformPLATFORM_DECLARE 注册的平台。

为什么用段不用链表? 因为很多注册发生在静态数据里,链接器表比运行时数组更省代码、且顺序确定(按链接顺序)。

一个易混点:

  • __init_func_N初始化函数(initcall,call 优先级)
  • __init_text初始化代码段(启动后整个释放)
    两者是不同概念,别混淆。

2. 汇编启动核心

这是 Phase 2 的重头戏。逐段精读。

2.1 _start

源码位置:boot.S:55-105

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_start:
msr daifset, #2 // 关中断(DAIF.I)
b do_start
... (Image header 64字节,符合 ARM64 Linux kernel Image 格式)
do_start:
mov x27, x0 // 保存 dtb 地址到 x27(uboot 传来)
mrs x0, CurrentEl // 当前在哪个 EL
lsr x1, x0, #2
and x1, x1, #3 // x1 = EL 编号 (2 = EL2)
cmp x1, #3
b.eq minos_panic // 不支持 EL3
mov x26, #0
cmp x1, #2
#ifdef CONFIG_VIRT
b.ne minos_panic // ★ 虚拟化模式:必须从 EL2 启动
msr HCR_EL2, x0 // HCR_EL2 = 0(清空陷阱位)
isb
#else
b.ne minos_os_start // 非虚拟化:跳 EL1 的启动
...
#endif

要点

  • minos 虚拟化模式强制要求从 EL2 上电(QEMU virtualization=on,u-boot 直接起在 EL2)。
  • 一开始就把 HCR_EL2 清零——干净起点,后续各阶段再逐步设。

2.2 EL2 环境初始化

源码位置:boot.S:108-123

1
2
3
4
5
6
7
8
9
mrs	x0, midr_el1            // 把真实 MIDR/MPIDR 存入
mrs x1, mpidr_el1 // VPIDR/VMPIDR(guest 能看到伪造的 ID)
msr vpidr_el2, x0
msr vmpidr_el2, x1
ldr x0, =0x300000
msr CPTR_EL2, x0 // 放行 EL1 用 FP/SIMD(CPACR 不 trap)
msr VTTBR_EL2, xzr // 清 guest 页表基址
dsb sy
isb

要点

  • 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
2
3
4
5
6
7
8
9
minos_os_start:
adr x1, _start
and x0, x1, #0xfff
cbnz x0, minos_panic // ★ 入口必须 4K 对齐,否则 panic

ldr x3, =0xffffffffffe00000 // ~2M 对齐掩码
and x1, x1, x3 // 向下取整到 2M 边界
adr x2, minos_start // 全局变量 minos_start 的地址
str x1, [x2] // 把镜像基址存进 minos_start(供 C 代码使用)
  • 为什么要求 4K 对齐? 后面要把镜像当页表/段映射,页表要求 4K 对齐。
  • 为什么再 2M 对齐存 base? 镜像起始地址记录成 2M 对齐的值,配合大页映射(Phase 4 学过的 PMD 2M 块)效率最高。注释也说”minos_start 到 __code_start 是保留内存,必须 2M 对齐”。

2.4.2 拿 cpuid、选栈、装向量表、开 Abort

源码位置:boot.S:167-178

1
2
3
4
5
6
7
8
bl	arch_raw_smp_processor_id  // 从 MPIDR 算出当前核编号
mov x19, x0 // ★ x19 = cpuid,全程保留(后面多次用)

msr spsel, #1 // ★ 用 SP_ELx(当前 EL 栈),不用 SP_EL0
ldr x1, =elx_vectors
msr VBAR_EL2, x1 // ★ 装异常向量表基址
msr daifclr, #4 // 清 DAIF.A → 允许 Abort 异常(开 MMU 失败能报错)
isb
  • spsel #1:AArch64 每个 EL 有两套 SP(SP_EL0/SP_ELx),选 SP_ELx 用专用栈。
  • VBAR_EL2:Phase 1 学过的向量表基址,此处指向 entry.Selx_vectors

2.4.3 刷 dcache + TLB

源码位置:boot.S:180-191

1
2
3
4
5
6
7
8
9
10
	bl	inv_dcache_all            // 逐级逐路失效数据缓存(Cache.S:69)
dsb sy
isb
#ifdef CONFIG_VIRT
tlbi alle2 // 失效 EL2 全部 TLB
#else
tlbi vmalle1
#endif
dsb sy
isb
  • 上电后 cache/TLB 内容不可信(可能残留旧数据),先全部失效,建立干净基础。
  • inv_dcache_all 用 set/way 方式(MMU 未开、无法按地址操作)。

2.4.4 设 SCTLR 初值

源码位置:boot.S:193-196

1
2
3
4
ldr	x1, =ARM64_SCTLR_VALUE
msr SCTLR_EL2, x1 // 0x30c51878(见 aarch64_el2_reg.h:87)
dsb nsh
isb
  • 此时只设非 MMU 相关位(对齐检查/异常行为等),MMU 的 M 位还没置(最后一步才置)。
  • nsh = non-shareable,单核配置阶段足够。

2.4.5 建 Stage1 页表四件套

源码位置:boot.S:198-239

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
	// (a) 页表基址
ldr x1, = __stage1_page_table
asm_vtop x1 // 虚拟→物理(此平台 no-op,因 PTOV_MASK=0)
msr TTBR0_EL2, x1 // 低地址空间页表
msr TTBR1_EL2, x1 // 高地址空间页表(同一张表)
dsb nsh
isb

// (b) MAIR 内存属性表
ldr x1, =0xbbff440c0400
msr MAIR_EL2, x1 // 8 个 AttrIdx 槽(见下方分解)

// (c) TCR 翻译控制
mrs x0, ID_AA64MMFR0_EL1
and x0, x0, #0xf // 取 PaRange 字段(硬件支持的 PA 位数)
#ifdef CONFIG_VIRT
mov x2, x0, lsl #16 // 填入 TCR.IPS(bit16-19)
ldr x3, =0xfffffffffff8ffff // 清掉 IPS 位
#else
mov x2, x0, lsl #32
ldr x3, =0xfffffff8ffffffff
#endif
ldr x1, =ARM64_TCR_VALUE // 基础 TCR:T0SZ(39)|4K|WBWA|InnerSH
and x1, x1, x3
orr x1, x1, x2 // 合并硬件 PA range
msr TCR_EL2, x1
dsb nsh
isb

MAIR 值 0xbbff440c0400 分解(每 8bit 一个 AttrIdx,对应 stage1.h 的 MT_*):

1
2
3
4
AttrIdx0=0x00 Device-nGnRnE   AttrIdx4=0x44 Normal WB, Inner/Outer
AttrIdx1=0x04 Device-nGnRE AttrIdx5=0xff Normal WB
AttrIdx2=0x0c Device-GRE AttrIdx6=0xbb Normal WT
AttrIdx3=0x44 Normal-NC

为什么要动态读 PaRange? 不同 CPU 支持的最大物理地址位数不同(36/40/44/48…)。
TCR.IPS 必须≤硬件能力,硬编码可能让 MMU 配置非法。从 ID_AA64MMFR0_EL1 读出来动态填。

2.4.6 设 SPSR + 分叉 BSP / secondary

源码位置:boot.S:241-272

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
mov	x1, #ARM64_SPSR_VALUE    // 0x1c9(EL2h,中断屏蔽)
msr SPSR_EL2, x1 // 为 idle task 预置返回状态

cbnz x19, secondary_start_up // ★ 非 0 号核 → 跳 secondary 分支

// ---- 只有 BSP(x19=0)执行以下栈/内存布局 ----
ldr x0, =__minos_end // 镜像末尾
asm_vtop x0
adr x1, minos_stack_top // 3 个全局变量的地址
adr x2, minos_bootmem_base
adr x3, minos_stack_bottom

add x0, x0, #4095 // 向上取整到 4K 对齐(bootmem 起始)
mov x4, #4095
mvn x5, x4
and x0, x0, x5 // x0 = 对齐后的 bootmem 基址
str x0, [x2] // minos_bootmem_base = 镜像末尾(对齐)
str x0, [x3] // minos_stack_bottom = 同上

mov x4, #CONFIG_TASK_STACK_SIZE
mov x5, #CONFIG_NR_CPUS
mul x5, x4, x5 // 全部核的栈总大小
add x0, x0, x5
str x0, [x1] // minos_stack_top = bottom + NR_CPUS*栈大小

sub x0, x0, x19, lsl #CONFIG_TASK_STACK_SHIFT
asm_ptov x0
mov sp, x0 // ★ 当前核的栈指针
  • BSP 负责一次性规划内存:镜像末尾→ bootmem 基址 → 各核栈区(每核一块 CONFIG_TASK_STACK_SIZE)。
  • x19(cpuid)在这里二次使用:给本核挑自己那块栈。
  • secondary 分支(boot.S:329)也会设自己的 sp,但不做 bootmem 规划(那是 BSP 的活)。

全局内存布局(真实链接地址,QEMU 平台,镜像入口 0x40000000):

mem_layout

布局要点:

  • 镜像区(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
2
3
4
add	x0, x0, #4095    // 先加 (4K-1)
mov x4, #4095
mvn x5, x4 // x5 = ~0xfff(低 12 位清零掩码)
and x0, x0, x5 // 再抹掉低 12 位

等价于 bootmem_base = (__minos_end + 0xfff) & ~0xfff,即”向上取整到 4K 边界”。
例:0x4004e3b9 + 0xfff = 0x4004f3b8& ~0xfff → 0x4004f000

  • 为什么加 4095and ~4095?加满一个 4K 让非对齐部分进位,再清零低 12 位得到下一个 4K 边界(若本身已对齐,加完无进位,值不变)。
  • 这个”加(2^n-1)再按 ~(2^n-1) 清零”是向上取整的标准位运算技巧。

2.4.7 清 bss / percpu、映射启动内存、设 idle & percpu

源码位置:boot.S:274-301

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// (a) 清 bss
ldr x0, =__bss_start
asm_vtop x0
ldr x2, =__bss_end
asm_vtop x2
sub x2, x2, x0
bl memset // bss 清零(C 语言要求未初始化全局=0)

// (b) 清 percpu 区
ldr x0, =__percpu_start
ldr x2, =__percpu_end
...
bl memset

// (c) 映射启动内存
bl map_boot_mem // ★ 把镜像/内存按属性映射进 Stage1
dsb ishst
isb

// (d) 建立"当前任务 / pcpu"全局指针
set_percpu_idle_task x0, x1, x19 // ★ x18 = 本核 idle task(当前任务约定)
set_percpu_pcpu x0, x1, x19 // ★ TPIDR_EL2 = 本核 pcpu 数据(见宏)
  • **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
2
3
4
5
6
7
8
9
10
11
12
13
ldr	x26, =mmu_on            // ★ 提前把目标地址放 x26

// 使能 MMU 并关对齐检查
mrs x1, SCTLR_EL2
orr x1, x1, #SCTLR_ELx_M // 开 MMU(bit0)
orr x1, x1, #SCTLR_ELx_C // 开 D-cache(bit2)
orr x1, x1, #SCTLR_ELx_I // 开 I-cache(bit12)
bic x1, x1, #SCTLR_ELx_SA // 关栈对齐检查
bic x1, x1, #SCTLR_ELx_A // 关对齐检查
msr SCTLR_EL2, x1
dsb sy
isb
br x26 // ★ 跳到 mmu_on(在已映射的地址上)

为什么 br x26 而不是直接 b mmu_on 开 MMU 那一刻起,PC 变成”虚拟地址”执行。
mmu_on 这个标签在编译时被赋了虚拟地址,直接 b 也是 OK 的——但代码刻意先 ldr x26,=mmu_on
再用 br,是把”开关 MMU”和”跳转到新地址空间”两步明确分开,防止编译器在 msr SCTLR
和跳转之间插入依赖旧地址空间的指令。

四个”瞬间”节点(地址翻译相关的寄存器必须在 MMU 开之前设好):

  1. 页表:TTBR0/1_EL2 = __stage1_page_table(lds 里预留的一页 4K)。
  2. 属性:MAIR_EL2 = 0xbbff440c0400(8 个 memory type 槽)。
  3. 控制:TCR_EL2 = T0SZ(39) | 4K granule | Inner/Outer WBWA | Inner Shareable。
  4. 开关: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 做一次。

boot_flow

图上要点

  • 红色路径(只有 CPU0 做一次):清 bss/percpu、map_boot_mem 填共享页表、内存规划。
    这些是全局资源,BSP 干完,其他核直接复用。
  • 绿色路径(每核各做各的):设 SP、设 TTBR/MAIR/TCR、设 idle/percpu、开 MMU。
    这些是每核私有的寄存器/状态,谁也替不了谁。
  • secondary 进 C 之前必须等 BSPboot_secondarywhile(!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
2
3
4
5
6
7
8
9
mmu_on:
ic ialluis // 刷指令缓存
dsb sy
isb
clear_ttbr0 // CONFIG_VIRT: tlbi alle2
mov x0, x27 // 恢复 dtb 地址
bl arch_main // ★ 进入 C 世界,永不返回
dead_loop:
b dead_loop
  • 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
2
.macro asm_vtop reg      // 虚拟→物理:and reg, #CONFIG_VTOP_MASK
.macro asm_ptov reg // 物理→虚拟:orr reg, #CONFIG_PTOV_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 x26arch_main

核心知识点(要记住的 4 件事)

  1. 强制 EL2:虚拟化版启动绝不停留 EL1,b.ne minos_panic 兜底。
  2. 开 MMU 四件套顺序:TTBR(页表)→ MAIR(属性)→ TCR(控制)→ 最后 SCTLR.M。地址翻译相关的必须先设好再点火
  3. identity map(pa=va):开 MMU 瞬间 PC 不能变,QEMU 因 PTOV_MASK=0 天然满足。
  4. 多核分叉:公共段所有核跑,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
2
3
4
5
6
7
8
9
10
void arch_main(void *dtb)
{
pr_notice("Starting Minos AARCH64\n");
dtb = (void *)ptov(dtb); // uboot 传来的物理 dtb → 虚拟地址
dtb = relocate_dtb_address(dtb); // 若 dtb 与镜像重叠则搬走
of_init(dtb); // 解析 device tree
of_get_console_name(&name);
console_init(name); // 初始化串口(能打印了)
boot_main(); // ★ 真正启动内核
}

要点:汇编开完 MMU 后,先解决两件事——dtb 落地 + 串口能打印(后续 log 都靠它)。

3.2 boot_main:C 世界初始化主流程

源码位置:minos.c:45-96

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void boot_main(void)
{
allsymbols_init(); // 符号表(kgdb/backtrace 用)
percpu_init(0); // percpu 区域初始化

kernel_vspace_init(); // hypervisor 自身虚拟地址空间
mm_init(); // 物理内存分配器

of_init_bootargs(); // bootargs

early_init(); // ① 早期架构初始化
early_init_percpu();

arch_init(); // ② 架构层
arch_init_percpu();

platform_init(); // ③ 平台(QEMU/FVP...)
irq_init(); // ④ 中断控制器(GIC)

smp_init(); // ⑤ 多核

subsys_init(); // ⑥ 子系统
subsys_init_percpu();

module_init(); // ⑦ 模块(vmodule 等)
module_init_percpu();

sched_init(); // ⑧ 调度器
local_sched_init();

device_init(); // ⑨ 设备驱动
device_init_percpu();

ramdisk_init(); // ⑩ ramdisk(加载 guest 镜像来源)

create_idle_task(); // 创建 idle 任务

smp_cpus_up(); // 唤醒其他核

virt_init(); // ⑪ ★ 虚拟化初始化(创建 VM)
cpu_idle(); // ⑫ 进入 idle,等待调度
}

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:158 subsys_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
2
3
4
5
6
7
8
9
boot_main()
├─ 第 2~4 节全部:hypervisor 自身初始化(MMU/栈/GIC/timer/sched)
│ ← 和普通 OS 启动逻辑几乎一样,没有 guest 动作
├─ virt_init() ← ★ 真正的分界线,guest 相关从这里开始
│ ├─ vmm_init() 为 guest 建内存池
│ ├─ vm_daemon_init() 建 VM 守护线程
│ ├─ parse_and_create_vms() 从 dts 解析 VM → 创建 host VM
│ └─ start_all_vm() 启动 VM:创建 vCPU / 建 Stage2 / 进入 guest
└─ cpu_idle()

结论:第 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
2
3
4
5
6
7
8
9
10
11
12
13
14
static int __init_text aarch64_init_percpu(void)
{
reg = read_CurrentEl();
pr_notice("current EL is %d\n", GET_EL(reg));
#ifdef CONFIG_VIRT
reg = read_sysreg64(HCR_EL2);
reg |= HCR_EL2_IMO | HCR_EL2_FMO | HCR_EL2_AMO; // ★
write_sysreg64(reg, HCR_EL2);
dsb();
isb();
#else
write_sysreg64(0x3 << 20, CPACR_EL1);
#endif
}

要点(虚拟化关键):设 HCR_EL2.IMO/FMO/AMO —— 让物理 IRQ/FIQ/SError 直接路由到 EL2。不设的话,物理中断根本送不进 EL2,hypervisor 无法接管(这是虚拟化能工作的前提之一)。

4.2 arch_early_init / __arch_init

  • arch_early_initof_setup_platform(),从 dtb 识别平台。
  • __arch_initof_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_secondaryis_cpus_all_up
最终进 idle cpu_startup_entrycpu_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
2
3
4
5
6
drop_to_el1:
adr x1, minos_os_start
msr ELR_EL2, x1
mov x1, #(AARCH64_SPSR_EL1h | ...) // ★ 返回后 EL1
msr SPSR_EL2, x1
eret // 降到 EL1 继续跑

Minos 式(强制留 EL2)——boot.S:90-91

1
2
#ifdef CONFIG_VIRT
b.ne minos_panic // ★ 虚拟化:必须在 EL2 启动,否则 panic

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
2
3
4
5
6
7
传统 Linux:      硬件 ← 管理 → 应用(进程)

EL1 (为"普通程序"提供环境)

Minos: 硬件 ← 管理 → guest OS(Linux/Android/RTOS...)

EL2 (为"操作系统"提供环境)

**关键在”服务对象”**:

  • 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)