操作系统结构 - Minix3

已经看到了操作系统的外观,即程序设计者接口,是时候看看内部结构了。

本文将介绍业界已尝试过的五种不同操作系统结构,及实践中已被证实过的设计思想。

五种设计:

单片系统、分层系统、虚拟机、外核(exokernels)、客户端 - 服务器模式的系统。

单片系统

到目前为止,最常见的组织方式。

这种方式很可能被称为“大混乱”。它的结构就是没有结构。

操作系统被编写成一组程序,每个程序都能在需要时调用任何其他程序。

使用这种技术,系统中每个程序在参数和结果方面都有一个明确定义的接口,若后者提供了前者需要的一些有用计算,则每个程序都可以自由的调用其他程序。

使用此方法构造操作系统的应用程序,首先,要编译所有单个程序,或包含程序的文件,然后,使用系统链接器,将它们全部绑定到单个目标文件中。

在信息隐藏方面,程序间基本上都是互相可见。

与模块或包的结构不同,模块或包中的大部分信息隐藏在模块内部,只有指定的入口点才能被外部模块调用。

即使在单片系统中,也会有少量的一点结构。

操作系统提供的服务(系统调用),是将参数放在一个定义良好的位置(如:寄存器或堆栈中)执行请求,然后,执行一个特殊的trap指令(称为内核调用/kernel call 或管理器调用/supervisor call)。

该指令将机器从用户模式切换到内核模式,将控制权转移到操作系统。

大多数CPU有两种模式:

内核模式,用于操作系统,执行所有指令。

用户模式,用于用户程序,不允许I/O和某些指令。

现在是研究如何执行系统调用的好时机,回想一下read函数的调用过程大概如下:

count = read(fd, buffer, nbytes);

准备调用read库函数(实际上是进行read系统调用)时,调用程序首先将参数推入堆栈,如下图1-16步骤13所示。

由于历史原因,C和c++编译器以相反的顺序将参数推入堆栈(必须让printf第一个参数出现在堆栈顶部)。

第一个和第三个参数按值调用,但第二个参数通过引用传递,这意味着传递的是缓冲区的地址(由&表示),而非缓冲区的内容。

然后是对库函数的实际调用(步骤4),该指令是程序的常规调用指令。

The 11 steps in making the system call

图1-16 read系统调用过程:

库函数(可能是用汇编语言编写的)通常将系统调用号,放在操作系统期望位置,如:寄存器(步骤5)。

然后,执行trap指令,从用户模式切换到内核模式,在内核固定地址上开始执行(步骤6)。

启动后的内核代码会检查系统调用号,然后,转给合适的处理程序(一般通过系统调用号去指针表中,找到对应用的系统调用处理程序)。

此时,系统调用处理程序运行(步骤8),一旦处理程序完成工作,就能够在trap指令之后的地方,将控制权返回给用户空间的库函数(步骤9)。

然后,该函数以常规方式返回到用户程序,程序调用返回(步骤10)。

调用结束时,用户程序必须像在其他程序调用那样,调用完成后清理堆栈(步骤11)。

假设堆栈向下增长,编译后的代码会对堆栈指针执行自增,增加到足以删除调用read之前推入的参数。

在上面的步骤9中,我们说“可能会返回到用户空间的库函数”是有充分理由的。

系统调用可能会阻止调用者,使它不能继续。

如:尝试从键盘读取,但此时尚未有任何输入,调用者会被阻止。

此时,操作系统环顾四周,看看下一步是否能够运行其他进程。

稍后,当有输入时,该进程将引起系统注意,然后,执行步骤9至11。

基本结构

一个根据请求执行服务程序的主程序。

一组执行系统调用的服务程序。

一组帮助服务程序的应用程序。

此模型中,每个系统调用,都有一个服务程序来处理它。

应用程序执行多个服务程序所需的操作,如:从用户程序中获取数据。

将程序划分为三层,如下图1-17所示: 

单片系统的简单结构模型

图1-17 单片系统的简单结构模型

