这是我阅读xv6源码过程中对其进行解读的笔记。
第1篇:初探xv6
xv6的接口
xv6是一个类Unix的教学操作系统,既然它是一个操作系统,那么它就可以通过接口来向用户程序提供服务。xv6使用传统的内核形式,即内核是一个特殊的程序,为正在进行的程序提供服务。内核所提供的系统调用就是用户程序看到的接口,通过接口用户程序可以获得操作系统的服务。
xv6的架构
启动吧,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.c
的uchar initcode[]
中展现了这个程序的二进制形式,事实上它对应了一段汇编代码,在user/initcode.S
中。它通过调用exec
系统调用来重新进入内核,然后exec
会用一个新程序/init
来替换当前进程的內存和寄存器,一旦exec
完成,就会返回/init
进程中的用户空间。init
会在控制台上启动一个shell,具体细节可以在user/init.c
中RTFSC。在这里,还有一句奇怪的代码:
__sync_synchronize();
事实上,这个函数是一个内存屏障,告诉编译器和CPU不要越过屏障重排
load
和store
指令,也就是说将这条语句之前和之后的读写指令分隔开。这么做是因为编译器在编译的过程中可能会做一些优化,导致代码的顺序发生一些变化,使用这条语句相当于加了一个锁。最后,每一个CPU都会执行
scheduler
函数,用于进行任务调度。
Loading Comments...