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. Lec18 OS organization (Robert)

18.5 Improving IPC by Kernel Design

Previous18.4 L4 micro kernelNext18.6 Run Linux on top of L4 micro kernel

Last updated 4 years ago

Was this helpful?

接下来我们讨论微内核里面一个非常重要的问题:IPC的速度。首先让我展示一个非常简单,但是也非常慢的设计。这个设计基于Unix Pipe。我之所以介绍这种方法,是因为一些早期的微内核以一种类似的方式实现的IPC,而这种方式实际上很慢。

假设我们有两个进程,P1和P2,P1想要给P2发送消息。这里该怎么工作呢?一种可能是使用send系统调用,传入你想将消息发送到的线程的ID,以及你想发送消息的指针。这个系统调用会跳到内核中,假设我们是基于XV6的pipe来实现,那么这里会有一个缓存。或许P2正在做一些其他的事情,并没有准备好处理P1的消息,所以消息会被先送到内核的缓存中。所以当你调用send系统调用,它会将你的消息追加到一个缓存中等待P2来接收它。在实际中,几乎很少情况你会只想要发送一个消息,你几乎总是想要能再得到一个回复。所以P1在调用完send系统调用之后,会立即调用recv来获取回复。但是现在让我们先假设我们发送的就是单向的IPC消息,send会将你的消息追加到位于内核的缓存中,我们需要从用户空间将消息逐字节地拷贝到内核的缓存中。之后再返回,这样P1可以做一些其他的事情,或许是做好准备去接受回复消息。

过了一会,P2可以接收消息了,它会调用recv系统调用,这个系统调用会返回发送消息线程的ID,并将消息从内核拷贝到P2的内存中。所以这里会从内核缓存中取出最前的消息,并拷贝到P2的内存中,之后再返回。

这种方式被称为异步传输,因为P1发完消息之后,只是向缓存队列中追加了一条消息,并没有做任何等待就返回了。同时这样的系统这也被称作是buffered system,因为在发送消息时,内核将每条消息都拷贝到了内部的缓存中,之后当接收消息时,又从buffer中将消息拷贝到了目标线程。所以这种方法是异步buffered。

如果P1要完成一次完整的消息发送和接收,那么可以假设有两个buffer,一个用来发送消息,一个用来接收消息。P1会先调用send,send返回之后。之后P1会立即调用recv,recv会等待接收消息的buffer出现数据,所以P1会出让CPU。在一个单CPU的系统中,只有当P1出让了CPU,P2才可以运行。论文中的讨论是基于单CPU系统,所以P1先执行,之后P1不再执行,出让CPU并等待回复消息。这时,P2才会被调度,之后P2调用recv,拷贝消息。之后P2自己再调用send将回复消息追加到buffer,之后P2的send系统调用返回。假设在某个时间,或许因为定时器中断触发导致P2出让CPU,这时P1可以恢复运行,内核发现在接收消息buffer有了一条消息,会返回到用户空间的P1进程。

这意味着在这个慢的设计中,为了让消息能够发送和回复,将要包含:

  • 4个系统调用,两个send,两个recv

  • 对应8次用户空间内核空间之间的切换,而每一次切换明显都会很慢

  • 在recv的时候,需要通过sleep来等待数据出现

  • 并且需要至少一次线程调度和context switching来从P1切换到P2