分层系统

图1-17将操作系统按层次结构进行组织,每一层都基于下层进行构建。

以这种方式进行构建的第一个系统是由荷兰E. W. Dijkstra和学生在Technische Hogeschool Eindhoven创建的系统。

该系统是一个简单的批处理系统,适用于一台Electrologica X8荷兰计算机,只有32K 27位。

系统共6层,如下图1-18所示。

最底层(第0层)分配处理器,发生中断或定时器到期时,在进程间切换。

第0层之上,系统由时序进程组成,每个进程都可以编程,不用担心多个进程在一个处理器上运行。

换句话说,第0层为基于CPU的多道程序设计提供基础。

Layer   Function
5          The operator
4          User programs
3          Input/output management
2          Operator-process communication
1          Memory and drum management
0          Processor allocation and multiprogramming

第1层执行内存管理,为进程分配主内存,主内存空间不足时,使用pages(512K )为进程保存数据。

第1层之上,进程不必担心是在内存中还是在pages,第1层软件负责在需要时将页面放入内存。

第2层处理进程与控制台间的通信,在这一层之上,每个进程都有自己的(操作者)控制台。

第3层负责管理I/O设备,并缓冲进出的信息流。

第3层之上,是经过良好抽象每个进程都可使用的I/O设备,而非具有许多特性的真实设备。

第4层是用户程序运行的地方,不必担心进程、内存、控制台、I/O管理。

系统操作者进程位于第5层。

MULTICS系统中,分层概念得到进一步推进。

MULTICS不是分层的,而是由一系列同心环组成,内部环比外部环更具特权。

外环中的程序,想要调用内环中的程序,需执行相当于系统调用的操作,即,在执行调用前仔细检查trap指令的参数。

MULTICS中,尽管每个用户进程的地址空间是操作系统的一部分,但从硬件方面(实际上是内存段)可将个别程序,读、写、执行进行保护(防止读、写或执行)。

鉴于这种分层方案只是一种设计手段,因为,系统的所有部分最终都会链接在一起,形成一个整体。

MULTICS中,环形机制在运行时非常常见,且由硬件强制执行。

这种环形结构的优点可以方便地扩展到用户子系统。如:教授编写的一个程序运行在环n中,测试和评估学生所写的程序。学生的程序运行在n + 1环中,这样,学生的程序就不能改变对他们的评分,奔腾硬件支持多环结构,但目前主流操作系统都未采用这种方式。

虚拟机

OS/360初始版本是严格的批处理系统。

尽管如此,许多OS/360用户都想要分时系统,因此,IBM各团队决定编写分时系统。

IBM官方的分时系统TSS/360交付时间较晚,它最终实现时却是庞大且缓慢,很少有用户愿意切换到此系统。

在消耗了大约5000万美元的开发成本后,最终还是被放弃了(Graham,1970)。

位于马萨诸塞州剑桥市IBM科研中心的一个小组,制作了一个完全不同的系统,IBM最终将其纳为产品,在IBM大型机上得到广泛使用。

这个系统最初被称为CP/CMS,后来被重新命名为VM/370 (hairight and MacKinnon, 1979)。

它基于一个非常敏锐的观察:多道程序设计的分时系统相对裸机而言,拥有更多更灵活接口的扩展(操作系统概念就是对物理机器的扩展),VM/370本质是将这两个功能完全分开。

系统的核心称为虚拟机监视器,在裸硬件上运行,并进行多道程序设计,但不向下一层提供多个虚拟机,如下图1-19所示。

与其他操作系统不同,这些虚拟机不是对机器的扩展(有文件和其他的功能)。

相反,它们是物理机的精确副本,包括内核/用户模式、I/O、中断、及物理机具备的所有特征。

 The structure of VM/370 with CMS.

由于每个虚拟机都与真实硬件相同,所以,能在物理机上运行的操作系统,在每虚拟机上一样运行。

不同的虚拟机运行不同的操作系统。

可能是运行批处理或事务处理的OS/360,也可能是CMS(会话监视器系统)单用户交互式系统。

