MIT6.S081
  • 简介
  • Lec01 Introduction and Examples (Robert)
    • 1.1 课程内容简介
    • 1.2 操作系统结构
    • 1.3 Why Hard and Interesting
    • 1.4 课程结构和资源
    • 1.5 read, write, exit系统调用
    • 1.6 open系统调用
    • 1.7 Shell
    • 1.8 fork系统调用
    • 1.9 exec, wait系统调用
    • 1.10 I/O Redirect
  • Lec03 OS Organization and System Calls (Frans)
    • 3.1 上一节课回顾
    • 3.2 操作系统隔离性(isolation)
    • 3.3 操作系统防御性(Defensive)
    • 3.4 硬件对于强隔离的支持
    • 3.5 User/Kernel mode切换
    • 3.6 宏内核 vs 微内核 (Monolithic Kernel vs Micro Kernel)
    • 3.7 编译运行kernel
    • 3.8 QEMU
    • 3.9 XV6 启动过程
  • Lec04 Page tables (Frans)
    • 4.1 课程内容简介
    • 4.2 地址空间(Address Spaces)
    • 4.3 页表(Page Table)
    • 4.4 页表缓存(Translation Lookaside Buffer)
    • 4.5 Kernel Page Table
    • 4.6 kvminit 函数
    • 4.7 kvminithart 函数
    • 4.8 walk 函数
  • Lec05 Calling conventions and stack frames RISC-V (TA)
    • 5.1 C程序到汇编程序的转换
    • 5.2 RISC-V vs x86
    • 5.3 gdb和汇编代码执行
    • 5.4 RISC-V寄存器
    • 5.5 Stack
    • 5.6 Struct
  • Lec06 Isolation & system call entry/exit (Robert)
    • 6.1 Trap机制
    • 6.2 Trap代码执行流程
    • 6.3 ECALL指令之前的状态
    • 6.4 ECALL指令之后的状态
    • 6.5 uservec函数
    • 6.6 usertrap函数
    • 6.7 usertrapret函数
    • 6.8 userret函数
  • Lec08 Page faults (Frans)
    • 8.1 Page Fault Basics
    • 8.2 Lazy page allocation
    • 8.3 Zero Fill On Demand
    • 8.4 Copy On Write Fork
    • 8.5 Demand Paging
    • 8.6 Memory Mapped Files
  • Lec09 Interrupts (Frans)
    • 9.1 真实操作系统内存使用情况
    • 9.2 Interrupt硬件部分
    • 9.3 设备驱动概述
    • 9.4 在XV6中设置中断
    • 9.5 UART驱动的top部分
    • 9.6 UART驱动的bottom部分
    • 9.7 Interrupt相关的并发
    • 9.8 UART读取键盘输入
    • 9.9 Interrupt的演进
  • Lec10 Multiprocessors and locking (Frans)
    • 10.1 为什么要使用锁?
    • 10.2 锁如何避免race condition?
    • 10.3 什么时候使用锁?
    • 10.4 锁的特性和死锁
    • 10.5 锁与性能
    • 10.6 XV6中UART模块对于锁的使用
    • 10.7 自旋锁(Spin lock)的实现(一)
    • 10.8 自旋锁(Spin lock)的实现(二)
  • Lec11 Thread switching (Robert)
    • 11.1 线程(Thread)概述
    • 11.2 XV6线程调度
    • 11.3 XV6线程切换(一)
    • 11.4 XV6线程切换(二)
    • 11.5 XV6进程切换示例程序
    • 11.6 XV6线程切换 --- yield/sched函数
    • 11.7 XV6线程切换 --- switch函数
    • 11.8 XV6线程切换 --- scheduler函数
    • 11.9 XV6线程第一次调用switch函数
  • Lec13 Sleep & Wake up (Robert)
    • 13.1 线程切换过程中锁的限制
    • 13.2 Sleep&Wakeup 接口
    • 13.3 Lost wakeup
    • 13.4 如何避免Lost wakeup
    • 13.5 Pipe中的sleep和wakeup
    • 13.6 exit系统调用
    • 13.7 wait系统调用
    • 13.8 kill系统调用
  • Lec14 File systems (Frans)
    • 14.1 Why Interesting
    • 14.2 File system实现概述
    • 14.3 How file system uses disk
    • 14.4 inode
    • 14.5 File system工作示例
    • 14.6 XV6创建inode代码展示
    • 14.7 Sleep Lock
  • Lec15 Crash recovery (Frans)
    • 15.1 File system crash概述
    • 15.2 File system crash示例
    • 15.3 File system logging
    • 15.4 log_write函数
    • 15.5 end_op函数
    • 15.6 File system recovering
    • 15.7 Log写磁盘流程
    • 15.8 File system challenges
  • Lec16 File system performance and fast crash recovery (Robert)
    • 16.1 Why logging
    • 16.2 XV6 File system logging回顾
    • 16.3 ext3 file system log format
    • 16.4 ext3如何提升性能
    • 16.5 ext3文件系统调用格式
    • 16.6 ext3 transaction commit步骤
    • 16.7 ext3 file system恢复过程
    • 16.8 为什么新transaction需要等前一个transaction中系统调用执行完成
    • 16.9 总结
  • Lec17 Virtual memory for applications (Frans)
    • 17.1 应用程序使用虚拟内存所需要的特性
    • 17.2 支持应用程序使用虚拟内存的系统调用
    • 17.3 虚拟内存系统如何支持用户应用程序
    • 17.4 构建大的缓存表
    • 17.5 Baker's Real-Time Copying Garbage Collector
    • 17.6 使用虚拟内存特性的GC
    • 17.7 使用虚拟内存特性的GC代码展示
  • Lec18 OS organization (Robert)
    • 18.1 Monolithic kernel
    • 18.2 Micro kernel
    • 18.3 Why micro kernel?
    • 18.4 L4 micro kernel
    • 18.5 Improving IPC by Kernel Design
    • 18.6 Run Linux on top of L4 micro kernel
    • 18.7 L4 Linux性能分析
  • Lec19 Virtual Machines (Robert)
    • 19.1 Why Virtual Machine?
    • 19.2 Trap-and-Emulate --- Trap
    • 19.3 Trap-and-Emulate --- Emulate
    • 19.4 Trap-and-Emulate --- Page Table
    • 19.5 Trap-and-Emulate --- Devices
    • 19.6 硬件对虚拟机的支持
    • 19.7 Dune: Safe User-level Access to Privileged CPU Features
  • Lec20 Kernels and HLL (Frans)
    • 20.1 C语言实现操作系统的优劣势
    • 20.2 高级编程语言实现操作系统的优劣势
    • 20.3 高级编程语言选择 --- Golang
    • 20.4 Biscuit
    • 20.5 Heap exhaustion
    • 20.6 Heap exhaustion solution
    • 20.7 Evaluation: HLL benefits
    • 20.8 Evaluation: HLL performance cost(1)
    • 20.9 Evaluation: HLL performance cost(2)
    • 20.10 Should one use HLL for a new kernel?
  • Lec21 Networking (Robert)
    • 21.1计算机网络概述
    • 21.2 二层网络 --- Ethernet
    • 21.3 二/三层地址转换 --- ARP
    • 21.4 三层网络 --- Internet
    • 21.5 四层网络 --- UDP
    • 21.6 网络协议栈(Network Stack)
    • 21.7 Ring Buffer
    • 21.8 Receive Livelock
    • 21.9 如何解决Livelock
  • Lec22 Meltdown (Robert)
    • 22.1 Meltdown发生的背景
    • 22.2 Speculative execution(1)
    • 22.3 Speculative execution(2)
    • 22.4 CPU caches
    • 22.5 Flush and Reload
    • 22.6 Meltdown Attack
    • 22.7 Meltdown Fix
  • Lec23 RCU (Robert)
    • 23.1 使用锁带来的问题
    • 23.2 读写锁 (Read-Write Lock)
    • 23.3 RCU实现(1) - 基本实现
    • 23.4 RCU实现(2) - Memory barrier
    • 23.5 RCU实现(3) - 读写规则
    • 23.6 RCU用例代码
    • 23.7 RCU总结
