这一章仍然是总体介绍xv6-OS,但是会稍微深入一点点细节。
操作系统需要让所有的进分时(time-shared) 来是用来硬件资源。即时进程数目比cpu数要多,仍然能够给用户提供所有进程都在同时运行的“假象”。操作系统同时也需要维持不同进程之间的隔离关系(isolation),这个也是非常重要的功能,不能因为一个进程出错,而让其他的进程受到影响。此外为了进行一些特点的操作,我们还需要打破这个隔离,使得操作系统可以让不同的进程进行交互。简单来说OS必须满足这三个特点:硬件多路复用,隔离以及交互。本章主要是围绕这三个特点来介绍。
Abstract physical resource
这一节课本介绍了一些硬件资源的抽象以及这样抽象的原因。在第一章的时候已经简单介绍过了。这里就不赘述
User mode,supervisor mode,and system calls
在OS中我们为了实现隔离性,用户的代码肯定不能直接访问内核的数据,也不能对硬件直接进行读写,这些敏感的操作只能通过操作系统来完成。cpu只是一个 “取址执行”的机器罢了,他又如何知道现在这个代码能不能被执行呢?所以这里os就引入了三种模式:machine mode, supervisor mode, and user mode(其中supervisor mode 也可以说是kernel mode)。不同的地址有着不同的运行模式,只有在目前权限大于等于要求的模式的情况下才能够正常的让cpu执行这个指令。
machine mode 比较少见,他用处也比较少,只是在开机的时候会配置计算机,我们就不细看。
kernel mode 下 cpu可以执行很多敏感操作,例如启动/禁止中断等。(后面会具体介绍中断)
user mode 只能执行用户态的代码,如果想要执行内核态的代码,例如磁盘写入数据等,就需要利用系统调用,陷入内核态后才能进行敏感操作。而内核态的代码都是事先写好的,会对用户的传入的参数进行验证,只有满足要求才会执行,防止用户对内核或其他进程进行破坏。注意从用户态跳转到内核态只有一个入口! 所有的系统调用都是通过这个入口进入的。后面我们会具体来说这个入口。
Kernel organization
这里文章主要介绍了两个内核的组织模式,跟主题内容没啥关系,不用看了。
Process overview
进程为我们的程序提供了这样的一种抽象:每一个程序都好像运行在一个单独的机器上,有自己独自的内存地址,有独自的cpu等。而其他的程序无法干扰。而页表(page table)就是实现这种抽象的一种手段(页表是后面的最重要的内容之一)。每一个进程都有自己的页表,页表提供一种虚拟地址 到 物理地址的映射。有了这一层映射后,无论虚拟地址怎么操作,怎么蹦跶,都只是在物理地址规定的大小内蹦跶,不会影响其他进程的内容。而xv6的进程的虚拟地址分布如下:
从低地址到高地址依次是 代码区,数据区,栈区,和堆区。上面两个trapframe 与 trapoline是 后面讲trap时用到的关键两个区域(trapframe 用来存数据,而trapline是一段固定代码)。其中MAXVA 是 2^38 -1 ,这也意味着xv6中有效的虚拟地址是38位的。在内核中每一个进程都会维护一个结构体proc
,里面有会有 pagetable
就是指向该进程页表的指针。每一个进程都有一个执行线程来执行指令,当然xv6中有且只有一个。(这里注意理解进程与线程之间的关系,进程是一种抽象,一个资源集合的抽象,包括代码段,堆区等等资源等并不会执行指令。而实际执行指令的抽象是线程,不同的线程有自己的栈空间,但是共用一个堆空间以及数据区和代码区),此外每一个进程都会有自己的内核栈(内核线程切换就是通过这个栈来保存相应的数据)。
starting xv6 and the first process
这里会具体介绍内核是如何启动,以及第一个进程是如何运行的。首先是在配置好硬件后,又一段汇编程序开始:
# 此时不存在什么页表,直接都是用的物理地址。在开始前内核代码被加载到物理内存的0x80000000开始,而从0到该地址都是表示的硬件。真正的物理内存从0x80000000开始。 _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 # 上面汇编简单来说就是设置了一个4k大小的栈空间 用来执行c代码 # jump to start() in start.c call start # 执行start spin: j spin
具体的start 代码就不看了不是重点。只需要知道在start中将 机器模式 改变成了 内核模式。 并且开启了时钟中断。然后将指针指向了一个main函数。 这个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 cache 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(); } 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)); //将一段硬编码的 initcode塞进这个进程的代码段。 //也就是initcode.S汇编程序 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); } //执行下面的汇编
# Initial process that execs /init. # This code runs in user space. #include "syscall.h" # exec(init, argv) .globl start start: la a0, init la a1, argv li a7, SYS_exec #系统调用进入sys_exec 代码 ,执用户态的init程序 也就开启了第一个用户进程 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
上面的看个大概就行,不用特别懂。我也没系统学过riscv-V的汇编,不过无所谓。后面会经常进入这个main函数来看看的。