Skip to content

启动流程

启动流程是从硬件上电到内核正常运行的过程。理解启动流程是理解内核如何一步步建立运行环境的关键。

启动的本质挑战

硬件启动时的状态

当 QEMU 启动 RISC-V virt 平台时:

  1. 内存是空的:没有内核代码
  2. CPU 在 M-Mode:最高特权级
  3. 没有栈:无法调用函数
  4. 没有中断:中断系统未初始化
  5. 没有页表:虚拟内存未启用

内核启动的任务是将系统从这种"原始状态"引导到"可运行状态"。

需要解决的问题

  1. 如何加载内核:内核代码从哪里来?
  2. 如何切换特权级:从 M-Mode 到 S-Mode
  3. 如何设置栈:让 C 代码可以运行
  4. 如何初始化硬件:中断控制器、定时器等
  5. 如何初始化内存:页表、分配器

第一阶段:硬件启动与 OpenSBI

QEMU 的启动过程

1. QEMU 初始化虚拟硬件
2. 加载 OpenSBI 固件到 0x80000000
3. 设置 CPU 的 PC = 0x80000000
4. CPU 开始执行 OpenSBI(M-Mode)

OpenSBI 的作用

OpenSBI 是 RISC-V 的 M-Mode 固件,负责:

硬件初始化: - 配置中断控制器(PLIC) - 设置定时器 - 初始化串口

异常委托: - RISC-V 允许 M-Mode 将异常委托给 S-Mode - OpenSBI 设置 medelegmideleg 寄存器 - 这样内核(S-Mode)可以直接处理中断

加载内核: - OpenSBI 从 QEMU 的 -kernel 参数读取内核路径 - 将内核加载到 0x80200000

切换到 S-Mode

1. 设置 mepc = 0x80200000(内核入口)
2. 设置 mstatus.mpp = S-Mode
3. 设置 a0 = hartid, a1 = fdt 地址
4. 执行 mret 指令

mret 会: - 跳转到 mepc 指向的地址 - 切换到 mstatus.mpp 指定的特权级

为什么需要 OpenSBI?

直接在 M-Mode 运行内核是可行的,但:

  1. M-Mode 代码难以调试
  2. 需要处理更多硬件细节
  3. 不符合 RISC-V 的设计哲学

OpenSBI 提供了标准的 M-Mode 服务,让内核专注于 S-Mode 功能。

第二阶段:内核入口(汇编)

为什么用汇编?

C 函数的隐含需求: - 需要栈(局部变量、返回地址) - 需要初始化的全局变量(.data 段) - 需要清零的 BSS 段

但在启动时: - 没有设置栈指针 - BSS 段未清零

所以必须用汇编完成这些准备工作。

_entry 的工作

1. 计算栈位置
   sp = boot_stack_top - hartid * STACK_SIZE

2. 调用 main 函数
   call main

关键设计:每个 CPU 核有独立的栈。

为什么?如果多核共享栈: - CPU 0 开始执行,使用栈顶 - CPU 1 也开始执行,也使用栈顶 - 冲突!数据互相覆盖

通过 hartid 计算偏移,每个核有独立的栈区域。

为什么先设置栈再清 BSS?

因为: - clear_bss 是 C 函数,需要栈 - 必须先设置栈,才能调用 C 函数

第三阶段:main 函数初始化

初始化的依赖关系

初始化顺序由依赖关系决定:

init_cpu
    │ 初始化 CPU 结构,设置 tp 寄存器
    │
pm_init
    │ 初始化物理内存管理
    │ 后续的 kmalloc 才能工作
    │
plic_init
    │ 初始化中断控制器
    │ 后续才能处理外部中断
    │
trap_init
    │ 设置中断向量
    │ 后续中断才能被处理
    │
timer_init
    │ 初始化定时器
    │ 设置第一次定时器中断
    │
kvminit
    │ 创建内核页表
    │ 开启虚拟内存
    │
init_runq
    │ 初始化运行队列
    │ 后续才能调度
    │
blk_init / bcache_init / virtio_init
    │ 初始化块设备和缓存
    │ 后续才能访问磁盘
    │
vfs_init / ramfs_init
    │ 初始化文件系统
    │ 后续才能操作文件

为什么是这个顺序?

例子 1:为什么 pm_initkmalloc 之前?

因为 kmalloc 需要分配器工作,分配器由 pm_init 初始化。

