xv6源码解读1:初探xv6
⌨️

xv6源码解读1:初探xv6

Last edited time
2023-05-20 / 02:13 AM
Created time
2023-04-23 / 14:42 PM
Tags
Operating Systems
这是我阅读xv6源码过程中对其进行解读的笔记。

第1篇:初探xv6

xv6的接口

xv6是一个类Unix的教学操作系统,既然它是一个操作系统,那么它就可以通过接口来向用户程序提供服务。xv6使用传统的内核形式,即内核是一个特殊的程序,为正在进行的程序提供服务。内核所提供的系统调用就是用户程序看到的接口,通过接口用户程序可以获得操作系统的服务。

xv6的架构

xv6-riscv的源代码可以从此处获取,其中,内核源代码位于kernel/子目录中,源代码按照模块化划分为多个文件,模块间的接口都定义在了kernel/defs.h

启动吧,xv6!

作为一个操作系统,当然是要启动了才拥有它无比强大的能力,接下来我们来看看xv6的启动。
首先,在RISC-V计算机打开电源上电之后,它会初始化自己并运行一个在只读内存中的boot loader。Boot loader将xv6的内核加载到物理地址为0x80000000內存中,至于为什么不从0x0开始,那是因为在0x0~0x80000000的地址范围里包含了I/O设备。
然后在machine mode下,CPU从_entry(位于kernel/entry.S)开始运行xv6。我们来看看这段代码。
.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
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
        # jump to start() in start.c
        call start
spin:
        j spin
可以看到,这是一段汇编代码,会做一些必要的事情来启动xv6,比如,设置一个栈区,这样就xv6就可以运行C代码。关于初始栈stack0的空间声明,我们可以在kernel/start.c中找到代码。
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
这段代码看起来有些奇怪,但不必太在意,它只是表明了每个CPU的栈是4096个字节。RISC-V的栈也是向下扩展的,高地址为栈底,低地址为栈顶,所以sp = stack0 + (hartid * 4096),将高地址加载到sp寄存器中。
在上面的汇编代码中,注释里有一个hartid,事实上这是hart的编号。什么是hart?RISC-V处理器对底层提供了一种抽象,叫Hardware Thread,简称hart,中文可以翻译为硬件线程。可以把hart理解为是真实CPU提供的一种模拟,关于hart、core、CPU的一些区别并不是操作系统层面需要关心的,我们在这里可以简单地将三者视为同样的概念,把hartid看作是cpuid。
经过一段处理之后,程序跳转到了函数start(位于kernel/start.c)中。
函数start执行一些仅在machine mode下允许的配置,然后才会切换到supervisor mode,下面我们来看看该函数的代码。
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");
}
在这里我们可以看到,函数使用指令mret进入supervisor mode之前,进行了一些工作。
首先是切换工作模式,r_mstatus()函数的功能其实是相当于执行了一个csrr指令,读取了mstatus寄存器的值存储在了一个变量x中。接下来是对寄存器中的位进行修改,此处修改涉及到了RISC-V中mstatus寄存器的结构,有兴趣的可以自己RTFM。修改完之后,同样的,w_mstatus()函数的功能其实是相当于执行了一个csrw指令,将x值写入了mstatus寄存器中。
接下来的工作大同小异,主要是在处理一些寄存器的值,进行一些设置。将main函数的地址写入mepc寄存器,由此将返回地址设为main函数,以便于在main函数中执行代码。向页表寄存器satp写入0来禁止虚拟地址转换,然后赋予supervisor mode对所有物理内存的访问权限,还有将中断和异常委托给supervisor mode。此外,还需要对时钟芯片进行编程以产生计时器中断。
上述其他行为的具体细节不再讲述,有兴趣的可以打开kernel/start.c,然后RTFSC。
最后,start就可以通过调用mret返回,然后进入到到supervisor mode了,此时PC的值将更改为main函数的地址。如果你对mret指令的行为感到好奇,可以去RTFM,然后你就会理解为什么start要做这么多看起来很复杂的工作了。
进入到main函数之后,我想你们已经很期待了,没错,我们马上就能启动xv6了!下面让我们来看看main函数。
void
main()
{
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    procinit();      // process table
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    plicinit();      // set up interrupt controller
    plicinithart();  // ask PLIC for device interrupts
    binit();         // buffer cache
    iinit();         // inode table
    fileinit();      // file table
    virtio_disk_init(); // emulated hard disk
    userinit();      // first user process
    __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();        
}
main函数中,启动操作系统之前我们需要做一些初始化配置。首先调用了consoleinit函数,事实上,这个函数内部有一个对UART进行初始化的操作,然后连接到读和写的系统调用。接着再对printf进行初始化,就可以在屏幕打印信息了,在这里,系统打印了:
xv6 kernel is booting
接下来,进行了对一些设备和系统中一些必要模块的初始化,具体细节可以RTFSC。完成了上面的初始化之后,就可以调用userinit函数来创建第一个用户进程了,我们总是需要有一个用户进程在运行,这样才能实现与操作系统的交互。第一个进程会执行一个小程序,kernel/proc.cuchar initcode[]中展现了这个程序的二进制形式,事实上它对应了一段汇编代码,在user/initcode.S中。它通过调用exec系统调用来重新进入内核,然后exec会用一个新程序/init来替换当前进程的內存和寄存器,一旦exec完成,就会返回/init进程中的用户空间。init会在控制台上启动一个shell,具体细节可以在user/init.c中RTFSC。
在这里,还有一句奇怪的代码:
__sync_synchronize();
事实上,这个函数是一个内存屏障,告诉编译器和CPU不要越过屏障重排loadstore指令,也就是说将这条语句之前和之后的读写指令分隔开。这么做是因为编译器在编译的过程中可能会做一些优化,导致代码的顺序发生一些变化,使用这条语句相当于加了一个锁。
最后,每一个CPU都会执行scheduler函数,用于进行任务调度。
你觉得怎么样?
YYDS
比心
加油
菜狗
views

Loading Comments...