以下任务每个CPU都执行
1. kernel/entry.S:_entry
qemu使用0x80000000
作为起始地址,_entry程序的任务是设置栈来使得xv6能够运行C代码(后续的C代码),因为初始没有栈的话即使是操作系统的C代码也不能执行(RISC-V的栈地址向下增长),现在可以运行C代码后,_entry调用kernel/start.c中的start程序。
2. kernel/start.c:start
在Machine Mode
模式下执行一些特定的寄存器配置,随后待用mret指令从M-Mode进入内核态
,并且跳转到内核态的main.c中的main函数
。(mret使用前提是上一次系统调用是从内核态到MM的,作用是从MM到内核态,虽然在启动过程没有这个前提,但通过设置一些变量和寄存器可以模拟这样的前提)
以下任务只有CPU0执行,当然指的是初始化部分,后面的shell被哪个CPU执行都可能
3. kernel/main.c:main
执行一些初始化工作如console初始化、内存映射、文件系统初始化,且保证只有一个cpu执行该工作(避免重复初始化),通过userinit()函数创建第一个用户进程
4. kernel/proc.c:userinit
init进程:分配第一个进程,初始化进程信息,最重要的是将initcode的硬编码的程序放到进程的虚拟地址的第一页,即执行initcode.S中的程序(在user/initcode.S汇编中定义),随后设置该进程为RUNNABLE
。其具体任务为:中执行exec,参数为init(init即为user/init.c)和argv,通过kernel/syscall.c:syscall进行执行(在syscall中打断点可以看到,num为7对应exec系统调用)
5. user/initcode.S:start
第一步跳转(实际上不是跳转,直接把initcode.o的二进制硬编码
然后复制到了内存里,让其被执行, initcode.S就是给人看的,另外走这个路径实际上是只有第一次会走,以后都是走usys的路径。当然从二进制的视角看的确也是跳转)至initcode.S即在4中要执行的程序,start程序负责设置一些参数,例如下一步要执行的程序(user/init.c)、参数列表、要执行的系统调用的编号(SYS_exec宏定义),最后通过ecall进入内核态执行exec程序(本质上是要执行user/init.c),在最终执行init之前,要先进入syscall函数
6. kernel/syscall.c:syscall
进入syscall函数后就可以获取进程所要执行的系统调用的syscall num(原始定义在kernel/syscall.h中),即通过syscalls[]数组获取要执行的指令,在此例中是exec,num为7,参数分别是/init和init,0(user/initcode.S)
7. kernel/sysfile.c:sys_exec
exec系统调用,将第一个进程覆盖,执行init.c中的程序
实际上这里的第一个进程都是在scheduler之后进行的,通过CPU调度!
8. user/init.c:main
init进程(覆盖后):真正的父进程,创建console设备,链接0,1,2文件描述符,打印信息,创建子进程启动shell,原进程负责回收子进程(wait系统调用):
1// init: The initial user-level program
2
3#include "kernel/types.h"
4#include "kernel/stat.h"
5#include "kernel/spinlock.h"
6#include "kernel/sleeplock.h"
7#include "kernel/fs.h"
8#include "kernel/file.h"
9#include "user/user.h"
10#include "kernel/fcntl.h"
11
12char *argv[] = { "sh", 0 };
13
14int
15main(void)
16{
17 int pid, wpid;
18
19 if(open("console", O_RDWR) < 0){
20 mknod("console", CONSOLE, 0);
21 open("console", O_RDWR);
22 }
23 dup(0); // stdout
24 dup(0); // stderr
25
26 for(;;){
27 printf("init: starting sh\n");
28 pid = fork();
29 if(pid < 0){
30 printf("init: fork failed\n");
31 exit(1);
32 }
33 if(pid == 0){
34 // 执行shell进程
35 exec("sh", argv);
36 printf("init: exec sh failed\n");
37 exit(1);
38 }
39 // 在此处回收子进程
40 for(;;){
41 // this call to wait() returns if the shell exits,
42 // or if a parentless process exits.
43 wpid = wait((int *) 0);
44 if(wpid == pid){
45 // the shell exited; restart it.
46 break;
47 } else if(wpid < 0){
48 printf("init: wait returned an error\n");
49 exit(1);
50 } else {
51 // it was a parentless process; do nothing.
52 }
53 }
54 }
55}
9. 小结
实际上第一个系统调用是initcode中的ecall来调用exec,执行的是init.c中的程序,由init.c负责创建shell:
硬编码进程
调用exec执行init进程
,init进程
创建shell进程
并负责回收子进程
还是要让代码跑起来看看才清楚是怎么启动的~