操作系统MIT6.S081:启动xv6和第一个进程

前言:本文介绍了机器的基本配置,内核在刚开始时被加载在内存地址的何处。然后介绍了第一个进程的创建过程和一些系统调用的过程。

1.启动操作系统

1.1基本配置

QEMUOPTS = -machine virt -bios none -kernel $K/kernel -m 128M -smp $(CPUS) -nographic
//Makefile 200行处,

命令make qemu ,首先会把kernel/子目录下的源文件编译成可执行文件kernel/kernel(内核编译:Makefile会读取所有kernel下的.c文件。然后调用gcc编译器,生成对应的.s文件,这个是RISC-V汇编语言文件。之后在调用汇编解释器,生成.o文件,这个是汇编语言的二进制格式。最后,系统加载器(Loader)会收集所有的.o文件,将它们链接在一起,并生成内核文件。), 然后使用一系列命令行参数启动模拟器qemu. 上面的命令中配置了:

  • -machine virt  指定了模拟器的机器类型为virt
  • -bios none  告知模拟器不启用任何 BIOS, 即由用户决定将什么内容加载到内存
  • -m 128M  指定了我们给模拟器分配的内存大小
  • -smp $(CPUS)  指定了我们给模拟器分配的多少的CPU核心数

 1.2 xv6在哪里开始

当RISC-V计算机上电时,它会初始化自己并运行一个存储在只读内存中的引导加载程序。引导加载程序将xv6内核加载到内存中。qemu会把物理内存的起始地址设置为0x80000000。而我们给模拟器分配了128M的内存,所以我们拥有的物理内存地址空间为0x80000000-0x88000000。我们的xv6内核(也就是kernel/kernel)会被加载到内存地址0x80000000开始的一片物理内存区域,然后将 PC 设置为可执行文件的入口地址。

内核的入口地址在kernel/kernel.ld中规定:

OUTPUT_ARCH( "riscv" )
ENTRY( _entry )

1.3 kernel/entry.S

1.2说到引导加载程序将xv6内核加载到内存地址0x80000000开始的一片物理内存区域。然后,在机器模式下,中央处理器从_entry(kernel/entry.S:6)开始运行xv6。Xv6启动时页式硬件(paging hardware)处于禁用模式:也就是说虚拟地址将直接映射到物理地址。

	# qemu -kernel loads the kernel at 0x80000000
        # and causes each CPU to jump there.
        # kernel.ld causes the following code to
        # be placed at 0x80000000.
.section .text
_entry:
	# set up a stack for C.
        # stack0 is declared in start.c,
        # with a 4096-byte stack per CPU.
        # sp = stack0 + (hartid * 4096)
        la sp, stack0
        li a0, 1024*4
	csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
	# jump to start() in start.c
        call start
spin:
        j spin

 其中:

  • la sp, stack0:将栈顶指针寄存器sp初始化为stack0的地址。stack0是kernel/start.c中定义的一个数组。
// entry.S needs one stack per CPU.
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

#  其中 NCPU = 8 表示最大可支持的 CPU 数. 也就是说, 
# 我们 为每个 CPU 分配独立的 4096 字节的栈区. 
# 这些栈区占据一段连续的内存, 其起始地址为 stack0.

# 在RISC_V中,栈是向下扩展的,
# 所以_entry的代码将栈顶地址stack0+4096加载到sp中
  • 接下来的6行代码
li a0, 1024*4
csrr a1, mhartid
mul a0, a0, a1
add sp, sp, a0
# jump to start() in start.c
call start

# 意思是sp += 4096 * (mhartid + 1).
# 也就是说, 对于每个 CPU, 我们让sp指向他自己的栈顶.

#现在内核有了栈区,_entry便调用C代码start(kernel/start.c)

1.4 start()

