18.6 Run Linux on top of L4 micro kernel

前一节对于IPC的优化使得人们开始认真考虑使用微内核替代monolithic kernel。然而,这里仍然有个问题,即使IPC很快了,操作系统的剩余部分从哪里去获取?现在的微内核大概只有一个完整操作系统的百分之几,我们该怎么处理操作系统剩下的部分?这个问题通常会在一些有着相对较少资源的学校研究项目中被问到,我们需要从某个地方获取到所有这些用户空间服务。

实际上在一些特殊的应用场合,以上的问题并不是问题,比如说我们运行的一些设备的控制器,例如车里的点火控制器,只运行了几千行代码,它并且不需要一个文件系统,这样我们就只需要很少的用户空间内容,微内核也特别适合这种应用程序。但是微内核项目发起时,人们非常有雄心壮志,人们想的是完全替换操作系统,人们希望可以构建一些运行在工作站,服务器等各种地方的微内核操作系统,并取代大的monolithic kernel。对于这种场景,你需要一个传统操作系统所需要的所有内容。

一种可能是,重新以微内核的方式,以大量的进程实现所有的内容。实际上有项目在这么做,但是这涉及到大量的工作。具体的说,比如我想要使用笔记本电脑,我的电脑必须要有emacs和我最喜欢的C编译器,否则我肯定不会用你的操作系统。这意味着,微内核要想获得使用,它必须支持现有的应用程序,它必须兼容或者提供相同的系统调用或者更高层的服务接口,它必须能够完全兼容一些现有的操作系统,例如Unix,Linux,这样人们才愿意切换到微内核。所以这些微内核项目都面临一个具体的问题,它们怎么兼容一些为Linux,Windows写的应用程序?对于论文中提到的项目,也就是L4,对标的是Linux。与其写一些完全属于自己的新的用户空间服务,并模仿Linux,论文中决定采用一种容易的多的方法,其实许多项目也都采用了这种方法,也就是简单的将一个现有的monolithic kernel运行在微内核之上,而不是重新实现一些新的东西。这就是今天论文要介绍的内容。

在今天论文的讨论中,L4微内核位于底部,但是同时,一个完整的Linux作为一个巨大的服务运行在用户空间进程中。听起来有点奇怪,一般的kernel都是运行在硬件之上,而现在Linux kernel是一个用户空间进程。

实际上,如你在QEMU上运行XV6时所见,内核也是运行在用户空间。Linux kernel不过就是一个程序,对其做一些修改它就可以运行在用户空间,所以现在Linux需要被修改。论文中提到需要对Linux的底层做一些修改,例如Linux中期望能直接修改Page Table的内容,读写CPU寄存器。Linux中一部分需要被修改以将它们改成调用L4微内核的系统调用,或者发送IPC,而不是直接访问硬件。但是Linux的大部分内容都可以不做修改而直接运行。所以按照这种方式,作为Linux的一部分,现在得到了文件系统,网络支持,各种设备驱动等等,而不需要自己实现这些。

这里的实现方式是将Linux内核作为一个L4 Task运行,每一个Linux进程又作为一个独立的L4 Task运行。所以当你登录到Linux中时,你要它运行一个Shell或者terminal,它会在用户空间创建一个L4 Task来运行这个Linux程序。所以现在有一个Task运行Linux,以及N个Task来运行每一个你在Linux中启动的进程。

Linux不会直接修改进程的Page Table,而是会向L4发送正确的IPC让L4来修改进程的Page Table。

这里有很多小的改动,其中一个有意思的地方是,当VI想要执行一个系统调用时,VI并不知道它运行在L4之上,在上面的方案中,所有的程序都以为它们运行在Linux中。当VI要执行系统调用时,L4并不支持,因为VI要执行的是Linux系统调用而不是L4系统调用。所以对于Linux进程,会有一个小的库与之关联,这个库会将类似于fork,exec,pipe,read,write的系统调用,转换成发送到Linux kernel Task的IPC消息,并等待Linux kernel Task的返回,然后再返回到进程中。从VI的角度看起来好像就是从系统调用返回了。所以这些小的库会将系统调用转成发送到Linux kernel Task的IPC消息。这意味着,如果Linux kernel Task没有做其他事情的话,它会在一个recv系统调用中等待接收从任何一个进程发来的下一个系统调用请求IPC。

这导致了这里的Linux和普通的Linux明显不同的工作方式。在普通的Linux中,就像XV6一样,会有一个内核线程对应每一个用户空间进程。当用户空间进程调用系统调用时,内核会为这个系统调用运行一个内核线程。并且,在普通的Linux中,如果内核在内核线程之间切换,这基本上意味着从一个用户进程切换到另一个用户进程。所以这里Linux kernel的内核线程以及当Linux完成工作之后要运行的用户进程之间有一对一的关系。

在这里架构中,这种一对一的关系断了,这里的Linux kernel运行在一个L4线程中。然而,就像XV6一样,这个线程会使用与XV6中的context switching非常相似的技术,在与每个用户进程对应的内核线程之间切换。不过这些内核线程完全是在Linux中实现的,与L4线程毫无关系,唯一的L4线程就是运行了Linux kernel的控制线程。

但是哪个用户进程可以运行,是由L4决定的。所以在这里的设置中,Linux kernel或许在内核线程中执行来自VI的系统调用,同时,L4又使得Shell在用户空间运行了。这在XV6或者Linux极不可能发生,在这两个系统中,活跃的内核线程和用户进程有直接的对应关系,而L4会运行它喜欢的任何Task。因为Linux kernel中的内核线程都是私有的实现,Linux可以同时执行不同阶段的多个系统调用,或许一个进程在它的内核线程中在等待磁盘,这时Linux可以运行另一个进程的内核线程来处理另一个进程的系统调用。

你或许会想知道为什么不直接使用L4线程来实现Linux内的内核线程,或者说Linux为什么要实现自己内部的内核线程,而不是使用L4线程,答案是,

  • 在论文发表时,还没有用到多核CPU硬件,他们使用的是单核CPU硬件。所以在内核中同时运行多个内核线程并没有性能优势,因为只有一个CPU核,所以第二个线程不能执行,由于硬件的限制,一次只能执行一个线程。

  • 另一个或许是更强大的原因是,在论文发表时,他们使用的Linux版本并不支持将Linux kernel运行在多个CPU核上。所以他们使用的是旧版本的单核Linux,一次只能期望在内核中使用一个CPU,它并没有类似于XV6的spinlock,可以使得它能正确的在内核中使用多核。所以在Linux内核中使用多个L4线程并没有性能优势。如果一定要使用的话,在没有性能优势的前提下,又需要加入spinlock和其他的内容来支持并发。所以论文中没有在Linux内核使用L4线程。

这种架构的一个缺点是,在普通原生的Linux中,存在大量复杂的线程调度机制,例如在不同进程上增加优先级,确保调度公平性等等。Linux可以在你的笔记本上运行这些机制,因为Linux控制了哪些进程可以运行在哪些CPU核上。但是在这里的架构中,Linux完全控制不了哪些进程可以运行,因为现在是L4而不是Linux在完成调度,这些进程都是被L4所调度。所以这里的架构失去了Linux的调度能力,这是这种架构的缺点,我相信L4的后续版本有一些方法能够让Linux通知L4调度器,来给某个进程更高优先级等等。

Last updated