当CMS程序执行系统调用时,调用会被虚拟机中的操作系统捕获(而非VM/370),就像运行在物理计算机上一样。

当CMS发出硬件I/O指令读取虚拟磁盘时,I/O指令会被VM/370捕获,VM/370将它们当作真实硬件的一部分执行。

通过完全分离的多道程序功能,提供对机器的扩展,每个组件更简单、灵活,易于维护。

虚拟机概念,现今有很多应用场景:如,奔腾上运行旧的MS-DOS程序。

在设计奔腾及软件时,英特尔和微软都意识到,在新硬件上运行旧软件的需求量很大。

为此,Intel在奔腾上提供了一个虚拟8086模式。在这种模式下,机器的行为类似于8086(软件角度它与8088相同),包括16位寻址,限制为1mb。

Windows和其他操作系统,都采用这种模式运行旧的MS-DOS程序。

这些程序在虚拟8086模式下启动,只要它们能执行正常指令,就可以在物理机器上运行。

但是,当程序尝试trap到操作系统,进行系统调用(或尝试执行受保护的I/O),会引发虚拟机监视器trap。

这种设计有两种可能的变体。

1、MS-DOS本身被加载到虚拟8086地址空间,因此,虚拟机监视器只是将trap反射回MS-DOS,就像发生在真正8086上那样。

当MS-DOS以后试图自己执行I/O时,操作会被虚拟机监视器捕获并执行。

2、虚拟机监视器只捕获第一次trap并自己执行I/O,因为,它知道所有MS-DOS的系统调用,知道每个trap应该做什么。

与第一版本相比,这个版本不够纯粹,因为,它只模拟MS-DOS,而不像第一个版本那样模拟其他操作系统。

另一方面,它省去了启动MS-DOS执行I/O的过程,所以会更快。

在虚拟8086模式下运行MS-DOS的另一个缺点,对中断启用/禁用位进行太多调整,这些模拟成本相当高。

值得注意的是,这两种方法实际上与VM/370有所不同,因为,被仿真的机器不是完整的奔腾而是8086。

使用VM/370系统,可以在虚拟机中运行VM/370本身。

即使是最早期的Windows也需要至少286,且不能在虚拟8086上运行。

各种虚拟机实现目前都进行了商业化。对于web托管服务商而言,单个高效服务器上运行多个虚拟机,比运行许多小型计算机更经济。

VMWare和Microsoft Virtual PC面向此类市场。

这些程序在主系统上创建大文件作为客户系统的磁盘。为提高效率,他们分析客户系统程序二进制文件,并允许安全代码直接在主机硬件上运行,捕获执行操作系统调用的指令。

这种系统在教育中很有用。如,从事minix3实验室作业的学生可以在windows,linux或UNIX主机上使用minix3,作为VMWare上的客户操作系统,不会损坏安装在同一台pc上的其他软件。

大多数教授其他科目的教授,非常担心将实验室计算机与操作系统课程共享,学生的错误可能会破坏或删除磁盘数据。

虚拟机另一个用武之地,运行Java程序,这是一种完全不同的方式。

当Sun Microsystem推出java编程语言时,提供了一种称为JVM(Java虚拟机)的虚拟机(即计算机体系结构)。

Java语言编译器为JVM生成代码,然后,由JVM中的解释器执行(常规情况下)。

这种方式的优点是JVM代码能通过Internet传送到装有JVM的计算机上,在其上运行。

假如,编译器生成了运行在SPARC或奔腾上的二进制程序,那么,它们就不能在其他架构的处理器上发布和运行,当然,Sun公司也可以提供生成SPARC二进制文件的编译器,然后,分发SPARC解释器,但最终以JVM作为解释器的体系结构是一种更好的做法。

使用JVM的另一个优点是,如果,解释器得到了正确的实现,那么,可以检查传入JVM程序的安全性,然后,在受保护的环境中执行,这样,就不会出现程序窃取数据或有损害计算机的恶意行为。

