Skip to the content.

操作系统有三个需求:多路复用、隔离和交互。本章概述了操作系统如何实现如上三个需求,本章还概述了xv6的进程。xv6-riscv在低层的实现很多是特定于riscv的,比如进程。

抽象物理资源

把物理资源抽象成系统调用接口,可以简化应用程序的编写,并使应用程序相互隔离,实践证明是个比较好的硬件抽象方法。

特权模式和系统调用

应用程序处于用户模式,以被强制隔离。内核处于管理模式,以执行特权指令。CPU从机器模式开始执行,主要是配置计算机。

应用程序通过ecall指令调用内核的功能。应用程序不应知道调用内核功能的内存地址,因为恶意程序可借此威胁系统安全。

内核的组织

操作系统的哪一部分放在管理模式下是设计的一个核心问题,宏内核的设计是把整个操作系统都放在内核里。这样的内核组织方式使得操作系统拥有完整的硬件权限,这有利于操作系统的不同部分间相互协同。但这也使操作系统的不同部分之间的接口变得复杂,从而出容易出现错误。为了减少内核产生错误的风险,仅让少量的操作系统代码运行在管理模式,而让大部分的操作系统都运行在用户模式,这样的内核组织方式叫微内核。

在微内核里,操作系统的大量功能都以服务的形式运行于用户态,其它程序以进程间通信的方式使用操作系统提供的服务。这样内核接口就仅包含少量的低级别的功能,诸如启动程序、发送消息、访问硬件等。

xv6的组织

xv6内核的源文件在kernel子目录下。以模块化的粗略概念组织在一起,各模块之间的接口定义在defs.h。

文件名 描述
bio.c 文件系统的硬盘块缓存
console.c 连接到用户的键盘和屏幕
entry.S 最初的启动指令
exec.c exec系统调用
file.c 对文件描述符的支持
fs.c 文件系统
kalloc.c 物理页分配
kernelvec.S 管理来自内核的trap和计时器中断
log.c 文件系统日志和崩溃恢复
main.c 在启动的时候控制其它模块的初始化
pipe.c 管道
plic.c RISC-V的中断控制器
printf.c 格式化输出到控制台
proc.c 进程与调度
sleeplock.c 让出CPU的锁
spinlock.c 不让出CPU的锁
start.c 早期机器模式下的启动代码
string.c C字符串和字节数组(byte-array)的库
swtch.S 线程切换
syscall.c 把系统调用对应到处理函数
sysfile.c 与文件相关的系统调用
sysproc.c 与进程相关的系统调用
trampoline.S 用户与内核切换的汇编代码
trap.c 管理traps和中断
uart.c 串口设备的驱动
virtio_disk.c 磁盘设备的驱动
vm.c 管理页表和地址空间

进程概述

像其它Unix操作系统一样,xv6把进程作为隔离单元。内核实现进程所用到的机制包括用户模式\管理模式的标志(flag),地址空间,线程的时间片。

进程的存在,使程序看上去拥有自己的CPU和内存。xv6使用页表来实现进程各自的地址空间。

每个进程都有自己的页表,这使得它们的物理内存互不重合。进程的虚拟地址空间从零开始,首先是指令,接下来是全局变量,然后是栈,最后是堆区(用于进程扩展内存)。最顶端的两个页是保留的,上面是trampoline,下面则映射的是切换到内核的trapframe。

xv6用struct proc记录每个进程的状态。重要的状态有页表,内核栈和运行状态。

每个进程都有一个运行着的线程来执行进程的指令。线程可以挂起和恢复。不同进程之间的切换,就是内核挂起当前运行着的线程并恢复其它进程的线程。每个进程都有两个栈:用户栈和内核栈。当进程执行用户指令的时候仅使用它的用户栈,它的内核栈是空的。当进程进入内核(因为系统调用或中断),内核代码使用进程的内核栈,这时进程的用户栈还保存了数据只是不被使用了。内核栈是独立的,即使进程破坏了它的用户栈内核也可以继续执行。

进程使用ecall指令来做出一个系统调用。这个指令提升特权级并把程序计数器切换到内核定义的入口上。入口处的代码做的事就是切换到内核栈并执行对应的系统调用实现。当系统调用完成后,内核切换回用户栈并通过sret指令返回用户空间。

第一个进程

加电后先完成初始化,然后运行ROM里的bootloader,bootloader再把xv6内核载入内存。然后从xv6内核的_entry开始执行。 内核被加载到0x80000000处,之所以不加载到0x0处是因为在0x0:0x80000000之间包含了I/O设备。 _entry标记的作用是为C代码的运行指定栈,然后就跳转到start标记执行C代码。 start标记的作用是进行一些配置,这些配置只能在机器模式下来做,然后就切换到管理模式从main函数开始执行。start函数做了两件事,为切换到管理模式做好准备,初始化计时器中断。 main函数首先是初始化了一些设备和子系统,然后调用userinit来创建第一个进程。第一个进程执行了一个小程序initcode.S,通过系统调用exec执行一个新的程序/init/init创建了一个控制台设备文件,并作为文件描述符0,1,2来打开它。然后在无限循环中,启动shell并处理僵尸进程。系统就这样启动了。