# 18.5 Improving IPC by Kernel Design

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

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

![](/files/-MXb4THOG9IFV2gx_n7y)

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

![](/files/-MXb58rm-JLSUcmR3OvX)

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

![](/files/-MXb7MaGVYdO0PbfOOsQ)

如果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进程。

![](/files/-MXbAdzYVCKjwImrf-6q)

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

* 4个系统调用，两个send，两个recv
* 对应8次用户空间内核空间之间的切换，而每一次切换明显都会很慢
* 在recv的时候，需要通过sleep来等待数据出现
* 并且需要至少一次线程调度和context switching来从P1切换到P2

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

有关简单的设计在一篇著名的论文中有提到，论文是[Improving IPC by Kernel Design](https://www.cse.unsw.edu.au/~cs9242/19/papers/Liedtke_93.pdf)，这篇论文在今天要讨论的论文前几年发布。相比上面的慢设计，它有几点不同：

* 其中一点是，它是同步的（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一部分原因是因为它是同步的。

![](/files/-MXbHYvr-Y5wGhNMOBxA)

* 当send和recv都在内核中时，内核可以直接将消息从用户空间P1拷贝到用户空间P2，而不用先拷贝到内核中，再从内核中拷出来。因为现在消息收发的两端都在等待另一端系统调用，这意味着它们消息收发两端的指针都是确定的。recv会指定它想要消息被投递的位置，所以在这个时间点，我们知道两端的数据内存地址，内核可以直接拷贝消息，而不是需要先拷贝到内核。
* 如果消息超级小，比如说只有几十个字节，它可以在寄存器中传递，而不需要拷贝，你可以称之为Zero Copy。前面说过，发送方只会在P2进入到recv时继续执行，之后发送方P1会直接跳转到P2进程中。从P1进入到内核的过程中保存P1的用户寄存器，这意味着，如果P1要发送的消息很短，它可以将消息存放到特定的寄存器中。当内核返回到P2进程的用户空间时，会恢复保存了的寄存器，这意味着当内核从recv系统调用返回时，特定寄存器的内容就是消息的内容，因此完全不需要从内存拷贝到内存，也不需要移动数据，消息就存放在寄存器中，可以非常快的访问到。当然，这只对短的消息生效。

![](/files/-MXgJ60wirrdZcEEzMy1)

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

![](/files/-MXgKbLEILFmniweH2jF)

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

![](/files/-MXgS5nz_-IqkIniNx3l)

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

![](/files/-MXgTV2ldpCNR3Aqitf4)

> 学生提问：当使用这些系统调用时，进程是什么时候发送和接收消息的？
>
> 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。所以这里有点奇怪，但是却非常的快。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec18-os-organization-robert/18.5-improving-ipc-by-kernel-design.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
