MIT 6.S081 课程总结
不含xv6的 具体代码 的 总结
Lec 1 Introduction
- 操作系统的共性
- 抽象硬件 (CPU和内存)
- 硬件的可复用性
- 程序之间的隔离性
- 程序之间的协同、交互
- 权限控制/安全、
- 提供强大性能
- 支持大量不同类型的应用程序
-
操作系统结构
硬件资源 + 内核空间(计算机资源的守护者) + 用户空间 = 计算机
(本课程 关注点在Kernel、连接Kernal和用户空间程序的接口、Kernel内软件的架构)
Kernel中的服务,其中一个服务是文件系统,另一个就是进程管理系统
- 文件系统——管理文件内容并找出文件具体在磁盘中的哪个位置。文件系统还维护了一个独立的命名空间,其中每个文件都有文件名,并且命名空间中有一个层级的目录,每个目录包含了一些文件。
- 进程管理系统——管理内存的分配。不同的进程需要不同数量的内存,Kernel会复用内存、划分内存,并为所有的进程分配内存。
- System Call——应用程序是如何与Kernel交互,它们之间的接口长什么样感兴趣。这里通常成为Kernel的API,它决定了应用程序如何访问Kernel。 其区别是系统调用会实际运行到系统内核中。
-
Hard and Interesting
- 内核的编程环境比较困难 (hard)
- 操作系统的一些列矛盾的需求 (hard)
- 高效(与底层交互) 与 易用 (高层次的封装)
- 简单 与 强大 (接口的功能)
- 安全性(限制应用程序) 与 灵活性 (不限制应用程序)
- 操作系统特供的 特性/服务 的交互性(hard and interesting)—— 例如 open 和 fork —— 与接口的强大性相似
- 广泛的使用场景是如何被设计的 (interesting)
Lec 3 OS Organization and System Calls
-
操作系统隔离性(isolation)
- 使用操作系统的一个原因,甚至可以说是主要原因就是为了实现 multiplexing (CPU在多进程分时复用) 和 内存隔离 。如果你不使用操作系统,并且应用程序直接与硬件交互,就很难实现这两点。所以,将操作系统设计成一个库,并不是一种常见的设计。你或许可以在一些实时操作系统中看到这样的设计,因为在这些实时操作系统中,应用程序之间彼此相互信任。但是在大部分的其他操作系统中,都会强制实现硬件资源的隔离。
- 所以,操作系统不是直接将CPU提供给应用程序,而是向应用程序提供“进程”,进程抽象了CPU,这样操作系统才能在多个应用程序之间复用一个或者多个CPU。
-
操作系统防御性(Defensive)
- 如果操作系统需要具备防御性,那么在应用程序和操作系统之间需要有一堵厚墙,并且操作系统可以在这堵墙上执行任何它想执行的策略。
- 通常来说,需要通过硬件来实现这的强隔离性。这里的硬件支持包括了两部分,第一部分是user/kernel mode,;第二部分是page table或者虚拟内存(Virtual Memory)。
-
硬件对于强隔离的支持
- user/kernel mode
- 当运行在kernel mode时,CPU可以运行特定权限的指令(privileged instructions);当运行在user mode时,CPU只能运行普通权限的指令(unprivileged instructions)。
- 在用户空间(user space)尝试执行一条特殊权限指令
- 用户程序会通过系统调用来切换到kernel mode。当用户程序执行系统调用,会通过ECALL触发一个软中断(software interrupt),软中断会查询操作系统预先设定的中断向量表,并执行中断向量表中包含的中断处理程序。中断处理程序在内核中,这样就完成了user mode到kernel mode的切换,并执行用户程序想要执行的特殊权限指令。
- 虚拟内存
- 处理器包含了page table,而page table将虚拟内存地址与物理内存地址做了对应
- 每一个进程都会有自己独立的page table,这样的话,每一个进程只能访问出现在自己page table中的物理内存。操作系统会设置page table,使得每一个进程都有不重合的物理内存,这样一个进程就不能访问其他进程的物理内存,因为其他进程的物理内存都不在它的page table中。一个进程甚至都不能随意编造一个内存地址,然后通过这个内存地址来访问其他进程的物理内存。这样就给了我们内存的强隔离性。
- user/kernel mode
-
User/Kernel mode切换
- ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call,ECALL会跳转到内核中一个特定的、由内核控制的位置。
- 操作系统在什么时候检查是否允许执行fork或者write
- 在Unix中,任何应用程序都能调用fork,我们以write为例吧,write的实现需要检查传递给write的地址(需要写入数据的指针)属于用户应用程序,这样内核才不会被欺骗向别的不属于应用程序的位置写入数据。
- 操作系统在什么时候检查是否允许执行fork或者write
- ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call,ECALL会跳转到内核中一个特定的、由内核控制的位置。
-
宏内核 vs 微内核
- 让整个操作系统代码都运行在kernel mode。大多数的Unix操作系统实现都运行在kernel mode。比如,XV6中,所有的操作系统服务都在kernel mode中,这种形式被称为Monolithic Kernel Design(宏内核)。
- 出现Bug的可能性更大了。
- 这些子模块现在都位于同一个程序中,它们可以紧密的集成在一起,这样的集成提供很好的性能。
- 微内核的目的在于将大部分的操作系统运行在内核之外。所以,我们还是会有user mode以及user/kernel mode的边界。但是我们现在会将原来在内核中的其他部分,作为普通的用户程序来运行。
- 更少的代码意味着更少的Bug。
- 性能更差
- 在user/kernel mode反复跳转带来的性能损耗。
- 在一个类似宏内核的紧耦合系统,各个组成部分,例如文件系统和虚拟内存系统,可以很容易的共享page cache。而在微内核中,每个部分之间都很好的隔离开了,这种共享更难实现。进而导致更难在微内核中得到更高的性能。
- 让整个操作系统代码都运行在kernel mode。大多数的Unix操作系统实现都运行在kernel mode。比如,XV6中,所有的操作系统服务都在kernel mode中,这种形式被称为Monolithic Kernel Design(宏内核)。
Lec 4 Page tables
地址空间(Address Spaces)
-
初步实现地址转换
- 使用页表(Page Tables)
CPU—(virtual address)—MMU—(physical address)—memory
MMU会去查看一个表单,表单中,一边是虚拟内存地址,另一边是物理内存地址
- 使用页表(Page Tables)
-
页表如何工作 (细节)
-
以page为颗粒读,为每个page创建一条表单条目,所以每一次地址翻译都是针对一个page。
-
page table是一个多级的结构(节省空间)
-
Directory中的一个条目被称为PTE(Page Table Entry),PTE中的存的flag位用于权限等页面附带信息
-
-
页表缓存(Translation Lookaside Buffer)
- 当处理器第一次查找一个虚拟地址时,硬件通过3级page table得到最终的PPN,TLB会保存虚拟地址到物理地址的映射关系。这样下一次当你访问同一个虚拟地址时,处理器可以查看TLB,TLB会直接返回物理地址,而不需要通过page table得到结果。
-
Lec05 Calling conventions and stack frames RISC-V
-
C程序到汇编程序的转换
- 任何一个处理器都有一个关联的ISA(Instruction Sets Architecture),ISA就是处理器能够理解的指令集
- C -> .s文件 -> .o文件
汇编语言中的函数是以label(标签) 的形式存在而不是真正的函数定义
-
RISC-V vs x86
- 精简指令集 vs 复杂指令集
- 指令的数量
- RISC-V指令也更加简单。在x86-64中,很多指令都做了不止一件事情.
- RISC另一件有意思的事情是它是开源的
- RISC-V的特殊之处在于:它区分了Base Integer Instruction Set和Standard Extension Instruction Set。Base Integer Instruction Set包含了所有的常用指令,比如add,mult。除此之外,处理器还可以选择性的支持Standard Extension Instruction Set。每一个RISC-V处理器可以声明支持了哪些扩展指令集,然后编译器可以根据支持的指令集来编译代码。
- 精简指令集 vs 复杂指令集
-
Stack
- 每一次我们调用一个函数,函数都会为自己创建一个Stack Frame,并且只给自己用。函数通过移动Stack Pointer来完成Stack Frame的空间分配。
- 对于Stack来说,是从高地址开始向低地址使用。所以栈总是向下增长。当我们想要创建一个新的Stack Frame的时候,总是对当前的Stack Pointer做减法。一个函数的Stack Frame包含了保存的寄存器,本地变量,并且,如果函数的参数多于8个,额外的参数会出现在Stack中。
- Return address总是会出现在Stack Frame的第一位
- 指向前一个Stack Frame的指针也会出现在栈中的固定位置
- 有关Stack Frame中有两个重要的寄存器,第一个是SP(Stack Pointer),它指向Stack的底部并代表了当前Stack Frame的位置。第二个是FP(Frame Pointer),它指向当前Stack Frame的顶部。
Lec06 Isolation & system call entry/exit
-
用户空间和内核空间的切换(Trap机制)
- 切换的时机 (用户->内核)
- 执行系统调用(软中断(software interrupt))
- 异常(例如,除以0)
- 触发了中断(interrupt) 使得当前程序运行需要响应内核设备驱动
- 相关寄存器
- 包含堆栈寄存器在内的32个用户寄存器 (Lec5.4)
- 程序计数器(Program Counter Register)
- 当前mode的标志位(supervisor mode \ user mode)
- 控制CPU工作方式的寄存器,如SATP(Supervisor Address Translation and Protection)寄存器,指向page table的物理内存地址
- STVEC(Supervisor Trap Vector Base Address Register)寄存器,它指向了内核中处理trap的指令的起始地址。
- SEPC(Supervisor Exception Program Counter)寄存器,在trap的过程中保存程序计数器的值。
- SSRATCH(Supervisor Scratch Register)寄存器…
- 具体操作
- 保存32个用户寄存器和程序计数器 (恢复用户状态的执行)
- 将mode寄存器 改写成 supervisor mode
- 将 SATP 指向 kernel page table
- 将堆栈寄存器指向位于内核的一个地址 (调用内核的C函数)
- 关键点
- trap中涉及到的硬件和内核机制不能依赖任何来自用户空间东西
- trap机制对用户代码是透明的 (不对用户代码产生任何影响)
- supervisor mode可以控制什么
- 可以读写控制寄存器
- 如读写SATP寄存器,也就是page table的指针;STVEC,也就是处理trap的内核指令地址;SEPC,保存当发生trap时的程序计数器;SSCRATCH等等
- 可以使用PTE_U标志位为0的PTE(只有supervisor mode可以使用这个页表,且在当前由SATP指向的page table中的),但不能使用SATP指向的page table中PTE_U=1
- 可以读写控制寄存器
- 切换的时机 (用户->内核)
-
Trap代码执行流程(跟踪如何在Shell中调用write系统调用)
-
write通过执行ECALL指令来执行系统调用。ECALL指令会切换到具有supervisor mode的内核中,其后依次为trampoline.s里的uservec函数,trap.c里的usertrap函数,syscall函数,sys_write函数将要显示数据输出到console上,syscall函数中执行usertrapret函数 (trap.c里的,完成了部分方便在C代码中实现的返回到用户空间的工作),trampoline.s里的userret函数 (部分工作通过汇编语言实现)
-
ECALL指令之前的状态
- write(2, "$ ", 2);
- 将SYS_write(是一个常量)加载到a7寄存器,告诉内核要运行第16个系统调用,然后执行ecall
- 此时的寄存器,a0,a1,a2是Shell传递给write系统调用的参数。所以a0是文件描述符2;a1是Shell想要写入字符串的指针;a2是想要写入的字符数
-
ECALL做的事情(CPU自己的指令,无法gdb)
- 将代码从user mode改到supervisor mode
- ecall将程序计数器的值保存在了SEPC寄存器
- ecall会跳转到STVEC寄存器指向的指令(指向了内核中处理trap的指令的起始地址)
-
ECALL指令之后的状态
- 保存32个用户寄存器的内容
- 切换到kernel page table
- 创建或者找到一个kernel stack,并将Stack Pointer寄存器的内容指向那个kernel stack,给C代码提供栈
- 跳转到内核中C代码的某些合理的位置
-