宏内核与微内核
完整内容请见:https://mit-public-courses-cn-translatio.gitbook.io/mit6-s081/lec18-os-organization-robert
Monolithic Kernel
宏内核优势
monolithic 的意思是指操作系统内核是一个完成了各种事情的大的程序,我们熟知的 Linux 是由这种方式所构建。通常来说 monolithic kernel 集成了文件系统,内存分配,调度器,虚拟内存系统等一系列复杂的组件,以及许多强大的抽象。这样带来的好处是显而易见的:
这些高度抽象的接口通常是可移植的,可以在各种各样的存储设备上实现文件和目录,而不需要改变代码
可以向应用程序隐藏复杂性。Linux 提供了地址空间的抽象,而不是让程序直接访问 MMU。另外一个例子是,我们可以直接对 fd 调用 read/write,而不需要直接接触文件系统中各个层级
帮助管理共享资源,比如内存与磁盘
因为所有这些功能都在一个程序里面,所以组件之间可以访问彼此的数据结构,进而使得依赖多个组件的工具更容易实现。比如 exec 系统调用,它依赖文件系统,因为它要从磁盘中读取二进制文件并加载到内存中,同时它也依赖内存分配和虚拟内存系统,因为它需要设置好新的进程的地址空间。但是在内核态我们可以没有隔离地访问 inode,proc,pgtbl 等结构体,所以 exec 的中实现是相对简单的
内核的所有代码都以完整的硬件权限(内核都在 Supervisor mode)在运行,这也简化了软件的实现
宏内核劣势
但是其缺点也很明显,这也是为什么其他内核架构,比如微内核,出现的原因,monolithic kernel 并不适用于所有的场景:
它们大且复杂。到目前为止,Linux 的代码量来到了千万行。一个组件可以很方便地与另一个组件交互,这确实使得编程更容易了,但同样意味着内部代码有大量的交互和依赖,以至于看明白代码都有挑战。而且大型的程序与复杂的结构也同样伴随着大量的 Bug 与安全漏洞,如果使用宏内核,这些几乎不可避免
另一个人们不喜欢 monolithic kernel 的原因是,随着时间的推移,它们倾向于发展成拥有所有的功能。Linux 应用在各种场合中,从移动电话到桌面工作站,从笔记本电脑到平板电脑,从服务器到路由器。Linux可以支持这么多设备是极好的,这也使得 Linux 非常的通用。这很好,但是另一方面,通用就意味着慢。对于各种不同的场景都能支持,或许就不能对某些特定场景进行优化。况且很多时候我们根本用不着这么多功能。另外,这也使得完成一些简单的工作需要做很多额外的事情:比如当我们使用 pipe 时,我们需要 buffering,locking,sleep/wakeup,还有 context switching...... 但是对于从一个进程移动一个字节到另一个进程来说,这里有大量的内容或许并不是必须的
宏内核还可能因为太大反而削弱了抽象能力。你可以 wait 你自己 fork 的子进程,但不可以等待其他的;你可以 mmap 自己的地址空间,但是为了隔离性不能修改其他的;你可以在磁盘上建一个 B 树,但是读取磁盘时,文件系统不知道这是一个 B 树,这反而不利于效率
还有就是可扩展性,你只能使用内核提供的能力
Micro Kernel
微内核是指一种通用的方法或者概念,并不特指任何特定的产品
微内核的核心就是实现了 IPC(Inter-Process Communication)以及线程和任务的 tiny kernel。所以微内核只提供了进程抽象和通过IPC进程间通信的方式,除此之外别无他物。任何你想要做的事情,例如文件系统,你都会通过一个用户空间进程来实现,完全不会在内核中实现
在这种实现中,假如一个文本编辑器需要读取一个文件,它通过 IPC 会发送一条消息到文件系统进程。文件系统进程需要与磁盘交互,所以它会发送另一个 IPC 到磁盘驱动程序。之后磁盘驱动会返回一个磁盘块给文件系统。之后文件系统再将VI请求的数据通过 IPC 返回给文本编辑器
可以看到,在内核中唯一需要做的是支持进程/任务/线程,以及支持 IPC 来作为消息的传递途径,除此之外,内核不用做任何事情。内核中没有任何文件系统,没有任何设备驱动,没有网络协议栈,所有这些东西以普通用户进程在运行
研究微内核的动机
其实这里面还有审美因素:有人觉得像 Linux 内核这样大的复杂的程序并不十分优雅。我们可以构建一些小得多且专注得多的设计,而不是这样一个巨大的拥有各种随机特性的集合体
但其实微内核有更多具体的优势:
更小的内核或许会更加的安全
在特殊场景下,你需要证明一个操作系统是正确且安全的。而一个几百万行的内核不太可能保证。内核通常都很小,这是它能够被证明是安全的一个关键因素
少量代码的程序比巨大的程序更容易被优化
小内核可能会运行的更快,你不用为很多用不上的功能付出代价
小内核在设计上或许有更少的限制,应用程序的编写可以更加灵活
除了由小带来的有点,它的设计也使得其有独特的特性:
有很多我们习惯了位于内核的功能和函数,现在都运行在用户空间的不同部分,可以使得代码更模块化
运行在用户空间的代码更容易被修改,所以更容易被定制化
此外,这种设计还可以使 OS 更加健壮。如果内核出错,通常需要 panic 并重启。但是故障部分被转移到了用户空间,我们可以只是重新启动这一个服务,而不影响其他功能。这对于驱动来说尤其明显,内核中大部分 Bug 都在硬件驱动中,如果我们能将设备驱动从内核中移出的话,那么内核中可能会有少的多的 Bug 和 Crash
可以在微内核上模拟或者运行多个操作系统。我们可以把一个完整的 Linux 内核当作一个·任务运行在微内核上
微内核的挑战
第一个挑战是,我们希望微内核的系统调用接口尽可能的简单,但是什么才是有用的系统调用的最小集?我们需要考虑如何设计这些底层的系统调用,在简单的同时,需要能够足够强大以支持人们需要做的各种事情
我们仍然需要开发一些用户空间服务来实现操作系统的其他部分。比如我们的系统调用需要支持 exec,但是内核没有文件的抽象
微内核的设计需要进程间通过 IPC 有大量的通信,这要求 IPC 非常快,否则很影响性能
L4
L4 是最早的一批可工作的微内核
L4 简介
L4 是微内核,它只有7个系统调用
L4 并不大,论文发表的时候,它只有 13000 行代码
Task,线程,地址空间,IPC 是 L4 唯四有的抽象
L4里面不包含其他的功能,没有文件系统,没有fork/exec系统调用,除了这里非常简单的IPC之外,没有其他例如pipe的通信机制,没有设备驱动,没有网络的支持等等。任何其他你想要的功能,你需要以用户空间进程的方式提供
L4能提供的一件事情是完成线程间切换。L4会完成线程调度和context switch,来让多个线程共用一个CPU。它的实现方式其实和我们熟知的线程切换方式非常类似
Improving IPC
如果是像 pipe 的实现,P1 与 P2 通信完成一次消息的发送与接收。P1 会调用 send,陷入内核,向 buffer 中写消息。在 send 返回后,又调用 recv 来获取回复,期间会 sleep 等待,可能会在计时器到来时 context switching。过一会 P2 有空,调用 recv 接受消息,处理完后调用 send 追加到 buffer。等到调度器使 P1 回复运行,看到 buffer 中有消息,recv 才返回......
在这个设计中,为了让消息能够发送和回复,期间有多次的用户态与内核态切换,4 次系统调用,若是单核,还得至少有一次 context switching 在 P1 与 P2 中切换。每一次用户空间和内核空间之间的切换和 context switching 都很费时,因为每次切换,都需要切换 Page Table,进而清空 TLB。而且不仅如此还牵涉到消息的拷贝、缓存的分配等等。可见这种异步的设计是非常慢的
但是我们只是期望发送一个消息然后得到一个回复,我们完全可以有更简单的设计
Synchronized
比如我们的 send 和 recv 是一体的:send 会等待消息被接收,并且 recv 会等待回复消息被发送。如果我是进程 P1,我想要发送消息,我会调用 send,send 不会写完 buffer 就走人,而是等待 P2 的 recv。当 P2 调用 recv,二者都在内核中,此时内核可以直接将消息从用户空间 P1 拷贝到用户空间 P2,而不用先拷贝到内核中,再从内核中拷出来
Avoid Copying
如果消息超级小,比如说只有几十个字节,它可以在寄存器中传递,而不需要拷贝。如果P1要发送的消息很短,它可以将消息存放到特定的寄存器中。当内核返回到P2进程的用户空间时,会恢复保存了的寄存器,这意味着当内核从recv系统调用返回时,特定寄存器的内容就是消息的内容,因此完全不需要从内存拷贝到内存
对于非常长的消息,可以在一个 IPC 消息中携带一个 Page 映射。你可以发送一个物理内存 Page,这个 Page 会被再次映射到目标 Task 地址空间,这里也没有拷贝。这里提供的是共享 Page 的权限