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可以做一些其他的事情,或许是做好准备去接受回复消息。

过了一会,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就是采用了后者。

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

  • 其中一点是,它是同步的(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。所以这里有点奇怪,但是却非常的快。

Last updated