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