Powered by GitBook
On this page

Was this helpful?

  1. Lec20 Kernels and HLL (Frans)

20.4 Biscuit

Previous20.3 高级编程语言选择 --- GolangNext20.5 Heap exhaustion

Last updated 4 years ago

Was this helpful?

接下来我将对Biscuit稍作介绍,包括了Biscuit是如何工作的,以及在实现中遇到的问题。其中有些问题是预期内的,有些问题不在预期之内。

就像Linux和XV6一样,Biscuit是经典的monolithic kernel。所以它也有用户空间和内核空间,用户空间程序可能是你的编译器gcc,或者论文中主要用到的webserver。这里用户空间程序主要用C实现,尽管原则上它可以是任何编程语言实现的,但是因为这里只是性能测试,我们这里统一选用的是C版本的应用程序。大部分用户程序都是多线程的,所以不像在XV6中每个用户程序只有一个线程,在Biscuit中支持用户空间的多线程。基本上,对于每个用户空间线程,都有一个对应的位于内核的内核线程,这些内核线程是用Golang实现的,在Golang里面被称为goroutine。你可以认为goroutine就是普通的线程,就像XV6内核里的线程一样。区别在于,XV6中线程是由内核实现的,而这里的goroutine是由Go runtime提供。所以Go runtime调度了goroutine,Go runtime支持sleep/wakeup/conditional variable和同步机制以及许多其他特性,所以这些特性可以直接使用而不需要Biscuit再实现一遍。

