headPic

6.S081:xv6 启动过程

OAP

以下任务每个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进程并负责回收子进程

还是要让代码跑起来看看才清楚是怎么启动的~


Previous post

6.S081:实验