Boot and trap
好的,我们来一次最详尽的、端到端的旅程,从机器上电开始,到xv6内核初始化,再到第一个用户进程发起trap 以及内核如何处理内核自身的trap,最后返回用户态。
阶段一:启动与内核初始化
- 机器上电 ->
_entry(kernel/entry.S)- 前提:RISC-V机器的引导加载程序(Bootloader,如QEMU的内置BIOS)已经将xv6内核加载到物理地址0x80000000处。
- 跳转:Bootloader跳转到内核的入口点,即
_entry。 - 设置初始栈:C语言代码需要栈才能运行。
_entry的第一项任务就是设置一个临时的内核栈。它将sp(stack pointer)寄存器指向stack0内存区域的顶部。stack0在kernel/start.c中被定义为一个静态数组,为早期启动代码提供栈空间。 - 调用
start:设置好栈后,_entry通过call start指令,跳转到kernel/start.c中的start函数。
- M-mode -> S-mode的切换 (kernel/start.c)
start函数是内核的第一个C函数,但它运行在最高权限级别——Machine Mode (M-mode)。它的核心任务是配置好硬件,然后从M-mode切换到S-mode(Supervisor Mode),因为整个xv6内核都设计为在S-mode下运行。- 配置trap:
- 它将所有中断和异常委托(delegate)给S-mode处理。这是通过设置
medeleg(Machine Exception Delegation)和mideleg(Machine Interrupt Delegation)寄存器完成的。这意味着当一个trap发生时,硬件会直接在S-mode下处理,而不是先在M-mode中捕获。 - 设置时钟中断。
- 它将所有中断和异常委托(delegate)给S-mode处理。这是通过设置
- 设置返回点:它将
main函数的地址(kernel/main.c中)写入mepc寄存器(Machine Exception Program Counter)。 - 设置返回权限:它在
mstatus寄存器中设置MPP字段为S-mode,告诉硬件mret指令执行后应该进入S-mode。 - 执行
mret:这是关键的切换指令。硬件会:- 跳转到
mepc指向的地址,即main函数。 - 将权限级别从M-mode降为S-mode。
- 跳转到
- 内核服务初始化 (kernel/main.c)
- 现在CPU处于S-mode,并开始执行
main函数。 main函数会依次调用一系列*_init函数,初始化内核的各个子系统:kinit():初始化物理内存分配器。kvminit():创建内核页表。kvminithart():启用分页机制(将内核页表地址写入satp寄存器),从此地址都是虚拟地址。procinit():初始化进程表。trapinit():空函数,占位。trapinithart():设置stvec指向kernelvec。这是我们讨论过的关键点,确保内核态发生的任何trap都由kernelvec处理。plicinit()/plicinithart():初始化中断控制器。binit()、iinit()、fileinit():初始化文件系统相关模块。userinit():创建第一个用户进程。scheduler():启动调度器,永不返回。
- 现在CPU处于S-mode,并开始执行
阶段二:第一个用户进程的诞生与运行
- 创建
init进程 (kernel/proc.c)userinit函数负责创建第一个进程,它将执行一小段汇编程序initcode.S。- 它调用
allocproc分配一个进程结构体和内核栈。 - 它为进程分配一页物理内存,并将
initcode.S的二进制码复制进去。 - 精心构造Trapframe:它设置进程的trapframe (
p->trapframe),这相当于一个“假”的trap现场,为将来返回用户态做准备:epc(返回地址)设置为0,因为initcode被加载在进程虚拟地址空间的0地址处。sp(用户栈)设置为页的顶部。
- 最后将进程状态设为
RUNNABLE。
- 调度与第一次进入用户态
main函数调用scheduler(),它在一个死循环里寻找RUNNABLE的进程。它找到了init进程。scheduler通过swtch切换到init进程的内核栈,然后调用usertrapret。usertrapret(kernel/trap.c) 是进入用户态的闸门:w_stvec((uint64)uservec):将stvec切换到uservec,为即将到来的用户态trap做好准备。- 准备
sstatus寄存器的值,以便sret后能开启中断并进入U-mode。 - 调用
userret(kernel/trampoline.S),它从trapframe中恢复所有通用寄存器。 - 执行
sret:CPU切换到U-mode,开始从init进程的虚拟地址0处执行代码。
阶段三:用户态Trap与内核处理
- 用户进程发起Trap(例如
ecall)initcode.S执行ecall指令以调用exec系统调用。- 硬件响应:
- CPU处于U-mode,
stvec指向uservec。 - 硬件自动切换到S-mode,保存pc到
sepc,保存原因到scause,然后跳转到stvec指向的uservec。
- CPU处于U-mode,
uservec->usertrapuservec(trampoline.S) 保存所有用户寄存器到trapframe,然后跳转到usertrap(trap.c)。usertrap做的第一件事就是w_stvec((uint64)kernelvec),将stvec切回kernelvec,确保在内核处理期间的安全性。- 它判断
scause是系统调用,于是调用syscall()函数,最终执行sys_exec。
阶段四:内核态Trap的处理(假设内核有Bug)
- 内核代码触发Trap
- 假设在执行
sys_exec的过程中,内核代码访问了一个非法的空指针。 - 硬件响应:
- CPU当前处于S-mode,
stvec指向kernelvec。 - 硬件自动保存pc(指向内核中出错的指令)到
sepc,设置scause(例如,load page fault),然后跳转到stvec指向的kernelvec。
- CPU当前处于S-mode,
- 假设在执行
kernelvec->kerneltrapkernelvec(kernel/kernelvec.S) 是一个精简版的uservec。它也保存所有寄存器(此时是内核寄存器),然后跳转到kerneltrap(kernel/trap.c)。kerneltrap函数是内核错误的终点站。它会打印出详细的错误信息(scause,sepc,stval),然后调用panic()。panic()会打印”panic: “前缀和错误信息,然后使系统停机(进入一个无限循环)。因为内核自身的错误是不可恢复的。
阶段五:正常返回用户态
- 从
usertrap返回- 假设内核没有bug,
syscall()正常返回到usertrap。 usertrap调用usertrapret。usertrapret再次将stvec切换回uservec,并调用userret。userret恢复所有用户寄存器。- 执行
sret:CPU切换回U-mode,从sepc指定的地址(ecall的下一条指令)继续执行用户代码。
- 假设内核没有bug,
这个完整的闭环展示了xv6如何利用stvec的动态切换,
为S-mode和U-mode下的trap分别设置不同的处理流程,从而清晰地隔离了用户错误和致命的内核错误。