操作系统有三个需求:多路复用、隔离和交互。本章概述了操作系统如何实现如上三个需求,本章还概述了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并处理僵尸进程。系统就这样启动了。