18.4 L4 micro kernel

今天要讨论的论文,有许多有关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代码的几十分之一,所以它非常的小。

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

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

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

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

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

  • 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。

以上就是内核的内容,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系统需要非常快。

Last updated