Biscuit中的Go runtime直接运行在硬件上,稍后我将介绍更多这部分内容,但是你现在可以认为当机器启动之后,就会启动Go runtime。这里会稍微复杂,因为Go runtime通常是作为用户空间程序运行在用户空间,并且依赖内核提供服务,比如说为自己的heap向内核申请内存。所以Biscuit提供了一个中间层,使得即使Go runtime运行在裸机之上,它也认为自己运行在操作系统之上,这样才能让Go runtime启动起来。

Biscuit内核本身与XV6非常相似,除了它更加的复杂,性能更高。它有虚拟内存系统可以实现mmap,有更高性能的文件系统,有一些设备驱动,比如磁盘驱动,以及网络协议栈。所以Biscuit比XV6更加完整,它有58个系统调用,而XV6只有大概18-19个系统调用;它有28000行代码,而XV6我认为只有少于10000行代码。所以Biscuit有更多的功能。

学生提问:这里的接口与XV6类似对吧,所以进程需要存数据在寄存器中,进程也会调用ECALL。

Frans教授:我稍后会再做介绍,但是这里完全相同。

以上是Biscuit的特性,有些我已经提到过了。

  • 首先它支持多核CPU。Golang对于并发有很好的支持,所以Biscuit也支持多核CPU。类似的,XV6却只对多核CPU有有限的支持。所以在这里,我们相比XV6有更好的同步协调机制。

  • 它支持用户空间多线程,而XV6并没有。

  • 它有一个相比XV6更高性能的Journaled File System(注,Journaled就是指log,可以实现Crash Recovery)。如果你还记得EXT3论文,它与EXT3的Journaled File System有点类似。

  • 它有在合理范围内较为复杂的虚拟内存系统,使用了VMAs并且可以支持mmap和各种功能。

  • 它有一个完整的TCP/IP栈,可以与其他的服务器通过互联网连接在一起。

  • 它还有两个高性能的驱动,一个是Intel的10Gb网卡,以及一个非常复杂的磁盘驱动AHCI,这比virtIO磁盘驱动要复杂的多。

Biscuit支持的用户程序中:

  • 每个用户程序都有属于自己的Page Table。

  • 用户空间和内核空间的内存是由硬件隔离的,也就是通过PTE的User/Kernel bit来区分。

  • 每个用户线程都有一个对应的内核线程,这样当用户线程执行系统调用时,程序会在对应的内核线程上运行。如果系统调用阻塞了,那么同一个用户地址空间的另一个线程会被内核调度起来。

  • 如之前提到的,内核线程是由Go runtime提供的goroutine实现的。如果你曾经用Golang写过用户空间程序,其中你使用go关键字创建了一个goroutine,这个goroutine就是Biscuit内核用来实现内核线程的goroutine。

