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. Lec23 RCU (Robert)

23.7 RCU总结

Previous23.6 RCU用例代码

Last updated 4 years ago

Was this helpful?

你们应该已经看到了RCU并不是广泛通用的,你不能把所有使用spinlock并且性能很差的场景转化成使用 RCU,并获得更好的性能。主要的原因是RCU完全帮不到写操作,甚至会让写操作更慢,只有当读操作远远多于写操作时才有可能应用RCU。因为RCU有这样的限制:代码不能在sleep的时候持有指针指向被RCU保护的数据,所以这会使得一些代码非常奇怪。当一定要sleep的时候,在sleep结束之后需要重新进入RCU critical区域再次查找之前已经看过的数据,前提是这些数据还存在。所以RCU使得代码稍微复杂了一些。

另一方面可以直接应用RCU的数据结构在更新时,需要能支持单个操作的committing write。你不能在原地更新数据,而是必须创建一个新的链表元素对象来替代之前的元素对象。所以单链表,树是可以应用RCU的数据结构,但是一些复杂的数据结构不能直接使用RCU。里面提到了一些更复杂的方法,例如sequence lock,可以在允许原地更新数据的同时,又不用数据读取者使用锁。但是这些方法要复杂一些,并且能够提升性能的场景也是受限的。

另一个小问题是,RCU并没有一种机制能保证数据读取者一定看到的是新的数据。因为如果某些数据读取者在数据写入者替换链表元素之前,获取了一个指针指向被RCU保护的旧数据,数据读取者可能会在较长的时间内持有这个旧数据。大部分时候这都无所谓,但是论文提到了在一些场景中,人们可能会因为读到旧数据而感到意外。

作为一个独立的话题,你们或许会想知道对于一个写操作频繁的数据该如何提升性能。RCU只关心读操作频繁的数据,但是这类数据只代表了一种场景。在一些特殊场景中,写操作频繁的数据也可以获取好的性能,但是我还不知道存在类似RCU这样通用的方法能优化写操作频繁的数据。不过仍然有一些思路可以值得借鉴。

  • 最有效的方法就是重新构造你的数据结构,这样它就不是共享的。有的时候共享数据完全是没必要的,一旦你发现数据共享是个问题,你可以尝试让数据不共享。

  • 但是某些时候你又的确需要共享的数据,而这些共享数据并没有必要被不同的CPU写入。实际上你们已经在lab中见过这样的数据,在locking lab的kalloc部分,你们重构了free list使得每个CPU核都有了一个专属的free list,这实际上就是将一个频繁写入的数据转换成了每个CPU核的半私有数据。大部分时候CPU核不会与其他CPU核的数据有冲突,因为它们都有属于自己的free list。唯一的需要查看其他CPU核的free list的场景是自己的free list用光了。有很多类似的例子用来处理内核中需要频繁写入的数据,例如Linux中的内存分配,线程调度列表。对于每个CPU核都有一套独立的线程对象以供线程调度器查看(注,详见11.8,线程对象存储在struct cpu中)。CPU核只有在自己所有工作都完成的时候才会查看其他CPU核的线程调度列表。另一个例子是统计计数,如果你在对某个行为计数,但是计数变化的很频繁,同时又很少被读出,你可以重构你的计数器,使得每个CPU核都有一个独立的计数器,这样每个CPU核只需要更新属于自己的计数器。当你需要读取计数值时,你只需要通过加锁读出每个CPU核的计数器,然后再加在一起。这些都是可以让写操作变得更快的方法,因为数据写入者只需要更新当前CPU核的计数器,但是数据读取者现在变得更慢了。如果你的计数器需要频繁写入,实际上通常的计数器都需要频繁写入,通过将更多的工作转换到数据读取操作上,这将会是一个巨大的收益。

这里想说的是,即使我们并没有太讨论,但是的确存在一些技术在某些场合可以帮助提升需要频繁写入数据的性能。

