13.3 实现

看过用户界面和系统调用接口后,现在让我们来看一看如何实现一个操作系统。在下面8个小节,我们将分析涉及实现策略的某些一般的概念性问题。在此之后,我们将看一看某些低层技术,这些技术通常是十分有益的。

13.3.1 系统结构

实现必须要做出的第一个决策可能是系统结构应该是什么。我们在1.7节分析了主要的可能性,在这里要重温一下。一个无结构的单块式设计实际上并不是一个好主意,除非可能是用于电冰箱中的微小的操作系统,但是即使在这里也是可争论的。

1.分层系统

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

多年以来很好地建立起来的一个合理的方案是分层系统。Dijkstra的THE系统(图1-25)是第一个分层操作系统。UNIX和Windows Vista也具有分层结构,但是在这两个系统中分层更是一种试图描述系统的方法,而不是用于建立系统的真正的指导原则。

对于一个新系统,选择走这一路线的设计人员应该首先非常仔细地选择各个层次,并且定义每个层次的功能。底层应该总是试图隐藏硬件最糟糕的特异性,就像图11-7中HAL所做的那样。或许下一层应该处理中断、上下文切换以及MMU,从而在这一层的代码大部分是与机器无关的。在这一层之上,不同的设计人员可能具有不同的口味(与偏好)。一种可能性是让第3层管理线程,包括调度和线程间同步,如图13-2所示。此处的思想是从第4层开始,我们拥有适当的线程,这些线程可以被正常地调度,并且使用标准的机制(例如互斥量)进行同步。

阅读 ‧ 电子书库
图 13-2 现代分层操作系统的一种可能的设计

在第4层,我们可能会找到设备驱动程序,每个设备驱动程序作为一个单独的线程而运行,具有自己的状态、程序计数器、寄存器等,可能(但是不必要)处于内核地址空间内部。这样的设计可以大大简化I/O结构,因为当一个中断发生时,它就可以转化成在一个互斥量上的unlock,并且调用调度器以(潜在地)调度重新就绪的线程,而该线程曾阻塞在该互斥量之上。MINIX使用了这一方案,但是在UNIX、Linux和Windows Vista中,中断处理程序运行在一类“无主地带”中,而不是作为适当的线程可以被调度、挂起等。由于任何一个操作系统的大多数复杂性在于I/O之中,使其更加易于处理和封装的任何技术都是值得考虑的。

在第4层之上,我们预计会找到虚拟内存、一个或多个文件系统以及系统调用接口。如果虚拟内存处于比文件系统更低的层次,那么数据块高速缓存就可以分页出去,使虚拟内存管理器能够动态地决定在用户页面和内核页面(包括高速缓存)之间应该怎样划分实际内存。Windows Vista就是这样工作的。

2.外内核

虽然分层在系统设计人员中间具有支持者,但是还有另一个阵营恰恰持有相反的观点(Engler等人,1995)。他们的观点基于端到端的论据(end-to-end argument)(Saltzer等人,1984)。这一概念说的是,如果某件事情必须由用户程序本身去完成,在一个较低的层次做同样的事情就是浪费。

考虑该原理对于远程文件访问的一个应用。如果一个系统担心数据在传送中被破坏,它应该安排每个文件在写的时候计算校验和,并且校验和与文件一同存放。当一个文件通过网络从源盘传送到目标进程时,校验和也被传送,并且在接收端重新计算。如果两者不一致,文件将被丢弃并且重新传送。

校验比使用可靠的网络协议更加精确,因为除了位传送错误以外,它还可以捕获磁盘错误、内存错误、路由器中的软件错误以及其他错误。端到端的论据宣称使用一个可靠的网络协议是不必要的,因为端点(接收进程)拥有足够的信息以验证文件本身的正确性。在这一观点中,使用可靠的网络协议的惟一原因是为了效率,也就是说,更早地捕获与修复传输错误。

端到端的论据可以扩展到几乎所有操作系统。它主张不要让操作系统做用户程序本身可以做的任何事情。例如,为什么要有一个文件系统?只要让用户以一种受保护的方式读和写原始磁盘的一个部分就可以了。当然,大多数用户喜欢使用文件,但是端到端的论据宣称,文件系统应该是与需要使用文件的任何程序相链接的库过程。这一方案使不同的程序可以拥有不同的文件系统。这一论证线索表明操作系统应该做的全部事情是在竞争的用户之间安全地分配资源(例如CPU和磁盘)。Exokernel是一个根据端到端的论据建立的操作系统(Engler等人,1995)。

