2.6 Code: starting xv6, the first process and system call
运行 xv6 系统的 RISC-V 计算机启动的全过程:
- 电脑开机并初始化自身, 然后运行存储在某个只读内存中的一个称为启动加载器 (boot loader) 的程序.
- 启动加载器将 xv6 内核加载至内存中的物理地址
0x80000000
(7 个零) 处, 然后 CPU 在机器模式 (machine mode) 下从_entry
入口 (位于文件kernel/entry.S
中) 开始运行 xv6.- RISC-V 机器一开始是没有启用页表功能的.
- 之所以不将 xv6 加载至物理地址
0x0
处是因为从0x0
开始到0x80000000
为止的这段物理内存中存储着与 I/O 设备有关的信息.
_entry
的作用是为 xv6 初始化一个用于执行 C 程序的栈. 为了运行 C 程序, xv6 需要使用一个栈, 为此 xv6 在文件start.c
中已经定义了一个名为stack0
的栈 (位于文件kernel/start.c
中)._entry
入口处的指令负责将堆栈指针寄存器sp
设置为stack0 + (hartid * 4096)
来为 xv6 初始化该栈. 在初始化好栈之后,_entry
将调用start
处的 C 代码 (位于文件kernel/start.c
中).- 注: 在 RISC-V 下栈顶是向低地址方向增长的.
start
处的 C 代码的主要任务是将 CPU 从特权等级最高的机器模式切换至管理员模式 (supervisor mode). RISC-V 提供了一个用于从机器模式返回管理员模式 (也有可能是其他模式, 具体由 MPP (Machine Previous Privilege) 寄存器字段指定) 的指令, 即mret
, 但在当前情况下机器根本就没有从管理员模式进入到过机器模式, 因为机器本来就是以机器模式启动的.start
处的代码负责的就是为mret
指令建立一个与 "机器刚刚从管理员模式进入机器模式并即将返回" 等价的环境, 其所执行的操作包括将寄存器mstatus
(用于指示上一个特权模式) 设置为管理员模式, 将 寄存器mepc
设置为main
函数 (位于文件kernel/main.c
中) 的入口地址, 将寄存器satp
的值设置为0
以暂时禁用虚拟地址转换机制, 最后将所有中断和异常全部委托给管理员模式. 做好这些准备工作之后,start
还将时钟芯片 (clock chip) 设置好以便产生计时器中断 (timer interrupt), 然后执行mret
使 CPU "返回" 至管理员模式, 同时程序计数器也相应变为main
函数的入口地址.main
函数进行诸多初始化过程 (内容较多, 此处省略, 具体请查看代码), 然后调用userinit
函数 (位于文件kernel/proc.c
中).userinit
函数将创建整个系统自开机以来的第一个进程 (通过调用allocproc
函数), 这个进程将执行initcode
数组中所包含的机器指令, 这些机器指令实际上就是文件user/initcode.S
中的汇编代码所对应的机器指令.- 可以将
userinit
函数看作是 "没有父进程" 的fork
函数, 因为userinit
的作用正相当于创建子进程, 而该子进程负责调用exec
系统调用, 加载/init
程序并执行. 所不同的是在userinit
中子进程的代码是直接复制于initcode
数组中的, 毕竟此时根本就没有父进程.
- 可以将
initcode.S
的工作是调用exec("/init")
, 具体操作为将字符串"/init"
以及代表exec
系统调用的整数代码SYS_EXEC
(位于文件kernel/syscall.h
中) 放置在寄存器中然后执行ecall
指令.ecall
指令将触发一次陷入, 经过蹦床 (trampoline) 代码的路由后最终将调用syscall
函数 (位于文件kernel/syscall.c
中).syscall
函数使用系统调用表 (system call table)syscalls
(位于文件kernel/syscall.c
中) 将SYS_EXEC
映射至sys_exec
函数的入口地址.sys_exec
函数即为exec
系统调用的实际实现, 它将加载指定的目标程序 (此处即init
) 替换当前进程的内存和寄存器. 当内核从exec
返回后, 计算机将回到用户态并开始执行init
程序.init
程序 (位于文件user/init.c
中) 将分配三个文件描述符, 即 stdin, stdout, 以及 stderr, 然后启动 shell. 至此整个系统便成功跑起来了.