最后总结一下,论文中介绍的RCU对于Linux来说是一个巨大的成功。它在Linux中各种数据都有使用,实际中需要频繁读取的数据还挺常见的,例如block cache基本上就是被读取,所以一种只提升读性能的技术能够应用的非常广泛。尽管已经有了许多有趣的并发技术,同步(synchronization)技术,RCU还是很神奇,因为它对数据读取者完全去除了锁和数据写入(注,这里说的数据写入是指类似读写锁时的计数值,但是RCU在读数据的时候还是需要写标志位关闭context switch,只是这里的写操作代价并不高),所以相比读写锁,RCU是一个很大的突破。RCU能工作的核心思想是为资源释放(Garbage Collection)增加了grace period,在grace period中会确保所有的数据读取者都使用完了数据。所以尽管RCU是一种同步技术,也可以将其看做是一种特殊的GC技术。

学生提问:为什么数据读取者可以读到旧数据呢?在RCU critical区域里,你看到的应该就是实际存在的数据啊?

Robert教授:通常来说这不是个问题。通常来说,你写代码,将1赋值给x,之后print ”done“。

在print之后,如果有人读取x,可能会看到你在将1赋值给x之前x的数值,这里或许有些出乎意料。而RCU允许这种情况发生,如果我们在使用RCU时,并将数据赋值改成list_replace,将包含1的元素的内容改成2。

在函数结束后,我们print ”done“。如果一些其他的数据读取者在查看链表,它们或许刚刚看到了持有1的链表元素,之后它们过了一会才实际的读取链表元素内容,并看到旧的数值1(注,因为RCU是用替换的方式实现更新,数据读取者可能读到了旧元素的指针,里面一直包含的是旧的数值)。所以这就有点奇怪了,就算添加memory barrier也不能避免这种情况。不过实际上大部分场景下这也没关系,因为这里数据的读写者是并发的,通常来说如果两件事情是并发执行的,你是不会认为它们的执行顺序是确定的。

但是论文中的确举了个例子说读到旧数据是有关系的,并且会触发一个实际的问题,尽管我并不太理解为什么会有问题。

学生提问:RCU之所以被称为RCU,是因为它的基本实现对吧?

Robert教授:Read-Copy-Update,是的我认为是因为它的基本实现,它不是在原地修改数据,你是先创建了一个拷贝再来更新链表。

学生提问:在介绍读写锁时,我们讨论了为了实现缓存一致需要O(n^2)时间。对于spinlock这是不是也是个问题,为什么我们在之前在介绍spinlock的时候没有讨论这个问题,是因为spinlock有什么特殊的操作解决了这个问题吗?

Robert教授:并没有,锁的代价都很高。如果没有竞争的话,例如XV6中的标准spinlock会非常快。但是如果有大量的CPU核在相同的时候要获取相同的锁就会特别的慢。存在一些其他的锁,在更高负载的时候性能更好,但是在更低负载的时候性能反而更差。这里很难有完美的方案。

学生提问:或许并不相关,可能存在不同操作系统之间的锁吗?

Robert教授:在分布式系统中,有一种锁可以存在于多个计算机之间。一个场景是分布式数据库,你将数据分发给多个计算机,但是如果你想要执行一个transaction,并使用分布在多个计算机上的数据,你将需要从多个计算机上收集锁。另一个场景是,有一些系统会尝试在独立的计算机之间模拟共享内存,比如说一个计算机使用了另一个计算机的内存,背后需要有一些工具能够使得计算机之间能交互并请求内存。这样就可以在一个集群的计算机上运行一些现有的并行程序,而不是在一个大的多核计算机上,这样成本会更低。这时需要对spinlock或者任何你使用的锁做一些额外的处理,人们发明了各种技术来使得锁能很好的工作,这些技术与我们介绍的技术就不太一样了,尽管避免性能损失的压力会更大。

论文