Exokernels

使用VM/370每个用户进程都可获得物理计算机的精确副本。

通过Pentium上的虚拟8086模式,每个用户进程都可获得不同计算机的精确副本。

麻省理工学院的研究人员进一步构建系统,该系统为每个用户提供真实计算机的克隆,但只提供部分资源(Engler et al., 1995;和Leschke, 2004)。

因此,一个虚拟机可能获得0~1023的磁盘块,下一个虚拟机可能获得1024~2047的磁盘块,依此类推。

在底层,是一个名为exokernel内核模式运行的程序。

它的工作是将资源分配给虚拟机,并执行检查,确保没有机器试图使用其他人的资源。

每个用户级虚拟机都能运行自己的操作系统,就像在VM/370和奔腾虚拟8086上一样,只不过每个虚拟机都被限制,只能使用它自己的资源。

exokernel方案优点是节省了映射层。

在其他设计中,每个虚拟机都认为自己有独立的磁盘(从0运行到某个最大值),因此,虚拟机监视器必须维护一个表,以重新映射磁盘地址及其他资源。

对于exokernel不需要重新映射。

exokernel只需跟踪哪个虚拟机已分配到哪个资源。

这种方法仍具备多道程序设计(multiprogramming)的优点(从用户操作系统代码(在用户空间)中分离出来),开销更小,exokernel所要做的是让虚拟机互不干扰。

客户端 - 服务器模型

VM/370将大部分传统意义上的操作系统代码(实现扩展的机器)迁移到更高的CMS层,大大简化工作。

尽管如此,VM/370仍然是一个复杂的程序,因为,模拟大量虚拟370并不那么简单(特别是想做到合理高效)。

在现代操作系统中,有一种趋势是进一步将代码向上移动到更高的层,并尽可能地从操作系统中删除代码,只留下最小的内核。

通常的方法是在用户进程中实现大多数操作系统功能。请求一个服务(如,读取文件),用户进程(客户机进程)将请求发送到服务器进程,服务器进程会执行任务并返回结果。

在此模型中,如下图1-20所示,内核所做的是处理客户端和服务器间的通信。

通过将操作系统分解为多个部分,每个部分只处理系统的一个方面,如:文件服务、进程服务、终端服务/内存服务,每个部分都变得小且易于管理。

此外,由于所有服务器都作为用户模式进程运行,而非内核模式,因此,它们不能直接访问硬件。

因此,如果文件服务器中的bug被触发,文件服务可能会崩溃,但不会导致机器宕掉。

The client-server model.

客户端 - 服务器模型另一个优点是它适用于分布式系统(见下图1-21)。

如果客户端通过发送消息与服务器通信,则客户端无需知道消息是在本地计算机上处理,还是通过网络发送到远程计算机上进行处理。

对于客户端而言,在这两种情况下所做事情是相同的:发送请求和接收应答。

The client-server model in a distributed system.

上面描绘的内核只处理从客户端到服务器来回的消息传输,这并不切合实际。

某些操作系统功能很难在用户空间执行。有两种方法能解决这个问题。

一种是让一些关键服务器进程(如,I/O设备驱动程序)在内核模式运行,能完全访问硬件,使用消息机制与其他进程通信。

在早期minix中采用这种机制的变种,将驱动程序编译到内核中,作为单独进程运行。

另一种方式是,构建最小化内核,将决策权留给用户空间服务器,如:内核能够识别出发送到某个特定地址的消息,将该消息的内容加载到某个磁盘的I/O设备寄存器中。

在本例中,内核甚至不会检查消息中的字节,它们是否有效或有意义。

它只是盲目地将数据复制到磁盘的设备寄存器中,(显然,必须使用某种策略,确证此类消息仅为授权进程使用)这就是minix3的工作方式,驱动程序在用户空间中,使用特殊内核调用请求,读写I/O寄存器或者访问内核信息。

机制(mechanism)与策略(policy)的分离是一个重要概念,它在各种操作系统中一次又一次地出现。