技术记录栈

记录点滴:java、datebase、linux、spring、javascript、nginx

2019/01/03

操作系统结构

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

接下来,我们将研究已经尝试过的五种不同的结构,以便对各种可能性有所了解。这些并不是详尽无遗的,但是它们提供了一些,在实践中已经证实过的设计的想法。 这五种设计是单片系统,分层系统,虚拟机,外核(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位(bits在当时很昂贵)。

系统共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位寻址,限制为1 mb。

Windows和其他操作系统,都使用这种模式来运行旧的MS-DOS程序。这些程序在虚拟8086模式下启动。只要它们执行正常的指令,就可以在物理机器上运行。 但是,当程序尝试trap到操作系统,进行系统调用(或尝试直接执行受保护的I/O时),会引发虚拟机监视器的trap。

这种设计有两种可能的变体。 第一个,MS-DOS本身被加载到虚拟8086的地址空间,因此虚拟机监视器只是将trap反射回MS-DOS,就像发生在真正的8086上那样。当MS-DOS以后试图自己执行I/O时,该操作会被虚拟机监视器捕获并执行。

在另一种变体中,虚拟机监视器只捕获第一次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面向此类市场。这些程序在主系统上创建大文件,作为客户系统的磁盘。为了提高效率,他们分析客户系统程序二进制文件,并允许安全代码直接在主机硬件上运行,捕获进行操作系统调用的指令。这种系统在教育中也很有用。例如,从事MINIX 3实验室作业的学生可以在Windows,Linux或UNIX主机上使用MINIX 3,作为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设备寄存器中)。有两种方法可以解决这个问题。 一种是让一些关键服务器进程(例如,I/O设备驱动程序)在内核模式运行,能够完全访问硬件,但使用消息机制与其他进程通信。  在早期版本的MINIX中采用了这种机制的变种,把驱动程序编译到内核中,作为单独的进程运行。

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

在本例中,内核甚至不会检查消息中的字节,确定它们是否有效或有意义。它只是盲目地将数据复制到磁盘的设备寄存器中,(显然,必须使用某种策略,保证此类消息仅为授权进程使用)这就是MINIX 3的工作方式,驱动程序在用户空间中,使用特殊的内核调用请求,来读写I/O寄存器或者访问内核信息。机制(mechanism)与策略(policy)的分离是一个重要的概念。它在各种操作系统中一次又一次地出现。