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. Lec17 Virtual memory for applications (Frans)

17.5 Baker's Real-Time Copying Garbage Collector

Previous17.4 构建大的缓存表Next17.6 使用虚拟内存特性的GC

Last updated 4 years ago

Was this helpful?

接下来我会讨论另一个例子,也就是Garbage Collector(注,后面将Garbage Collector和Garbage Collection都简称为GC),并且我也收到了很多有关Garbage Collector的问题。

GC是指编程语言替程序员完成内存释放,这样程序员就不用像在C语言中一样调用free来释放内存。对于拥有GC的编程语言,程序员只需要调用类似malloc的函数来申请内存,但是又不需要担心释放内存的过程。GC会决定内存是否还在使用,如果内存并没有被使用,那么GC会释放内存。GC是一个很好的特性,有哪些编程语言带有GC呢?Java,Python,Golang,几乎除了C和Rust,其他所有的编程语言都带有GC。

你可以想象,GC有很大的设计空间。这节课讨论的论文并没有说什么样的GC是最好的,它只是展示了GC可以利用用户空间虚拟内存特性。论文中讨论了一种特定的GC,这是一种copying GC。什么是copying GC?假设你有一段内存作为heap,应用程序从其中申请内存。你将这段内存分为两个空间,其中一个是from空间,另一个是to空间。当程序刚刚启动的时候,所有的内存都是空闲的,应用程序会从from空间申请内存。假设我们申请了一个类似树的数据结构。树的根节点中包含了一个指针指向另一个对象,这个对象和根节点又都包含了一个指针指向第三个对象,这里构成了一个循环。

或许应用程序在内存中还有其他对象,但是没有别的指针指向这些对象,所以所有仍然在使用的对象都可以从根节点访问到。在某个时间,或许因为之前申请了大量的内存,已经没有内存空间给新对象了,也就是说整个from空间都被使用了。

Copying GC的基本思想是将仍然在使用的对象拷贝到to空间去,具体的流程是从根节点开始拷贝。每一个应用程序都会在一系列的寄存器或者位于stack上的变量中保存所有对象的根节点指针,通常来说会存在多个根节点,但是为了说明的简单,我们假设只有一个根节点。拷贝的流程会从根节点开始向下跟踪,所以最开始将根节点拷贝到了to空间,但是现在根节点中的指针还是指向着之前的对象。

之后,GC会扫描根节点对象。因为程序的运行时知道对象的类型是什么,当然也就知道对象中的指针。接下来GC会将根节点对象中指针指向的对象也拷贝到to空间,很明显这些也是还在使用中的对象。当一个对象被拷贝到to空间时,根节点中的指针会被更新到指向拷贝到了to空间的对象。

在之后的过程中,我们需要记住这个对象已经被拷贝过了。所以,我们还会存储一些额外的信息来记住相应的对象已经保存在了to空间,这里会在from空间保留一个forwarding指针。这里将对象从from空间拷贝到to空间的过程称为forward。

接下来还剩下一个对象,我们将这个对象从from空间拷贝到to空间,这个对象还包含一个指针指向第二个对象。

但是通过查看指针可以看到这个对象已经被拷贝了,并且我们已经知道了这个对象被拷贝到的地址(注,也就是之前在from空间留下的forwarding指针)。所以我们可以直接更新第三个对象的指针到正确的地址。

现在与根节点相关的对象都从from空间移到了to空间,并且所有的指针都被正确的更新了,所以现在我们就完成了GC,from空间的所有对象都可以被丢弃,并且from空间现在变成了空闲区域。

以上就是copying GC的基本思路。论文中讨论的是一种更为复杂的GC算法,它被称为Baker算法,这是一种很老的算法。它的一个优势是它是实时的,这意味着它是一种incremental GC(注,incremental GC是指GC并不是一次做完,而是分批分步骤完成)。在Baker算法中,我们还是有from和to两个空间。假设其中还是包含了上面介绍的几个对象。

这里的基本思想是,GC的过程没有必要停止程序的运行并将所有的对象都从from空间拷贝到to空间,然后再恢复程序的运行。GC开始之后,唯一必要的事情,就是将根节点拷贝到to空间。所以现在根节点被拷贝了,但是根节点内的指针还是指向位于from空间的对象。根节点只是被拷贝了并没有被扫描,其中的指针还没有被更新。

如果应用程序调用了new来申请内存,那就再扫描几个对象,并将这些对象从from空间forward到to空间。这很好,因为现在我们将拷贝heap中还在使用的所有对象的过程,拆分成了渐进的步骤。每一次调用new都使得整个拷贝过程向前进一步。

当然应用程序也会使用这里对象所指向的指针。举个例子,现在当根节点需要读出其指向的一个对象时,这个对象仍然在from空间。这是危险的,因为我们不应该跟踪from空间的指针(注,换言之GC时的指针跟踪都应该只在同一个空间中完成)。所以每次获取一个指针指向的对象时(dereference),你需要检查对象是否在在from空间,如果是的话,将其从from空间forward到to空间。所以应用程序允许使用指针,但是编译器需要对每个指针的访问都包上一层检查,这样我们就可以保证在to空间的任何指针指向的是位于to空间的对象。我们需要确保这一点,因为在最后当GC完成了所有对象的跟踪之后,我们会清空from部分并重用这部分内存。

论文对于这里的方案提出了两个问题:

  • 第一个是每次dereference都需要有以上的额外步骤,每次dereference不再是对于内存地址的单个load或者store指令,而是多个load或者store指令,这增加了应用程序的开销。

  • 第二个问题是并不能容易并行运行GC。如果程序运行在多核CPU的机器上,并且你拥有大量的空闲CPU,我们本来可以将GC运行在后台来遍历对象的图关系,并渐进的拷贝对象。但是如果应用程序也在操作对象,那么这里可能会有抢占。应用程序或许在运行dereference检查并拷贝一个对象,而同时GC也在拷贝这个对象。如果我们不够小心的话,我们可能会将对象拷贝两遍,并且最后指针指向的不是正确的位置。所以这里存在GC和应用程序race condition的可能。