例子 2:为什么 trap_inittimer_init 之前?

因为定时器会触发中断,中断需要 trap_init 设置的处理函数。

例子 3:为什么 sched_yield 在最后?

因为调度需要: - 运行队列(init_runq) - 当前进程状态(init_cpu) - 中断处理(trap_init

重要的标志:sched_enabled

volatile bool sched_enabled = false;

这个标志控制是否允许调度。在初始化完成前为 false,避免: - 定时器中断触发过早的调度 - 未初始化的子系统被访问

初始化完成后设为 true,调度器开始工作。

启动的关键设计

汇编与 C 的分界

汇编负责: - 设置栈指针 - 调用 C 入口函数

C 负责: - 所有初始化逻辑 - 硬件配置 - 数据结构初始化

这个分界最小化了汇编代码,提高了可维护性。

单核启动策略

当前只让 CPU 0 执行初始化,其他 CPU 等待:

if (hartid == 0) {
    // 初始化
} else {
    while (1) { wfi(); }
}

为什么?简化设计。多核启动需要: - 核间同步 - 共享数据保护 - 更复杂的初始化逻辑

单核足以理解内核原理。

idle 进程的作用

每个 CPU 有一个内置的 idle 进程。当没有其他进程可运行时,CPU 执行 idle:

void idle_loop(void) {
    while (1) {
        wfi();         // 等待中断,省电
        sched_yield(); // 尝试调度
    }
}

wfi 让 CPU 进入低功耗状态,直到中断到来。这比空循环更节能。

BSS 段清零的必要性

C 标准规定未初始化的全局变量值为 0。但内存加载后内容是随机的,必须手动清零。

不清零的后果:

int counter;  // 应该是 0,实际是随机值
if (counter == 0) { /* 可能不执行 */ }

启动时的内存状态

启动前

0x80000000: OpenSBI 代码
0x80200000: 内核镜像(刚加载)
0x80XXXXXX: 随机数据(BSS 段未清零)

启动后

0x80000000: OpenSBI 代码
0x80200000: 内核镜像
    .text: 代码
    .rodata: 只读数据
    .data: 已初始化数据
    .bss: 已清零
0x80XXXXXX: 页表、内核栈、分配器

谁设置了什么?

  • OpenSBI:加载内核镜像
  • 内核汇编:设置栈
  • 内核 C:清 BSS,初始化内存管理

多核启动的挑战

当前缺失的功能

多核启动需要:

核间中断(IPI): - CPU 0 完成初始化后唤醒其他核 - 通过 SBI 的 sbi_send_ipi

Per-CPU 变量: - 每个核有自己的变量副本 - 避免锁竞争

同步机制: - 确保多核访问共享数据的正确性

多核启动流程(概念)

1. CPU 0 启动,其他核等待
2. CPU 0 完成初始化
3. CPU 0 发送 IPI 唤醒其他核
4. 其他核设置自己的栈和 tp
5. 其他核初始化自己的中断
6. 所有核参与调度

常见启动问题

1. 内核未加载

症状:QEMU 启动后无输出

原因: - 内核路径错误 - 内核格式错误

调试:检查 -kernel 参数和 kernel.asm

2. 栈溢出

症状:随机崩溃

原因:初始化函数调用层次深,或局部变量大

解决:增大 BOOT_STACK_SIZE

3. 初始化顺序错误

症状:访问空指针或死锁

原因:使用未初始化的模块

调试:在初始化函数中添加日志

4. 中断过早触发

症状:在 trap_init 之前崩溃

原因:硬件中断(如定时器)过早触发

解决:确保 trap_init 尽早执行

设计权衡

为什么用 OpenSBI 而不是自己写 M-Mode 代码?

OpenSBI 优点: - 成熟稳定 - 标准接口 - 支持多平台

自己实现的代价: - 工作量大 - 难以调试 - 不通用

为什么单核而不是多核?

教学目的:单核足以理解核心概念

多核复杂度: - 并发问题 - 缓存一致性 - 调试困难

为什么用简单的 FIFO 调度?

简单:易于理解和实现

公平:每个进程平等获得 CPU

代价:无法区分优先级

思考题

  1. 如果忘记清零 BSS 段,会出现什么问题?如何调试?
  2. 为什么 OpenSBI 需要设置异常委托?
  3. 如果初始化顺序错误,会发生什么?
  4. 多核启动时,CPU 0 如何通知其他核初始化完成?

下一步