> For the complete documentation index, see [llms.txt](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec18-os-organization-robert/18.4-l4-micro-kernel.md).

# 18.4 L4 micro kernel

今天要讨论的[论文](https://pdos.csail.mit.edu/6.828/2020/readings/microkernel.pdf)，有许多有关L4微内核的内容。这是今天论文作者开发和使用的一种微内核。L4必然不是最早的微内核，但是从1980年代开始，它是最早一批可以工作的微内核之一，并且它非常能展现微内核是如何工作的。在许多年里面它一直都有活跃的开发和演进。如果你查看Wikipedia，L4有15-20个变种，有一些从1980年代开始开发的项目现在还存在。接下来我将从我的理解向你们解释L4在今天的论文发表的时候是如何工作的。

首先，L4是微内核，它只有7个系统调用，虽然其中有一些稍微有点复杂，但是它还是只有7个系统调用。然而现在的Linux，我上次数了下有大概350个系统调用。甚至XV6这个极其简单的内核，也有21个系统调用。从这个指标来看，L4更加简单。

其次，L4并不大，论文发表的时候，它只有13000行代码，这并不多。XV6的代码更少，我认为XV6内核只有6000-7000行代码，所以作为内核XV6非常的简单。L4也没有复杂太多，它只有Linux代码的几十分之一，所以它非常的小。

![](/files/-MXaSPF2zUj4-VZDA24n)

第三，它只包含几个非常基础的抽象。

它在内部有一个叫做Task或者地址空间的概念，这或多或少的对应了Uinx内的进程概念。Task包含了一些内存，地址从0开始，并且可以像进程一样执行指令。区别于XV6的是，每个Task可以有多个线程，L4会调度每个Task内的多个线程的执行。这样设计的原因是，可以非常方便地用线程来作为组织程序结构的工具。我不知道在论文发表的时候，L4是否支持了多处理器，或许它包含了在多个处理器上运行同一个程序的能力。所以L4内核知道Task，知道线程，也知道地址空间，这样你就可以告诉L4如何映射地址空间内的内存Page。

另一个L4知道的事情是IPC。每一个线程都有一个标识符，其中一个线程可以说，我想要向拥有这个标识符的另一个线程发送几个字节。

这里的Task，线程，地址空间，IPC是L4唯一有的抽象。

![](/files/-MXaSY4qx2aglEPoTV2r)

我不确定是否能列出所有的系统调用，这里涉及到的系统调用有：

* Threadcreate系统调用，你提供一个地址空间ID并要求创建一个新的线程。如果地址空间或者Task不存在，系统调用会创建一个新的Task。所以这个系统调用即可以创建线程，又可以创建Task。
* Send/Recv IPC系统调用。
* Mapping系统调动可以映射内存Page到当前Task或者其他Task的地址空间中。你可以要求L4来改变当前Task的地址空间和Page Table，如果你有足够的权限，你也可以要求L4改变其他Task的地址空间。这实际上是通过IPC完成的，你会发送一个特殊的IPC消息到目标线程，内核可以识别这个IPC消息，并会修改目标线程的地址空间。如果你创建一个先的线程，新线程最开始没有任何内存。所以如果你想创建一个线程，你先调用Threadcreate系统调用来创建新的线程，新的Task和地址空间。然后你创建一个特殊 IPC，将你自己内存中的一部分，其中包含了指令和数据，映射到新的Task的地址空间中。之后你再发送一个特殊的Start IPC消息到这个新的Task，其中包含了你期望新的Task开始执行程序的程序计数器和Stack Pointer。之后新的Task会在你设置好的内存中，从你要求的程序计数器位置开始执行。
* 虽然我不知道具体是怎么实现的，但是Privileged Task可以将硬件控制寄存器映射到自己的地址空间中。所以L4并不知道例如磁盘或者网卡的设备信息，但是实现了设备驱动的用户空间软件可以直接访问设备硬件。
* 你可以设置L4将任何一个设备的中断转换成IPC消息。这样，运行设备驱动的Task不仅可以读写了设备，并且也可以设置L4将特定设备的中断通过IPC消息发送给自己。
* 最后，一个Task可以设置L4内核通知自己有关另一个Task的Page Fault。所以如果一个Task发生了Page Fault，L4会将Page Fault转换成一个IPC消息，并发送给另一个指定的Pager Task。每一个Task都有个与之关联的Pager Task用来处理自己相关的Page Fault。这就是关联到Page Fault的方法，通过它可以实现类似copy-on-write fork或者lazy allocation。

![](/files/-MXabbn503do5ixrzCNt)

以上就是内核的内容，L4里面不包含其他的功能，没有文件系统，没有fork/exec系统调用，除了这里非常简单的IPC之外，没有其他例如pipe的通信机制，没有设备驱动，没有网络的支持等等。任何其他你想要的功能，你需要以用户空间进程的方式提供。

L4能提供的一件事情是完成线程间切换。L4会完成线程调度和context switch，来让多个线程共用一个CPU。它实现的方式你会觉得非常熟悉，L4会为每个Task保存寄存器，当它执行一个线程时，它会跳到用户空间，切换到那个线程对应Task的Page Table，之后那个线程会在用户空间执行一会。之后或许会有一个定时器中断，定时器是L4知道的一个设备，定时器中断会使代码执行返回到L4内核，L4会保存线程的用户寄存器，然后在一个类似于XV6的线程调度循环中，选择一个Task来运行。通过将这个Task之前保存的寄存器恢复出来，切换Page Table，就可以跳转到Task中再运行一会，直到再发生另一个定时中断，或者当前Task出让了CPU。所以我认为L4或许还有一个yield系统调用。在这种情况下Task可以等待接收一个IPC消息，这时代码会跳转回L4内核，L4内核会保存寄存器，并切换到一个新的Task。所以L4中有关线程切换的部分你们会非常熟悉。

我之前提到过这个概念，Pager。如果一个进程触发了Page Fault，通过trap走到了内核，内核会将Page Fault转换成IPC消息并发送到指定的Pager Task，并告诉Pager Task是哪个线程的哪个地址触发了Page Fault。在Pager Task中，如果它实现了lazy allocation，那么它会负责从L4分配一些内存，向触发Page Fault的Task发送一个特殊的IPC，来恢复程序的运行。所以Pager Task实现了XV6或者Linux在Page Fault Handler中实现的所有功能。如果你想的话，你可以在Pager Task中实现copy-on-write fork或者memory mapped files，Pager Task可以实现基于Page Fault的各种技巧。

这是类似L4的微内核相比传统的内核，对于用户程序要灵活的多的众多例子之一。如果Linux并没有copy-on-write fork，并且你想要有这个功能，你不可能在不修改内核的前提下完成这个功能。Linux中没有办法写一些可移植的用户空间代码来实现copy-on-write fork。这样描述可能并不完全正确，但是一定要这么做的话会很复杂。然而，在L4里面，这就相对简单了。L4就好像是完全设计成让你去写用户空间代码来获取Page Fault，并实现copy-on-write fork。所有这些都可以在用户空间完成，而不用弄乱内核。

> 学生提问：能说明一下Task和线程之间的区别吗？
>
> Robert教授：可以。一个Task就像XV6的一个进程一样，它有一些内存，一个地址空间，你可以在其中运行用户代码。如果你在XV6中有一个进程，它只能包含一个线程。但是在现代的操作系统和L4中，在一个进程，一个地址空间中，可以有多个线程。如果你有多个CPU核，那么多个CPU核可以同时运行一个Task。每个线程在Task的地址空间中都有一个设置好的Stack，这意味着你可以写一个程序，并通过并行运行在多个CPU核上得到性能的提升，其中的每个线程都运行在不同的CPU核上。

所以你们可以看到，这里的设计非常依赖IPC，因为如果你想与你的文件系统交互，文件系统想要与设备驱动交互，你都需要来回发送IPC消息。对于每个系统调用，每个Page Fault，每个设备中断，都会有反复的IPC消息。所以IPC系统需要非常快。
