MIT6.S081课程学习笔记 Chapter 2
Chapter 2 Operating system organization
对操作系统的关键要求是同时支持多种事件,有三个重要的要求:复用、隔离、交流
本章主要讨论宏内核的情况,这也是许多类Unix系统的情况
Abstracting physical resources抽象物理资源
为了实现更强的隔离,禁止应用程序直接访问敏感的硬件资源,而将硬件资源抽象为服务
例如,Unix 应用程序只通过文件系统的打开、读取、写入和关闭系统调用与存储进行交互,而不是直接读写磁盘。操作系统能更好地管理,应用程序也更方便
Unix 在进程间透明地切换硬件 CPU,必要时保存和恢复寄存器状态,这样应用程序就不必意识到时间共享。这种透明度使操作系统即使在某些应用程序处于无限循环的情况下也能共享 CPU。
Unix 进程之间的许多交互方式都是通过文件描述符实现的。文件描述符不仅抽象掉了许多细节(如管道或文件中的数据存储位置),其定义方式也简化了交互。例如,如果管道中的一个应用程序出现故障,内核就会为管道中的下一个进程生成文件结束信号。
为实现强隔离性,操作系统必须不让应用触碰到操作系统的内核和其他应用
硬件支持的隔离性的方式:
- user/kernel model 用户/内核模式
- virtul memery (page table) 虚拟内存
User mode, supervisor mode, and system calls
RISC-V有三种模式:machine mode supervisor mode user mode
supervisor mode 可翻译为监督模式、管理模式、特权模式、内核模式。
国内教材将supervisor mode 和 user mode 分别称为管态和目态
机器模式主要用于启动时配置计算机,启动后切换至内核模式
用户程序不能直接调用内核函数。CPU 提供了一种特殊指令可将 CPU 从用户模式切换到内核模式,并在内核指定的入口点进入内核。内核就可以验证系统调用的参数,决定是否运行
内核必须控制入口点,如果应用程序可以决定内核入口点,恶意程序就可以在跳过参数验证的位置进入内核。
Kernel organization
一个关键的设计问题是,操作系统的哪一部分应在监督模式下运行。
一种是整个操作系统服务都位于内核模式中,这中模式被成为monolithic kernel design 宏内核,linux和xv6就是使用这种模式
然而更多的代码意味着更容易出错,因此另一种方法是只将部分的操作系统服务置于内核模式中,而在用户模式下执行操作系统的大部分代码,这种模式被成为mirco kernel design 微内核,这样减少了bug的风险但损失了性能,比如shell和file的交互需要通过内核中转,多次进出内核
为了让应用程序与文件服务器交互,内核提供了一种进程间通信机制,用于从一个用户模式进程向另一个用户模式进程发送消息。例如,如果shell 等应用程序想要读取或写入文件,它就会向文件服务发送消息并等待响应。

Code: xv6 organization
xv6 内核源代码位于 kernel/ 子目录下,模块间的接口定义在defs.h
Process overview 进程总览
xv6 中的隔离单元是进程,与其他 Unix 操作系统一样
进程抽象为程序提供了一种假象,即程序拥有自己的私有机器。进程为程序提供了看似私有的内存系统或地址空间,其他进程无法读写
Xv6 使用页表(由硬件实现)为每个进程提供自己的地址空间。页表将虚拟地址转换为物理地址。Xv6 为每个进程维护一个单独的页表,定义该进程的地址空间
进程的虚拟空间布局
下图为进程的虚拟地址布局,从0开始,数据代码、栈、可拓展堆。限制虚拟地址大小的因素有很多:RISC-V为64位,硬件只用低39位进行寻址,xv6只用39位中的38位进行寻址,因此最大虚拟地址为2^38-1= 0x3fffffffff,即定义在kernel/riscv.h:363中的MAXVA。在最高处定义一页trappoline和一页trapframe,trappoline包含进出内核的代码,trapframe用于保存加载进程状态