3.基于微内核的客户-服务器系统

在让操作系统做每件事情和让操作系统什么也不做之间的折衷是让操作系统做一点事情。这一设计导致微内核的出现,它让操作系统的大部分作为用户级的服务器进程而运行,如图13-3所示。在所有设计中这是最模块化和最灵活的。在灵活性上的极限是让每个设备驱动程序也作为一个用户进程而运行,从而完全保护内核和其他驱动程序,但是让设备驱动程序运行在内核会增加模块化程度。

阅读 ‧ 电子书库
图 13-3 基于微内核的客户-服务器计算

当设备驱动程序运行在内核态时,可以直接访问硬件设备寄存器,否则需要某种机制以提供这样的访问。如果硬件允许,可以让每个驱动程序进程仅访问它需要的那些I/O设备。例如,对于内存映射的I/O,每个驱动程序进程可以拥有页面将它的设备映射进来,但是没有其他设备的页面。如果I/O端口空间可以部分地加以保护,就可以保证只有相应的正确部分对每个驱动程序可用。

即使没有硬件帮助可用,仍然可以设法使这一思想可行。此时需要的是一个新的系统调用,该系统调用仅对设备驱动程序进程可用,它提供一个(端口,取值)对列表。内核所做的是首先进行检查以了解进程是否拥有列表中的所有端口,如果是,它就将相应的取值复制到端口以发起设备I/O。类似的调用可以用一种受保护的方式读I/O端口。

这一方法使设备驱动程序避免了检查(并且破坏)内核数据结构,这(在很大程度上)是一件好事情。一组类似的调用可以用来让驱动程序进程读和写内核表格,但是仅以一种受控的方式并且需要内核的批准。

这一方法的主要问题,并且一般而言是针对微内核的主要问题,是额外的上下文切换导致性能受到影响。然而,微内核上的所有工作实际上是许多年前当CPU还非常缓慢的时候做的。如今,用尽CPU的处理能力并且不能容忍微小性能损失的应用程序是十分稀少的。毕竟,当运行一个字处理器或Web浏览器时,CPU可能有95%的时间是空闲的。如果一个基于微内核的操作系统将一个不可靠的3GHz的系统转变为一个可靠的2.5GHz的系统,可能很少有用户会抱怨。毕竟,仅仅在几年以前当他们得到具有1GHz的速度(就当时而言十分惊人)的系统时,大多数用户是相当快乐的。

4.可扩展的系统

对于上面讨论的客户-服务器系统,思想是让尽可能多的东西脱离内核。相反的方法是将更多的模块放到内核中,但是以一种“受保护的”方式。当然,这里的关键字是“受保护的”。我们在9.5.6节中研究了某些保护机制,这些机制最初打算用于通过Internet引入小程序,但是对于将外来的代码插入到内核中的过程同样适用。最重要的是沙盒技术和代码签名,因为解释对于内核代码来说实际上是不可行的。

当然,可扩展的系统自身并不是构造一个操作系统的方法。然而,通过以一个只是包含保护机制的最小系统为开端,然后每次将受保护的模块添加到内核中,直到达到期望的功能,对于手边的应用而言一个最小的系统就建立起来了。按照这一观点,对于每一个应用,通过仅仅包含它所需要的部分,就可以裁剪出一个新的操作系统。Paramecium就是这类系统的一个实例(Van Doorn,2001)。

5.内核线程

此处,另一个相关的问题是系统线程,无论选择哪种结构模型。有时允许存在与任何用户进程相隔离的内核线程是很方便的。这些线程可以在后台运行,将脏页面写入磁盘,在内存和磁盘之间交换进程,如此等等。实际上,内核本身可以完全由这样的线程构成,所以当一个用户发出系统调用时,用户的线程并不是在内核模式中运行,而是阻塞并且将控制传给一个内核线程,该内核线程接管控制以完成工作。

除了在后台运行的内核线程以外,大多数操作系统还要启动许多守护进程。虽然这些守护进程不是操作系统的组成部分,但是它们通常执行“系统”类型的活动。这些活动包括接收和发送电子邮件,并且对远程用户各种各样的请求进行服务,例如FTP和Web网页。