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. Lec16 File system performance and fast crash recovery (Robert)

16.2 XV6 File system logging回顾

Previous16.1 Why loggingNext16.3 ext3 file system log format

Last updated 4 years ago

Was this helpful?

首先来回顾一下XV6的logging系统。我们有一个磁盘用来存储XV6的文件系统,你可以认为磁盘分为了两个部分:

  • 首先是文件系统目录的树结构,以root目录为根节点,往下可能有其他的目录,我们可以认为目录结构就是一个树状的数据结构。假设root目录下有两个子目录D1和D2,D1目录下有两个文件F1和F2,每个文件又包含了一些block。除此之外,还有一些其他并非是树状结构的数据,比如bitmap表明了每一个data block是空闲的还是已经被分配了。inode,目录内容,bitmap block,我们将会称之为metadata block(注,Frans和Robert在这里可能有些概念不统一,对于Frans来说,目录内容应该也属于文件内容,目录是一种特殊的文件,详见14.3;而对于Robert来说,目录内容是metadata。),另一类就是持有了文件内容的block,或者叫data block。

  • 除了文件系统之外,XV6在磁盘最开始的位置还有一段log。XV6的log相对来说比较简单,它有header block,之后是一些包含了有变更的文件系统block,这里可以是metadata block也可以是data block。header block会记录之后的每一个log block应该属于文件系统中哪个block,假设第一个log block属于block 17,第二个属于block 29。

在计算机上,我们会有一些用户程序调用write/create系统调用来修改文件系统。在内核中存在block cache,最初write请求会被发到block cache。block cache就是磁盘中block在内存中的拷贝,所以最初对于文件block或者inode的更新走到了block cache。

在write系统调用的最后,这些更新都被拷贝到了log中,之后我们会更新header block的计数来表明当前的transaction已经结束了。在文件系统的代码中,任何修改了文件系统的系统调用函数中,某个位置会有begin_op,表明马上就要进行一系列对于文件系统的更新了,不过在完成所有的更新之前,不要执行任何一个更新。在begin_op之后是一系列的read/write操作。最后是end_op,用来告诉文件系统现在已经完成了所有write操作。所以在begin_op和end_op之间,所有的write block操作只会走到block cache中。当系统调用走到了end_op函数,文件系统会将修改过的block cache拷贝到log中。

在拷贝完成之后,文件系统会将修改过的block数量,通过一个磁盘写操作写入到log的header block,这次写入被称为commit point。在commit point之前,如果发生了crash,在重启时,整个transaction的所有写磁盘操作最后都不会应用。在commit point之后,即使立即发生了crash,重启时恢复软件会发现在log header中记录的修改过的block数量不为0,接下来就会将log header中记录的所有block,从log区域写入到文件系统区域。

这里实际上使得系统调用中位于begin_op和end_op之间的所有写操作在面对crash时具备原子性。也就是说,要么文件系统在crash之前更新了log的header block,这样所有的写操作都能生效;要么crash发生在文件系统更新log的header block之前,这样没有一个写操作能生效。

在crash并重启时,必须有一些恢复软件能读取log的header block,并判断里面是否记录了未被应用的block编号,如果有的话,需要写(也有可能是重写)log block到文件系统中对应的位置;如果没有的话,恢复软件什么也不用做。

这里有几个超级重要的点,不仅针对XV6,对于大部分logging系统都适用:

  • 包括XV6在内的所有logging系统,都需要遵守write ahead rule。这里的意思是,任何时候如果一堆写操作需要具备原子性,系统需要先将所有的写操作记录在log中,之后才能将这些写操作应用到文件系统的实际位置。也就是说,我们需要预先在log中定义好所有需要具备原子性的更新,之后才能应用这些更新。write ahead rule是logging能实现故障恢复的基础。write ahead rule使得一系列的更新在面对crash时具备了原子性。

  • 另一点是,XV6对于不同的系统调用复用的是同一段log空间,但是直到log中所有的写操作被更新到文件系统之前,我们都不能释放或者重用log。我将这个规则称为freeing rule,它表明我们不能覆盖或者重用log空间,直到保存了transaction所有更新的这段log,都已经反应在了文件系统中。

所以在XV6中,end_op做了大量的工作,首先是将所有的block记录在log中,之后是更新log header。在没有crash的正常情况,文件系统需要再次将所有的block写入到磁盘的文件系统中。磁盘中的文件系统更新完成之后,XV6文件系统还需要删除header block记录的变更了的block数量,以表明transaction已经完成了,之后就可以重用log空间。

在向log写入任何新内容之前,删除header block中记录的block数量也很重要。因为你不会想要在header block中记录的还是前一个transaction的信息,而log中记录的又是一个新的transaction的数据。可以假设新的transaction对应的是与之前不同的block编号的数据,这样的话,在crash重启时,log中的数据会被写入到之前记录的旧的block编号位置。所以我们必须要先清除header block。

freeing rule的意思就是,在从log中删除一个transaction之前,我们必须将所有log中的所有block都写到文件系统中。

这些规则使得,就算一个文件系统更新可能会复杂且包含多个写操作,但是每次更新都是原子的,在crash并重启之后,要么所有的写操作都生效,要么没有写操作能生效。

要介绍Linux的logging方案,就需要了解XV6的logging有什么问题?为什么Linux不使用与XV6完全一样的logging方案?这里的回答简单来说就是XV6的logging太慢了。

XV6中的任何一个例如create/write的系统调用,需要在整个transaction完成之后才能返回。所以在创建文件的系统调用返回到用户空间之前,它需要完成所有end_op包含的内容,这包括了:

  • 将所有更新了的block写入到log

  • 更新header block

  • 将log中的所有block写回到文件系统分区中

  • 清除header block

之后才能从系统调用中返回。在任何一个文件系统调用的commit过程中,不仅是占据了大量的时间,而且其他系统调用也不能对文件系统有任何的更新。所以这里的系统调用实际上是一次一个的发生,而每个系统调用需要许多个写磁盘的操作。这里每个系统调用需要等待它包含的所有写磁盘结束,对应的技术术语被称为synchronize。XV6的系统调用对于写磁盘操作来说是同步的(synchronized),所以它非常非常的慢。在使用机械硬盘时,它出奇的慢,因为每个写磁盘都需要花费10毫秒,而每个系统调用又包含了多个写磁盘操作。所以XV6每秒只能完成几个更改文件系统的系统调用。如果我们在SSD上运行XV6会快一些,但是离真正的高效还差得远。

另一件需要注意的更具体的事情是,在XV6的logging方案中,每个block都被写了两次。第一次写入到了log,第二次才写入到实际的位置。虽然这么做有它的原因,但是ext3可以一定程度上修复这个问题。