xv6操作系统启动过程

  1. 当按下系统电源按键后,做一些硬件层面的配置和初始化:

上电复位:在开机时,计算机进行硬件复位,确保寄存器和其他硬件组件处于初始状态。
检查和测试硬件:计算机进行一系列硬件检查和自检操作,以确保硬件组件功能正常。这可能包括内存检测、CPU测试等。
初始化硬件组件:初始化和配置计算机上的各种硬件组件,例如内存控制器、输入输出设备和其他外设。这包括为这些组件提供电源、设置时钟信号等。
读取配置数据:从非易失性存储器(例如CMOS)读取系统配置数据,如BIOS设置、引导设备顺序等。
这些操作通常由计算机的固件(例如BIOS或UEFI)完成。完成硬件和固件的初始化后,

  1. 计算机将运行引导加载程序(boot loader),该程序存储在只读存储器(ROM)中。引导加载程序负责将xv6内核加载到内存,并将执行权交给内核。

Machine mode中,cpu执行内核的起点是 _entry,这个时候虚拟内存还没有工作,所以此时虚拟地址是和物理地址直接映射的

	# 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
.global _entry
_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 # 将当前CPU的硬件线程ID(hartid)读取到寄存器a1中。这个ID是唯一的,用于区分不同的CPU。
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
	# jump to start() in start.c
        call start
spin:
        j spin

这段汇编代码中,设置了stack0这个栈,这样就可以调用C代码了。sp栈指针指向stack0 + 4096这个地址(栈顶)。然后调用start(call start)

  1. 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);

  // configure Physical Memory Protection to give supervisor mode
  // access to all of physical memory.
  w_pmpaddr0(0x3fffffffffffffull);
  w_pmpcfg0(0xf);

  // 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");
}

先做一些只能在Machine mode做的配置,然后进入Supervisor mode.
riscv提供了mret这个指令来实现这个操作。
mret指令的功能是:从Supervisor mode进入Machine mode时,在Machine mode调用这个mret指令,将会返回到Supervisor mode。
此时,虽然在Machine mode,但是并不是从Supervisor mode进来的。

所以,将mstatus设置为S mode,表示此前的特权模式是 supervisor。
然后,把返回地址设置为main,通过把main的地址写到mepc
然后,禁止虚拟地址翻译,通过把0这个值写入satp(页表寄存器)
然后,把所有的中断和异常交给S mode
然后,对时钟芯片进行编程,以便产生定时器中断。

对时钟芯片编程意味着配置时钟芯片的相关寄存器,以便在特定时间间隔后产生定时器中断。定时器中断在操作系统中非常重要,因为它们使操作系统能够实现多任务调度、计时等功能。操作系统可以根据需要设置定时器中断的间隔,从而控制时间片长度或其他相关参数。

在xv6操作系统中,对时钟芯片编程主要涉及以下几个步骤:

选择一个合适的时间间隔,用于触发定时器中断。这通常基于处理器的时钟频率和期望的中断频率来计算。
将计算出的时间间隔值写入时钟芯片的相关寄存器。在RISC-V平台上,这通常涉及编程mtimecmp寄存器。mtimecmp寄存器存储了下一个定时器中断的时间值。每当mtime寄存器的值达到mtimecmp寄存器的值时,时钟芯片就会产生一个定时器中断。
配置处理器以接收时钟芯片产生的定时器中断。这可能涉及设置中断控制器的相关寄存器,例如启用定时器中断并配置优先级等。
完成上述操作后,时钟芯片将定期产生定时器中断。操作系统会在每个中断发生时执行特定的中断处理程序,例如更新系统计时器、调度进程等。这使得操作系统能够有效地进行多任务处理、资源管理和时间跟踪。

设置时钟芯片的代码(暂时不重要)

// set up to receive timer interrupts in machine mode,
// which arrive at timervec in kernelvec.S,
// which turns them into software interrupts for
// devintr() in trap.c.
void
timerinit()
{
  // each CPU has a separate source of timer interrupts.
  int id = r_mhartid();

  // ask the CLINT for a timer interrupt.
  int interval = 1000000; // cycles; about 1/10th second in qemu.
  *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

  // prepare information in scratch[] for timervec.
  // scratch[0..2] : space for timervec to save registers.
  // scratch[3] : address of CLINT MTIMECMP register.
  // scratch[4] : desired interval (in cycles) between timer interrupts.
  uint64 *scratch = &timer_scratch[id][0];
  scratch[3] = CLINT_MTIMECMP(id);
  scratch[4] = interval;
  w_mscratch((uint64)scratch);

  // set the machine-mode trap handler.
  w_mtvec((uint64)timervec);

  // enable machine-mode interrupts.
  w_mstatus(r_mstatus() | MSTATUS_MIE);

  // enable machine-mode timer interrupts.
  w_mie(r_mie() | MIE_MTIE);
}

最后,返回到main

  1. main
    main函数初始化设备和子系统;main调用userinit创建第一个进程;第一个进程执行initcode.S中的RISC-V汇编代码。
// Set up first user process.
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);
}

initcode.S加载SYS_EXEC到寄存器a7。
initcode.S调用ecall指令,使CPU重新进入内核。

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

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        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

当ecall指令被执行时,CPU将重新进入内核,处理发起的exec系统调用。exec系统调用用于加载和运行一个用户程序,替换当前进程的内存映像。在这种情况下,第一个进程将加载和运行xv6操作系统中定义的初始用户程序。

在这段代码中,exec读取到的file是init,这就是初始化程序,创建了一个新的console,和一些fd。打开fd 0,1,2,打开shell。

posted @ 2023-04-20 18:58  ijpq  阅读(215)  评论(0编辑  收藏  举报