每一次用户空间和内核空间之间的切换和context switching都很费时,因为每次切换,都需要切换Page Table,进而清空TLB,也就是虚拟内存的查找缓存,这些操作很费时。所以这是一种非常慢的实现方式,它包含了大量的用户空间和内核空间之间的切换、消息的拷贝、缓存的分配等等。实际中,对于这里的场景:发送一个消息并期待收到回复,你可以抛开这种方法并获得简单的多的设计,L4就是采用了后者。

  • 其中一点是,它是同步的(Synchronized)。所以这里不会丢下消息并等待另一个进程去获取消息,这里的send会等待消息被接收,并且recv会等待回复消息被发送。如果我是进程P1,我想要发送消息,我会调用send。send并不会拷贝我的消息到内核的缓存中,P1的send会等待P2调用recv。P2要么已经在内核中等待接收消息,要么P1的send就要等P2下一次调用recv。当P1和P2都到达了内核中,也就是P1因为调用send进入内核,P2因为调用recv进入内核,这时才会发生一些事情。这种方式快的一个原因是,如果P2已经在recv中,P1在内核中执行send可以直接跳回到P2的用户空间,从P2的角度来看,就像是从recv中返回一样,这样就不需要context switching或者线程调度。相比保存寄存器,出让CPU,通过线程调度找到一个新的进程来运行,这是一种快得多的方式。P1的send知道有一个正在等待的recv,它会立即跳转到P2,就像P2从自己的recv系统调用返回一样。这种方式也被称为unbuffered。它不需要buffer一部分原因是因为它是同步的。

  • 当send和recv都在内核中时,内核可以直接将消息从用户空间P1拷贝到用户空间P2,而不用先拷贝到内核中,再从内核中拷出来。因为现在消息收发的两端都在等待另一端系统调用,这意味着它们消息收发两端的指针都是确定的。recv会指定它想要消息被投递的位置,所以在这个时间点,我们知道两端的数据内存地址,内核可以直接拷贝消息,而不是需要先拷贝到内核。

  • 如果消息超级小,比如说只有几十个字节,它可以在寄存器中传递,而不需要拷贝,你可以称之为Zero Copy。前面说过,发送方只会在P2进入到recv时继续执行,之后发送方P1会直接跳转到P2进程中。从P1进入到内核的过程中保存P1的用户寄存器,这意味着,如果P1要发送的消息很短,它可以将消息存放到特定的寄存器中。当内核返回到P2进程的用户空间时,会恢复保存了的寄存器,这意味着当内核从recv系统调用返回时,特定寄存器的内容就是消息的内容,因此完全不需要从内存拷贝到内存,也不需要移动数据,消息就存放在寄存器中,可以非常快的访问到。当然,这只对短的消息生效。

  • 对于非常长的消息,L4可以在一个IPC消息中携带一个Page映射,所以对于巨大的消息,比如说从一个文件读取数据,你可以发送一个物理内存Page,这个Page会被再次映射到目标Task地址空间,这里也没有拷贝。这里提供的是共享Page的权限。所以短的消息很快,非常长的消息也非常快。对于长的消息,你需要调整目的Task的Page Table,但是这仍然比拷贝快的多。

  • 最后一个L4使用的技巧是,如果它发现这是个RPC,有request和response,并且有非常标准的系统调用包括了send和recv,你或许会结合这两个系统调用,以减少用户态和内核态的切换。所以对于RPC这种特别的场景,同时也是人们使用IPC的一个常见场景,有一个call系统调用,它基本上结合了send和recv,区别是这里不会像两个独立的系统调用一样,先返回到用户空间,再次进入到内核空间。在消息的接收端,会有一个sendrecv系统调用将回复发出,之后等待来自任何人的request消息。这里基本是发送一个回复再加上等待接收下一个request,这样可以减少一半的内核态和用户态切换。

实际中,所有的这些优化,对于短的RPC请求这样一个典型的场景,可以导致20倍速度的提升。这是论文中给出的对比之前慢设计提升的性能倍数。这个数字很了不起。Improving IPC by Kernel Design这篇论文是由今天这篇论文的同一个作者在前几年发表的,因为现在IPC可以变得非常的快,它使得人们可以更加认同微内核。

学生提问:当使用这些系统调用时,进程是什么时候发送和接收消息的?

Robert教授:对于包含request和response的RPC,进程使用call和sendrecv这一对系统调用,而不是send和recv。对于call,你会传入两个参数,你想要发送的消息,以及你要存放回复消息的位置,这个系统调用在内核中会结合发送和接收两个功能。你可以认为这是一种hack,因为IPC使用的是如此频繁,它值得一些hack来使得它变得更快。

学生提问:在上面的图中,P2会调用recv系统调用,P2怎么知道应该去调用这个系统调用?

Robert教授:在RPC的世界中,我们有client会发送request到server,server会做一些事情并返回。因为P2是一个server,我们会假设P2会一直在一个while循环中,它随时准备从任何client接收消息,做一些数据处理工作,比如在数据库中查找数据,之后再发送回复,然后再回到循环的最开始再等待接收消息。所以我们期望P2将所有时间都花费在等待从任何一个客户端接收消息上。前面讨论的设计需要依赖P2进程在暂停运行时,一直位于内核的recv系统调用中,并等待下一个request。这样,下一个request才可以直接从这个系统调用返回,这种快速路径在这里的设计中超级有效率。

学生提问:这里提到从P1返回到P2,为了能返回到P1,需要P2发送response吗?

Robert教授:是的,我们期望P2发送一个response,发送response与发送request是同一个代码路径,只是方向相反(之前是P1到P2现在是P2到P1),所以当P2发送一个response,这会导致返回到P1。P1实际调用的是call系统调用,通过从call系统调用返回到P1,会将P2的response送到P1。

这里与你们以为的通常的设置略有不同,通常情况下,你从P1通过系统调用进入到内核,在内核中执行系统调用然后再返回,所有的工作都在P1这边,这也是pipe的read/write的工作方式。在这里,P1进入到内核,但是却返回到了P2。所以这里有点奇怪,但是却非常的快。

有关简单的设计在一篇著名的论文中有提到,论文是,这篇论文在今天要讨论的论文前几年发布。相比上面的慢设计,它有几点不同:

Improving IPC by Kernel Design