|
最近在学习xv6-riscv的源码,也顺便在编写解读源码的指导手册,开坑记录一下学习过程。编写过程中主要的参考有:
下面先从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.c的uchar initcode[]中展现了这个程序的二进制形式,事实上它对应了一段汇编代码,在user/initcode.S中。它通过调用exec系统调用来重新进入内核,然后exec会用一个新程序/init来替换当前进程的內存和寄存器,一旦exec完成,就会返回/init进程中的用户空间。init会在控制台上启动一个shell,具体细节可以在user/init.c中RTFSC。
在这里,还有一句奇怪的代码:
__sync_synchronize();这个函数是一个内存屏障,告诉编译器和CPU不要越过屏障重排load和store指令,也就是说将这条语句之前和之后的读写指令分隔开。这么做是因为编译器在编译的过程中可能会做一些优化,导致代码的顺序发生一些变化,使用这条语句相当于加了一个锁。
最后,每一个CPU都会执行scheduler函数,用于进行任务调度。 |
|