当前位置: 代码迷 >> 综合 >> MIT 6.S081 lec1 ~ lec6 总结 —— introducion、system call、pagetable、tarp机制
  详细解决方案

MIT 6.S081 lec1 ~ lec6 总结 —— introducion、system call、pagetable、tarp机制

热度:62   发布时间:2023-11-26 10:05:51.0

MIT 6.S081 课程总结
不含xv6的 具体代码 的 总结

Lec 1 Introduction

  • 操作系统的共性
  1. 抽象硬件 (CPU和内存)
  2. 硬件的可复用性
  3. 程序之间的隔离性
  4. 程序之间的协同、交互
  5. 权限控制/安全、
  6. 提供强大性能
  7. 支持大量不同类型的应用程序
  • 操作系统结构

    硬件资源 + 内核空间(计算机资源的守护者) + 用户空间 = 计算机

    (本课程 关注点在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切换

    • ECALL接收一个数字参数,当一个用户程序想要将程序执行的控制权转移到内核,它只需要执行ECALL指令,并传入一个数字。这里的数字参数代表了应用程序想要调用的System Call,ECALL会跳转到内核中一个特定的、由内核控制的位置。
      • 操作系统在什么时候检查是否允许执行fork或者write
        • 在Unix中,任何应用程序都能调用fork,我们以write为例吧,write的实现需要检查传递给write的地址(需要写入数据的指针)属于用户应用程序,这样内核才不会被欺骗向别的不属于应用程序的位置写入数据。
  • 宏内核 vs 微内核

    • 让整个操作系统代码都运行在kernel mode。大多数的Unix操作系统实现都运行在kernel mode。比如,XV6中,所有的操作系统服务都在kernel mode中,这种形式被称为Monolithic Kernel Design(宏内核)。
      • 出现Bug的可能性更大了。
      • 这些子模块现在都位于同一个程序中,它们可以紧密的集成在一起,这样的集成提供很好的性能。
    • 微内核的目的在于将大部分的操作系统运行在内核之外。所以,我们还是会有user mode以及user/kernel mode的边界。但是我们现在会将原来在内核中的其他部分,作为普通的用户程序来运行。
      • 更少的代码意味着更少的Bug。
      • 性能更差
        • 在user/kernel mode反复跳转带来的性能损耗。
        • 在一个类似宏内核的紧耦合系统,各个组成部分,例如文件系统和虚拟内存系统,可以很容易的共享page cache。而在微内核中,每个部分之间都很好的隔离开了,这种共享更难实现。进而导致更难在微内核中得到更高的性能。

Lec 4 Page tables

地址空间(Address Spaces)

  • 初步实现地址转换

    • 使用页表(Page Tables)
      CPU—(virtual address)—MMU—(physical address)—memory
      MMU会去查看一个表单,表单中,一边是虚拟内存地址,另一边是物理内存地址
  • 页表如何工作 (细节)

    • 以page为颗粒读,为每个page创建一条表单条目,所以每一次地址翻译都是针对一个page。

    • page table是一个多级的结构(节省空间)

      • Directory中的一个条目被称为PTE(Page Table Entry),PTE中的存的flag位用于权限等页面附带信息

        image-20211011014334316
    • 页表缓存(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处理器可以声明支持了哪些扩展指令集,然后编译器可以根据支持的指令集来编译代码。
  • 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系统调用)

    image-20211011213714570

    • 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个系统调用,然后执行ecallimage-20211011214759739
      • 此时的寄存器,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代码的某些合理的位置
  相关解决方案