xv6 内核会为每个进程维护许多状态片段,并将其归入 struct proc(kernel/proc.h:86)。进
程最重要的内核状态包括页表、内核堆栈和运行状态。
我们将使用符号 p->xxx 来指代 proc结构中的元素;例如,p->pagetable 是指向进程页表的指针。
xv6中每个进程都有一个线程,负责执行进程的指令。线程可以暂停,随后再恢复。
进程的栈
每个进程都有两个栈:用户栈和内核栈(p->kstack)
当进程执行用户指令时,只有用户栈在使用,内核栈是空的。
当进程处于内核中时,其用户堆栈仍包含已保存的数据,但不会被实际使用。
当进程进入内核(执行系统调用或中断)时,内核代码会在进程的内核堆栈上执行。
内核堆栈是独立的(并与用户代码隔离),因此即使进程破坏了用户堆栈,内核也能执行。
系统调用过程中的栈切换
进程可通过执行 RISC-V 的ecall 指令进行系统调用,ecall number 中数字number代表不同的系统调用编号,该指令可提高硬件权限级别,并将程序计数器更改为内核定义的入口点。
假设shell使用fork指令,实际系统执行的指令是ecall number_fork,内核中的特定系统调用处理函数syscall根据number_fork进行fork系统调用
入口点的代码会切换到内核堆栈,并执行实现系统调用的内核指令。
系统调用完成后,内核会切换回用户堆栈,并通过调用 sret 指令返回用户空间,该指令会降低硬件权限级别,并恢复执行系统调用指令后的用户指令。
总之,进程包含两个设计理念:一个是地址空间,让进程产生拥有自己内存的错觉;另一个是线程,让进程产生拥有自己 CPU 的错觉。在 xv6 中,一个进程由一个地址空间和一个线程组成。在实际操作系统中,一个进程可能有多个线程,以利用多个 CPU 的优势
Code: starting xv6, the first process and system call
我们将概述内核如何启动和运行第一个进程
机器模式
当 RISC-V 计算机开机时,它会进行初始化并运行一个存储在只读存储器中的引导加载器,引导加载器将 xv6 内核加载到内存中。
然后,在机器模式下,CPU 从 _entry 开始执行 xv6(kernel/entry.S:7)。
RISC-V 启动时分页硬件被禁用:虚拟地址直接映射到物理地址。
加载程序会将 xv6 内核加载到物理地址为 0x80000000 的内存中。之所以将内核放在0x80000000 而不是 0x0,是因为地址范围 0x0:0x80000000 包含 I/O 设备。
_entry 处的指令设置了一个堆栈,以便 xv6 可以运行 C 代码。Xv6 在 start.c 文件(kernel/start.c:11)中声明了初始堆栈 stack0 的空间
函数 start 执行一些只允许在机器模式下进行的配置,然后切换到监督模式。
start 并非从这样的调用中返回,而是将一切设置得如同调用过一样:在寄存器 mstatus 中将先前的特权模式设置为监督模式;将main 的地址写入寄存器 mepc,从而将返回地址设置为 main;将 0 写入页表寄存器satp,从而禁用监督模式下的虚拟地址转换;将所有中断和异常委托给监督模式。
在跳转到监督模式之前,start 还需要执行一项任务:对时钟芯片进行编程,以产生定时器中断
监督模式
要进入监督模式,RISC-V 提供了 mret 指令,这将导致PC变为 main。
main初始化几个设备和子系统后,通过调用 userinit(kernel/proc.c:226)创建了第一个进程。
第一个进程执行一个用 RISC-V 汇编编写的小程序,并在 xv6 中进行第一次系统调用,具体过程如下:
initcode.S(user/initcode.S:3)将系统调用 SYS_EXEC 编号载入寄存器 a7,然后调用ecall 重新进入内核
内核中 syscall 使用寄存器 a7 中的数字调用所需的系统调用
系统调用表(kernel/syscall.c:108)有着 SYS_EXEC 映射到 sys_exec
exec 会用一个新程序替换当前进程的内存和寄存器(本例中为 /init),执行完毕后,会返回用户空间的 init 进程。
如果需要,init(user/init.c:15)会创建一个新的控制台设备文件,然后将其作为文件描述符 0、1 和 2 打开。然后在控制台上启动 shell。系统启动

浙公网安备 33010602011771号