// entry.S jumps here in machine mode on stack0.
void
start()
{
  // set M Previous Privilege mode to Supervisor, for mret.
  unsigned long x = r_mstatus();
  x &= ~MSTATUS_MPP_MASK;
  x |= MSTATUS_MPP_S;
  w_mstatus(x);

  // set M Exception Program Counter to main, for mret.
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

  // disable paging for now.
  w_satp(0);

  // delegate all interrupts and exceptions to supervisor mode.
  w_medeleg(0xffff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  // ask for clock interrupts.
  timerinit();

  // keep each CPU's hartid in its tp register, for cpuid().
  int id = r_mhartid();
  w_tp(id);

  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

 函数start执行一些仅在机器模式下允许的配置,然后切换到管理模式。start通过调用mret“返回”到管理模式。这将导致程序计数器(PC)的值更改为main(kernel/main.c:11)函数地址。

2. 启动第一个进程

2.1 kernel/main.c

#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "riscv.h"
#include "defs.h"

volatile static int started = 0;
/*
* xv6 维护了一个 flag:volatile static int started = 0
*当 started == 0 时, 除了核心号为 0 的 CPU 在执行, 其余 CPU 都在空转. 
*直到核心号为 0 的 CPU 把该做的事情都做完了, 它才把 started 设为 1, 
*让其他核心启动并完成它们各自的初始化工作.
*/


// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // 设置好页表分配器(page allocator)
    kvminit();       // 设置好虚拟内存
    kvminithart();   // 打开页表
    procinit();      // 设置好初始进程或者说设置好进程表单
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // 分配buffer cache
    iinit();         // 初始化inode缓存
    fileinit();      // 初始化文件系统
    virtio_disk_init(); // 初始化磁盘
    userinit();      // 最后当所有的设置都完成了,操作系统也运行起来了,
                     // 会通过userinit运行第一个进程
    __sync_synchronize();
    started = 1;
  } else {
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();        
}

2.2 userinit() (kernel/pro.c)

// a user program that calls exec("/init")
// od -t xC initcode
# 这这里直接是程序的二进制形式,它会链接或者在内核中直接静态定义。
# 实际上,这段代码对应了2.3的initcode.S汇编程序。
uchar initcode[] = {
  0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
  0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,
  0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,
  0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,
  0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,
  0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00
};

// Set up first user process.
#userinit有点像是胶水代码/Glue code(胶水代码不实现具体的功能,只是为了适配不同的部分而存在),#它利用了XV6的特性,并启动了第一个进程。我们总是需要有一个用户进程在运行,
#这样才能实现与操作系统的交互,所以这里需要一个小程序来初始化第一个用户进程。
#这个小程序定义在initcode中。
void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}

 userinit会创建初始进程,返回到用户空间,执行2.3initcode.S中的3条指令,再回到内核空间。

2.3 initcode.S

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init         //将init中的地址加载到a0
        la a1, argv         //argv中的地址加载到a1
        li a7, SYS_exec     //exec系统调用对应的数字加载到a7
        ecall               //最后调用ECALL

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

2.4 syscall() (kernel/syscall.c)

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

这里syscall通过 num = p->trapframe->a7读取使用的系统调用对应的整数。然后由syscalls[num]()进入相应的系统调用.根据2.3可知,要运行的系统调用是sys_exec().

uint64
sys_exec(void)
{
  char path[MAXPATH], *argv[MAXARG];
  int i;
  uint64 uargv, uarg;

  if(argstr(0, path, MAXPATH) < 0 || argaddr(1, &uargv) < 0){
    return -1;
  }
  memset(argv, 0, sizeof(argv));
  for(i=0;; i++){
    if(i >= NELEM(argv)){
      goto bad;
    }
    if(fetchaddr(uargv+sizeof(uint64)*i, (uint64*)&uarg) < 0){
      goto bad;
    }
    if(uarg == 0){
      argv[i] = 0;
      break;
    }
    argv[i] = kalloc();
    if(argv[i] == 0)
      goto bad;
    if(fetchstr(uarg, argv[i], PGSIZE) < 0)
      goto bad;
  }

        sys_exec中的第一件事情是从用户空间读取参数,它会读取path,也就是要执行程序的文件名。这里首先会为参数分配空间,然后从用户空间将参数拷贝到内核空间。 这里要执行的是init程序。

        综合来看,通过上述程序,initcode完成了通过exec调用init程序。

2.5 user/init.c

// init: The initial user-level program

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/spinlock.h"
#include "kernel/sleeplock.h"
#include "kernel/fs.h"
#include "kernel/file.h"
#include "user/user.h"
#include "kernel/fcntl.h"

char *argv[] = { "sh", 0 };

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    printf("init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf("init: fork failed\n");
      exit(1);
    }
    if(pid == 0){
      exec("sh", argv);
      printf("init: exec sh failed\n");
      exit(1);
    }

    for(;;){
      // this call to wait() returns if the shell exits,
      // or if a parentless process exits.
      wpid = wait((int *) 0);
      if(wpid == pid){
        // the shell exited; restart it.
        break;
      } else if(wpid < 0){
        printf("init: wait returned an error\n");
        exit(1);
      } else {
        // it was a parentless process; do nothing.
      }
    }
  }
}

init会为用户空间设置好一些东西,比如配置好console,调用fork,并在fork出的子进程中执行shell。

  • 32
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值