为什么需要操作系统
每个应用程序可以根据自己的需要自定义操作系统资源的程序,应用程序可以直接与硬件资源进行交互,并以最适合应用程序的方式使用这些资源(实现高性能)。一些用于嵌入式设备或实时系统的操作系统就是以这种方式组织的。
但我们无法保证每个应用程序可以正确地使用系统资源,甚至转让已占有资源给其他进程共享。例如,每个应用程序必须定期放弃 CPU,以便其他应用程序能够运行。为了实现强隔离,应该禁止应用程序直接访问敏感的硬件资源,将资源抽象为服务,提供服务地程序就是操作系统。
操作系统,准确说是操作系统提供的接口的作用:
- CPU资源使用的管理者,进程管理:使用fork创建进程。进程本身不是CPU,是CPU的一个执行任务。应用程序不需要直接与CPU交互,直接与进程交互即可,内核对进程使用CPU资源进行管理。
- 物理内存的隔离者,内存管理
- 磁盘空间的管理者,文件管理:确保用户A不能操作用户B的文件,可以实现不同用户之间和同一个用户的不同进程之间的文件强隔离
xv6内核组织
操作系统设计者需要考虑操作系统的哪一部分应该在监督者模式下运行。
一种可能是整个操作系统都在监督者模式下运行。这种组织方式称为宏内核。设计者不必决定操作系统哪一部分不需要高级权限,操作系统的不同部分更容易合作。但是这样存在一定弊端。由于整个操作系统都运行在监督者模式下,一旦某个模块出现错误,可能引发整个内核的崩溃,这对于系统的稳定性和可靠性构成了严重威胁。
为了降低内核出错的风险,操作系统设计者应该尽量减少在监督者模式下运行的操作系统代码量,而在用户模式下执行操作系统的大部分代码。这种内核组织方式称为微内核。
xv6 和大多数 Unix 操作系统一样,内核实现了完整的操作系统。虽然由于 xv6 功能较少,所以它的内核比一些微内核要小,但从概念上讲 xv6 是宏内核。
宏内核:操作系统所有功能都在一个内核中,如Unix。优点是系统子模块之间调用更为快捷,缺点是代码组织在一起出错概率更大。
微内核:内核模式中运行尽可能少的代码,文件系统的运行、虚拟内存系统的一部分在用户空间完成。如嵌入式系统Minix、Cell。优点是,缺点是user/kernel mode反复跳转带来的性能损耗。
xv6启动流程
RISC-V 开机时,通过存储在只读存储器的boot loader,将存储在磁盘指定位置的xv6内核程序加载到物理地址0x80000000的内存中。
然后,在机器模式下,CPU 从 _entry 开始执行 xv6。
kernel/entry.S
_entry将每个CPU栈指针寄存器sp为stack0 + ((id+1) * 4096),从而为每个CPU分配一个4KB大小的栈,之后跳转到start()
由于RISC-V的栈是向下扩张的,所以sp初始化为下一个CPU栈的首地址,id+1
_entry:
# set up a stack for C.
# stack0 is declared in start.c,
la sp, stack0
li a0, 1024*4 # stack size is 4KB
csrr a1, mhartid # 加载CPU序号到a1寄存器
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0 # sp指向当前CPU的stack顶(从上往下扩张)
# jump to start() in start.c
call start
spin:
j spin
kernel/start.c
start()执行一些只有在机器模式下才允许的配置,然后切换到监督者模式。
为了进入监督者模式,RISC-V 提供了指令 mret。这条指令最常用来从上一次的调用中返回,上一次调用从监督者模式到机器模式。==start 并不是从这样的调用中返回,而是把事情设置得像有过这样的调用一样:它在寄存器 mstatus 中把上一次的特权模式设置为监督者模式,它把 main 的地址写入寄存器 mepc 中,把返回地址设置为 main 函数的地址,在机器模式中把 0 写入页表寄存器 satp 中,禁用虚拟地址转换,并把所有中断和异常委托给监督者模式。==在完成了这些基本管理后,start 通过调用 mret 返回到监督者模式,这将导致程序计数器变为 main的地址。
// 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");
}
kernel/main.c
统一由第0个CPU初始化xv6运行所需资源,包括物理页分配器、内核根页表等。然后调用 userinit() 创建第一个进程。;其他CPU核无需初始化资源核创建第一个进程。
之后调用scheduler()开始调度进程
// 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(); // 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();
}
kernel/proc.c -> userinit
initcode.S
# Initial process that execs /init.
# This code runs in user space.
# 初始进程/init进程
# 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
第一个进程执行一个用 RISC-V 汇编编写的程序 initcode.S,它通过调用 exec 系统调用重新进入内核。exec 用一个新的程序(本例中是/init)替换当前进程的内存和寄存器。一旦内核完成 exec,它就会在/init 进程中返回到用户空间。
// a user program that calls exec("/init")
// od -t xC initcode
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.
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
# Initial process that execs /init.
# This code runs in user space.
# 初始进程/init进程
# 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
user/init.c
init 在需要时会创建一个新的控制台设备文件,然后以文件描述符 0、1 和 2 的形式打开它。
它在控制台上启动一个 shell
// 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;
// 打开三个文件描述符0、1、2
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
open("console", O_RDWR);
}
dup(0); // stdout
dup(0); // stderr
// init进程是一个无限死循环
for(;;){
printf("init: starting sh\n");
pid = fork();
if(pid < 0){
printf("init: fork failed\n");
exit(1);
}
if(pid == 0){
// 子进程执行sh程序
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.
// 注意:如果一个父进程退出,子进程会被委托给init进程
// 因此wait的返回不一定是因为shell进程的退出
wpid = wait((int *) 0);
if(wpid == pid){
// the shell exited; restart it.
// shell程序退出,需要重启一个shell子进程
break;
} else if(wpid < 0){
// init进程肯定存在子进程,所以wait不可能返回-1
printf("init: wait returned an error\n");
exit(1);
} else {
// 委托给init进程的子进程退出,init进程继续等待
// it was a parentless process; do nothing.
}
}
}
}