来看一下系统调用。就像刚刚的问题一样,这里的系统调用工作方式与XV6基本一致:

  • 用户线程将参数保存在寄存器中,通过一些小的库函数来使用系统调用接口。

  • 之后用户线程执行SYSENTER。现在Biscuit运行在x86而不是RISC处理器上,所以进入到系统内核的指令与RISC-V上略有不同。

  • 但是基本与RISC-V类似,控制权现在传给了内核线程。

  • 最后内核线程执行系统调用,并通过SYSEXIT返回到用户空间。

所以这里基本与XV6一致,这里也会构建trapframe和其他所有的内容。

学生提问:我认为Golang更希望你使用channel而不是锁,所以这里在实现的时候会通过channel取代之前需要锁的场景吗?

Frans教授:这是个好问题,我会稍后看这个问题,接下来我们有几页PPT会介绍我们在Biscuit中使用了Golang的什么特性,但是我们并没有使用太多的channel,大部分时候我们用的就是锁和conditional variable。所以某种程度上来说Biscuit与XV6的代码很像,而并没有使用channel。我们在文件系统中尝试过使用channel,但是结果并不好,相应的性能很差,所以我们切换回与XV6或者Linux类似的同步机制。

在实现Biscuit的时候有一些挑战:

  • 首先,我们需要让Go runtime运行在裸机之上。我们希望对于runtime不做任何修改或者尽可能少的修改,这样当Go发布了新的runtime,我们就可以直接使用。在我们开发Biscuit这几年,我们升级了Go runtime好几次,所以Go runtime直接运行在裸机之上是件好事。并且实际上也没有非常困难。Golang的设计都非常小心的不去依赖操作系统,因为Golang想要运行在多个操作系统之上,所以它并没有依赖太多的操作系统特性,我们只需要仿真所需要的特性。大部分这里的特性是为了让Go runtime能够运行起来,一旦启动之后,就不太需要这些特性了。

  • 我们需要安排goroutine去运行不同的应用程序。通常在Go程序中,只有一个应用程序,而这里我们要用goroutine去运行不同的用户应用程序,这些不同的用户应用程序需要使用不同的Page Table。这里困难的点在于,Biscuit并不控制调度器,因为我们使用的是未经修改过的Go runtime,我们使用的是Go runtime调度器,所以在调度器中我们没法切换Page Table。Biscuit采用与XV6类似的方式,它会在内核空间和用户空间之间切换时更新Page Table。所以当进入和退出内核时,我们会切换Page Table。这意味着像XV6一样,当你需要在用户空间和内核空间之间拷贝数据时,你需要使用copy-in和copy-out函数,这个函数在XV6中也有,它们基本上就是通过软件完成Page Table的翻译工作。

  • 另一个挑战就是设备驱动,Golang通常运行在用户空间,所以它并不能从硬件收到中断。但是现在我们在裸机上使用它,所以它现在会收到中断,比如说定时器中断,网卡中断,磁盘驱动中断等等,我们需要处理这些中断。然而在Golang里面并没有一个概念说是在持有锁的时候关闭中断,因为中断并不会出现在应用程序中,所以我们在实现设备驱动的时候要稍微小心。我们采取的措施是在设备驱动中不做任何事情,我们不会考虑锁,我们不会分配任何内存,我们唯一做的事情是向一个非中断程序发送一个标志,之后唤醒一个goroutine来处理中断。在那个goroutine中,你可以使用各种各样想要的Golang特性,因为它并没有运行在中断的context中,它只是运行在一个普通goroutine的context中。

  • 前三个挑战我们完全预料到了,我们知道在创造Biscuit的时候需要处理它们,而最难的一个挑战却不在我们的预料之中。这就是heap耗尽的问题。所以接下来我将讨论一下heap耗尽问题,它是什么,它怎么发生的,以及我们怎么解决的?