操作系统是应用软件的运行环境,只要深入了解操作系统才能进一步优化应用程序,并充分利用操作系统提供的便利性和高效性,而且操作系统中采用的一些策略在开发 应用程序时也是可以借鉴的。
- 概述
- 处理器(进程和线程)管理
- 内存管理
- 设备管理
- 文件管理
- 操作系统接口
计算机软件大致可以分为两类,即系统软件和应用软件。系统软甲
负责管理计算机本身的运作,而应用软件
则负责完成用户所需要的各种功能。最基本的系统
软件是操作系统
,它负责管理计算机的所有资源并提供一个可以在其上编写应用程序的平台。
概述
下面先给出计算机系统层次图:
上图中的微体系结构是用于解释执行机器语言指令的,一条机器语言指令可能对应一段微程序(详细请参考“计算机组成原理”CPU相关章节)。所谓操作系统,一般是指在
内核态
(或称为管态
)下运行的软件,它受到硬件的保护,用户不能随便去篡改它的内容。不过,要想在操作系统和其他系统软件之间画上一条清晰的边界,
是比较困难的。因为有些运行在用户态
的程序也可以视为操作系统的一部分,至少跟它是密切相关的。
从自顶向下的观点出发,操作系统可以认为是一台虚拟机(相对于裸机而言);从自底向上的观点而言,操作系统可以视为资源管理器(如计算机硬件资源和软件资源等)。
资源管理主要包括两种形式的资源共享:
- 时间上的资源共享:
它是指,各个程序或用户轮流使用该资源。这是资源的重复利用和分时利用的特性。
- 空间上的资源共享:
它是指,每个程序不是轮流去使用资源,而是把资源划分为若干份,然后分配给各个程序。这是资源的局部同时可用的特性,利用该特性可以虚拟出多台同样的逻辑设备。
计算机发展史
- 第一代计算机:真空管和插接板
最早的机器使用的是机械继电器,速度非常慢,周期时间的计量单位是秒。后来这些继电器被真空管所取代。不过真空管体积过大,运算速度也不快。 在计算机出现的早期,每台机器都由一个专门的小组来设计、制造、编程、操作和维护。编程全部采用机器语言。
后来,出现了穿孔卡片,这时不再使用插接板,而是将程序写在卡片上,然后读入计算机,但其他过程则依然如此。
- 第二代计算机:晶体管和批处理系统
集体管的使用使得计算机已经比较可靠,这时第一次出现了设计人员、生产人员、操作员、程序员和维护人员的分开。不过还是非常昂贵。
运行一个作业
时,程序员首先将程序写在纸上(用 FORTRAN 或汇编语言),然后用穿孔机制成卡片,并将这些卡片交给操作员,然后就可以去做其他任何事情了,之后
等计算结果出来之后再回来从输出室取走计算结果。
不过这样浪费大量的计算机时间(操作员读卡片、手动装入 FORTRAN 编译器,而此时计算机并没有真正进行计算任务),于是批处理系统
应运而生。
从上图可知,批处理系统使得主机从输入和输出中某种程度的解放出来了。因为输入和输出通常是比较慢的,如果联机输入和输出的话,必然浪费具有更快速度的主机时间。 只有使输入、输出与主机运算达到并行才能尽可能的提高主机利用率。
- 第三代计算机:集成电路和多道程序
此时出现了多道程序
和假脱机
技术。多道程序技术的目的是,解决在输入输出时 CPU 等待的情形,使得 CPU 此时也能做执行其他的程序。而假脱机技术则是,
不再需要像批处理系统那样将磁带(输入磁带和输出磁带)搬来搬去了(详见前面的图)。第三代操作系统相当适合于大型的科学计算和大规模的商务数据处理,但从本质上
说,它们仍旧是批处理系统,程序员一旦提交作业,就失去了控制权,如果程序有错误,只能等到运行完之后才能修正,交互性非常差。
程序员们都希望能有快速的响应时间,这种需求导致了分时系统
的出现。它实际上是多道程序的一个变体,不同之处在于每个用户都有一个联机的终端。
需要指出的是,多道程序技术需要用到特殊的硬件机制(这也促使硬件技术的发展)。
- 第四代计算机:个人计算机
随着大规模和超大规模集成电路的普及,计算机越来越便宜,于是出现了个人计算机,进而出现了通用或专用的操作系统。后来还出现了网络操作系统
和分布式操作系统
。
网络操作系统
与单处理器的操作系统本质上没有区别。它们需要一个网络接口控制器以及相应的底层驱动软件,此外还需要一些程序来进行远程登录和远程文件访问,但这些
并没有改变操作系统的本质结构。
真正的分布式操作系统
并不仅仅是在单处理器系统的基础上增添一小段代码。事实上,分布式系统与集中式系统有本质的区别。它需要更复杂的处理器调度算法来获得
最好的并行性能。另外网络中的通信延迟往往导致不完整、过时甚至错误的信息,而分布式算法必须在这种环境下运行。而单处理器系统中,操作系统能够掌握整个系统的
所有信息。
多道程序设计
操作系统的形成离不开多道程序设计。实现多道程序设计必须妥善地解决以下三个问题:
- 存储保护与程序浮动
- 处理器的管理与分配
- 资源的管理与调度。
中断和通道技术的出现以及大容量存储器的诞生使得实现多道程序系统已不存在问题。
操作系统资源管理
操作系统的主要目标可归结为:
- 方便用户使用
- 扩充机器功能
- 管理各类资源
- 提高系统效率
- 构筑开放环境
由于计算机系统的硬件相对不足导致各应用程序之间对资源的共享和争用。
- 必须要解决资源数量不足和合理分配资源这两个问题。
- 提供相应的资源使用接口,屏蔽直接使用资源的复杂性和不安全性。
而解决以上问题的最好方法是通过共享硬件资源的方式来实现虚拟机抽象
。所利用的资源管理技术如下:
三种资源管理技术:
资源复用、资源虚化和资源抽象。
- (1)资源复用
由于多道程序设计,必须要求资源复用。通过适当资源复用可以创建虚拟资源和虚拟机,以解决物理资源数量不足的问题。物理资源的复用有两种基本方法:
空分复用共享和时分复用共享
空分复用共享表明资源可以进一步分割成更多和更小的单位供进程使用;时分复用共享表明资源可以被一个一个进程单独占用一个时间片,而下一时间片将分配给下一个进程。
进程能够空分复用主存资源,不同的进程映像装入不同的主存区域,拥有各自的地址空间,且通过硬件存储保护机制进行隔离。操作系统必须跟踪当前执行进程,确定其执行
时间;而时分复用共享使得处理器可以执行已装入不同地址空间中的程序代码。这种共享硬件的技术称之为“多道程序设计
”。
- (2)资源虚化
虚化又称虚拟性。虚化的本质是对资源进行转化、模拟或整合,把一个资源转变成逻辑上的多个对应物,创建无须共享的多个独占资源的假象,以达到多用户共享一套计算机物理 资源的目的。
空分复用与虚化两者相比较,空分复用所分割的是实际存在的物理资源,而虚化则是实现假想的虚拟同类资源。虚化技术可以解决单类资源绝对不足(如,主存不足 时可以使用磁盘来虚拟主存)和不能复用资源(如虚拟打印机技术)的共享问题。
虚拟设备技术如同多台外部设备同时联机操作,该技术被称为 SPOOLing 技术
。
从本质上讲,SPOOLing 技术、窗口技术(虚拟屏幕终端)、时分信道多路复用技术都建立在时分复用共享的基础上;虚拟存储器则是通过虚拟存储技术把物理上的多级存储器 (主存和辅助存储器)映射为逻辑上的、单一的(虚拟)存储器,它与频分信道多路复用技术一样都是建立在空分复用共享基础之上的例子。
- (3) 资源抽象
资源复用和资源虚化的主要目标是解决物理资源(单类资源相对或绝对)数量不足的问题。而资源抽象则是解决物理资源易用性问题。资源抽象
是指通过创建软件来屏蔽
硬件资源的物理特性和接口细节,简化对硬件资源的操作、控制和使用,即不考虑物理细节而对资源执行操作。比如使用设备的一些系统调用、更高级的文件系统(对磁盘的抽象)。
- (4) 组合使用抽象和虚化技术
对于某一类资源,操作系统往往同时实施抽象和虚化技术。如打印机,既使用了系统调用,又使用了虚化技术 SPOOLing。
操作系统的基础抽象
计算机系统的物理资源可被分为两大类:计算类(处理器和主存)、存储与接口类(辅存和其他外部设备等)。为了方便对物理资源的管理和使用,现代操作系统对资源进行
了三种最基础的抽象:进程抽象、虚存抽象和文件抽象
。
- 进程抽象
进程是对于进入主存的当前运行程序在处理器上操作的状态集的一个抽象,它是并发和并行操作的基础。实际上,若干进程透明地时分复用共享一个(或多个)处理器,操作 系统内核的主要任务之一是将处理器“虚化”,制造一种每个运行进程都独自拥有一个处理器的假象。由于进程执行依赖于主存和设备上的信息资源,所以还需要后面的几种 资源抽象进行配合。
- 虚存抽象
虚存抽象的目的是让用户认为它正在独占和使用整个主存,从而不需要考虑程序如何在主存中存放的细节,也不用直接操作物理地址,而是使用虚拟地址(逻辑地址)来使用 主存(操作系统会将虚拟地址转化为物理地址)。
- 文件抽象
Unix 类系统把磁盘、外部设备都抽象成文件,如此就可像操作普通文件一样操作设备了。从而屏蔽了诸多设备的不同细节,提供了友好的统一接口。
虚拟计算机
虚拟计算机是一台抽象计算机,它在硬件基础上由软件来实现,处于不同层次的用户将用到不同的虚拟机,并且和物理机器一样,具有指令集(如系统调用、函数库等)及 可用的存储空间。下面讲解一下操作系统虚拟机。
- 虚拟处理器
虚拟计算机的虚拟处理器是由物理处理器实现的,利用处理器调度技术,虚拟处理器每次分得时间片后可供进程执行时使用(时分复用)。
- 虚拟主存
虚拟主存利用虚拟存储技术供进程作为物理地址空间使用(空分复用)。
- 虚拟辅存
磁盘被抽象成命名文件,运行在虚拟机上的进程通过文件管理系统来使用和共享磁盘。磁盘的部分空间可以作为主存空间的扩充,存放进程映像副本(空分复用)。
- 虚拟设备
外部设备也被抽象为命名文件,运行在虚拟机上的进程通过设备管理来使用设备,根据外部设备自身物理特性的不同,有些类型设备基于空分复用共享,有些类型的设备基于 时分复用共享。
总之,虚拟机是由操作系统通过共享硬件资源的方式来实现的,它定义进程运行的逻辑计算环境。
操作系统的作用与功能
操作系统在计算机系统中起到 3 个方面的作用。
- 提供用户接口和服务
- 扩展机器功能和提供虚拟机
- 管理和控制计算机资源
概括地说,操作系统既是“管理员”,又是“服务员”。
- 对内作为“管理员”,做好计算机系统软硬件资源的管理、控制与调度,提高系统效率和资源利用率。
- 对外作为“服务员”,是用户与硬件之间的接口和人机界面,为用户提供尽可能友善的运行环境和最佳服务,所以,资源管理和调度是操作系统的重要任务。
从资源管理的观点来看,操作系统具有 6 项主要功能。
- 处理器管理:对处理器的管理和调度最终归结为对进程和线程的管理和调度,包括:
- 进程控制和管理;
- 进程同步和互斥;
- 进程通信;
- 进程死锁;
- 线程控制和管理;
- 处理器调度,又可分为高级调度、中级调度和低级调度。
- 存储管理:
- 主存分配
- 地址转换与存储保护;
- 主存共享;
- 存储扩充:用辅存逻辑上扩充主存。
- 设备管理:
- 提供设备中断机制;
- 提供缓冲区管理;
- 提供设备独立性,实现逻辑设备到物理设备之间的映射;
- 设备的分配和回收;
- 实现共享型设备的驱动调度;
- 实现虚拟设备。
- 文件管理:
- 提供文件的逻辑组织方法;
- 提供文件的物理组织方法;
- 提供文件的存取和使用方法;
- 实现文件的目录管理;
- 实现文件的共享和安全性控制;
- 实现文件的存储空间管理。
- 网络与通信管理:
- 网络资源管理;
- 数据通信管理;
- 网络管理。
- 用户接口:
为了使用户能够灵活、方便地使用计算机硬件和系统所提供的服务,操作系统向用户提供一组使用其功能的手段,称为用户接口,包括两大类:程序接口和操作接口。
操作系统的主要特性
操作系统具有并发性
(两个或两个以上活动或事件在同一时间间隔内发生)、共享性、异步性
(又称为“随机性”,导致“走走停停”)。
其中共享性
是指,计算机系统中的资源可以被多个并发执行的程序共同使用,而不是被某个程序独占。并发性必然会产生资源共享的需要。资源共享方式有以下两种:
- 通明资源共享
操作系统采用复用、虚化和抽象技术创建虚拟机使用户感觉就是独占设备一般。透明资源共享机制必须妥善处理资源隔离和授权访问。
处理器(进程和线程)管理
进程可被调度在一个处理器上交替地执行,或在多个处理器上并行执行。不同类型的操作系统可能采取不同的调度策略,交替执行和并行执行都是并发的类型。为了提高并发 粒度和降低并发开销,现代操作系统引进线程的感念,此时进程仍然是资源分配和管理的单位,线程则成为处理器调度的基本单位。
程序的顺序执行
我们把一个具有独立功能的程序独占处理机,直到最后结束的过程称为程序的顺序执行
。
程序顺序(并发)执行时的特征
- 顺序(间断)性
- (失去)封闭性:程序运行时独占全机资源。
- (不)可再现性:程序的最终结果与执行速度及执行的时刻无关。
进程
进程是执行中的程序。进程不只是程序代码,程序代码有时称为“程序区(或文本段)”,进程还包括当前活动,通过程序计数器
的值和处理器寄存器的内容来表示。
另外,进程还包括进程堆栈段和数据段(包含全局变量)。
如果说程序是提供计算机操作的一组工作流程的话,进程就是具体的工作过程,按照同样的工作流程,针对不同的原料,可以同时开始多个工作过程,得到多种不同的成品。
进程和程序时两个完全不同的概念,但又有密切联系,它们之间的主要区别如下:
- 程序是静态概念,本身可以作为一种软件资源长期保存,而进程是程序的一次执行过程,是动态的概念。
- 进程是一个能独立运行的单位,能与其他进程并发执行。进程是作为资源申请和调度单位存在的;通常程序不能作为一个独立运行的单位而并发执行。
- 程序和进程不存在一一对应的关系。
- 各个进程在并发执行过程中会产生相互制约的关系,造成各自前进速度的不可预测性,而程序本身是静态的,不存在这种异步特征。
进程的特点
从进程与程序的区别可以看出,进程具有如下特征:
- 动态性
- 并发性
- 独立性:进程是一个能独立运行的基本单位,同时也是系统中独立获得资源和独立调度的基本单位。
- 异步性:进度之间需要协调运行。
- 结构特征(进程映像):进程实体是由程序段、数据段及进程控制块组成。
进程的状态和转换
进程是具有生命周期的,根据进程在执行过程中不同情况需要定义不同的状态。
三态模型:
- 运行态:进程正占用处理器。
- 就绪态:只缺处理器资源。
- 等待(阻塞)态:正在等待某个时间完成或资源空闲而不具备运行条件
五态模型
在很多系统中,增加两个进程状态:新建态和终止态。
- 新建态:
对应于进程被创建时的状态。又是将根据系统性能的要求或主存容量的限制推迟新建态进程的提交。创建进程需要两个步骤:
①为新进程分配所需资源,建立必要的
管理信息;
②设置此进程为就绪态,等待被调度执行。
- 终止态:
处于终止态的进程不再被调度执行,下一步将被系统撤销,最终从系统中消失。类似地,进程终止也要通过两个步骤实现:
①等待操作系统或相关进程进行善后处理
(如抽取信息);
②回收被占用的资源并由系统删除进程。
进程终止通常由下列条件引起:
- 正常退出(自愿的);
- 出错退出(自愿的);
- 严重错误(非自愿的);
- 被其他进程杀死(非自愿的)。
具有挂起功能的进程状态
很多系统引入了挂起状态。所谓挂起状态,实际上就是一种静止状态。一个进程被挂起之后,不管它是否在就绪状态,系统都不分配给它处理机。
引起挂起状态的原因:
- 终端用户的请求
- 父进程请求考察子进程的活动
- 系统负荷的需要:资源紧缺,挂起不重要的进程。
- 操作系统的需要:检查和统计运行中的资源使用情况。
挂起状态又引入了两个新状态:挂起就绪态和挂起等待态。挂起就绪态表明进程具备运行条件,但目前在辅存中,只有当进程被兑换到主存时才能被调度执行;挂起等待态则 表明进程正在等待某一事件发生且进程在辅存中。
引进挂起状态后,进程状态可分为新建态、活动就绪态、运行、活动阻塞、静止就绪、静止阻塞和终止状态。
进程描述
进程的活动包括占用处理器执行程序以及对相关数据进行操作,因而,程序和数据
是进程必需的组成部分,两者描述的是进程的静态特征;进程的动态特性则由以下的
数据结构来描述:
进程控制块;
进程程序块(即程序区);
进程核心栈(核心态与用户态切换时用);
进程数据块(用户数据和用户栈)。
进程控制块(PCB):
不同的操作系统中的 PCB 不尽相同。可以按照功能大概分成 4 个组成部分:进程标识符、处理机状态(现场信息)、进程调度信息、进程控制信息
。
- 进程标识符:
- 进程内部标识符:操作系统给予的位移数字标识符;
- 进程外部标识符:由创建者提供,便于用户访问该进程。为了描述进程的家族关系,还应设置父进程标识及子进程标识。此外,还可设置用户标识、用以标识拥有该 进程的用户。
- 处理机状态:
处理机状态信息主要由处理机的各种寄存器中的内容组成。用于保护和恢复现场从断点继续执行。处理机的寄存器包括通用寄存器、指令计数器、程序状态字 PSW、用户栈指针。
- 进程调度信息:PCB 中还存放一些与进程调度和进程对换有关的信息。
- 进程状态:指明进程当前状态。
- 进程优先级;
- 进程调度所需要的其他信息:
它们与所采用的进程调度算法有关; - 事件或阻塞原因。
- 进程控制信息:
- 程序和数据的首地址;
- 进程同步和通信机制;
- 资源清单:除 CPU 之外的其他已获得资源清单。
- 链接指针:给出了本进程 PCB 所在队列的下一个进程 PCB 的首地址。
当系统创建一个新进程时,就为它建立一个 PCB;当进程终止后,系统回收其 PCB,该进程在系统中就不存在了。所以,PCB 是进程存在的唯一标志。
进程控制块的组织方式:
系统中有许多处于不同状态的进程,同时阻塞的原因也可能各不相同,所以需要不同的队列将它们组织起来,以便对所有进程进行有效管理。这就需要适当的方式将 PCB 组织 起来。有三种通用的队列组织方式:线性方式、链接方式和索引方式。
- 线性方式:
把所有进程的 PCB 都放在一个线性表中。该线性表是静态分配空间。为了采用某种调度算法,必须扫描整个线性表,从而降低了调度效率。
- 链接方式:
根据不同的进程状态和阻塞原因或其他某种特定需求分成不同的队列。从而减少了每次扫描的 PCB 数,提高了灵活性和效率。
- 索引方式:
索引方式是线性方式的一种改进,结合了链接方式的优点。可以认为索引方式是用静态链表的方式来实现的链接方式。
进程切换与模式切换
进程上下文切换
中断和异常是激活操作系统的仅有方法,它暂停当前运行进程的执行,把处理器切换至核心态,内核获得处理器的控制权之后,如果需要就可以实现进程切换。所以,进程切换 必定在核心态而非用户态发生。这种切换通过核心栈来完成。
内核在下列情况会发生上下文切换:
进程在运行过程中执行系统调用、产生中断或异常时,操作系统从当前运行进程那里获得控制权,此后,进程切换可以在任何时刻发生。
在执行进程上下文切换时,保存老进程的上下文且装入被保护的新进程的上下文,以便新进程运行。进程切换的实现步骤如下:
处理器模式切换
与进程上下文切换有关的是 CPU 模式切换,用户态和核心态之间的相互切换(称为“模式切换”),此时仍然在同一个进程中进行。仍在自己的上下文中执行。
模式切换的步骤如下:
模式切换不同于进程切换,它不一定会引起进程状态的转换,在大多数情况下,也不一定引起进程切换,在完成系统调用服务或中断处理之后,可通过逆向模式切换来恢复 被中断进程的运行。
CPU 上所执行进程在任何时刻必定处于三个活动范围之内:
进程控制
系统中的进程不断地产生和消亡,进程生命周期的动态变化过程由进程管理程序来控制,对于进程的控制和管理包括:创建进程、阻塞进程、唤醒进程、挂起进程、激活进程、 终止进程和撤销进程等,这些功能均由系统中的原语来实现。
原语在核心态执行,是完成系统特定功能的不可分割的过程,它具有原子操作性,其程序段不允许被中断,或者说原语不能并发执行。系统对进程的控制如果不适用原语,就会 造成状态的不确定性,不能达到进程控制的目的。
原语可分为两类:
- 机器指令级的
其特点是执行期间不允许中断,是一个不可分割的基本单位。
- 功能级的
其特点是作为原语的程序段不允许并发执行。
需要注意的是:
阻塞原语是进程自己阻塞自己;
唤醒是被系统进程或事件完成进程唤醒;
进程的挂起或解挂都是被动的
线程
如果说操作系统中引入进程的目的是为了使多个程序并发执行,以便改善资源利用率和提高系统效率,那么,在进程之后再引入线程的概念,则是为了减少程序并发执行 (进程切换)时所付出的时空开销,使得并发粒度更细,并发性更好。此时,进程成为了独立分配资源的基本单位,无须频繁地切换;而线程则作为系统(处理机) 调度和分派的基本单位,会被频繁地调度和切换。进一步产生了多线程进程。
线程和进程的比较如下:
线程的组成部分有:
线程的状态:
线程和进程一样,也有自己的状态。线程有 3 种基本状态,即执行、阻塞和就绪,但没有挂起(由于线程不是资源的拥有单位,挂起状态对于线程是没有意义的)。
进程中可能有多个线程,至于单个线程是否要阻塞整个进程取决于系统实现。有的系统只有所有的线程都阻塞之后才阻塞整个进程(这种方式更能体现多线程的优越性)。
线程切换:
针对线程的 3 种基本状态,存在 5 种基本操作来转换线程的状态。
- 派生:线程在进程中派生出来,也可再派生线程。
- 调度;
- 阻塞;
- 激活;
- 结束。
多线程程序设计的优点
线程的组织
多线程实现
多线程的实现分为三类:用户级线程(ULT)、内核级线程(KLT)或者混合方式。
- 用户级线程:
其由用户应用程序建立,并由用户应用程序负责调度和管理,操作系统内核不知道有用户级线程的存在。
ULT 的优点:
ULT 的缺点:
- 内核级线程:
内核级线程中所有线程的创建、调度和管理全部由操作系统内核负责完成,一个应用程序可按多线程方式编写程序,其他交给内核处理。
多线程技术利用线程库提供一整套有关线程的过程调用或系统调用来支持多线程运行,有的操作系统直接支持多线程,有的语言则提供线程库。因而,线程库可分为用户空间 线程库和内核空间库。线程库实际上是多线程应用程序的开发和运行环境。
多线程模型
许多系统都提供对用户和内核线程的支持,从而有不同的多线程模型。以下是 3 种常用类型:
- 多对一模型
该模型将许多用户线程映射到一个内核线程。详情请参看“用户级线程”。
- 一对一模型(参见“内核级线程”)
- 多对多模型
多对多模型多路复用了许多用户线程到同样数量或更小数量的内核线程上。开发人员可创建任意多的必要用户线程,并且相应内核线程能在多处理器系统上并行执行。而且, 当一个线程执行阻塞系统调用时,内核能调用另一个线程来执行。为了防止无限制的创建线程,可使用线程池。
处理器调度
某些进程花费了绝大多数时间在计算上(注意,某些 I/O 活动可以看做是计算),称之为“计算密集型进程
”;而有些进程则在等待 I/O 上花费了绝大多数时间,
称之为“I/O 密集型进程
”。如果需要运行 I/O 密集型进程,那么就应该让它尽快得到机会,以便发出磁盘请求并保持始终忙碌。
调度时机
CPU 调度决策可以在如下四种环境下发生:
分级调度
一个批处理型作业,从进入系统并驻留在外存的后备队列上开始,直至作业运行完毕,可能要经历以下三级调度:作业调度、对换和进程调度。
- 高级调度
其又称为作业调度、长程调度
。用于选择把外存上处于后备队列中的哪些作业调入内存,并为它们创建进程、分配必要的资源,然后,再将新创建的进程排在就绪队列上,
准备执行。高级调度控制多道程序的道数,被选择进入主存的作业越多,每个作业所获得的 CPU 时间就越少,所以有时为了满足某种特定需求,
需要限制道数。每当有作业执行完毕并撤离时,作业调度会选择一个或多个作业补充进入主存。此外,如果 CPU 的空闲时间超过一定的阈值,系统也会引出作业调度选择
后备作业。可见,高级调度负责作业的调入和撤离,与交换(对换或中级调度)有着很大的区别。
- 中级调度
中级调度又称为“平衡调度、中程调度
”,根据主存资源决定主存中所能容纳的进程数目,并根据进程的当前状态来决定辅助存储器和主存中的进程的对换。当主存
资源紧缺时,会把暂时不能运行的进程换出主存,此时这个进程处于“挂起”状态,不参与低级调度;当进程具备运行条件且主存资源有空闲时,再将进程重新调回主存工作,
起到短期均衡系统负载的作用,充分提高主存的利用率和系统吞吐率。
- 低级调度
低级调度又称为进程调度/线程调度、短程调度和微观调度
,其主要功能是:根据某种原则决定就绪队列中的哪个进程/内核级线程获得处理器,并将处理器出让给它还用。
低级调度是操作系统最为核心的部分,执行十分频繁,其调度策略的优劣将直接影响整个系统的性能,因而,这部分代码要求精心设计,并常驻内存。
进程调度可分为如下两种方式:
- 非抢占方式:
不允许进程抢占已经分配出去的处理机。该方式的优点是实现简单、系统开销小,适用于大多数的批处理系统环境。但它很难满足紧急任务的要求。因而可能造成难以预料的 后果。显然,在要求比较严格的实时系统中,不宜采用这种调度方式。
- 抢占方式:
抢占方式允许调度程序根据某种原则暂停某个正在执行的进程,将处理机收回,重新分配给另一个进程。抢占的原则有优先权原则、短作业(或进程)优先原则、时间片原则等。
各级调度的关系:
调度算法的评价及准则
在操作系统的设计中,如何选择作业调度及进程调度的方式和算法取决于操作系统的类型和目标。显然,根据不同的目标,会有不同的调度算法。
面向用户的准则:
- 公平性
- 周转时间短:
周转时间是指,作业被提交给系统开始,到作业终止为止的这段时间间隔。
- 响应时间快:
响应时间指的是,从用户提交一个作业请求开始,直至系统首次产生响应(如屏幕显示提示信息)为止的时间。
- 截止时间保证
截止时间是指,某任务必须开始执行的最晚时间,或必须完成的最晚时间。
面向系统的准则:
这是为了提高整个系统的效率。
- 系统的吞吐量:
吞吐量是指,在单位时间内系统所完成的作业数,它与批处理作业的平均长度有密切关系。
- 处理机的利用率
- 各类资源的平衡利用。
- 尽量保持系统所有部分尽可能忙碌
调度算法分类
不同的环境需要不同的调度算法。
调度机制
从概念上来看,调度机制由 3 个逻辑功能程序模块组成:
- 队列管理程序
- 上下文切换程序
- 分派程序:转入上下文,开始执行获得 CPU 的进程。
调度算法
在操作系统中,存在多种调度算法,有的算法仅适用于作业调度,有的算法仅适用于进程/线程调度,但大多数调度算法对两者都适用。有的调度算法适合批处理系统或其他 特定的系统,但一些算法既适合批处理系统也适合交互式系统等。
批处理系统中的调度算法
- 先来先服务(FCFS)
易于理解和实现,但没有考虑作业的特点和用户的实际需要,所以无法满足用户的大部分需求,也不能充分利用系统资源。不过,它是其他算法的基础,当各种条件都一样时, 此时,就需要先来先服务原则来保证公平性。
- 最短作业优先(SJF)
该算法一般是非抢占式的,所以这里的最短作业指的是,调度的当时是最短(虽然有时很难估计时间)的。而不是在该作业运行期间(如,后面又来了一个更短的作业)。
- 最短剩余时间优先:SRTF 和最短作业优先一样,有时该时间是很难预先知道的。
- 高响应比优先:
HRN 调度算法为了克服短作业优先算法的缺点,采用了一种折中的方法,既让短作业优先,又考虑到系统内等待时间过长的作业。
交互式系统中的调度
下面的调度算法也可以用于批处理系统的调度器中,尽管三级调度不大可行,但两级调度是可行的。
- 时间片轮转调度:
时间片设得太短会导致过多的进程切换开销;而设得太长有可能引起对短交互请求的响应变差。一般设为 20-50 ms。
- 优先级调度:
优先级可以是静态的,也可以是动态的,系统和用户均可指定优先级。优先级调度可以是抢占式的,也可以是非抢占式的。不过,可能造成高优先级的进程无限制执行下去, 而低优先级的进程处于饥饿状态,所以优先级标准和如何变化将会影响用户体验和系统性能。
- 多级反馈队列调度算法
不论哪一种算法都无法满足不同的需要。为此,可以将不同的需求分到不同的队列中,而且不同的队列具有不同的优先级,不同队列中可以根据具体的需求采用最适合该队列 的调度算法。而且进程根据不同的运行情况会被动态的分配到不同的队列中。此种算法称为“多级反馈队列调度算法 MLFQ ”或 “反馈循环队列”。可见,该算法中,同一个 进程随着占用 CPU 的次数的增加,优先级在不断递减。
MLFQ 调度算法具有较好的性能,能满足各类应用的需要。但仍会导致“饥饿”问题。例如,一个耗时很长的作业,最终将进入优先级最低的队列,然后,系统不断的进入新的 作业,那么,该长作业就很难得到再运行的机会。为此,可以允许使用高响应比来提升优先级(通常只允许降低优先级)。
- 彩票调度算法
其基本思想是:为进程/线程发放针对各种资源(如 CPU 时间)的彩票,当调度程序需要作出决策时,随机选择一张彩票,彩票的持有者将获得相应的系统资源。对于 CPU 调度, 系统可能每秒钟抽取彩票 50 次,中奖者每次可以获得 20 ms 的运行时间。
一般情况下,所有进程都是平等的,不过某些进程需要更多机会,所以需要得到额外的彩票以增加中奖的机会。进程拥有多少彩票份额,就能获得多少资源。合作进程如果愿意, 可以交换彩票,以便相应进程得到更多的机会。可见,彩票调度法很灵活,而且反应非常迅速,因为中奖机会与其持有的彩票数成正比。
- 公平分享调度
该算法考虑了进程的拥有者。主要应对不同作业拥有的进程数是不一样的情况,如果不考虑拥有者,则拥有更多进程的作业显然获得 CPU 时间更多。
实时系统调度
实时系统通常分为硬实时
系统和软实时
系统。前者意味着存在必须满足的时间限制;后者意味着偶尔超过时间限制是可以容忍的。实时系统根据响应的事件可进一步
分为周期性
(每隔一段固定时间发生)和非周期性
(在不可预知的时间发生)。一个系统更可能必须响应多个周期的事件流。根据每个事件需要多长的处理时间,
系统可能根本来不及处理所有事件。
实时调度算法可以是静态的或动态的。前者在系统启动之前完成所有的调度决策;后者在运行时做出调度决策。如果使用静态调度算法,必须预先知道足够多的需要做的工作 和必须满足的约束的时间信息。
- 单比率调度算法
对于周期性事件,单比率调度是视周期长度而定的抢占式策略:周期越短,优先级越高。
- 限期调度算法
当一个事件发生时,对应的实时进程就被加入就绪队列,此队列按照截止期限排序。对于周期性事件,截止期限即事件下一次发生的时间。系统检测队首截止期限是否比 当前运行者早,以决定是否剥夺当前运行的进程资源。
- 最少裕度法
多处理机调度算法
多处理机调度的设计要点有 3 个:为进程分配处理机、在单个处理机上是否使用多道程序设计技术和实际指派进程的方法。
- 负载共享调度算法:
进程并不被指派到特定的处理机上,系统维护全局性进程就绪队列,当处理机空闲时,就选择进程的一个线程去运行。可见,该算法没有考虑同一个进程的多个线程的同步和 切换问题,因为具有同步和互斥等关系的线程很难被按照一定顺序执行,也不能保证被切换的进程在原有的处理机上再次执行,从而增加了切换开销。
具体的负载共性调度算法有:先来先服务、最少线程数优先和剥夺式最少线程数优先等。
- 群调度算法
其基本思想是:给予一对一原则,一群相关线程被同时调度到一组处理机上运行。紧密相关线程的并行执行能够减少同步阻塞,从而减少进程切换,降低调度代价。当进程的 相关线程数小于处理机数时会造成处理机资源空闲。
- 专用处理机调度算法
将同属于一个进程的一组线程同时分派到一组处理机上运行。是群调度的一种极端方式。不过该方式也会使有些处理机因线程等待事件阻塞时空闲。然而,对于数目很大的处理机 群而言,个别处理机的使用率只是代价的一小部分,对整体影响不大。
- 动态调度算法
针对能够动态改变线程数的应用程序。其基本思想是:由操作系统和应用进程共同作出调度决策,操作系统负责在应用进程之间分配处理机;应用进程所分配的处理机上执行 可运行线程的子集,这些处理机如何分配到具体的线程完全是应用进程的任务,可借助于运行时库函数完成。
注意:
多处理机调度不宜采用复杂的调度算法,
复杂的调度算法意味着过多的时间开销,
然后这些开销乘以期间空闲的处理机数,
将使开销被放大。
进程间关系
由于多道程序技术、多处理技术、分布式处理技术等导致了进程并发,并且并发会在不同的上下文中出现:
- 多个应用程序;
- 同一个应用程序内部;
- 操作系统自身内部。
支持并发进程必需解决进程间的同步、互斥和通信问题。并发在单处理机上表现为进程的交替执行,在多处理机上表现为重叠执行。并发必然要求资源共享,共享的资源包括 全局数据、硬件软件资源等,而且对共享资源的访问或读写顺序不同可能得到的结果也不同。所以,需要操作系统控制好进程对资源的互斥访问和顺序访问。
并发带来的困难:
- 全局资源的共享充满了危险,不同进程使用的时机不同,资源被使用的前后状态也不同。
- 操作系统很难对资源进行最优化分配,可能导致死锁。
- 定位程序设计错误是非常困难的。这是因为结果通常是不确定的和不可再现的。
操作系统需要为并发做的工作如下:
- 记录各个活跃进程的状态,为进程的同步、互斥等管理工作收集信息。
- 为每个活跃进程分配和释放各种资源。
- 必须保护每个进程的数据和物理资源。
- 保证一个进程的功能和输出结果与执行速度无关。
进程的交互
实际情况并不总是像上表中给出的那么清晰,多个进程可能既表现出竞争,又表现出合作。
临界资源
竞争进程面临三个控制问题(互斥、死锁和饥饿)。首先是互斥的要求。这涉及到不可共享或同时访问的资源的互斥访问问题。该类资源称之为临界资源
。不论硬件临界资源,还是软件临界
资源,多个进程必需互斥对其进行访问。每个进程中访问临界资源的那段代码称为临界区
。
所以,若能保证各进程互斥地进入临界区,便可实现各进程对临界资源的互斥访问。为此,必须在临界区前面增加一段用于检查临界资源是否在使用的代码,该段代码称为
“进入区
”;相应地,在临界区后面再加一段用于设置刚用完临界资源的状态,以便临界资源被其他进程使用的代码,该代码称为“退出区
”,其他部分代码
称为“剩余区
”。
进程同步机制
进程同步是指有协作关系的进程之间不断地调整它们之间的相对速度或执行过程,以保证临界资源的合理利用和进程的顺利执行。实现进程同步的机制称为进程同步机制。
同步机制应遵循的规则:
为实现进程互斥地进入自己的临界区,可用软件或硬件方法。不过所有同步机构都应遵循下列准则:
- 空闲让进;
- 忙则等待;
- 有限等待;
- 让权等待:当进程不能进入自己的临界区时,应立即释放处理机。
实现临界区管理的设施
硬件设施:
- 关中断:在多处理机环境下,很难有效工作。
- 专用机器指令:用于保证两个动作的原子性。
- 如比较和交换指令(使用了忙等待或者自旋等待)。
忙等待
或自旋等待
指的是这样一种技术:进程在得到临界区访问权之前,它只能继续执行测试变量的指令来得到访问权,除此之外不能做其他事情。
软件算法实现互斥
- 锁机制:
实现互斥的一种软件方法是采用锁机制,即提供一对上锁和开锁原语,以及一个锁变量 w 或者是锁位。进入临界区之前不断地检测 w 的状态,若没有上锁则进入临界区,否则继续 测试 w 的状态;进入后上锁,退出时开锁。
- 信号量机制
信号量机制是一种广义的锁机制或者成为计数锁的同步机制,既能解决互斥,又能解决同步。后来发展成了 P 操作(原语)和 V 操作(原语)。P 操作用于检测和申请临界资源, V 操作用于释放临界资源。
信号量
也叫信号灯,是在信号量同步机制中用于实现进程的同步和互斥的有效数据结构。可以为每类资源设置一个信号量。信号量有多种类型的数据结构,如整型信号量,
记录型信号量、AND 型信号量及信号量集等。
信号量类型举例:
- 整型信号量:
它是信号量的最简单的类型,也是各种信号量类型中必须包含的类型。整型信号量的数值表示当前系统中可用的该类临界资源的数量。如,
- 记录型信号量
- AND 型信号量
AND 同步机制的基本思想是将进程在整个运行过程中需要的所有资源,一次性全部分配给进程,待进程使用完成后再一起释放。只要有一个资源尚未分配给进程,其他所有 可能分配的资源也不能分配给它。
AND 型信号量集机制可描述如下:
- 信号量集
如果某进程一次需要 N 个某类资源时,就要进行 N 次 wait 操作,这使系统的效率较低,有可能造成死锁。
信号量实现互斥:
经典同步问题
生产–消费者问题
- 问题的描述:
- 问题的分析:
- 算法程序:
- 注意事项:
读者–写者问题
- 问题的提出:
- 问题的分析:
- 算法程序:
- 注意事项:
哲学家进餐问题
- 问题的提出:
- 问题的分析:
- 算法程序:
- 其他算法:
理发师问题
- 问题提出:
- 问题分析:
- 算法程序:
管程(进程高级同步)
虽然 PV 操作可以解决进程间的同步互斥问题,但用于同步互斥的共享变量及信号量的操作被分散于各个进程中,它是否能达到同步互斥的功能还需要依靠程序员的正确编写。
PV 同步机制的缺点:
- 易读性差:
因为要了解对于一组共享变量及信号量的操作是否正确,则必须通读整个系统或者并发程序。
- 不利于修改和维护:
因为程序的局部性很差,所以任一组变量或一段代码的修改都可能影响全局。
- 正确性难以保证:
因为操作系统或并发程序通常很大,要保证这样一个复杂的系统没有逻辑错误是很难的。
为了克服 PV 同步机制的缺点,提出了管程的概念。
管程定义:
管程是一种抽象数据类型。它将描述共享资源的数据(私有数据)及操作这些数据的一组过程或方法(公有,当需要通过”管程名.方法名”的方式调用,不过也有内部函数, 只允许管程方法使用,对外部隐藏)封装在一个具有名字的对象中。该对象可以引用外部方法或变量。可见管程是用于管理资源的公用数据结构(而进程是占有资源 的私有数据结构),管程和调用它的进程不能同时工作(而进程之间可以并发),而且管程是语言或操作系统的成分,不必创建和撤销。
管程组成:
- 名称:
即,管程名称。对不同类的共享资源可能有不同管程,而且也需要引用管程名来调用其中的方法。
- 数据结构说明:
局部于管程的共享变量说明,也是该管程所管理的共享资源的清单。
- 对该数据结构进行操作的一组过程/函数
- 初始化语句:规定数据结构中数据的初始值。
管程的属性:
- 共享性:通过调用管程的过程或方法进行共享。
- 安全性:管程内变量(私有变量)只允许管程的过程访问。
- 互斥性:任一时刻最多只有一个调用者能真正引入管程,其他进程将在管程入口处等待。
- 易用性:进入管程的互斥由编译器负责,从而减轻了写管程的程序员工作。
管程基本形式:
具体例子:
条件变量:
前面提到的管程(并不完整)实现了临界资源的正常进入和退出,但没有考虑临界区内因为某种原因必须中途暂时退出的情况。也就是说,还需要一种方法使得进程在临界区
内其他资源不能满足而无法继续运行时被阻塞,等条件满足之后再次运行。而条件变量
同步机制,以及在其上操作的仅有的两个同步原语 wait 和 signal 的引入就是为了解决这一问题。
当进程中途等待资源时将被加入资源等待队列(称为紧急等待队列
),该队列由相应的条件变量维护,资源等待队列可以有多个,每种资源一个队列。紧急等待队列的
优先级应当高于入口等待队列
的优先级。
- 当一个管程过程发现无法继续执行下去时,它将在相应的条件变量上执行 wait ,这个操作引起调用进程阻塞;当然,这是允许先前被挡在管程之外的一个进程进入管程。
- 另一个进程可以通过对其伙伴在等待的同一个条件变量上执行 signal 操作来唤醒等待进程。
- wait 和 signal 是两条原语,在执行时不允许被中断。它们分别表示把某个进程加入等待使用资源的条件变量的等待队列,从等待资源的条件变量的等待队列上释放一个 进程。
- 当执行 wait 之后,相应的进程被置成等待状态,同时开放管程,允许其他进程调用管程中的过程或方法。
- 当执行 signal 之后,指定条件变量上的一个进程被释放。
某个进程(P)在管程内运行时可能中途释放某个条件变量(及时尽早释放临界资源和条件变量的原则),这将唤醒等待该条件变量的等待队列队首进程(Q), 按照条件变量机制,该被唤醒的进程将再次进入管程(P 仍在管程内),很显然是不允许。可采用两种方法来防止这种现象的出现。
- 进程 P 释放管程转为等待直至进程 Q 退出管程,或者进程 Q 等待另一条件(中途又被阻塞);
- 进程 Q 等待直至进程 P 退出管程(类似“非剥夺式”),或者进程 P 等待另一个条件(被阻塞);
- 规定唤醒为管程中最后一个可执行的操作(即,最后统一释放所有的条件变量,统一唤醒)。
霍尔采用了第一种办法,而汉森选择了第三种方法,进程执行 signal 操作后立即退出管程,因而,进程 Q 马上被恢复执行。
注意事项:
虽然条件变量也是一种信号量,但它并不是 P、V 操作中所论述的纯粹计数信号量,不能像信号量那样积累供以后使用,仅仅起到维护等待进程队列的作用。
当一个条件变量上不存在等待条件变量的进程时,signal 操作发出的信号将丢失,等于做了一次空操作。wait 操作一般应在 signal 操作之前发出,这一规则大大简化了 实现。
管程实现互斥和同步
- 互斥:
进入管程的互斥由编译器负责,写管程的人无需关心。
- 同步:
管程实现同步,需设置:
具体例子:
- 生产者消费者问题:
- 哲学家用餐问题:
- 读者写者问题:
除了前面说过的进程同步互斥机制外,有些操作系统还支持原子事务。对于事务的细节可以参考数据库原理。而且消息传递机制(参本文后面章节) 也可以解决进程互斥和同步问题。
进程通信
进程之间互相交换信息的工作成为进程通信
。通信分为两大类:低级通信和高级通信。
- 低级通信
将进程间控制信息的交换称为低级通信
,如信号量通信机制、信号通信机制。
- 高级通信:
进程之间大批量数据的交换称为高级通信
。
通信方式列举:
- 信号通信机制;
- 信号量通信机制;
- 管道通信机制;
- 消息传递通信机制;
- 共享主存通信机制;
- 网络进程通信机制。
信号通信机制
信号是一种软终端,是传递短消息的简单通信机制,通过发送指定信号来通知进程某个异步事件发生,以迫使进程执行信号处理程序(在用户态下执行)。信号处理完毕后, 被中断进程将恢复执行。一般地,分成操作系统标准信号和应用进程定义信号,这种机制模拟硬中断,但部分优先级,简单且有效,但不能传送数据,故能力较弱。
管道通信机制
管道
是指用于连接一个读进程和一个写进程,以实现它们之间通信的一个共享文件(又名“pipe 文件”)。
向管道(共享文件)提供输入的发送进程(写进程),以字符流的形式将大量的数据送入管道;而接收管道输出的接收进程(“读进程”),则从管道接收(读)数据。由于
发送进程和接收进程是利用管道进行通信的,故称为管道通信
。
管道通信必须提供以下能力:
- 互斥:
即当一个进程正在对 pipe 执行读/写操作时,其他进程必需等待。
- 同步:
同步是指,当写(输入)进程把一定数量的数据写入 pipe,便去等待,直到读(输出)进程取走数据后,再把它唤醒;当读进程读一空 pipe 时,也应睡眠等待,直至写 进程将数据写入管道后才将之唤醒。
管道是一种功能机制很强的通信机制,但仅用于连接具有共同祖先的进程,使用时需要临时建立,难以提供全局服务。为了克服这些缺点, UNIX 推出管道的一个变种,
称为有名管道
或FIFO 通信机制
,用来在不同的地址空间之间进行通信,特别为服务器通过网络与多个客户进行交互而设计。
共享主存通信机制
共享存储通信有两种方式:
- 基于共享数据结构的通信方式:
公用数据结构的设置及对进程间同步的处理,都是程序员的职责,而操作系统只需提供共享存储器。因此,通信效率低,只适用于传递相对少量的数据。
- 基于共享存储区的通信方式:
进程在通信钱,先向系统申请获得共享存储区中的一个分区,并指定该分区的关键字:若系统已经给其他进程分配了这样的分区,则将该分区的描述符返回给申请者,然后, 由申请者把获得的共享存储区连接到本进程上;此后,便可以像读写普通存储器一样访问该公用存储分区。实际上,很多系统可以通过系统调用来操控共享分区。
消息传递机制
不论单机系统、多机系统还是计算机网络,消息传递机制都是应用最为广泛的一种进程间通信的机制。在消息传递系统中,进程间的数据交换是以格式化的消息(Message)为 单位的;在计算机网络中,又把 Message 称为报文。程序员直接利用系统提供的一组通信命令进行通信。操作系统隐藏了实现通信的细节,提高了透明性,因而获得了较为 广泛的使用。因实现方式不同可分为直接通信方式和间接通信方式。
- 直接通信方式:这种通信固定在一对进程之间。
- 间接通信方式:
又称为“信箱通信
”方式。信箱是一种数据结构,逻辑上可分为信箱头和信箱体两部分。
- 信箱头包含信箱体的结构信息以及多进程共享信箱体时的同步互斥信息。
- 信箱体由多个格子构成,它实际上就是一个有界缓冲器。
信箱通信的同步、互斥方式与生产者消费者问题的方式类似。它一般是进程之间的双向通信。
消息传递的复杂性在于:地址空间的隔离,发送进程无法将消息直接复制到接收进程的地址空间中,这项工作只能由操作系统来完成。为此,消息传递机制至少需要提供两条 原语 send 和 receive。为了实现异步通信,必须采用简洁的通信方式。
简洁通信解除了发送进程和接收进程之间的直接联系,在消息的使用上加大了灵活性。一个进程可以分别与多个进程共享信箱。于是,一个进程可以同时和多个进程通信, 一对一关系允许在两个进程间建立不受干扰的专用通信链接;多对一关系对客户服务器间的交互非常有用;
一个进程为其他进程提供服务,这时的信箱又称为端口
,端口通常划归接收进程所有并由接收进程创建,服务进程被撤销时,其端口也随之消失。当然还有多对多关系
的公用信箱。
信箱的设置:
信箱可以在用户空间或系统空间开辟。
- 用户空间信箱:创建者进程撤销时,信箱也随之消失,这时必须通知所有使用者。
- 系统空间设置公用信箱:可以充分利用预留空间(如果在系统空间内分别开辟私有空间则很难确定分配多大,当然可以延迟到接收时分配)。
通信进程的同步
两个进程间的消息通信就隐含着某种程度的同步,当发送进程执行 send 发出消息后,本身执行可分为两种情况:
- 同步的(阻塞型),等待接收进程回答消息后才继续进行;
- 异步的(非阻塞型),将消息传送到接收进程的信箱中,允许继续运行,直到某个时刻需要接收进程送来回答消息(如信箱已满)时,才查询和处理。
对于接收进程而言,执行 receive 后也可以是阻塞型和非阻塞型,前者指直到消息交付完成(一有消息就要停下来接收消息)它都处于等待消息的状态; 后者则不要求接收进程等待,当他需要消息时,再接收并处理消息。
消息传递机制解决进程的互斥和同步问题
- 解决进程互斥问题
- 解决同步问题:
生产者消费者问题的一种解法
消息缓冲队列通信机制
死锁
死锁的规范定义如下:
如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么,该进程集合就出现了死锁。
在多道程序设计环境下,多个进程可能竞争一定数量的资源。一个进程申请资源,如果该资源不可用,那么进程进入等待(阻塞)状态。如果所申请的资源被其他等待进程占有,
那么该等待进程有可能无法改变状态,这就出现了死锁。死锁可分为资源死锁
和调度死锁(外界强加优先权解决死锁)
。可见死锁与资源有关。
资源分类:
- 可抢占资源:
可抢占资源可从拥有它的进程处抢占而没有任何副作用。
- 不可抢占资源:
不可抢占资源是无法在不导致相关计算失败的情况下将其从占有它的进程处剥夺(如刻盘时)。
总的来说,死锁与不可抢占资源有关,有关可抢占资源的潜在死锁通常可以通过在进程间重新分配资源而化解。
资源的另一种分类:
资源通常可分为两类:可重用的和可消耗的。
- 可重用资源:
可重用资源是指一次只能供一个进程安全地使用,并且不会由于使用而耗尽的资源。进程得到资源单元,后来又释放这些单元,供其他进程再次使用。可重用资源的例子包括 处理器、I/O 通道、内外存、设备以及诸如文件、数据库和信号量之类的共享数据结构。
- 可消耗资源:
可消耗资源是指可以被创建(生产)和销毁(消耗)的资源。通常对某种类型可消耗资源的数目没有限制,一个无阻塞的生产进程可以创建任意数目的这类资源。 当消费进程得到一个资源时,该资源就不再存在了。可消耗资源的例子有中断、信号、消息和 I/O 缓冲区中的信息。
死锁原因
系统产生死锁的根本原因可归结为以下两点:
- 资源竞争:
大多数情况下,引起死锁的资源竞争是指对于不可剥夺性资源的竞争。另外一种因资源竞争而死锁的资源是一些临时性资源(也称为消耗性资源,如消息),如因为各个进程 都在等待其他进程发送消息然后自己才发送消息的情况。
- 进程推进顺序不当
死锁产生的必要条件
- 互斥条件
- 占有且申请条件
- 不可抢占条件
- 环路等待条件
资源分配图
总而言之,如果资源分配图没有环,那么系统就不处于死锁状态。另一方面,如果有环,那么系统可能会也可能不会处于死锁状态(因为还有其他必要条件)。在处理死锁 问题时,这一点很重要。
解决死锁问题的基本方法
- 死锁的预防:
采取某种策略,限制并发进程对资源的请求,从而保证死锁的必要条件在系统执行的任何时间都得不到满足。
- 死锁的避免:
在分配资源时,根据资源的使用情况提前做出预测,给定一个合适、安全的进程推进顺序,从而避免死锁的发生。
- 死锁的检测:
允许系统发生死锁。系统设有专门的机构,当死锁发生时,该机构能够检测到死锁的发生,并能确定参与死锁的进程及相关资源。
- 死锁的解除:
这是与死锁检测相配套的措施。用于将进程从死锁状态中解脱出来。
由于操作系统的并发与共享以及随机性等特点,通过预防和避免死锁的手段达到排除死锁的目的十分困难,需要相当大的系统开销,对资源的利用也不够充分。死锁的检测与 解除则相反,不必花费多少执行时间就能发现死锁并从死锁中恢复出来。因此,实际操作系统很多都采用了后两种方法。
- 鸵鸟算法:
大多数操作系统(如 UNIX、Windows),处理死锁的办法仅仅是忽略它,其假设前提是大多数用户宁可在极偶然的情况下发生死锁,也不愿接受只能创建一个进程、只能打开 一个文件等限制。要解决死锁问题的代价通常很大,而且常常会给进程带来许多不便的限制,于是我们不得不在方便性和正确性之间做出令人不愉快的权衡,要充分考虑哪一个 、对谁最重要。在这些条件下,很难找到通用的解决办法。
死锁的预防
死锁的必要条件中,“互斥条件”对于可分配的资源要互斥使用,这是由资源的固有特性决定的,不可改变(当然有些资源可以使用 SPOOLing 技术或其他抽象虚化技术变成 逻辑上的多个资源)。因此,只有通过打破后三个条件,使它们中的一条不成立,来达到预防死锁的目的。
摒弃占有且申请条件
可采用资源的静态预分配或释放已占资源策略。
- 资源的静态预分配:
在进程运行之前,一次性地向系统申请它所需的全部资源。系统要么满足所有要求,要么不分配任何资源。这种方法存在一些缺点:
- 释放已占资源策略:
仅当进程没有占用资源(先释放已有资源)时才允许它去申请资源。这种方法允许进程在开始时只申请磁盘文件等可重用资源。
该方法仍然有资源利用率低,可能发生饥饿(对于需要多个常用资源的进程)等缺点。
- 折中策略:
先释放已有资源再一次性申请所需所有资源,系统要么一次性满足,要么不分配任何所需资源。
摒弃不可抢占条件
我们采取的策略是隐式抢占。约定如果一个进程已经有了某些资源又要申请另外的资源,而被申请的资源不满足时,该进程必须等待,同时释放已占有的资源,以后再进行 申请。至于释放资源又有两种方式:
-
①占有资源的进程若要申请新的资源,必须主动释放已占用资源(剥夺式),若仍需要占用此资源,应该向系统重新提出申请,从而 破坏了不剥夺条件,但会造成进程重复地申请和释放资源。
-
②资源管理程序为进程分配新资源时,若有则分配之,否则将剥夺此进程所占有的全部资源,并让进程进入等待资源的状态,资源充足后再唤醒他重新申请所有所需资源。
它所释放的资源可以重新被分配给其他进程,这就相当于该进程占有的资源被隐式地抢占了,不过这种预防死锁的方法实现起来较为困难。
摒弃环路条件
采用层次分配策略,将系统中的所有资源排列到不同的层次中,一个进程得到某层的一个资源后,只能再申请较高一层的资源;当进程释放某层的一个资源时,必须先释放 所占用的较高层次资源;当进程获得某层的一个资源后,如果想申请同层的另一个资源,必须先释放此层中的已占用资源。
层次分配策略的一个变种是按序分配策略。把系统的所有资源按顺序编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行,使进程在申请、占有时不会形成环路。
这些预防死锁的策略与前面两种策略相比,资源利用率和系统吞吐量都有较为明显的改善。但也存在缺点:
- 限制了新类型设备的增加;
- 作业使用各类资源的顺序与系统规定的顺序不同时,造成资源的浪费;
- 为了解决前面两个问题,会增加复杂性;
- 按规定次序申请的方法必然会限制用户自然、简单地编程。
死锁避免
死锁的预防是排除死锁的静态策略,它使产生死锁的 4 个必要条件不能同时具备,从而对进程申请资源的活动加以限制,以保证死锁不会发生。死锁的避免
是一种
排除死锁的动态策略,能支持更多的进程并发执行,它不是对进程随意强加限制,而是对进程所发出的每一个申请资源的活动加以动态地检查,
并根据检查结果决定是否进行资源分配,即在资源分配过程中预测是否会出现死锁,如不会死锁,则分配资源;若有发生死锁的可能,则加以避免。
这种方法的关键是确保资源分配的安全性。
安全状态
安全状态是指系统中的所有进程能够按照某种次序分配资源,并且依次运行完毕,则进程序列就是安全序列
。如果存在这样一个安全序列(只要一个就行),则系统
是安全的,称此时系统处于安全状态
。如果系统不存在这样一个序列,则称系统是不安全
的。
安全状态不是死锁状态,相反,死锁状态是不安全状态;然而,并不是所有的不安全状态都是死锁状态。可见,死锁避免策略并不能确切地预测死锁,它仅仅是预料死锁 的可能性并确保永远不会出现这种可能性。
避免死锁的方法:
- 进程启动拒绝:
如果一个进程的请求会导致死锁,则不启动此进程;
- 资源分配拒绝:
如果一个进程增加的资源请求会导致死锁,则不允许此分配(“银行家算法
”)。
资源分配图算法
前面提到的“资源分配拒绝”方法中具体有:资源分配图算法和银行家算法等。
资源分配图除了申请边和分配边外,可引入一新类型的边,称为需求边
。这种边类似申请边,但是用虚线表示,如果需求被肯定,那么需求边就可以转化为分配边。
银行家算法
对于每种资源类型有多个实例的资源分配系统,资源分配图算法就不适用了。此时,可用银行家算法,但其效率要比资源分配图方案差。 银行家算法又称“资源分配拒绝”法,
其基本事项是:
- 系统中的所有进程放入进程集合,在安全状态下系统收到进程的资源请求后,先把资源
试探性
地分配给它。 - 然后,根据还剩余的资源,找出剩余资源能满足最大需求量的进程,以便释放出更多的该类资源。这时,把这个 进程从进程集合中删除(又少了一个竞争者),归还(试探性的,并非真正归还)该进程的所有其他资源(增加了剩余资源)。 反复执行上述步骤直到无法满足任何进程资源需求或进程集合为空截止。
- 最后,检查进程集合,若为空则表明本次申请可行,系统处于安全状态, 可以真正实施本次分配;否则,只要进程集合非空,系统便处于不安全状态,本次资源分配暂不实施,让申请资源的进程等待。
为了实现银行家算法,必须要有若干数据结构。这些数据结构对资源分配系统的状态进行了编码。设 n 为系统进程的个数,m 为资源类型的种类。需要如下数据结构:
- 可利用资源向量 Available :
它是长度为 m 的向量(含有 m 个元素的数组)表示每种资源的现有的实例的数量。每一个元素代表一类可利用的资源数目。 其初始值是系统中所配置的该类全部可用资源的数目。其数值随该类资源的分配和回收而动态地改变。
- 最大需求矩阵 Max
最大需求矩阵是一个 nXm 的矩阵,它定义了系统中 n 个进程中的每一个进程 对 m 类资源的最大需求。
- 分配矩阵 Allocation :
分配矩阵也叫做占有矩阵,是一个 nXm 的矩阵,它定义了系统中每一进程已占有的每一类资源数。
- 需求矩阵 Need :
需求矩阵也叫做申请矩阵,是一个 nXm 的矩阵,用以表示每一个进程尚需的各类资源数。
银行家算法的实现:
- 进程申请资源的情况
- 银行家算法的描述
- 安全性算法
安全算法步骤:
银行家算法粗略代码:
上述代码说明:
银行家算法实例:
死锁检测与解除
死锁的预防和避免都是对资源的分配加以限制,操作系统解决死锁问题的另一条途径是死锁检测方法。死锁检测方法与死锁预防和避免策略不同,这种方法对资源分配不加 限制,只要有剩余的资源,就可把资源分配给申请的进程,允许系统有死锁发生,这样做的结果可能会造成死锁,关键是当死锁发生时系统能够尽快检测到,以便及时解除 死锁,使系统恢复正常运行。因此,采用这种方法必须解决 3 个问题:
- 何时检测死锁的发生(启动检测程序的频率);
- 如何判断系统是否出现了死锁;
- 当发现死锁发生时如何解除死锁。
死锁的检测
- (1)利用资源分配图:
具体可参看“资源分配图”一节,这里只介绍如何利用资源分配图来检测死锁是否发生。
资源分配图的简化:
利用资源分配图进行死锁检测的目的是为了确定当前状态是否发生死锁。如果满足下列条件,那么一个连续、可重用资源图就能够通过进程 P 化简。
- 进程没有被阻塞;
- 进程没有请求边;
- 有分配边指向 P。
总之,只要进程 P 只有入度(有向图中的概念)就可以在资源分配图中“删除”P ,同时消除指向 P 的所有分配边(指向 P 的边),于是释放了资源,那么其他进程的
资源请求就可能重新得到满足。只要存在这样的 P(任意进程),就可以按照这种方式化简,直到不存在这样的 P 进程(不能完全简化
)
或资源图中没有进程(完全简化
)为止。
死锁定理:
通过资源分配图可以很直观地看出系统中的进程使用资源情况。显然,如果图中不出现封闭的环路,则系统中不会存在死锁。如果系统出现由各有向边组成的环路,则是否
产生死锁,还需进一步分析。如果环路可以通过化简取消,则系统一定不产生死锁;如果环路通过化简方式扔不能取消,即不能再进行简化,则系统一定会产生死锁。这就是
著名的死锁定理
。
资源分配图死锁检测实例:
下面介绍特殊的资源分配图。其中的每种资源类型只有单个实例。其死锁检测算法使用了资源分配图的变种,称为等待图
。
- (2)死锁检测算法
死锁检测算法实例:
死锁检测时机:
另一个不太昂贵的方法是在一个不频繁的时间间隔里调用检测算法,如每小时一次,或当 CPU 使用率低于 40% 时(死锁最终会使系统性能下降,并造成 CPU 使用率下降)。 如果在不定的时间点调用检测算法,那么资源图可能会有许多环。通常不能确定死锁进程中是哪些引起了死锁。
死锁解除
当死锁检测算法确定死锁已经存在,那么可以采取多种措施。一种措施是通知操作员死锁已发生,以便操作人员人工处理死锁。另一种措施是让系统从死锁状态中自动恢复 过来。打破死锁有两个方法:
- 简单地终止一个或多个进程以打破循环等待;
- 从一个或多个死锁进程那里抢占一个或多个资源。
下面是这两种方法的具体描述:
- 进程终止
- 终止所有死锁进程;
- 一次只终止一个进程直到取消死锁循环为止。
- 资源抢占
如果要求使用抢占资源来处理死锁,那么有三个问题需要处理:
- 选择一个牺牲品;
- 回滚:逐步回滚(到安全状态)或完全回滚(只能重新启动进程)。
- 饥饿:如何保证资源不会总是从同一个进程中抢占。
总之,常用的死锁解除方法有:资源剥夺法、进程回退法、进程撤销法和系统重启法等。具体方法描述小结如下:
选择剥夺资源或杀死进程的原则:
尽管检测死锁是否出现和发现死锁后实现恢复的代价大于防止和避免死锁所花费的代价,但由于死锁不是经常出现的,因而这样做还是值得的。检测策略的代价依赖于死锁 出现的频率,而恢复的代价是指处理器时间的损失。
综合的死锁策略
从上表可见,所有解决死锁的策略都各有其优缺点。与其将操作系统机制设计为只采用其中一种策略,还不如在不同情况下使用不同的策略更有效。
内存管理
存储管理是操作系统的重要组成部分。计算机系统的主要用途是执行程序。在执行时,这些程序及数据所访问的数据必须在内存里,至少部分是如此 (CPU 只能直接访问寄存器、高速缓存和主存),因此,存储管理的优劣直接影响系统性能。
在多道程序设计和分时系统的环境下,内存通常相对较小,不足以永久地容纳所有数据和程序,因此计算机系统必须提供次级存储以支持内存。现代计算机系统采用硬盘作为 信息(程序和数据)的主要在线存储媒介。文件系统为在线存储和访问驻留在硬盘上的数据提供了一种机制。如此就形成了存储器的层次。
存储器的层次:
可执行程序必须被保存在主存储中,与设备相交换的信息也依托于主存地址空间。由于处理器在执行指令时的主存访问时间远大于其处理时间(CPU 时间),所以,寄存器 和高速缓存被引入来加快指令的执行。
由于程序在执行和处理数据时往往存在顺序性和局部性,执行时并不需要将其全部调入主存,仅调入当前使用的一部分,其他部分待需要时再逐步调入。如此就可以在磁盘上 建立磁盘高速缓存以扩充主存器的存储空间,计算程序和所处理的数据可装入磁盘高速缓存,操作系统自动实现主存储器和磁盘高速缓存之间的程序和数据的调入和调出,从而 向用户提供比实际主存容量大得多的存储空间。
概述
主存空间一般分为两部分:一部分是系统区,用于存放操作系统内核程序和数据结构等;另一部分是用户区,用于存放应用程序和数据。所以通常说的主存管理主要是对用户区 的管理。
存储管理所研究的主要内容包括 3 个方面:
- 取:
“取”是指研究应将哪道程序(或程序的一部分)从辅存调入主存,一般有请调
和预调
之分。前者是需要时再调入;后者是采用某种策略,预测并调入即将使用
的某道程序(或程序的一部分)到主存。
- 放:
“放”是研究将“取”来的程序(或程序的一部分)按何种方式存放在主存的什么地方。“放”是存储器管理的基础。目前“放”的技术归结为两类:一类是连续的
,
即运行的程序必须存放在主存的一片连续空间中;另一类是不连续
的,即运行的程序可以放在主存的多个不相邻的块中。
- 替换:
“替换”是研究应将哪道程序(或程序的一部分)暂时从主存移到辅存以腾出主存空间供其他程序(或程序的一部分)占用。
内存管理需求
内存管理一般有 5 点需求:重定位、存储保护、共享、逻辑组织、物理组织。
- 重定位:
在多道程序设计系统中,通常情况下,应用程序员并不能事先知道主存中已经有几道程序以及占用了那部分内存,那么操作系统必须能把程序员使用的逻辑地址重定位到内存 地址空间中。而且在进程运行过程可能会被交换,这也需要重定位。
- 存储保护:
主存中驻留了多个进程,每个进程都应该受到保护,以免被其他进程有意或无意地干涉。由于重定位的存在,存储保护必须在运行时检查进程产生的所有内存访问,以确保 它们只访问分配给自己的存储空间或有权访问的其他共享空间。
需要注意的是,内存保护的需求必须有处理器(硬件)来满足,而不是由操作系统(软件)满足。
- 共享:节约主存空间,提高主存利用率和效率。
- 逻辑组织:是逻辑空间的组织满足用户编程和数据组织的需要,提高易用性。
- 物理组织:将主存用较好的物理方式组织起来,以尽可能提高主存利用率和效率。
存储管理功能
前面说到的需求似乎抽象一些。本节将需求具体化为内存管理应具有的一些功能:
- 分配和回收:
进程可请求对主存区的独占式使用(至少用户感觉是这样),主存区的请求和释放即主存空间的分配和回收操作由存储管理来完成。
- 抽象和映射
主存储器被抽象成这样:使用户认为分配给它的地址空间是从 0 开始的一个无限大和连续的空间。好像用户独占主存一样(多道程序设计系统使用起来和单道系统一样)。
- 隔离和共享
使用户无法感知其他程序的存在,同时也要满足用户对其他程序提供的信息共享的需要。即使用户没有明确的需要,系统也会为了提高主存利用率(使用一个副本总比使用 多个副本要节约空间)而对程序间相同的部分进行共享。
- 存储扩充
物理主存容量不应限制应用程序的大小,主存和辅助存储器被抽象为虚拟主存,允许用户的虚拟地址空间大于主存物理地址空间,存储管理自动在不同的存储层次中移动信息。
后面的章节将围绕如何实现这些功能而展开。而且几乎每一种内存管理方法都要实现这些功能。
用户程序
内存管理主要是对主存中的用户区进行管理,而用户区是用来安放用户程序的,所以要想充分利用用户区,还得充分了解用户程序结构特点和处理过程。
用户程序的处理过程
系统对用户程序的处理分为以下几个阶段:
- 编译:
由编译器的编译程序将用户源代码编译成若干个目标模块。每个目标模块都有自己的独立逻辑地址空间。有些库函数、系统调用或其他引入的文件有可能系统已经编译好了。
- 链接:
由链接程序将编译后形成的目标代码以及它们所需的库函数链接在一起,形成一个装入模块。
- 装入:
由装入程序将装入模块装入内存。
- 执行:
由调度程序分配处理机执行。
重要概念:
- 逻辑地址:
逻辑地址
是一个操作数在逻辑地址空间中的地址(CPU 所生成的地址,程序计数器 PC 值)。逻辑地址空间
是程序及数据所构成的空间,所有单独编译的模块都是从 0 开始编址的。可见,逻辑地址是可以
重叠的(实际上他们处于不同的作用域内,其实可以通过模块名加以区分)。逻辑地址只是同一个模块中的相对地址,其绝对地址取决于模块装入和位置和方式,此时的逻辑
地址又被称为虚拟地址
。linux 中 fork 出的子进程与父进程的逻辑地址空间相同,所以,它们的同名变量(没有被覆盖重定义的情况下)打印出的地址(逻辑地址)相同,
但是,它们的物理地址(堆栈段)实际上是不同的(然而,代码段和静态区段的物理地址相同,这是由其全局性决定的)。
- 物理地址:
物理地址
是指模块装入内存后的真实地址,即加载到内存地址寄存器
中的地址,是主存中的绝对地址,不允许没有空间共享的两个不同模块的物理地址在相同时刻有重叠。
- 重定位:
重定位
是指将一个操作数的逻辑地址转换为物理地址。可见用户程序处理的是逻辑地址,而看不到真正的物理地址。
目标程序装入内存的方式
对用户程序处理过程中的每一个环节都可能有不同的方式。而程序只有装入内存后才能运行。装入方式分为:绝对装入方式
、可重定位装入方式
和动态运行时装入方式
。
- 绝对装入方式:
如果在编译时就知道进程将在内存中的驻留地址,那么就可以生成绝对代码。如果将来开始地址发生变化,那么就必须重新编译代码。通常情况是在程序中采用符号地址,然后 在编译或汇编时,将这些符号地址再转化为绝对地址。MS-DOS 的 .COM 格式程序就是在编译时捆绑成绝对代码的。
- 可重定位装入方式:
在程序执行之前,由操作系统重定位装入程序完成。一般用于多道程序环境中。重定位程序根据装入程序的内存起始地址,直接用“起始地址 + 逻辑地址”的方式得到正确 的内存地址。
- 动态运行时装入方式:
该方式是在程序执行期间进行的。程序执行期间可能会发生移动或换入换出(每次换入的位置可能不同),即在执行过程中在主存中的位置发生改变。在这种情况下,静态重 定位不能解决问题,需要用动态重定位。
采用运行时动态装入时,一个子程序只有在调用时才被加载,所有子程序都以可重定位的形式保存在磁盘上。主程序装入内存并执行,当一个子程序需要调用另一个子程序时, 调用子程序首先检查另一个子程序是否已加载。如果没有,可重定位的链接程序将被用用来加载所需要的子程序,并更新程序的地址表以反映这一变化。之后,控制权传递给 新加载的子程序。
该方式的优点是不用的子程序绝不会被装入内存。如果大多数代码需要用来处理异常情况(如错误处理)或程序中含有很多分支处理子程序(只有满足条件的分支才会被执行)。 对这种情况,虽然总体上程序比较大,但是所使用的部分(即加载的部分)可能小很多。
上述两种动态装入不需要操作系统提供特别的支持。利用这种方法来设计程序主要是用户的责任。不过,操作系统可以帮助程序员,如提供子程序库以实现动态加载或链接。
一般说来,动态重定位需要有专门的硬件机构来完成,通常采用一个重定位寄存器,每次进行存储访问时,将取出的逻辑地址加上重定位寄存器的内容,形成正确的内存地址。
目标程序链接
链接程序的功能是将经过编译或汇编后得到的一组目标模块以及它们所需的库函数或外来模块装配成一个完整的装入模块。实现链接的方法有:静态链接
、
装入时动态链接
和运行时动态链接
。
- 静态链接:
静态链接之后形成一个完整的准入模块,又称为执行文件,通常不再拆开,运行时可直接装入内存。在多道程序环境下,不同的执行文件中可能具有相同的模块,如相同的 库函数调用,这实际上造成了多个副本,浪费空间。而且一般需要整体装入(需要更大的连续空间),必须以一个整体的形式进行,本来可以将其中不常用的一部分换出即可。
- 装入时动态链接:
用户源程序经编译后得到目标模块,在装入内存时边装入边链接,即在装入一个目标模块时,若发生一个外部模块调用,装入程序将去寻找相应的外部目标模块,并将它 装入内存。
装入时进行的链接虽然可以将整个模块装入内存的任何地方,但装入模块的结构是静态的,在程序执行期间装入模块时不可改变的,因为无法预知本次要运行哪个模块,只能将 所有可能要运行的模块,在装入时全部链接在一起。这种方法可以在当某个模块更新之后不需要重新链接成一整个执行文件,便于局部更新。
- 运行时动态链接:
在这种链接方式中,可将某些目标模块的链接推迟到执行时才进行,即在执行过程中,若发现一个被调用模块尚未装入内存时,由 OS 去找该模块,将它装入内存,并把它 链接到调用模块上。
这一特点(其实,装入时动态链接也具有本段的性质)通常用于系统库,如语言子程序库。没有这一点,系统上的所有程序都需要一份其语言库的拷贝(或至少那些被程序所引用的子程序)。这一要求将浪费磁盘和主存 空间。如果用运行时动态链接,二进制映像中对每个库程序的引用都有一个存根。存根是一小段代码,用来指出如何定位适当的内存驻留库程序,或如果该程序不在内存时应 如何装入库。
它与装入时动态链接的区别在于其是和动态装入(或加载)配套使用的。两种动态链接都可用于库更新,使用该库的所有程序会自动使用新的版本。没有动态链接,所有这些 程序如果要使用新的库。为了避免使程序错用新的、不兼容版本的库,程序和库(同时存在多个版本)可以包括版本信息。
与动态装入(或加载)不一样,动态链接通常需要操作系统的帮助。如果内存中进程是彼此保护的(共享的子程序或库函数段只需要装入一次即可被其他所有调用者共享), 那么只有操作系统才可以检查所需子程序是否在其他进程内存空间内(被链接到锁调用进程的空间内),或是允许多个进程访问用一内存地址。
无存储器抽象的内存管理
最简单的存储器抽象就是根本没有抽象,直接操纵物理地址空间。每一个程序都直接访问物理内存。使用多道程序设计时需要使用静态链接、绝对装入或静态重定位的方式。 而且要事先知道各道程序所需空间。
这种直接引用物理地址对于大型计算机、小型计算机、台式计算机和笔记本电脑来说已经成为很久远的记忆了,但是缺少内存抽象的情况在嵌入式系统和智能卡系统中是很常见 的。现在,像洗衣机和微波炉这样的设备都已经完全被(ROM 形式的)软件控制,在这些情况下,软件都采用访问绝对内存地址的寻址方式。这样能够正常工作是因为, 所有运行的程序都是可以事先确定的,用户不可能在洗衣机上自由地运行他们自己的软件。
虽然高端的嵌入式系统(如手机)有复杂的操作系统,但是一般的简单嵌入式系统并非如此。在某些情况下可以用一种简单的操作系统,它只是一个被链接到应用程序的库,该 库为程序提供 I/O 和其他任务所需要的系统调用。
注意:
本博客提到的存储管理都是基于有抽象的存储器管理。
连续分配存储管理方式
连续分配是指为一个用户程序分配一个连续的内存空间,包括单道程序的连续分配和多道程序的连续分配。连续分配方式又称为分区分配方式
,包括固定分区
、
动态分区
和动态重定位分区
3 种。
单道程序的连续分配
单道程序的连续分配是一种最简单的存储分配方式。只能用于单用户、单任务的操作系统。在这种存储管理方式下,内存分为系统区和用户区两个分区。
为了避免用户程序执行时访问操作系统所占空间,应将用户程序的执行严格控制在用户区域(即存储保护),保护措施主要是由硬件实现。硬件提供界地址寄存器和越界检查 机构。将操作系统所在空间的下界存放在界地址寄存器中,用户程序执行时,每访问一次主存,越界检查机构便将访问主存的地址和界地址寄存器的值进行比较,若出界 则报地址错。
固定分区分配方式
固定分区管理方式的基本思想是:主存空间被划分成数目固定不变的分区,各分区的大小固定不变(各分区的大小可以相等也可以不等),每个分区只装入一个作业,若多个
分区中都装有作业,则它们可以并发执行,这是支持多道程序设计的最简单的存储管理技术。固定分区存储管理又称为定长分区
或静态分区模式
。
在这种管理方式下,操作系统启动时系统操作员根据当天作业情况借助操作系统命令划分与确定好分区的大小和个数(一旦确定下来,可以并发的程序道数也就被限制了), 即使如此,在有比较多作业且无法一次性装下所有作业的情况下,划分出的分区不一定适合所有作业,可能出现空闲分区过小或过大的情形,这样可能出现:
- 小作业不能有效地利用分区空间;
- 作业需要排队进入主存区;
- 分区无法容纳整个作业时,还需要使用
覆盖
、换入换出
等技术加以补救,不但加重了用户的负担(覆盖需要程序员的手动规划和编写方案),而且极不方便。
在程序运行期间,整个系统中的分区不再变化。因此,为一个作业分配空间时,应先根据一定的分区分配策略,为作业选择一个分区,然后再分配。
作业进入分区排队策略:
动态分区
为了克服固定分区的一些缺点,出现了动态分区方法(不过已经被其他更好的方法所取代)。对于动态分区,分区长度和数目是可变的。当进程被装入内存时,系统会给它分配 一块和它所需容量完全相等的内存空间,不多不少。
动态分区方法在开始时是很好的,随着进程的退出和进入(退出和进入的顺序是很难预知的,当然可以根据空闲区的大小从等待队列中选择较为合适的作业进入,但是,进程
配对、退出、进入的随机性很强),它最终会导致内存中出现许多小的碎片(即外部碎片),可能任何碎片都无法被进程使用,即使所有碎片总和能够容下进程,
不经过紧缩
(请参考“可重定位分区”)处理也无法利用。
空闲区块的分配方法:
- 首次适应
分配第一个足够大的空闲块,查找可以从头开始,也可以从上次首次适应结束时开始,一旦找到足够大的空闲区就可以停止查找。
- 最佳适应
分配最小的足够的空闲区,必须查找整个列表,除非列表按大小排序。这种方法将会出现最小的剩余空闲区。
- 最差适应
分配最大的空闲区。同样查找整个列表,除非列表按大小排序。这种方法将产生最大剩余空闲区,该空闲区可能比最佳适应产生的较小剩余空闲区更为有用。
- 快速匹配法
它的基本思路是:对于一些常用的或特殊的请求大小,为它们分别设置各自的链表分别进行管理,如此分类管理减少了查找等的开销。不过,在一个进程终止或被换出(回收) 时,查找合并相邻空闲区是非常费时的。
评价这些算法好坏的标准有两条:算法本身的时间复杂度(排序和查找)、空间利用率。模拟结果显示,首次适配和最佳适配在执行时间和利用空间方面都好于最差适配。 首次适配和最佳适配在利用空间方面相差不大,但是首次适配要快些。不过,正如前面所说,这些算法都有外部碎片问题。
动态分区的内存回收:
回收分区的主要工作是首先检查是否有相邻的空闲区,如果有则合并,使之成为一个连续的空闲区。避免形成许多离散的小分区。
可重定位分区(紧缩)
紧缩(紧凑)
在连续分配方式中,必须把一个系统程序或用户程序装入到连续的内存空间中,如果系统中存在若干个小的分区,每个小分区都不能满足装入程序的需要,该程序就不能装入 内存,但各空闲分区之和大于要装入的程序。在这种情况下,要想装入作业,可采用的方法是:
将内存的作业进行移动,使它们相邻,使原来许多分散的小分区可以拼接成大的分区,称为“紧缩
”。
由于经过紧缩的用户程序在内存中的位置发生了变化,若不对程序中的数据地址进行修改变换,程序无法执行,因此必须进行重定位(运行时动态重定位)。
动态重定位分区分配算法
该算法与动态分区分配算法基本相同,差别仅在于这种分配算法中增加了“紧缩”功能。通常,若找不到足够大的空闲区来满足用户的需要,则进行“紧缩”或“交换” (根据开销进行选择。开销包括已经做了的工作类型及其工作量、剩余的工作量与交换所需开销),然后寻找合适的内存空间。
伙伴系统
固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程的数目,并且如果可用分区的大小与进程大小非常不匹配,则内存空间的利用率非常低。 动态分区的维护特别复杂,并且引入了进行紧缩的额外开销。一种更有吸引力的折中方案是伙伴系统。
分配算法:
- 确定允许的最小分区和最大分区,所有分区都是 2 的整数幂;
- 比较请求的大小和空闲分区集合中最佳分区的大小关系:
- 最佳分区为允许的最小分区,则直接分配,否则
- 请求大小 < 最佳分区的 1/2,则将最佳分区均分为二,直到分离出新的最佳分区(这个只是计算过程,实际上最后分成两部分,一个大分区和一个不能再分的最佳分区) 不满足再分条件为止,然后将其分配出去;
- 否则将最佳分区整个分配出去。
“不能再分的分区
”的条件:已经到了允许的最小分区或是再分就不能最佳适应请求分区的大小了。
内存回收:
回收内存时候要在空闲区集合中查找其伙伴,如果存在伙伴则合并(合并之后可能出现新的伙伴),直到没有伙伴为止。
为了实现伙伴系统算法,需要位图(标志已分配的分区)和空闲链表作为辅助工具,伙伴系统分配和合并操作速度快,但可能出现较大的内部碎片(因为最佳分区可能选择不当)。 而且,在当前的操作系统中,基于分页和分段机制的虚拟内存更先进。然而,伙伴系统在并行系统中有很多应用,它是为并行程序分配和释放内存的一种有效算法。
空闲内存管理
在动态分配内存时,操作系统必须对其进行管理。一般而言,有两种方式跟踪内存使用情况:位图
和空闲链表
。
基于位图的存储管理
使用位图方法时,内存可能被划分(相等的)成小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0 表示空闲,1 表示占用(或者相反)。 一块内存区和其对应的位图如下图所示:
分配单元的大小是一个重要的设计因素。分配单元越小,位图越大。如果分配单元过大,那么在最后一个分配单元中就会有一定数量的内存被浪费了。
因为内存的大小和分配单元的大小决定了位图的大小,所以它提供了一种简单的利用一块固定大小的内存,区就能对内存使用情况进行记录的方法。这种方法的主要问题是, 在决定把某个占 k 个分配单元的进程调用内存时,存储管理必须搜索位图,在位图中找出有 k 个连续 0 的串。这是很耗时的操作。
基于链表的存储管理
动态分区可以使用两张表来管理内存,它们是:已分配区表
和未分配区表
。如此,对内存的管理就变成了对表格的曾删查改(实际上并不是真的清除了内存中
的内容,只是将允许该进程访问该内存区块,原有的内容实际上还是存在的,只不过后面将被覆盖,所以,程序员最好对每个变量都初始化,当然一般编译器会默认初始化,
否则,未初始化变量中的内容是无法预知的)。
由于分区的数目不定,采用链表来管理空闲区可能更好。用链指针把所有空闲分区链接起来,每个主存空闲区的开头单元存放本空闲区长度及下一个空闲区起始地址指针, 系统设置指向空闲区链的头指针。在使用时,沿链查找并取一个长度能满足要求的空闲区给进程,再修改链表(已分配表和未分配区表);归还时,把此空闲区链入空闲区链表的相应位置即可。空闲区 链表管理比空闲区表格管理要复杂,但其优点是链表自身并不占用存储单元(至少不需要预先占用一大块连续的空间)。
为了提高链表查找效率,可以将进程(即,已分配空间链表)和空闲区使用不同的链表(甚至可以精简链表项,将一部分表项存储在对应的空闲区存储空间内)。 无论空闲区表管理还是空闲区链表管理,表格和链表中的空闲区项都可按一定规则排列。例如,按空闲区大小、空闲区地址等,以方便空闲区的查找和回收。 具体的查找回收算法请参考“动态分区”一节中相关内容。
内存不足的存储管理技术
这里说的内存不足有两种情况:
- 绝对不足:整个内存过小,不满足单道程序的大内存要求;
- 相对不足:空闲内存或分配给该进程的内存不足。
- 运行不足:分配给进程的内存空间开始是足够的,后面由于进程数据等的增长导致运行期间进程内存不足。
移动技术(主存紧凑)
当在未分配区表中找不到足够大的空闲区来装入新进程时,可采用移动技术(运行时动态重定位)把已在主存中的进程分区(即已经分配出去的空间,进程链表可以表征)
连接到一起,使分散的空闲区汇集成片,这就是移动技术
,也叫做主存紧凑
。这种技术有两种方法可选:
- 把所有当前占用的分区移动到主存的一端,直到所有空闲分区集中在一起。
- 把占用分区移动到主存的一端,但当产生足够大小的空闲分区时就停止移动。
使用移动技术的移动时机:
- 当一道程序正在与设备交换数据时往往不能移动,系统应设法减少移动(因为块设备在与主存交换信息时,通道或 DMA 不用 CPU 参与, 总是按确定的主存绝对地址完成信息传输,即使移动了,它也会按照原来的地址存取数据,从而导致错误和对移动到该区域的进程造成破坏);
- 进程撤销之后释放分区时,如果它不与空闲区邻接,立即实施移动,于是,系统始终保持只有一个空闲区;
- 进程装入分区时,若空闲区的总和够用,但没有一个空闲区能容纳此进程时,实施移动。
移动操作也为进程运行过程中动态扩充主存空间提供了方便。当进程在执行过程中要求增加主存分配区时,只需适当移动临近的占用分区 (可能要移动好几个进程或者换出某个进程)就可增加其所占有的连续区的长度,移动后的基值和经扩大的限长值都要做相应修改。
对换技术
对换技术广泛应用于分时系统的调度中,以解决主存容量不足的问题,使分时用户获得快速响应时间;也可用于批处理系统,以平衡系统负载。如果当前一个或多个驻留进程
都处于阻塞态,此时选择其中的一个或几个进程,将其暂时移出主存,腾出空间给其他进程使用,同时把磁盘中的某个进程换入主存,让其投入运行,这种互换称为对换
。
交换技术的应用场景:
该技术可用于同时为很多用户服务的分时系统,如此可以改善响应时间和大部分的用户体验。这种交换策略的变种被用在基于优先权的调度算法中。如果一个更优先级进程 来了且需要服务,内存管理可以交换出低优先级和低代价的进程,以便可以装入和执行更高优先级的进程。当更高优先级进程执行完后,低优先级进程可以交换回内存以继续执行。
交换需要的技术支持:
- 配套的装入方式:
通常一个交换出的进程需要交换回它原来所占有的内存空间。这一限制是由地址捆绑(逻辑地址到物理地址的转化)方式决定的。如果捆绑是在汇编时或加载时决定的。,那么 就不可以移动到不同的位置。如果捆绑在运行时确定,由于物理地址是在运行时才确定的,那么进程可以移动到不同的地址空间。
- 快速磁盘及交换处理过程:
交换需要备份存储。备份存储通常是快速磁盘。这必须足够大,以便容纳所有用户的内存映像拷贝,它也必须提供对这些内存映像的直接访问。系统有一个就绪队列,它包括
在备份存储或在内存中准备运行的所有进程。当 CPU 调度程序决定执行进程时,它调用派遣程序
来检查队列中的下一个进程是否在内存中。如果不在内存中且没有空闲
内存空间,派遣程序检查队列中的下一进程是否在内存中。如果不在内存中且没有空闲内存空间,派遣程序将一个已在内存中的进程交换出去,并换入索要的进程。然后,
它重新装载寄存器,并将控制权转交给所选择的进程。
对换决策:
- 进程选择:首先要解决的是选择哪个进程换出,选择标准如下:
- 进程阻塞的原因是不同的,换出时需要考虑其阻塞原因(因为这涉及到可不可以换出,换出的代价如何);
- 进程的工作类型,这涉及到进程变化部分的多少(假设变化很少,一般磁盘本来就存在一个副本,只需保存变化的信息即可);
- 换出的进程(最后只换出一个进程)空间足够用于新进程;
- 换入换出的时间开销要远小于该进程剩余的执行时间以及新进程需要执行的时间;
- 待换出的阻塞态进程阻塞时间够长(以便防止频繁的换入换出),优先级较低;
- 交换的本身时间消耗(与待换出的进程空间大小有关)要尽量少;
- 有些时候不一定要把整个进程都换出去,这就需要决定具体换出进程的那一部分,尽量换出信息变动频率小的部分;
如果进程 I/O 是以异步的方式访问用户内存的 I/O 缓冲区的,那么该进程就不能够被换出(可能导致 I/O 数据损失和新进程数据遭到破坏,因为 I/O 一般直接使用绝对物理地址 进行操作)。对这个问题有两种解决方法:
(1)不能换出有待处理的 I/O 进程;
(2)I/O 操作的执行只能使用操作系统缓冲,而且不剥夺正在使用的缓冲区,仅当换入进程后,才执行操作系统缓冲与进程内存之间的数据转移。
假设一个被对换的进程映像占用 k 个磁盘块,那么,一次进程对换的所有开销是 2k 个磁盘块输入/输出的时间,在加上进程重新请求主存资源所造成的时间延迟。
- 信息备份:决定备份那些信息,如何备份。
开始时,进程从可执行文件被装入主存,其未曾修改的部分(如代码段)在主存与磁盘中始终保持一致,这些信息不必保存,当进程换回主存时,只需简单地从最初的可执行 文件再加载一次;数据区和堆栈是进程运行时所创建和修改的,操作系统可通过文件系统把这些可变信息作为特殊文件保存。
- 对换空间:尽量加快对换空间的访问速度。
有些系统从降低开销的角度考虑,开辟一块特殊的磁盘区域作为对换空间,它包含连续的柱面和磁道,可通过底层磁盘读写实现高效访问。此时,交换不需要或只需要很少磁头 移动,因此较普通的磁盘访问要快。
- 对换时机:提高整机效率。
- 在批处理系统中,当进程要求动态扩充主存空间且得不到满足时可触发对换;
- 在分时系统中,对换可与调度结合,每个时间片结束或执行 I/O 操作时实施(CPU 空闲时),调度程序启动一个被换出的进程换入(预换入)。这样,轮到它执行 时立即可以启动。
普通交换使用不多。交换需要很多时间,而且提供很少的时间片执行时间(导致相对时间开销过大),因此这不是一种有效内存管理解决方案。然而,一些交换的变种却在 许多系统中得以使用。
UNIX 中,交换通常不执行,但当有许多进程运行且内存空间吃紧时,交换开始启动。如果系统负荷降低,那么交换就暂停。
对换与移动技术的比较:
对换技术比移动技术更有效,移动不能保证得到一个满足请求的空闲区,而利用对换技术总可按需换出若干驻留的阻塞进程,且对换仅涉及少量进程,只需更少的主存访问。 与移动不同的是,对换要访问磁盘,这是一个 I/O 集中型操作,会影响对用户的响应时间,但系统可让对换与计算型进程并行工作,不会造成系统性能的显著下降。
覆盖技术
移动和对换技术解决的是因其他程序存在而导致主存不足的问题,这种主存短缺只是暂时的;如果程序的长度超出了物理主存的总和,或超出了固定分区的大小,则出现主存
永久性短缺,大程序无法运行,前述的两种方法无能为力,解决方法之一是采用覆盖技术
。
覆盖的思想是:
在任何时候只在内存中保留所需的指令和数据。当需要其他指令时,他们会装入到刚刚 不再需要的指令所占用的内存空间内。覆盖驱动程序根据需要将它们读入内存。为了构造覆盖,需要使用特殊的重定位和链接算法。
覆盖的实现技术:
把用户空间分成固定区和一个或多个覆盖区,把控制或不可覆盖部分(如主函数)放在固定区,其余按调用结构及先后关系分段并存放在磁盘上,运行时一次调入覆盖区。 系统必须提供覆盖控制程序及相应的系统调用,当进程装入运行时,由系统根据用户给出的覆盖结构进程覆盖处理,程序员必须指明同时驻留在主存的是哪些程序段, 哪些是被覆盖的程序段,这种声明可从程序调用结构中获得。
覆盖技术的评价:
与动态加载(或装入)一样,覆盖不需要操作系统提供特别支持。用户通过简单文件结构,将文件读入内存,并执行所读指令就可以完全实现覆盖。操作系统只不过注意到 I/O 操作比平常多些而已。
另一方面,程序员必须适当地设计和编写覆盖结构。这并不简单,需要对程序结构、代码、数据结构有完全了解。由于程序比较大时才需要使用覆盖,而此时获取对程序的足够 且完整的理解可能比较困难。由于这些原因,覆盖的使用通常局限于微处理机和只有有限物理内存且缺乏更先进硬件支持的其他系统。
基本分页存储管理
用分区方式管理存储器,每道程序要求占用主存的一个或多个连续存储区域,导致主存中产生“碎片”。虽然可通过“紧凑”方法将许多碎片拼接成可用的大块空间,但 必须为之付出很大开销。如果允许将一个进程直接分散地装入到许多不相邻接的分区中,则无须再进行“紧凑”。基于这一思想产生了离散分配方式。如果离散分配的基本单位 是页,则称为分页存储管理方式;如果离散分配的基本单位是段,则称为分段存储管理方式。
在分页存储管理方式中,如果不具备页面对换功能,则称为基本的分页存储管理方式,或称为纯分页存储管理方式,它不具有支持实现虚拟存储器的功能,它要求把每个作业 全部装入内存后才能运行。
存储空间划分(分页)
分页存储管理是将一个进程的逻辑地址空间分成若干个大小相等的片,称为页面
或页
,并从 0 开始为各页加以编号。相应地,也把内存空间分成与页面相同大小
的若干个物理存储块,称为(物理)块
或页框
或帧
,也同样从 0 开始为它们加以编号。不过,页面编号占的位数要多于页框的编号位数,因为逻辑空间
一般大于物理空间。
在系统需要执行一个进程时,它将检查该进程所需要的页数。因此,如果进程需要 n 页,那么内存中至少有 n 个空余帧(请求分页机制除外)。如果有,那么就可分配给新
进程,装入一帧就把相应的帧号放入进程的页表对应项目中。在为进程分配内存时,以块为单位将进程中的若干个页分别装入到多个可以不相邻接的物理块中。
由于进程的最后一页经常装不满一块而形成了不可利用的碎片,称之为页内碎片
。
页面大小
在分页系统中的页面其大小应适中。
- 若页面太小:
- 虽然可使内存碎片减小,从而减少了内存碎片的总空间,有利于提高内存利用率;
- 也会使每个进程占用较多的页面,从而导致进程的页表过长,占用大量内存;
- 还会降低页面换进换出的效率。
- 如果选择的页面较大:
- 虽然可以减少页表的长度;
- 提高了页面的换进换出的速度;
- 却又会使也内碎片增大。
因此,页面的大小应选择适中,且页面大小应是 2 的幂,通常为 512 B ~ 8 KB。假设进程的大小是随机性的,那么可以推测每个进程平均可能有半页的内部碎片。
地址映射(页表)
逻辑地址(它是一个自相对于程序开始处的位置)由两部分组成:
- 页号:即页面编号,用于查询安放在物理地址空间中对应的物理块号;
- 页内位移:即相对于本页起始位置的偏移量,用于表述具体指令或数据在页面中的具体位置,从而确定在相应物理块中的具体物理位置。
逻辑地址空间的总大小(即逻辑地址的位数 = 页号位数 + 页内位移的位数)由地址总线的位数决定(一般而言,32 位地址总线最大支持的文件大小为 4G)。一页的大小由 页内位移所占的位数决定,逻辑空间能容纳的页数最大值由页号所占的位数决定。
采用分页存储管理时,逻辑地址是连续的,用户在编制程序时仍使用相对地址,不必考虑如何分页,由硬件地址装换机构和操作系统的管理需要来决定页面的尺寸,从而确定 主存分块大小。进程在主存中的每个页框内的地址是连续的,但同属一个进程的页框之间的地址可以不连续,进程主存地址由连续到离散的变化为虚拟存储器的实现奠定了基础。
基本概念:
- 页表
逻辑地址到物理地址的转换仍然采用动态地址重定位技术,让程序在执行时动态地进程地址变换。由于程序以页面为单位存储,所以为每个页面设立一个重定位寄存器,这些
重定位寄存器的集合称为页表
。由于寄存器的硬件代价过高,实际上页表由软件构成,存储在内存中或想联存储器(快表)中。
页表是操作系统为进程建立的,是程序页面和主存对应页框的对照表,页表中的每一栏指明程序中的一个页面和分得页框之间的对应关系。使用页表的目的是把页面映射为
页框。为了降低系统价格,不用硬件而是在主存中开辟存储区以存放进程页表,系统另外设置专用硬件—–页表基址寄存器
,存放当前运行进程的页表的起始地址,
以加快地址转换速度。系统应为主存中的进程进行存储分配,并建立页表,指出逻辑地址页号与主存页框号之间的对应关系,页表的长度随进程大小而定。
- 帧表
由于操作系统管理内存,它必须知道物理内存的分配细节:哪些帧已经分配,哪些帧空着,总共有多少帧等等,这些信息常常保存在称为帧表
的数据结构中。在帧表中,
每个条目对应着一个帧,以标识该帧是空闲还是已被占用,如果占用,是为哪个进程的哪个页所占用。
地址变换
进程运行前由系统把它的页表基地址送入页表基址寄存器,运行时借助于硬件的地址转换机构,按页面动态地址重定位,当 CPU 获得逻辑地址后,由硬件自动按设定的页面
尺寸分成两部分:页号 p 和页内位移 d,先从页表基址寄存器找到页表基地址(页表存放位置,其位置信息一般放在 PCB 中),再用页号 p 作为索引查页表
(查找操作由硬件执行),得到对应的页框号,根据关系式: 物理地址 = 页框号(第一个页框的基地址为 0) X 块长 + 页内位移
计算出欲访问的主存单元。因此,虽然进程存放在若干不连续的页框中,
但在执行过程中总能按正确的物理地址进行存取。
注意:在以页号为索引去检索页表前,先将页号与页表长度进行比较,如果页号大于或等于页表长度(从 0 开始编号),则表示本次所访问的地址已超越进程 的地址空间。于是,这一错误将被系统发现并产生一地址越界中断。若未出现越界错误,才进行下一步操作。
需要补充的是:页表项除了上图中的必要项之外,为了其他需求还会有其他数据项,详见“虚拟存储器管理”,如:
快表(TLB)
页表可存放在一组寄存器中,地址转换时只要从相应寄存器中取值就可得到页框号,这样做虽然能加快地址转换,但硬件代价太高;页表也可存放在主存中, 只需要一个页表基地址寄存器,如此可降低硬件开销,但按照给定的逻辑地址,进行读写操作时,至少访问主存两次:
- 一次访问页表,经过计算或变换得到物理地址;
- 另一次根据物理地址访问改地址中的指令或存取数据。
这将降低运算速度,比通常执行指令时速度慢一半。在绝大多数情况下,这种延迟是无法忍受的,还不如采用交换机制。
为了提高地址变换速度,可在地址变换机构中增设一个具有并行查询能力的特殊高速缓冲寄存器,又称为“联想寄存器
”或称为快表(TLB)
。用
以存放当前访问的那些页表项。此时的地址变换过程有两种形式:
- 查 TLB 和内存中的页表,两者同时进行;如果在 TLB 中,则内存页表查找立即停止,否则继续查内存页表;
- 先查 TLB,只有没有命中时,才开始查找内存中的页表。
详细装换过程如下:
在 CPU 给出有效地址后(未越界的逻辑地址),由地址变换机构自动地将页号 p 送入告诉缓冲寄存器(TLB),并将此页号与高速缓存中的所有页号进行比较 (这是并行比较的),若其中有与此相匹配的页号,便表示所要访问的页表项在快表中。于是,可直接从快表中读出该页所对应的物理块号,并送到物理地址 寄存器中;如果在快表中未找到对应的页表项,则还须再访问内存中的页表,找到后,把从页表项中读出的物理块号送地址寄存器;同时,再将此页表项存入快表的一个 寄存器单元中,即重新修改快表。但如果联想寄存器已满,则 OS 必须找到一个老的且已被认为不再需要或很久之后才需要的页表项,将它换出。
由于成本的关系,快表不可能做得很大,通常只存放 16~512 个页表项,这对中小型作业来说,已有可能把全部页表项放在快表中,但对于大型作业, 则只能将其一部分页表项放入其中。由于对程序和数据的访问往往带有局限性,因此,据统计,从快表中能找到所需页表项的几率可达 90% 以上。这样, 由于增加了地址变换机构而造成的速度损失,可减少到 10% 以下,达到了可接受的程度。
页表结构(或组织方式)
在原有的内存页表方案之上,引入快表(TLB)可以用来加快虚拟地址到物理地址的转换。不过这不是唯一需要解决的问题,另一个问题是怎样处理巨大的虚拟地址空间。 虚拟地址空间比较大时,页表就变得非常大,要占用相当大的内存空间,按照之前的页表连续存放方式(页表大小已经超出了一页的存储空间)来处理是不现实的。 至少可以有一下两种方法来解决这一问题:
- 采用离散方式来解决难以找到一块连续的大内存空间的问题;
- 只将当前需要的部分页表项调入内存,其余的页表项仍驻留在磁盘上,需要时再调入。
层次化分页
- 两级页表
对于要求连续的内存空间来存放页表的问题,可利用将页表进行分页,并离散地将各个页面分别存放在不同的物理块中的办法来加以解决, 同样也要为离散分配的页表再建立一张页表,称为外层页表(Outer Page Table),在每个页表项中记录了页表页面的物理块号。
两层页表结构中逻辑地址结构:
如此就可以利用外层页表和页表这两级页表,来实现从进程的逻辑地址到内存中物理地址间的变换。
两级页表地址转换:
为了地址变换实现上的方便起见,在地址变换机构中同样需要增设一个外层页表寄存器,用于存放外层页表的始址,并利用逻辑地址中的外层页号,作为外层页表的索引, 从中找到指定页表分页的始址,再利用 P2 作为指定页表分页的索引,找到指定的页表项,其中即含有该页在内存的物理块号, 用该块号和页内地址 d 即可构成访问的内存物理地址。
在采用两级页表结构的情况下,对于正在运行的进程,必须将其外层页表调入内存,而对页表则只需调入一页或几页。为了表征某页的页表是否已经调入内存, 还应在外层页表项中增设一个状态位S,其值若为 0,表示该页表分页尚未调入内存;否则,说明其分页已在内存中。进程运行时,地址变换机构根据逻辑地址中的 P1, 去查找外层页表;若所找到的页表项中的状态位为 0,则产生一中断信号,请求OS将该页表分页调入内存。
两级页表地址转换需 3 次访问主存,一次访问页目录、一次访问页表页、一次访问指令或数据。
- 多级页表
随着 64 位地址的出现和普及,二级页表仍不够用,所以,三级、四级页表也已被引入系统。多级页表实际上是对外层页表在进行分页,然后这些分页可以装入到不相邻 的物理块中。虽然级别越多,灵活性越大,但页表超过三级会带来更大的复杂性,这样做是否值得令人怀疑。
反置(向)页表(或倒排页表)
通常,每个进程都有一个相关页表,该进程所使用的每个页都在页表中有一项(或者每个虚拟页都有一项,不管后者是否有效,即使该页不在内存也要有一项)。 这种表示方式比较自然,这是因为进程是通过虚拟地址来引用页的,操作系统必须将这种引用转换成物理内存地址。由于页表是按虚拟地址排序的, 操作系统能够计算出所对应条目在页表中的位置,并可以直接使用该值。这种方法的缺点之一是:每个页表可能有很多项目,这些表可能消耗大量物理内存, 这些只不过用来跟踪物理内存是如何使用的。换句话说,如果找到跟踪的简便方法,可能减小开销。
为了解决这个问题,可以使用反置页表(IPT)
。反置页表对于每个真正的内存页或帧才有一个条目。每个条目包含保存在真正内存位置的页虚拟地址,以及拥有该页的进程
的信息。因此,整个系统只有一个页表。此表为主存中每个物理块建立一个 IPT 表项并按照块号进行排序,其表项包含:在此页框中的页面页号、页面所属进程的标识符
和哈希链指针,用来完成逻辑地址到物理地址的转换,与此相适应,逻辑地址由进程标识符、页号和页内位移 3 个部分组成。
反置页表地址转换过程:
需要访问主存地址时,地址转换机制用进程标识符与页号作为输入,由哈希函数先映射到哈希表,哈希表项存放的是指向 IPT 表项的指针,此指针要么就是指向匹配的 IPT 表项,否则,遍历哈希链直至找到进程标识符与页号均匹配的 IPT 表项,而此表项的序号就是页框号,通过拼接页内位移便可生成物理地址。若在反置页表中未能找到匹配 的 IPT 页表项,说明此页不在主存,触发缺页中断,请求操作系统通过页表调入。
为了使进程能共享主存中的同一页面,必须扩展 IPT 表的内容,使得每个表项可以记录多个进程。这样做虽然能解决问题,但却增加了复杂性。IPT 能减少页表对主存的 占用,然而 IPT 仅包含调入主存的页面,不包含未调入的页面,仍需要为进程建立传统页表,不过此页表不再放在主存中,而是存放在磁盘上。当发生缺页中断时,把 所需页面调入主存要多访问一次磁盘,速度会比较慢。
由于反向页表是按物理地址排序的,而查找是根据虚拟地址,因此可能需要查找整个表来寻求匹配,这种查找会花费很长时间。为了解决这一问题,可以使用前面提到的 哈希表来限制查找一个或少数几个条目。当然,每次访问哈希表也增加了一个对子程序的调用。
为了提高速度,可以使用 TLB。如果 TLB 能够记录所有频繁使用的页面,地址转换就可能变得像通常的页表一样快。
共享页
分页的另一个优点是可以共享共同代码。这一点对分时环境是特别重要的。当存在大量用户使用共享代码时,节约的时间和空间是可观的。
重入代码
或纯代码
是不能自我改变的代码。如果代码是可重入的,那么它在执行时绝不会改变自己。因此,两个或更多进程可以同时执行代码。每个进程都有自己
的寄存器拷贝和保存进程执行所需数据的数据存储拷贝。当然,两个不同进程的数据可以不同。
常用程序也可能共享,例如,编辑器、编译器、窗口系统、运行时库、数据库系统等。要共享,代码必须是重入的。共享代码的只读特点不能只通过正确代码来保证,而需要 操作系统来强制实现,因为,没有操作系统的帮助,后面的程序不知道共享代码或相同的部分代码的存在,而且即使知道也不能访问(因为存储保护机制, 只有操作系统才有权访问其他进程存在区)。一个系统多个进程的内存共享类似于一个任务的多线程地址空间的共享。事实上,共享内存可作为一种进程通信机制,有的操作 系统通常共享页来实现共享内存。
采用反向页表的系统在实现代码共享方面有一定困难。共享内存通常通过多个虚拟页映射到共同的物理地址来实现的(每个共享进程共用一个页)。这种标准方法在这里并不 适合,因为每个物理页只有一个虚拟页的条目,因此一个物理页不能有多个共享虚拟地址。
基本分段存储管理方式
促使存储管理方式从固定分区到动态分区,从分区方式向分页方式发展的主要原因是要提高主存空间利用率。那么,分段存储管理的引入主要是满足用户(程序员)编程和使用 上的要求,其他存储管理技术难以满足这些要求。
在分页存储管理中,经链接编址处理得到一维地址结构的可装配目标模块。这是从 0 开始编址的单一连续逻辑地址空间,虽然可以把程序划分成多个页面,但页面与源程序并不存在 逻辑关系,也就很难对源程序以模块为单位进行分配、共享和保护。事实上,程序更多是采用分段结构,高级语言往往采用模块化程序设计方法。
分段存储管理方式的引入
引入分段存储管理方式,主要是为了满足用户和程序员的下述一系列需要:
- 方便编程:
通常,用户把自己的作业按照逻辑关系划分为若干个段,每个段都是从 0 开始编址,并有自己的名字和长度。因此,希望要访问的逻辑地址是由段名(段号)和段内偏移量 (段内地址)决定的。如,函数段、类、goto 标签等
- 信息共享
在实现对程序和数据的共享时,是以信息的逻辑单位为基础的。而分页系统中的“页”只是存放信息的物理单位(块),并无完整的意义,不便于实现共享。
一个典型的例子是共享库
。虽然在页式存储管理中也能够实现共享库,但是要复杂得多,而且它们实际上是通过模拟分段来实现的。
- 信息保护
信息保护同样是对信息的逻辑单位进行保护,因此,分段管理方式能更有效和方便地实现信息保护功能。 不同的段可以使用不同的保护模式。如程序中可以对不同的变量、 函数、类等进行不同的保护措施(如不可变常量、私有、公有、只读等),这些保护机制能够帮助程序员发现程序中的错误。
- 动态增长
在实际应用中,往往有些段,特别是数据段,在使用过程中会不断地增长,而事先又无法确切地知道数据段会增长到多大。前述的其它几种存储管理方式, 都难以应付这种动态增长的情况,而分段存储管理方式却能较好地解决这一问题。
如果第 n 个段中的那个函数随后被修改并重新进行了编译,而且新版本比旧版本要长,那么并不会影响到其他的函数,其他的函数不用做任何的修改(因为大家的起始地址 都没有变)。而在一维地址中,函数被一个挨一个紧紧地放在一起,中间没有空隙,因此,如果修改了一个函数的长度,将会影响到其他函数的起始地址。这样一来,所有 相关的函数调用都要进行修改,以适应这些新的起始地址。如果一个程序包含上百个函数,那么相应的开销可能是相当大的。
- 动态链接
动态链接是指在作业运行之前,并不把几个目标程序段链接起来。要运行时,先将主程序所对应的目标程序装入内存并启动运行,当 运行过程中又需要调用某段时, 才将该段(目标程序)调入内存并进行链接。可见,动态链接也要求以段作为管理的单位。
段的特点
在每个段的内部,是一个一维的线性地址序列。从 0 开始,一直到某个最大值。一般来说,每个段的长度是不相等的,而且在执行过程中,段的长度可以动态变化。
由于每个段都是一个独立的地址空间,因此它们可以各自独立地增长或缩减,而不会相互影响。如果某个段中的栈需要更多的空间,那么它可以直接往上增长,因为在这个 地址空间中,在它的上方没有任何其他东西阻挡。当然,段也有可能会被装满,但这种情况发生的可能性非常小,因为一个段通常是很大的。
与动态分区的比较:
由于使用大小不等的段,分段类似于动态分区。在没有采用覆盖方案或使用虚拟内存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是: 在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,其和动态分区一样,它会产生外部碎片。不过由于进程被分成多个小块, 因此外部碎片也会很小。
与分页的比较:
分页对程序员来说是透明的,而分段通常是可见的,并且作为组织程序和数据的一种方便手段提供给程序员。一般情况下,程序员或编译器会把程序和数据指定到不同的段。 为了实现模块化程序设计的目的,程序或数据可能进一步分成多个段。这种方法最不方便的地方是程序员必须清楚段的最大长度限制。
分段系统的基本原理
采用大小不等的段的另一个结果是,逻辑地址和物理地址间不再具有简单的对应关系。类似于分页,在简单的分段方案中,每个进程都有一个段表,系统也会维护一个内存中 的空闲块列表。每个段表项必须给出相应的段在内存中的起始地址,还必须指明段的长度,以确保不会使用无效地址。
分段和分页的实现本质上是不同的,页面是定长的而段不是。
分段就是支持前述用户观点的内存管理方案。逻辑地址空间是由一组段组成,每个段都有名称和长度。地址指定了段名称和段内偏移。因此用户通过两个量来指定地址:段 名称和段内偏移。为了实现简单,通常用一个短号来代替段名。每个段从 0 开始编号,并采用一段连续的内存空间,各段长度不同。加载内存后, 段和段之间可以是不连续的。段的长度由相应的逻辑信息组的长度决定,因而各段长度不等。整个作业的地址空间由于是分成多个段,因而是二维的,亦即, 其逻辑地址由段号(段名)和段内地址所组成。
通常,在编译用户程序时,编译器会自动根据输入程序来构造段。
分段地址结构:
在分段存储管理中,地址结构是用户可见的,用户知道逻辑地址是如何划分为段和段内位移,在设计程序时,段的最大长度由地址结构规定,程序中所允许的最多段数会受到 限制。
分段存储原理:
分段存储管理是基于动态分区存储管理的原理实现的。动态分区以整个作业为单位来划分和连续存放,但独立作业之间不一定连续存放。而分段方法是以段为单位来划分和连续 存放,为作业各段分配一个连续的主存空间,而各段之间不一定连续。
分段的作业地址空间是二维的,与分页管理类似,可以采用动态重定位技术进行地址转换。在进行存储分配时,应为进入主存的作业建立段表, 各段在主存中的情况可由段表来记录,它指出主存中各分段的段号、段起始地址和段长度以及存取控制等信息。在撤销进程时,回收所占用的主存空间, 并清除此进程的段表。
地址转换:
段表表项实际上起到基址/限长寄存器的作用,进程运行时通过段表可将逻辑地址转换成物理地址,由于每个用户作业都有自己的段表,地址转换应按各自的段表进行。类似于 分页存储管理,也设置一个硬件—–段表基址寄存器,用来存放当前占用处理器的作业段表的起始地址和长度。在地址转换时,将段基址寄存器中的段表长度与逻辑地址中 的段号进行比较,若段号超过段表长度则触发越界中断,在利用段表项中的段长与逻辑地址中的段内位移进行比较,检查是否产生越界中断。
信息共享:
分段系统的一个突出优点,是易于实现段的共享,即允许若干个进程共享一个或多个分段,且对段的保护也十分简单易行。在分页系统中,虽然也能实现程序和数据的共享, 但远不如分段系统来得方便。
段的共享是指两个以上的作业使用同一个子程序段或数据段,该部分在内存中只包含一个副本,具体的操作是在每个进程的段表中, 用相应的表项指向共享段在内存中的起始地址。
当用户进程或作业需要共享内存中某段的程序或数据时,只要用户使用相同的名字,就可以在新的段表中填入已经存在段的内存起始地址,并设置访问权限,从而实现段的 共享。当共享此段的进程不再需要它时,应将该段释放,取消在该进程中共享段所对应的表项。
分段共享和分页共享的比较:
假设共享一个文本编辑程序,该程序比较大,需要 40 个页面。则在分页系统中共享时,每个进程都需要在其页表中添加这 40 个页表项,才能共享该文本编辑程序。而在 分段系统中,只需要在每个进程段表中添加一项即可。
碎片:
长程调度程序必须为程序的所有段寻找和分配空间,这与分页相似,只不过段是不同长度的,而页是等长的。因此,与不定长分区方案相似,内存分配是一个动态存储分配 问题,这可采用最佳适应或首次适应算法。
当所有空闲内存块因太小而不能容纳一个段时,分段就会引起外部碎片。这时,进程可能需要等待直至有更多内存为止,或通过合并来创建一大块内存。由于分段本质上 是动态重定向算法,只要需要,就可合并内存。如果 CPU 调度程序因内存分配问题而必须等待一个进程,那么这也可以(或不可以)查找 CPU 队列以便让更小,更低优先级 的进程执行。
段页式存储管理方式
分页式存储管理能有效地提高内存的利用率,分段式存储管理充分考虑程序的逻辑结构,能有效地满足用户的需要。段页式存储管理方式吸取了分页和分段存储管理两种方式 的优点,既考虑了程序的逻辑结构,又实现了不连续加载内存的目的。
基本原理
段页式系统的基本原理,是分段和分页原理的结合,即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。其地址结构由段号、段内页号以及 页内地址三部分组成。
段页式存储管理具体涉及到以下主要概念:
- 采用页式存储管理的方式,将内存划分为一些大小相等的物理块。
- 逻辑空间采用分段方式,按程序的逻辑关系把进程的地址空间分成若干逻辑段。
- 段内分页:将每个逻辑段按页式存储管理的方式分为一些大小相等的逻辑页,页大小等于内存块的大小。在每段内,从 0 开始依次编以连续的页号。
- 逻辑地址结构:见上图。
- 内存分配:内存以物理块(页框)为单位分配给每个进程。
- 段表、页表和段表地址寄存器。
为了实现从逻辑地址到物理地址的转换,系统要为每个进程或作业建立一张段表,并且还要为该作业中的每一段建立一个页表。这样,作业段表的内容是页表长度和页表地址, 为了指出运行作业的段表地址,系统有一个段表地址寄存器,它指出作业的段表长度和段表起始地址。
原理小姐:
在段页式存储管理系统中,面向物理实现的地址空间是页式划分的,而面向用户的地址空间是段式划分的,也就是说,用户程序被逻辑划分为若干段,每段又划分成若干页面, 内存划分成对应大小的块,进程映像是以页为单位进行的,从而逻辑上连续的段存入到分散的内存块中。
地址转换
在段页式系统中,为了获得一条指令或数据,须三次访问内存。第一次访问是访问内存中的段表,从中取得页表始址;第二次访问是访问内存中的页表, 从中取出该页所在的物理块号,并将该块号与页内地址一起形成指令或数据的物理地址;第三次访问才是真正从第二次访问所得的地址中,取出指令或数据。
显然,这使访问内存的次数增加了近两倍。为了提高执行速度,在地址变换机构中增设一个高速缓冲寄存器。每次访问它时,都须同时利用段号和页号去检索高速缓存, 若找到匹配的表项,便可从中得到相应页的物理块号,用来与页内地址一起形成物理地址;若未找到匹配表项,则仍须再三次访问内存。 由于它的基本原理与分页及分段的情况相似,故在此不再赘述。
管理算法
在地址转换过程中,软硬件应密切配合。
- 链接障碍中断:
这个模块的功能是实现动态链接,即给每个段一个段号,在相应的段表和现行调用表中为其设置表目,并利用段号改造链接间接字。
- 缺段中断:
这个模块的功能是在系统的现行分段表中建立一个表目,并为调进的段建立一张页表,在其段表的相应表目中登记此页表的起始地址。
- 缺页中断:
发生缺页时进行。这个模块的功能是在内存中查找空闲的存储块,如果找到,则将该页调入内存相应空闲块;如果没找到,则调用交换算法,交换内存中的页到外存,并 调进所需页面到内存,然后修改相应的页表表目。
段页式存储管理是分段技术和分页技术的结合,因而,它具备了这些技术的综合优点,便于处理变化的数据结构,段可以动态增长;便于共享和检测存取访问权限。
但是,它也有缺点,段页式存储管理增加了软件的复杂性和管理开销,也增加了硬件成本,需要更多的硬件支持;此外,各种表格要占用一定的存储空间。
虚拟存储器
前面所介绍的存储管理称为实存管理
,必须为进程分配足够的主存空间,装入其全部信息,否则进程无法运行。把进程的全部信息装入主存后,实际上并非同时使用,
有些部分只运行一遍。进程在运行时不用的,或暂时不用的,或某种条件下才用的程序和数据,全部驻留与主存是对宝贵资源的一种浪费,会降低主存利用率,显然这种做法
是不合理的。
事实上,不必装入进程的全部信息,仅将当前使用部分先装入主存,其余部分存放在磁盘中,待使用时由系统自动将其装进来,这就是虚拟存储技术的基本思路。当进程
所访问程序和数据在主存中时,可顺利执行;如果处理器所访问的程序或数据不在主存中,为了继续执行,由系统自动将这部分信息从磁盘装入,这叫做“部分装入
”;
如果此刻没有足够的空闲物理空间,便把主存中暂时不用的信息移至磁盘,这叫做“部分替换
”。如果“部分装入、部分替换
”能够实现,那么,当主存空间
小于进程的需要量时,进程也能运行;更进一步地,当多个进程的总长超出主存总容量时,也可将进程全部装入主存,实现多道程序运行。这样,不仅能充分地利用主存空间,
而且用户编程时不必考虑物理空间的实际容量,允许用户的逻辑地址空间大于主存物理地址空间,对于用户而言,好像计算机系统具有一个容量很大的主存储器,称其为
虚拟存储器
。
虚拟存储器定义:
在具有层次结构存储器的计算机系统中,自动实现部分装入和部分替换功能,能从逻辑上为用户提供一个比物理主存容量大得多的,可寻址的“主存储器”。
实际上,虚拟存储器对用户隐蔽可用物理存储器的容量和操作细节,虚拟存储器的容量与物理主存大小无关,而受限于计算机的地址结构和可用的磁盘容量(内存容量和外存容量之和)。 可见,用户直接把“虚拟存储器”当做了传统的“主存”使用。其运行速度接近于内存速度,而每位的成本却又接近于外存。
虚拟存储器的引入(局部性原理)
引入的必要性
前面已经谈过常规存储器管理方式及其特征,这里再次简单提一下“实体存储器”的特点:
- 一次性:
在前面所介绍的几种存储管理方式中,都要求将作业全部装入内存后方能运行,即作业在运行前需一次性地全部装入内存,而正是这一特征导致了:
(1) 有的作业很大,其所要求的内存空间超过了内存总容量,作业不能全部被装入内存,致使该作业无法运行。
(2) 有大量作业要求运行,但由于内存容量不足以容纳所有这些作业,只能将少数作业 装入内存让它们先运行,而将其它大量的作业留在外存上等待。
出现上述两种情况的原因,都是由于内存容量不够大。一个显而易见的解决方法,是从物理上增加内存容量,但这往往会受到机器自身的限制,而且无疑要增加系统成本, 因此这种方法是受到一定限制的。另一种方法是从逻辑上扩充内存容量,这正是虚拟存储技术所要解决的主要问题。
此外,还有许多作业在每次运行时,并非其全部程序和数据都要用到。如果一次性地装入其全部程序,也是一种对内存空间的浪费。
- 驻留性:
作业装入内存后,便一直驻留在内存中,直至作业运行结束。尽管运行中的进程会因I/O而长期等待,或有的程序模块在运行过一次后就不再需要(运行)了, 但它们都仍将继续占用宝贵的内存资源。
由此可以看出,上述的一次性及驻留性,使许多在程序运行中不用或暂不用的程序(数据)占据了大量的内存空间,使得一些需要运行的作业无法装入运行。 现在要研究的问题是:一次性及驻留性在程序运行时是否是必需的。
引入的可行性(局部性原理)
局部性原理:程序在执行时将呈现出局部性规律,即在一较短的时间内,程序的执行仅局限于某个部分;相应地,它所访问的存储空间也局限于某个区域。程序局部性原理 的几个表现:
- 程序执行时,除了少部分的转移和过程调用指令外,在大多数情况下仍是顺序执行的。在高级语言中尤为明显。
- 过程调用将会使程序的执行轨迹由一部分区域转至另一部分区域,但经研究看出,过程调用的深度在大多数情况下都不超过5。这就是说, 程序将会在一段时间内都局限在这些过程的范围内运行。
- 程序中存在许多循环结构,这些虽然只由少数指令构成,但是它们将多次执行。
- 程序中还包括许多对数据结构的处理,如对数组进行操作,它们往往都局限于很小的范围内。
局部性还表现在下述两个方面:
- 时间局限性:
如果程序中的某条指令一旦执行,则不久以后该指令可能再次执行;如果某数据被访问过,则不久以后该数据可能再次被访问。产生时间局限性的典型原因是 由于在程序中存在着大量的循环操作。
- 空间局限性:
一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,其典型情况便 是程序的顺序执行。
虚拟存储器就是基于程序局部性原理实现的。虚拟内存将用户逻辑内存与物理内存分开。这在现有物理内存有限的情况下,为程序员提供了巨大的虚拟内存。虚拟内存使 编程更加容易,因为程序员不再需要担心有限的物理内存空间或究竟哪些代码需要覆盖;他只需要关注所要解决的问题。采用虚拟内存的系统几乎用不到覆盖。
虚拟存储器的实现方法
在虚拟存储器中,允许将一个作业分多次调入内存。如果采用连续分配方式时,应将作业装入一个连续的内存区域中。为此,须事先为它一次性地申请足够的内存空间, 以便将整个作业先后分多次装入内存。这不仅会使相当一部分内存空间都处于暂时或“永久”的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量。 因此,虚拟存储器的实现,都毫无例外地建立在离散分配的存储管理方式的基础上。目前,所有的虚拟存储器都是采用下述方式之一实现的。
请求分页系统
这是在分页系统的基础上,增加了请求调页功能和页面置换功能所形成的页式虚拟存储系统。它允许只装入少数页面的程序(及数据),便启动运行。以后, 再通过调页功能及页面置换功能,陆续地把即将要运行的页面调入内存,同时把暂不运行的页面换出到外存上。置换时以页面为单位。为了能实现请求调页和置换功能, 系统必须提供必要的硬件支持和相应的软件。
所需硬件:
(1)请求分页的页表机制;
(2)缺页中断机构;
(3)地址变换机构。
请求分段系统
这是在分段系统的基础上,增加了请求调段及分段置换功能后所形成的段式虚拟存储系统。它允许只装入少数段(而非所有的段)的用户程序和数据,即可启动运行。 以后再通过调段功能和段的置换功能将暂不运行的段调出,同时调入即将运行的段。置换是以段为单位进行的。
所需硬件支持:
(1)请求分段的段表机制;
(2)缺段中断机构;
(3)地址变换机构。
目前,有不少虚拟存储器是建立在段页式系统基础上的,通过增加请求调页和页面置换功能而形成了段页式虚拟存储器系统, 而且把实现虚拟存储器所需支持的硬件集成在处理器芯片上。
可见,实现虚拟存储技术需要一定物质基础:
- 需要相当容量的辅存,以便足以存放多用户作业的地址空间;
- 要有一定容量的主存,以便作为虚拟存储器的高速缓存;
- 需要地址变换机构和所需要配套表格等。
虚拟存储器的特征
虚拟存储器具有多次性、对换性和虚拟性三大主要特征。
- 多次性:
多次性是指一个作业被分成多次调入内存运行,亦即在作业运行时没有必要将其全部装入,只需将当前要运行的那部分程序和数据装入内存即可; 以后每当要运行到尚未调入的那部分程序时,再将它调入。多次性是虚拟存储器最重要的特征,任何其它的存储管理方式都不具有这一特征。 因此,我们也可以认为虚拟存储器是具有多次性特征的存储器系统。
- 对换性:
对换性是指允许在作业的运行过程中进行换进、换出,亦即,在进程运行期间,允许将那些暂不使用的程序和数据,从内存调至外存的对换区(换出), 待以后需要时再将它们从外存调至内存(换进);甚至还允许将暂时不运行的进程调至外存,待它们重新具备运行条件时再调入内存。换进和换出能有效地提高内存利用率。 可见,虚拟存储器具有对换性特征。
与对换技术的对比:
虚拟存储器与对换技术虽说都是在主存储器和磁盘之间交换信息,但却存在很大区别。对换技术以进程为单位,当其所需主存空间大于当前系统的拥有量时,进程无法被 对换进主存工作;而虚拟存储器管理以页或段为单位,即使进程所需主存空间大于当前系统拥有的主存容量,仍然能正常运行,因为系统可将其他进程的一部分页或段 换出至磁盘。
- 虚拟性:
虚拟性是指能够从逻辑上扩充内存容量,使用户所看到的内存容量远大于实际内存容量。这是虚拟存储器所表现出来的最重要的特征,也是实现虚拟存储器的最重要的目标。
值得说明的是,虚拟性是以多次性和对换性为基础的,或者说,仅当系统允许将作业分多次调入内存,并能将内存中暂时不运行的程序和数据换至盘上时, 才有可能实现虚拟存储器;而多次性和对换性又必须建立在离散分配的基础上。
- 离散性:
离散性是指内存分配时采用离散分配方式,没有离散性就不可能实现虚拟存储器。
请求分页存储管理方式
请求分页系统是建立在基本分页基础上的,为了能支持虚拟存储器功能而增加了请求调页功能和页面置换功能。相应地,每次调入和换出的基本单位都是长度固定的页面。 这使得请求分页系统在实现上要比请求分段系统简单(后者在换进和换出时是可变长度的段)。
请求分页中的硬件支持
为了实现请求分页,系统必须提供一定的硬件支持。除了需要一台具有一定容量的内存及外存的计算机系统外,还需要有页表机制、缺页中断机构以及地址变换机构。
页表机制
在请求分页系统中所需要的主要数据结构是页表。其基本作用仍然是将用户地址空间中的逻辑地址变换为内存空间中的物理地址。由于只将应用程序的一部分调入内存,还有 一部分仍在盘上,故须在页表中再增加若干项,供程序(数据)在换进、换出时参考。在请求分页系统中的每个页表项如下所示:
- 状态位P:用于指示该页是否已调入内存,供程序访问时参考。
- 访问字段A:用于记录本页在一段时间内被访问的次数,或记录本页最近已有多长时间未被访问,供选择换出页面时参考。
- 修改位M:
该位表示该页在调入内存后是否被修改过。由于内存中的每一页都在外存上保留一份副本,因此,若未被修改,在置换该页时就不需再将该页写回到外存上,以减少 系统的开销和启动磁盘的次数;若已被修改,则必须将该页重写到外存上,以保证外存中所保留的始终是最新副本。简言之,M位供置换页面时参考。
- 外存地址:用于指出该页在外存上的地址,通常是物理块号,供调入该页时参考。
缺页中断机构
在请求分页系统中,每当所要访问的页面不在内存时,便产生一缺页中断,请求OS将所缺之页调入内存。它与一般的中断相比,有着明显的区别,主要表现在下面两个方面:
(1) 在指令执行期间产生和处理中断信号。通常,CPU都是在一条指令执行完后,才检查是否有中断请求到达。若有,便去响应,否则,继续执行下一条指令。然而, 缺页中断是在指令执行期间,发现所要访问的指令或数据不在内存时所产生和处理的。
(2) 一条指令在执行期间,可能产生多次缺页中断。在下图中示出了一个例子。如在执行一条指令COPY A TO B 时,可能要产生6次缺页中断,其中指令本身跨了两个页面, A 和 B 又分别各是一个数据块,也都跨了两个页面。基于这些特征,系统中的硬件机构应能保存多次中断时的状态, 并保证最后能返回到中断前产生缺页中断的指令处继续执行。不过,编译器在编译等处理时最好能进行字对齐以及页面长度为字的整数倍,这样可以减少指令跨 页面的情况发生,虽然浪费了一定的存储空间。
地址变换机构
请求分页系统中的地址变换机构,是在分页系统地址变换机构的基础上,再为实现虚拟存储器而增加了某些功能而形成的,如产生和处理缺页中断, 以及从内存中换出一页的功能等等。
如果在快表中未找到该页的页表项时,应到内存中去查找页表,再从找到的页表项中的状态位P,来了解该页是否已调入内存。若该页已调入内存, 这时应将此页的页表项写入快表,当快表已满时,应先调出按某种算法所确定的页的页表项,然后再写入该页的页表项;若该页尚未调入内存, 这时应产生缺页中断,请求OS从外存把该页调入内存。
以下是协助理解的一组图:
事实上,支持操作系统存储管理的已经集成为一个芯片,该芯片称为主存管理部件
(MMU),它提供地址转换和存储保护功能,并支持虚拟存储管理和多
任务管理。MMU 由一组集成电路芯片组成,逻辑地址作为输入,物理地址作为输出,直接送达总线,对主存单元进行寻址。其主要功能如下图所示:
请求分页的基本原理
请求分页虚拟存储管理是将进程信息的副本存放在辅助存储器中,当它被调度投入运行时,并不把程序和数据全部装入主存,仅装入当前使用的页面,进程执行 过程中访问到不在内存的页面时,产生缺页异常(或中断),系统从磁盘中把此指令或数据所在的页面装入,这样做能够保证用不到的页面不会被装入主存。
请求分页与分页实存管理的比较:
请求分页虚拟存储管理与分页实存管理不同,仅让进程当前使用部分装入,必然会发生某些页面不再主存中的情况。这就需要:扩充页表项的内容,增加驻留标志 位(或失效异常位),用来之处页面是否装入主存。待调入的页面再辅存的具体位置也需要再页表项中体系那出来。
为了对页面实施保护和淘汰等各种控制,可在页表中增加标志位,其他标志位包括修改位、引用位和访问权限位等,用来跟踪页面的使用情况和状态。
下图给出使用快表的请求分页地址转换过程:
请求分页的内存分配
前面讲述的是请求分页管理所需要的信息和硬件。本小节之后将会具体讲述管理策略。在躲到程序设计环境下,多个进程进入内存,就有以下问题需要处理:
- 各个进程进入内存的最开始应分配多少个页面;
- 进程运行过程中是么时候调入将要用到的页面;
- 从何处调入将要用或正缺的页面;
- 进程运行过程中调入多少将要用的页面;
- 将要用的页面调入到内存的哪个具体的区域(进程的自身空间、其他进程的空闲空间还是系统预留空间等);
- 当选好调入页面放入主存的具体位置之后,没有空闲的页框时,如何腾出(替换)页面;
- 对将被替换出的页面应该做怎么样的处理货应该在什么时候处理(就在替换的时候进行还是在替换之前的某个时候进行);
上面列出的问题实际上是根据请求分页的处理过程给出的。可见,对于不同过程有不同的处理方法。本小节及后面的章节都是围绕这些问题展开的。
在为进程分配内存时,将涉及到三个问题:第一,最小物理块数的确定;第二,物理块的分配策略;第三,物理块的分配算法。
最小物理块数的确定
这里所说的最小物理块数,是指能保证进程正常运行所需的最小物理块数。当系统为进程分配的物理块数少于此值时,进程将无法运行。 进程应获得的最少物理块数与计算机的硬件结构有关,取决于指令的格式、功能和寻址方式。对于某些简单的机器,若是单地址指令且采用直接寻址方式, 则所需的最少物理块数为2。其中,一块是用于存放指令的页面,另一块则是用于存放数据的页面。如果该机器允许间接寻址时,则至少要求有三个物理块。 对于某些功能较强的机器,其指令长度可能是两个或多于两个字节,因而其指令本身有可能跨两个页面,且源地址和目标地址所涉及的区域也都可能跨两个页面。 正如前面所介绍的在缺页中断机构中要发生 6 次中断的情况一样,对于这种机器,至少要为每个进程分配6个物理块,以装入6个页面。
物理块的分配策略
在请求分页系统中,可采取两种内存分配策略,即固定和可变分配策略。在进行置换时,也可采取两种策略,即全局置换和局部置换。 于是可组合出以下三种适用的策略。
- 固定分配局部置换
这是指基于进程的类型(交互型或批处理型等),或根据程序员、程序管理员的建议,为每个进程分配一定数目的物理块,在整个运行期间都不再改变。 采用该策略时,如果进程在运行中发现缺页,则只能从该进程在内存的 n 个页面中选出一个页换出,然后再调入一页,以保证分配给该进程的内存空间不变。 实现这种策略的困难在于:应为每个进程分配多少个物理块难以确定。若太少,会频繁地出现缺页中断,降低了系统的吞吐量;若太多, 又必然使内存中驻留的进程数目减少,进而可能造成CPU空闲或其它资源空闲的情况,而且在实现进程对换时,会花费更多的时间。
- 可变分配全局置换
这可能是最易于实现的一种物理块分配和置换策略,已用于若干个OS中。在采用这种策略时,先为系统中的每个进程分配一定数目的物理块, 而 OS自身也保持一个空闲物理块队列。当某进程发现缺页时,由系统从空闲物理块队列中取出一个物理块分配给该进程,并将欲调入的(缺)页装入其中。 这样,凡产生缺页(中断)的进程,都将获得新的物理块。仅当空闲物理块队列中的物理块用完时,OS才能从内存中选择一页调出,该页可能是系统中任一进程的页, 这样,自然又会使那个进程的物理块减少,进而使其缺页率增加。
- 可变分配局部置换
这同样是基于进程的类型或根据程序员的要求,为每个进程分配一定数目的物理块,但当某进程发现缺页时,只允许从该进程在内存的页面中选出一页换出, 这样就不会影响其它进程的运行。如果进程在运行中频繁地发生缺页中断,则系统须再为该进程分配若干附加的物理块, 直至该进程的缺页率减少到适当程度为止;反之,若一个进程在运行过程中的缺页率特别低,则此时可适当减少分配给该进程的物理块数, 但不应引起其缺页率的明显增加。
物理块分配算法
在采用固定分配策略时,如何将系统中可供分配的所有物理块分配给各个进程,可采用下述几种算法。
- 平均分配算法
这是将系统中所有可供分配的物理块平均分配给各个进程。例如,当系统中有 100 个物理块,有5个进程在运行时,每个进程可分得20个物理块。 这种方式貌似公平,但实际上是不公平的,因为它未考虑到各进程本身的大小。如有一个进程其大小为 200 页,只分配给它20个块,这样, 它必然会有很高的缺页率;而另一个进程只有10页,却有10个物理块闲置未用。
- 按比例分配算法:这是根据进程的大小按比例(在所有进程大小中的占比)分配物理块的算法。
- 考虑优先权的分配算法:
在实际应用中,为了照顾到重要的、紧迫的作业能尽快地完成,应为它分配较多的内存空间。通常采取的方法是把内存中可供分配的所有物理块分成两部分: 一部分按比例地分配给各进程;另一部分则根据各进程的优先权,适当地增加其相应份额后,分配给各进程。在有的系统中,如重要的实时控制系统, 则可能是完全按优先权来为各进程分配其物理块的。
调页策略
调入页面的时机
为了确定系统将进程运行时所缺的页面调入内存的时机,可采取预调页策略或请求调页策略,现分述如下。
- 预调页策略:
如果进程的许多页是存放在外存的一个连续区域中,则一次调入若干个相邻的页,会比一次调入一页更高效些。但如果调入的一批页面中的大多数都未被访问,
则又是低效的。可采用一种以预测为基础的预调页策略,将那些预计在不久之后便会被访问的页面预先调入内存。如果预测较准确,那么,
这种策略显然是很有吸引力的。但遗憾的是,目前预调页的成功率仅约 50%。故这种策略主要用于进程的首次调入时,由程序员指出应该先调入哪些页
。
- 请求调页策略:
当进程在运行中需要访问某部分程序和数据时,若发现其所在的页面不在内存,便立即提出请求,由 OS 将其所需页面调入内存。由请求调页策略所确定调入的页, 是一定会 被访问的,再加之请求调页策略比较易于实现,故在目前的虚拟存储器中大多采用此 策略。但这种策略每次仅调入一页,故须花费较大的系统开销, 增加了磁盘 I/O 的启动频率。
确定从何处调入页面
在请求分页系统中的外存分为两部分:用于存放文件的文件区和用于存放对换页面的对换区。通常,由于对换区是采用连续分配方式, 而文件区是采用离散分配方式,故对换区的磁盘I/O速度比文件区的高。这样,每当发生缺页请求时,系统应从何处将缺页调入内存,可分成如下三种情况:
(1) 系统拥有足够的对换区空间,这时可以全部从对换区调入所需页面,以提高调页速度。为此,在进程运行前, 便须将与该进程有关的文件从文件区拷贝到对换区。
(2) 系统缺少足够的对换区空间,这时凡是不会被修改的文件都直接从文件区调入;而当换出这些页面时,由于它们未被修改而不必再将它们换出, 以后再调入时,仍从文件区直接调入。但对于那些可能被修改的部分,在将它们换出时,便须调到对换区,以后需要时,再从对换区调入。
(3) UNIX方式。由于与进程有关的文件都放在文件区,故凡是未运行过的页面,都应从文件区调入。而对于曾经运行过但又被换出的页面,由于是被放在对换区, 因此在下次调入时,应从对换区调入。由于 UNIX 系统允许页面共享,因此,某进程所请求的页面有可能已被其它进程调入内存,此时也就无须再从对换区调入。
页面调入过程
每当程序所要访问的页面未在内存时,便向CPU发出一缺页中断,中断处理程序首先保留CPU环境,分析中断原因后转入缺页中断处理程序。该程序通过查找页表, 得到该页在外存的物理块后,如果此时内存能容纳新页,则启动磁盘I/O将所缺之页调入内存,然后修改页表。如果内存已满, 则须先按照某种置换算法从内存中选出一页准备换出;如果该页未被修改过,可不必将该页写回磁盘;但如果此页已被修改,则必须将它写回磁盘, 然后再把所缺的页调入内存,并修改页表中的相应表项,置其存在位为“1”,并将此页表项写入快表中。在缺页调入内存后,利用修改后的页表, 去形成所要访问数据的物理地址,再去访问内存数据。整个页面的调入过程对用户是透明的。
页面清除策略
页面清除策略与调入策略相对应,要考虑何时把修改过的页面写回辅助存储器,常用方法是:请页式和预约式。
- 请页式清除:
该方法仅当一夜被选中进行替换且其被修改过,才把它写回磁盘。写出一页是在读进新页之前进行的,它要完成对操作,苏安然仅需写回一页,但进程不得不等待两次 I/O 操作完成,可能会降低系统性能。
- 预约式清除:
预约式清除是对于所有更改过的页面,在需要替换之前把它们都写回磁盘,可成批进行。对于预约式清除,写出的也仍然在主存中,直到页替换算法选中此页从主存 中移出。但如果刚刚写回很多页面,在它们被替换之前,其中大部分又被修改过,那么预约式清除就毫无意义。
页面置换算法
在进程运行过程中,若其所要访问的页面不在内存而需把它们调入内存,但内存已无空闲空间时,为了保证该进程能正常运行, 系统必须从内存中调出一页程序或数据送磁盘的对换区中。但应将哪个页面调出,须根据一定的算法来确定。通常,把选择换出页面的算法称为页面置换算法 (Page-Replacement Algorithms)。置换算法的好坏,将直接影响到系统的性能。
如果选择不合适的算法,会出现这样的现象:刚被淘汰的页面立即又要调用,而调入不就随即被淘汰,淘汰不久再被调入,如此反复,使得整个系统的页面调度
非常频繁以致大部分时间都花费在来回调度页面上,而不是执行计算任务,这种现象叫做“抖动
”,一个好的调度算法应减少和避免抖动现象。
一个好的页面置换算法,应具有较低的页面更换频率。从理论上讲,应将那些以后不再会访问的页面换出,或把那些在较长时间内不会再访问的页面调出。 目前存在着许多种置换算法,它们都试图更接近于理论上的目标。置换算法可以用于全局页面也可用于局部页面,下面介绍几种常用的置换算法。
缺页中断率
某个进程的缺页中断率是指该进程缺页次数和总的访问次数的比值。影响缺页中断率的因素有:
- 主存页框数:进程所分得的快熟多,缺页中断率低,反之缺页率就高;
- 页面大小:页面大,缺页中断率低,否则缺页率就高;
- 页面替换算法:算法的优劣影响缺页中断次数;
- 程序特性:程序局部性号,它对缺页中断率有很大影响。
最佳置换算法
最佳置换算法是一种理想化的算法,它具有最好的性能,但实际上却难于实现,采用最佳置换算法,通常可保证获得最低的缺页率。 但由于人们目前还无法预知一个进程在内存的若干个页面中,哪一个页面是未来最长时间内不再被访问的,因而该算法是无法实现的, 但可以利用该算法去评价其它算法。
先进先出(FIFO)页面置换算法
这是最早出现的置换算法。该算法总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面予以淘汰。该算法实现简单, 只需把一个进程已调入内存的页面,按先后次序链接成一个队列,并设置一个指针,称为替换指针,使它总是指向最老的页面。 但该算法与进程实际运行的规律不相适应,因为在进程中,有些页面经常被访问,比如,含有全局变量、常用函数、例程等的页面, FIFO 算法并不能保证这些页面不被淘汰。
Belady 异常:
对有的页置换算法,缺页中断率可能会随着所分配的页框数的增加而增加。
最近最久未使用(LRU)置换算法
FIFO置换算法性能之所以较差,是因为它所依据的条件是各个页面调入内存的时间,而页面调入的先后并不能反映页面的使用情况。 最近最久未使用(LRU)的页面置换算法,是根据页面调入内存后的使用情况进行决策的。由于无法预测各页面将来的使用情况, 只能利用“最近的过去”作为“最近的将来”的近似,因此,LRU 置换算法是选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段, 用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最久未使用的页面予以淘汰。
算法实现:
LRU 页置换算法可能需要大量硬件支持。它的问题是为页框确定一个排序序列,这个序列按页框上次使用时间来定义。有两种可行实现:
- 计算器:
最为简单的情况是,为每个页表项关联一个使用时间域,并为 CPU 增加一个逻辑始终或计数器。对每次内存引用,计数器都会增加。每次内存引用时,始终寄存器 的内容会复制到相应页所对应的页表项的使用时间域内。当呀淘汰页面时,置换具有最小时间的页。这种方案需要搜索页表以查找 LRU 页,且每次内存访问都要 写入内存(到页表的使用时间域)。在页表改变时(因 CPU 调度)也必须要保持时间,同时也要考虑始终溢出。
- 堆栈:
可利用一个特殊的栈来保存当前使用的各个页面的页面号。每当进程访问某页面时,便将该页面的页面号从栈中移出,将它压入栈顶。因此, 栈顶始终是最新被访问页面的编号,而栈底则是最近最久未使用页面的页面号。
算法评价:
最优置换和 LRU 置换都没有 Belacy 异常。
如果只有标准 TLB 寄存器而没有其他硬件支持,那么这两种 LRU 实现都是不可能的。每次内存引用,都必须更新时钟域或堆栈。如果每次引用都采用 中断,以允许软件更新这些数据结构,那么它会使内存引用慢至少 10 倍,进而使用户进程运行慢 10 倍。几乎没有系统可以容忍如此程度的内存管理开销。
堆栈算法:
有一类算法,称为堆栈算法
,都绝不可能有 Belady 异常。堆栈算法可以证明为:对于页框数为 n 的内存页集合对于页框数为 n+1 的内存页集合
的子集。对于 LRU 算法,如果内存页集合是最近引用的页,那么对于页框的增加,这些 n 页仍然是最近引用的页,所以仍然在内存中。
LRU 近似页置换
LRU 算法是较好的一种算法,但由于它要求有较多的硬件支持,因此,很少有计算机系统能支持真正的 LRU 页置换算法。故在实际应用中, 大多采用 LRU 的近似算法。Clock 算法就是用得较多的一种LRU近似算法。
简单的 Clock 置换算法(最近未用算法 NRU)
当采用简单 Clock 算法时,只需为每页设置一位访问位,再将内存中的所有页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位被置1。 置换算法在选择一页淘汰时,只需检查页的访问位。如果是 0,就选择该页换出;若为 1,则重新将它置 0,暂不换出, 而给该页第二次驻留内存的机会,再按照 FIFO 算法检查下一个页面。当检查到队列中的最后一个页面时,若其访问位仍为 1, 则再返回到队首去检查第一个页面。
由于该算法是循环地检查各页面的使用情况,故称为 Clock 算法
。但因该算法只有一位访问位,只能用它表示该页是否已经使用过,
而置换时是将未使用过的页面换出去,故又把该算法称为最近未用算法 NRU
(Not Recently Used)。
改进型Clock置换算法
在将一个页面换出时,如果该页已被修改过,便须将该页重新写回到磁盘上;但如果该页未被修改过,则不必将它拷回磁盘。在改进型 Clock 算法中, 除须考虑页面的使用情况外,还须再增加一个因素,即置换代价,这样,选择页面换出时,既要是未使用过的页面,又要是未被修改过的页面。 把同时满足这两个条件的页面作为首选淘汰的页面。
由访问位 A 和修改位 M 可以组合成下面四种类型的页面:
- 1类(A=0,M=0):表示该页最近既未被访问,又未被修改,是最佳淘汰页。
- 2类(A=0,M=1):表示该页最近未被访问,但已被修改,并不是很好的淘汰页。
- 3类(A=1,M=0):表示该页最近已被访问,但未被修改,该页有可能再被访问。
- 4类(A=1,M=1):表示该页最近已被访问且被修改,该页可能再被访问。
在内存中的每个页必定是这四类页面之一,在进行页面置换时,可采用与简单 Clock算法相类似的算法,其差别在于该算法须同时检查访问位与修改位, 以确定该页是四类页面中的哪一种。
该算法的执行过程可分成以下三步:
(1) 从指针所指示的当前位置开始,扫描循环队列,寻找 A=0 且 M=0 的第一类页面,将所遇到的第一个页面作为所选中的淘汰页。 在第一次扫描期间不改变访问位 A。
(2) 如果第一步失败,即查找一周后未遇到第一类页面,则开始第二轮扫描,寻找 A=0 且 M=1 的第二类页面,将所遇到的第一个这类页面作为淘汰页。 在第二轮扫描期间,将所有扫描过的页面的访问位都置 0。
(3) 如果第二步也失败,亦即未找到第二类页面,则将指针返回到开始的位置,并将所有的访问位复 0。然后重复第一步,如果仍失败, 必要时再重复第二步,此时就一定能找到被淘汰的页。
该算法与简单Clock算法比较,可减少磁盘的I/O操作次数。但为了找到一个可置换的页,可能须经过几轮扫描。换言之, 实现该算法本身的开销将有所增加。
最少使用置换算法
在采用最少使用置换算法(LFU)时,应为在内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。 该置换算法选择在最近时期使用最少的页面作为淘汰页。由于存储器具有较高的访问速度(一页中有大量的指令或数据), 通常不能直接利用计数器来记录某页被访问的次数,而是采用移位寄存器方式。每次访问某页时,便将该移位寄存器的最高位置 1, 再每隔一定时间(例如100 ms)右移一次。这样,在最近一段时间使用最少的页面将是 ∑Ri(移位次数)最小的页。
LFU置换算法的页面访问图与LRU置换算法的访问图完全相同;或者说,利用这样一套硬件既可实现 LRU 算法,又可实现 LFU 算法。应该指出, LFU 算法并不能真正反映出页面的使用情况,因为在每一时间间隔内,只是用寄存器的移位来记录页的使用情况,因此,在这期间访问一次和很多次是 等效的(都只是右移一位),而且当移动到最右时候将会循环,再右移就相当于未曾访问的状态(所有位都变成了 0)。
页面缓冲算法
虽然 LRU 和 Clock置换算法都比 FIFO 算法好,但它们都需要一定的硬件支持,并需付出较多的开销,而且, 置换一个已修改的页比置换未修改页的开销要大。而页面缓冲算法(PBA)则既可改善分页系统的性能,又可采用一种较简单的置换策略。
VAX/VMS 操作系统便是使用页面缓冲算法。它采用了前述的可变分配和局部置换方式,置换算法采用的是 FIFO。
该算法规定将一个被淘汰的页放入两个链表中的一个,即如果页面未被修改,就将它直接放入空闲链表
中;否则,便放入已修改页面的链表中。
须注意的是,这时页面在内存中并不做物理上的移动,而只是将页表中的表项移到上述两个链表之一中。
空闲页面链表,实际上是一个空闲物理块链表,其中的每个物理块都是空闲的,因此,可在其中装入程序或数据。当需要读入一个页面时, 便可利用空闲物理块链表中的第一个物理块来装入该页。当有一个未被修改的页要换出时,实际上并不将它换出内存, 而是把该未被修改的页所在的物理块挂在自由页链表的末尾。类似地,在置换一个已修改的页面时,也将其所在的物理块挂在修改页面链表的末尾。 利用这种方式可使已被修改的页面和未被修改的页面都仍然保留在内存中。当该进程以后再次访问这些页面时,只要改页还在这两个链表中且没有被 重写,那么,只需花费较小的开销,使这些页面又返回到该进程的驻留集中。当被修改的页面数目达到一定值时,例如 64 个页面,再将它们一起写回到磁盘上, 从而显著地减少了磁盘I/O的操作次数。一个较简单的页面缓冲算法已在 MACH 操作系统中实现了,只是它没有区分已修改页面和未修改页面。
工作集替换算法
最开始工作集模型用来对局部最佳页面替换算法进行模拟实现,也使用滑动窗口的概念,但并不向前查看页面引用串,而是基于程序局部性原理向后看,这意味着在任何给定 的时刻,一个进程不就的将来所需主存页框数可通过考查其最近时间内的主存需要做出估计。
进程工作集指“在某一段时间间隔内进程运行所需访问的页面集合
”。工作集是程序局部性的近似表示,可通过它来确定驻留集的大小。
- ①监视每个进程的工作集,只有属于工作集的页面才能驻留在主存;
- ②定期地从进程驻留集中删去那些不在工作集中的页面;
- ③仅当一个进程的工作集在主存时,进程才能执行。
模拟工作集替换算法
工作集策略在概念上很有吸引力,但实现中监督驻留页面变化的开销却很大,估算合适的窗口大小也是一个难题,为此,已经设计出各种模拟工作集替换算法。下面介绍两种:
- 老化算法:
进程在运行前要把它的工作集预先装入主存,为每个页设置引用位 r 及年龄寄存器,寄存器初始值为 0,每隔时间 t,系统扫描主存中所有页面,先将寄存器右移一位,再把 引用位 r 的值加到对应寄存器的最左边,这样,未引用页面其年龄寄存器的值逐步减小,当达到下限或值 0 时,由于页面已经落在窗口之外,就可以把它从工作集中移出去。
修改后的算法称为“老化算法
”,年龄寄存器各位的累加值反映页面最近使用的情况,访问次数越多,累加值越大,而较早被访问的页面随着寄存器各位的右移,由于
老化使得其作用也来越小。
- 方法二:
为每个页面设置引用位及关联的时间截,通过超时中断,至少每隔若干条指令就周期性地检查引用位及时间截,当发现引用位为 1 时,就将其置 0 并把这次改变的时间作为 时间截记录下来。每当发现引用位为 0 时,通过系统当前时间减去时间截的时间,计算出从上次使用以来未被再次访问的时间量,记作 t_off 。t_off 值会随着每次超时 中断的处理而不断增加,除非页面在此期间被再次引用,导致其引用位为 1.把 t_off 与系统时间参数 t_max 相比,若 t_off > t_max,就把页面从工作集中移出,释放 相应的页框。
缺页频率替换算法
在工作集算法中,保证最少缺页次数是通过调整工作集的大小来间接实现的,一种直接改善系统性能的方法是使用缺页频率替换算法(PFF)。这种算法根据连续的缺页之间 的时间间隔来对缺页频率进行测量,每次缺页时,利用测量时间调整进程工作集尺寸。其股则是:
如果本次缺页与前次缺页之间的时间间隔超过临界值 r,那么,在这个时间间隔内未引用的所有页面都被移出工作集。这就能保证进程工作集不会不必要地扩大,与工作集 模型相比,实现效率高,只在发生缺页中断时才调整页面,而不是每次引用时都需要调整。
分页系统颠簸
频繁的页调度行为成为颠簸
。如果一个进程在换页上用的时间要多于执行时间,呢么这个进程就在颠簸。
系统颠簸的原因
系统颠簸会导致严重的性能问题。下图显示了 CPU 利用率与多道程序程度的关系。随着多道程序程度增加,CPU 利用率(虽然有点慢)增加,直至达到最大值。如果多道程序 程度还要继续增加,那么系统颠簸就开始了,且 CPU 利用率急剧下降。这时,为了增加 CPU 利用率和降低系统颠簸,必须降低躲到程序的程度。
通过局部置换算法(或优先置换算法)能限制系统颠簸的结果。但是也没有多大作用。采用局部置换,如果一个进程开始颠簸,那么它不能从其他进程取页框,且不能使后者 也颠簸,所置换的页必须是进程自己的页,然而,如果进程颠簸,那么绝大多数时间也会排队来等待调页设备。由于调页设备的更长的平均队列,页错误的平均等待时间也 会增加。因此,即使对没有颠簸的进程,其有效访问时间也会增加。
颠簸防止
为了防止颠簸,必须提供进程所需的足够多的页框,但是如何知道进程“需要”多要页框呢?可以采用很多技术。工作集策略(请参看前面的替换算法)检查进程真正需要多少
页框。这种方法定义了进程执行的局部模型
(放映程序的局部性原理)。
局部模型说明,当进程执行时,它从一个局部移向另一个局部,局部是一个经常使用页的集合。一个程序通常由多个不同局部组成,它们可能重叠。
假设为每个进程都分配了可以满足其当前局部的页框,该进程在其局部内会出现页错误,知道所有野均在内存中,接着他不在会出现页错误知道它改变局部为止。如果分配 的页框数少于现有的局部的大小,那么进程会颠簸,这是因为他不能讲所有经常使用的页放在内存中。
请求分段存储管理方式
在请求分段系统中,程序运行之前,只需先调入若干个分段(不必调入所有的分段),便可启动运行。当所访问的段不在内存中时,可请求 OS将所缺的段调入内存。 像请求分页系统一样,为实现请求分段存储管理方式,同样需要一定的硬件支持和相应的软件。
请求分段中的硬件支持
如同请求分页系统一样,应在系统中配置多种硬件机构,以快速地完成请求分段功能。请求分段管理所需的硬件支持有段表机制、缺段中断机构,以及地址变换机构。
段表机制
在请求分段式管理中所需的主要数据结构是段表。由于在应用程序的许多段中,只有一部分段装入内存,其余的一些段仍留在外存上,故须在段表中增加若干项, 以供程序在调进、调出时参考。下面给出请求分段的段表项。
在段表项中,除了段名(号)、段长、段在内存中的起始地址外,还增加了以下诸项:
- (1) 存取方式:用于标识本分段的存取属性是只执行、只读,还是允许读/写。
- (2) 访问字段A:其含义与请求分页的相应字段相同,用于记录该段被访问的频繁程度。
- (3) 修改位M:用于表示该页在进入内存后是否已被修改过,供置换页面时参考。
- (4) 存在位P:指示本段是否已调入内存,供程序访问时参考。
- (5) 增补位:这是请求分段式管理中所特有的字段,用于表示本段在运行过程中是否做过动态增长。
- (6) 外存始址:指示本段在外存中的起始地址,即起始盘块号。
缺段中断机构
在请求分段系统中,每当发现运行进程所要访问的段尚未调入内存时,便由缺段中断机构产生一缺段中断信号,进入 OS 后由缺段中断处理程序将所需的段调入内存。 缺段中断机构与缺页中断机构类似,它同样需要在一条指令的执行期间,产生和处理中断,以及在一条指令执行期间,可能产生多次缺段中断。但由于分段是信息的逻辑单位, 因而不可能出现一条指令被分割在两个分段中和一组信息被分割在两个分段中的情况。缺段中断的处理过程如图所示。由于段不是定长的, 这使对缺段中断的处理要比对缺页中断的处理复杂。
地址变换机构
请求分段系统中的地址变换机构是在分段系统地址变换机构的基础上形成的。因为被访问的段并非全在内存,所以在地址变换时,若发现所要访问的段不在内存, 必须先将所缺的段调入内存,并修改段表,然后才能再利用段表进行地址变换。为此,在地址变换机构中又增加了某些功能,如缺段中断的请求及处理等。 下图出了请求分段系统的地址变换过程。
共享段
为了实现分段共享,应配置相应的数据结构共享段表,以及需要对共享段进行操作的过程。
共享段表
为了实现分段共享,可在系统中配置一张共享段表,所有各共享段都在共享段表中占有一表项。表项中记录了共享段的段号、段长、内存始址、存在位等信息, 并记录了共享此分段的每个进程的情况。共享段表如下图所示。其中各项说明如下:
(1) 共享进程计数 count。非共享段仅为一个进程所需要。当进程不再需要该段时,可立即释放该段,并由系统回收该段所占用的空间。而共享段是为多个进程所需要的, 当某进程不再需要而释放它时,系统并不回收该段所占内存区,仅当所有共享该段的进程全都不再需要它时,才由系统回收该段所占内存区。 为了记录有多少个进程需要共享该分段,特设置了一个整型变量 count。
(2) 存取控制字段。对于一个共享段,应给不同的进程以不同的存取权限。例如,对于文件主,通常允许他读和写;而对其它进程,则可能只允许读,甚至只允许执行。
(3) 段号。对于一个共享段,不同的进程可以各用不同的段号去共享该段。
共享段的分配
由于共享段是供多个进程所共享的,因此,对共享段的内存分配方法与非共享段的内存分配方法有所不同。在为共享段分配内存时,对第一个请求使用该共享段的进程,由系 统为该共享段分配一物理区,再把共享段调入该区,同时将该区的始址填入请求进程的段表的相应项中,还须在共享段表中增加一表项,填写有关数据,把 count 置为1;之后,当 又有其它进程需要调用该共享段时,由于该共享段已被调入内存,故此时无须再为该段分配内存,而只需在调用进程的段表中增加一表项,填写该共享段的物理地址;在共享段的 段表中,填上调用进程的进程名、存取控制等,再执行 count :=count+1 操作,以表明有两个进程共享该段。
共享段的回收
当共享此段的某进程不再需要该段时,应将该段释放,包括撤消在该进程段表中共享段所对应的表项,以及执行 count :=count-1 操作。若结果为 0, 则须由系统回收该共享段的物理内存,以及取消在共享段表中该段所对应的表项,表明此时已没有进程使用该段;否则(减 1 结果不为 0), 只是取消调用者进程在共享段表中的有关记录。
分段保护
在分段系统中,由于每个分段在逻辑上是独立的,因而比较容易实现信息保护。目前,常采用以下几种措施来确保信息的安全。
越界检查
在段表寄存器中放有段表长度信息;同样,在段表中也为每个段设置有段长字段。在进行存储访问时,首先将逻辑地址空间的段号与段表长度进行比较,如果段号等于或大于 段表长度,将发出地址越界中断信号;其次,还要检查段内地址是否等于或大于段长,若大于段长,将产生地址越界中断信号, 从而保证了每个进程只能在自己的地址空间内运行。
存取控制检查
在段表的每个表项中,都设置了一个“存取控制”字段,用于规定对该段的访问方式。通常的访问方式有:
- (1) 只读,即只允许进程对该段中的程序或数据进行读访问。
- (2) 只执行,即只允许进程调用该段去执行,但不准读该段的内容,也不允许对该段执行写操作。
- (3) 读/写,即允许进程对该段进行读/写访问。
对于共享段而言,存取控制就显得尤为重要,因而对不同的进程,应赋予不同的读写权限。这时,既要保证信息的安全性,又要满足运行需要。
环保护机构
这是一种功能较完善的保护机制。在该机制中规定:低编号的环具有高优先权。OS核心处于 0 环内;某些重要的实用程序和操作系统服务占居中间环; 而一般的应用程序则被安排在外环上。在环系统中,程序的访问和调用应遵循以下规则:
- (1) 一个程序可以访问驻留在相同环或较低特权环中的数据。
- (2) 一个程序可以调用驻留在相同环或较高特权环中的服务。
请求段页式虚拟存储管理
请参考请求分页或分段虚拟存储管理。实际上就是在普通段页式存储管理的基础上加上换人换出功能。
设备管理
计算机系统的一个重要组成部分是 I/O 系统。在该系统中包括有用于实现信息输入、 输出和存储功能的设备和相应的设备控制器,在有的大、中型机中, 还有 I/O 通道或 I/O 处理机。设备管理的对象主要是 I/O 设备,还可能要涉及到设备控制器和 I/O 通道。而设备管理的基本任务是完成用户提出的 I/O 请求, 提高 I/O 速率以及提高 I/O 设备的利用率。设备管理的主要功能有: 缓冲区管理、设备分配、设备处理、虚拟设备及实现设备独立性等。由于 I/O 设备不仅种类繁多, 而且它们的特性和操作方式往往相差甚大,这就使得设备管理成为操作系统中最繁杂且与硬件最紧密相关的部分。
I/O 系统概述
不同的人对于 I/O 硬件的理解是不同的。对于电子工程师而言,I/O 硬件就是芯片、导线、电源、电机和其他组成硬件的物理部件。对程序员而言,则只注意 I/O 硬件提供 给软件的接口,如硬件能够接受的命令、它能够完成的功能以及它能够报告的错误。
I/O 设备类型
I/O 设备的类型繁多,从 OS 观点看,其重要的性能指标有: 设备使用特性、数据传输速率、数据的传输单位、设备共享属性等。因而可从不同角度对它们进行分类。
按设备的使用特性分类
- 存储设备
该类设备也称为外存或后备存储器、辅助存储器,是计算机系统用以存储信息的主要设备。该类设备存取速度较内存慢,但容量比内存大得多,相对价格也便宜。
- 输入/输出设备
又具体可分为输入设备、输出设备和交互式设备。
(1)输入设备用来接收外部信息,如键盘、鼠标、扫描仪、视频摄像、各类传感器等。
(2)输出设备是用于将计算机加工处理后的信息送向外部的设备,如打印机、绘图仪、显示器、数字视频显示设备、音响输出设备等。
(3)交互式设备则是集成上述两类设备,利用输入设备接收用户命令信息,并通过输出设备(主要是显示器)同步显示用户命令以及命令执行的结果。
按传输速率分类
按传输速度的高低,可将 I/O设备分为三类:低速设备、中速设备、高速设备。
按信息交换的单位分类
按信息交换的单位,可将 I/O 设备分成两类:
- 块设备
这类设备用于存储信息。由于信息的存取总是以数据块为单位,故而得名。它属于有结构设备。典型的块设备是磁盘。磁盘设备的基本特征是其传输速率较高; 另一特征是可寻址,即对它可随机地读/写任一块。此外,磁盘设备的 I/O 常采用 DMA 方式。
- 字符设备
用于数据的输入和输出。其基本单位是字符,故称为字符设备。它属于无结构类型。字符设备的基本特征是其传输速率较低; 另一特征是不可寻址,即输入/输出时不能指定数据的输入源地址及输出的目标地址;此外,字符设备在输入/输出时,常采用中断驱动方式。
按设备的共享属性分类
这种分类方式可将I/O设备分为如下三类:
- 独占设备
独占设备是指在一段时间内只允许一个用户(进程)访问的设备,即临界资源。因而,对多个并发进程而言,应互斥地访问这类设备。系统一旦把这类设备分配给了某 进程后, 便由该进程独占,直至用完释放。应当注意,独占设备的分配有可能引起进程死锁。
- 共享设备
共享设备是指在一段时间内允许多个进程同时访问的设备。当然,对于每一时刻而言,该类设备仍然只允许一个进程访问。显然, 共享设备必须是可寻址的和可随机访问的设备。典型的共享设备是磁盘。对共享设备不仅可获得良好的设备利用率,而且它也是实现文件系统和数据库系统的物质基础。
- 虚拟设备
虚拟设备是指通过虚拟技术将一台独占设备变换为若干台逻辑设备,供若干个用户(进程)同时使用。
设备与控制器之间的接口
通常,设备并不是直接与 CPU 进行通信,而是与设备控制器通信,因此,在 I/O设备中应含有与设备控制器间的接口,在该接口中有三种类型的信号(见下图所示), 各对应一条信号线。
- 1) 数据信号线
这类信号线用于在设备和设备控制器之间传送数据信号。对输入设备而言,由外界输入的信号经转换器转换后所形成的数据,通常先送入缓冲器中, 当数据量达到一定的比特(字符)数后,再从缓冲器通过一组数据信号线传送给设备控制器,如上图所示。对输出设备而言, 则是将从设备控制器经过数据信号线传送来的一批数据先暂存于缓冲器中,经转换器作适当转换后,再逐个字符地输出。
- 2) 控制信号线
这是作为由设备控制器向I/O设备发送控制信号时的通路。该信号规定了设备将要执行的操作,如读操作(指由设备向控制器传送数据)或写操作(从控制器接收数据), 或执行磁头移动等操作。
- 3) 状态信号线
这类信号线用于传送指示设备当前状态的信号。设备的当前状态有正在读(或写);设备已读(写)完成,并准备好新的数据传送。
设备控制器
设备控制器是计算机中的一个实体,其 主 要 职责是控制一个或多个I/O设备,以实现I/O设备和计算机之间的数据交换。它是CPU与I/O设备之间的接口, 它接收从CPU发来的命令,并去控制I/O设备工作,以使处理机从繁杂的设备控制事务中解脱出来。
设备控制器是一个可编址的设备,当它仅控制一个设备时,它只有一个唯一的设备地址;若控制器可连接多个设备时,则应含有多个设备地址, 并使每一个设备地址对应一个设备。
设备控制器的复杂性因不同设备而异,相差甚大,于是可把设备控制器分成两类:
- 一类是用于控制字符设备的控制器;
- 另一类是用于控制块设备的控制器。
在微型机和小型机中的控制器,常做成印刷电路卡形式,因而也常称为接口卡
,可将它插入计算机。有些控制器还可以处理两个、四个或八个同类设备。
设备控制器的基本功能
- 1) 接收和识别命令
- 2) 数据交换
- 3) 标识和报告设备的状态
- 4) 地址识别
就像内存中的每一个单元都有一个地址一样,系统中的每一个设备也都有一个地址,而设备控制器又必须能够识别它所控制的每个设备的地址。 为此,在控制器中应配置地址译码器。
- 5) 数据缓冲
由于 I/O 设备的速率较低而 CPU 和内存的速率却很高,故在控制器中必须设置一缓冲器。在输出时,用此缓冲器暂存由主机高速传来的数据, 然后才以 I/O 设备所具有的速率将缓冲器中的数据传送给 I/O 设备;在输入时,缓冲器则用于暂存从 I/O 设备送来的数据,待接收到一批数据后, 再将缓冲器中的数据高速地传送给主机。
- 差错控制
设备控制器还兼管对由 I/O 设备传送来的数据进行差错检测。若发现传送中出现了错误,通常是将差错检测码置位,并向CPU报告,于是CPU将本次传送来的数据作废,并重 新进行一次传送。这样便可保证数据输入的正确性。
设备控制器的组成
由于设备控制器位于CPU与设备之间,它既要与 CPU通信,又要与设备通信,还应具有按照CPU所发来的命令去控制设备工作的功能, 因此,现有的大多数控制器都是由以下三部分组成的。
- 1) 设备控制器与处理机的接口
该接口用于实现 CPU 与设备控制器之间的通信。共有三类信号线: 数据线、地址线和控制线。数据线通常与两类寄存器相连接:
- 第一类是数据寄存器(在控制器中可以有一个或多个数据寄存器,用于存放从设备送来的数据(输入)或从 CPU 送来的数据(输出));
-
第二类是控制/状态寄存器(在控制器中可以有一个或多个这类寄存器,用于存放从 CPU 送来的控制信息或设备的状态信息)。
- 2) 设备控制器与设备的接口
在一个设备控制器上,可以连接一个或多个设备。相应地,在控制器中便有一个或多个设备接口,一个接口连接一台设备。在每个接口中都存在数据、控制和状态
三种类型的信号。控制器中的I/O逻辑根据处理机发来的地址信号去选择一个设备接口。
- 3) I/O 逻辑
在设备控制器中的 I/O 逻辑用于实现对设备的控制。它通过一组控制线与处理机交互,处理机利用该逻辑向控制器发送 I/O 命令;I/O 逻辑对收到的命令进行译码。 每当 CPU 要启动一个设备时,一方面将启动命令发送给控制器;另一方面又同时通过地址线把地址发送给控制器,由控制器的 I/O 逻辑对收到的地址进行译码, 再根据所译出的命令对所选设备进行控制。
内存映射 I/O
每个控制器有几个寄存器用来与 CPU 进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据、接收数据、开启或关闭,或者执行某些其他操作。通过读取这些 寄存器,操作系统可以了解设备的状态,是否准备好接收一个新的命令等。
除了这些控制寄存器意外,许多设备还有一个操作系统可以读写的数据缓冲区。那么,CPU 如何与设备的控制寄存器和数据缓冲区进行通信?存在两个可选方法:
- 独立编址
每个控制寄存器被分配一个 I/O 端口号,所有 I/O 端口号形成 I/O 端口空间,该空间只有操作系统可以访问。在这一方案中,内存地址空间和 I/O 地址空间是不同的。 此时,CPU 访问内存和访问 IO 就需要不同的 CPU 指令去访问
- 统一编址
它将所有的控制寄存器映射到内存空间,即在内存中单独开辟一块专有区域来共 I/O 设备和 CPU 通信使用。该方法又称为内存映射 I/O
。
该方法的优势是 IO 当作内存来访问,编程简单;缺点是 IO 也需要占用一定的 CPU 地址空间,而 CPU 的地址空间是有限资源。而且,保持该缓存与设备的同步是一个
比较困难的事情。
I/O 通道
其目的是使一些原来由 CPU 处理的 I/O 任务转由通道来承担,从而把 CPU 从繁杂的 I/O 任务中解脱出来。在设置了通道后,CPU 只需向通道发送一条 I/O 指令。 通道在收到该指令后,便从内存中取出本次要执行的通道程序,然后执行该通道程序,仅当通道完成了规定的 I/O 任务后,才向 CPU 发中断信号。
实际上,I/O 通道是一种特殊的处理机,它具有执行 I/O 指令的能力,并通过执行通道(I/O)程序来控制 I/O 操作。但 I/O 通道又与一般的处理机不同, 主要表现在以下两个方面:
- 一是其指令类型单一,这是由于通道硬件比较简单,其所能执行的命令主要局限于与 I/O 操作有关的指令;
- 二是通道没有自己的内存,通道所执行的通道程序是放在主机的内存中的,换言之,是通道与 CPU 共享内存。
通道类型
通道是用于控制外围设备(包括字符设备和块设备)的。由于外围设备的类型较多,且其传输速率相差甚大,因而使通道具有多种类型。这里,根据信息交换方式的不同, 可把通道分成以下三种类型:
- 1) 字节多路通道(Byte Multiplexor Channel)
这是一种按字节交叉方式工作的通道。它通常都含有许多非分配型子通道,其数量可从几十到数百个,每一个子通道连接一台 I/O 设备,并控制该设备的 I/O 操作。 这些子通道按时间片轮转方式共享主通道。只要字节多路通道扫描每个子通道的速率足够快,而连接到子通道上的设备的速率不是太高时,便不致丢失信息。
- 2) 数组选择通道(Block Selector Channel)
字节多路通道不适于连接高速设备,这推动了按数组方式进行数据传送的数组选择通道的形成。这种通道虽然可以连接多台高速设备,但由于它只含有一个分配型子通道, 在一段时间内只能执行一道通道程序,控制一台设备进行数据传送,致使当某台设备占用了该通道后,便一直由它独占,即使是它无数据传送,通道被闲置, 也不允许其它设备使用该通道,直至该设备传送完毕释放该通道。可见,这种通道的利用率很低。
- 3) 数组多路通道(Block Multiplexor Channel)
数组选择通道虽有很高的传输速率,但它却每次只允许一个设备传输数据。数组多路通道是将数组选择通道传输速率高和字节多路通道能使各子通道(设备)分时并行操作的优 点相结合而形成的一种新通道。它含有多个非分配型子通道,因而这种通道既具有很高的数据传输速率,又能获得令人满意的通道利用率。也正因此, 才使该通道能被广泛地用于连接多台高、中速的外围设备,其数据传送是按数组方式进行的。
瓶颈问题
由于通道价格昂贵,致使机器中所设置的通道数量势必较少,这往往又使它成了 I/O 的瓶颈,进而造成整个系统吞吐量的下降。由于没引入通道之前,很多设备是可以并行 使用的,但引入通道之后,一旦通道被占用,同一个通道下的其他设备只能等待。
解决“瓶颈”问题的最有效的方法,便是增加设备到主机间的通路而不增加通道,换言之,就是把一个设备连接到多个控制器上,而一个控制器又连接到多个通道上。 多通路方式不仅解决了“瓶颈”问题,而且提高了系统的可靠性,因为个别通道或控制器的故障不会使设备和存储器之间没有通路。
I/O 数据传输控制方式
计算机是一个信息处理工具,人们将要处理的信息首先输入到内存中,然后由 CPU 执行程序对其进行处理,再将处理结果存储到内存中,最后送到输出设备,这便是一个完整 的数据传输过程。在此过程中,内存为核心,从就三级的整体流程看,数据传输指的是内存和 I/O 设备之间的数据传输,更具体地讲,是进程的数据存储区与 I/O 设备之间 的数据传输。
实际进程内存和 I/O 设备之间的数据传输时,不一定直接在内存和 I/O 设备之间进行,可以通过一些中间途径,如通过 CPU 的寄存器做中转。
着计算机技术的发展,I/O 控制方式也在不断地发展。在早期的计算机系统中,是采用程序 I/O 方式;当在系统中引入中断机制后,I/O 方式便发展为中断驱动方式;此后, 随着 DMA 控制器的出现,又使 I/O 方式在传输单位上发生了变化,即从以字节为单位的传输扩大到以数据块为单位进行转输,从而大大地改善了块设备的 I/O 性能; 而通道的引入,又使对 I/O 操作的组织和数据的传送都能独立地进行而无需 CPU 干预。应当指出,在 I/O 控制方式的整个发展过程中,始终贯穿着这样一条宗旨, 即尽量减少主机对 I/O 控制的干预,把主机从繁杂的 I/O 控制事务中解脱出来,以便更多地去完成数据处理任务。
程序直接控制方式
由程序直接控制内存与 I/O 设备之间的数据传输称为“忙等”方式或循环测试方式或轮询
,即当要在内存和 I/O 设备之间进行信息传输时,由 CPU 向相应的设备控制器发出命令,
由设备控制器控制 I/O 设备进行实际操作。在 I/O 设备工作时,CPU 执行一段循环测试程序,不断测试 I/O 设备的完成状况,以决定是否继续传输下一个数据。若设备未
完成此次数据传输,则继续测试;若设备完成了此次数据传输,则进行下一次数据传输或执行程序。程序直接控制方式进行数据传输的方式如图所示:
该方式使用 3 条指令:
- 查询指令:查询设备是否就绪;
- 读写指令: 当设备就绪时,执行数据交换;
- 转移指令: 当数据未就绪时,执行转移指令转向查询指令继续查询。
有的系统中同时有多个设备要求执行 I/O 操作,那么对每个设备都编写一段 I/O 数据处理程序,然后,轮流查询这些设备的状态位。
在程序 I/O 方式中,由于 CPU 的高速性和I/O设备的低速性,致使 CPU 的绝大部分时间都处于等待 I/O 设备完成数据 I/O 的循环测试中,造成对 CPU 的极大浪费。 在该方式中,CPU 之所以要不断地测试 I/O 设备的状态,就是因为在 CPU 中无中断机构,使 I/O 设备无法向 CPU 报告它已完成了一个字符的输入操作。
中断驱动 I/O 控制方式
在硬件层面,中断的工作如下所述。当一个 I/O 设备完成交给它的工作时,它就产生一个中断(假设操作系统已经开放中断),它是通过在分配给它的一条总线信号线上 置起信号产生中断信号。该信号被主板上的中断控制器芯片检测到,由中断控制器芯片决定做什么(详细的中断处理过程请参考“计算机组成原理”相关书籍)。
中断控制方式详细过程:
设备与中断控制器之间的连接实际上使用的是总线上的中断线而不是专用连线。当要在主机和 I/O 设备之间进行信息传输时,由 CPU 向相应的设备控制器发出命令,由设备 控制器控制 I/O 设备进行实际操作,每次的数据传输单位是设备控制器的数据缓冲寄存器的容量。I/O 设备工作时,相应进程放弃处理机,处于等待状态,由操作系统调度 其他就绪进程占用 CPU。I/O 完成工作时,由设备控制器向 CPU 发出中断信号,通知 CPU 本次 I/O 操作完成,然后由 CPU 执行一个中断处理程序,对此情况作出相应反应。 中断处理过程一般首先保护现场,然后将等待 I/O 操作完成的进程唤醒,使其进入就绪状态,最后转进程调度。
简而言之,当某进程要启动某个 I/O 设备工作时,便由 CPU 向相应的设备控制器发出一条 I/O 命令,然后立即返回继续执行原来的任务。 设备控制器于是按照该命令的要求去控制指定 I/O 设备。此时,CPU 与 I/O 设备并行操作。
中断识别:
绝大多数 CPU 有两个中断请求线。一个非屏蔽中断,主要用来处理如不可恢复内存错误等事件。另一个是可屏蔽中断,这可以由 CPU 在执行关键的不可中断的指令序列前加以 屏蔽。可屏蔽中断可以被设备控制器用来请求服务。
中断机制接受一个地址,以用来从一小集合内选择特定的中断处理程序。对绝大多数体系机构,这个地址是一个称为中断向量
的表中偏移量。该向量包含了特殊中断
处理程序的内存地址。向量中断机制的目的是用来减少单个中断处理的需要,这些中断处理搜索所有可能的中断源以决定哪个中断需要服务。事实上,计算机设备常常要比向量
内的地址多。解决这一问题的常用方法之一就是中断链接
技术,即中断向量内的每个元素都指向中断处理程序列表的头。当有中断发生时,相应链表上的所有中断处理
程序都将一一调用,直到发现可以处理请求的为止。这种结构是在大型中断向量表的大开销与分发到单个中断处理程序的低效率之间的一个折中。
中断机制也实现了中断优先级
。该中断机制使 CPU 延迟处理低优先级中断而不屏蔽所有中断,也可以让高优先级中断抢占低优先级中断处理。
中断控制方式的评价:
在 I/O 设备输入每个数据的过程中,由于无需 CPU 干预,因而可使 CPU 与 I/O 设备并行工作。仅当输完一个数据时,才需 CPU 花费极短的时间去做些中断处理。 可见,这样可使 CPU 和 I/O 设备都处于忙碌状态,从而提高了整个系统的资源利用率及吞吐量。
中断控制方式的缺点是:
由于每次的数据传输单位是设备控制器的数据缓冲寄存器容量,单位传输数据量较小,进程每次需要传输的数据被分为若干部分进行传输,中断次数较多,每次中断都要运行 一个中断处理程序,耗费 CPU 的时间很多,使 CPU 的有效计算时间减少。
DMA 控制方式
虽然中断驱动 I/O 比程序 I/O 方式更有效,但须注意,它仍是以字(节)为单位进行 I/O 的,每当完成一个字(节)的 I/O 时,控制器便要向 CPU 请求一次中断。 换言之,采用中断驱动 I/O 方式时的 CPU 是以字(节)为单位进行干预的。如果将这种方式用于块设备的 I/O,显然是极其低效的。 为了进一步减少 CPU 对 I/O 的干预而引入了直接存储器访问方式,该方式的特点是:
- (1) 数据传输的基本单位是数据块,即在 CPU 与 I/O 设备之间,每次传送至少一个数据块;
- (2) 所传送的数据是从设备直接送入内存的,或者相反;
- (3) 仅在传送一个或多个数据块的开始和结束时,才需 CPU 干预,整块数据的传送是在控制器的控制下完成的。
DMA 控制器的组成
DMA 控制器由三部分组成:主机与 DMA 控制器的接口;DMA 控制器与块设备的接口;I/O 控制逻辑。
为了实现在主机与控制器之间成块数据的直接交换,必须在DMA控制器中设置如下四类寄存器:
- (1) 命令/状态寄存器(CR)。用于接收从 CPU 发来的 I/O 命令,或有关控制信息,或设备的状态。
- (2) 内存地址寄存器(MAR)。在输入时,它存放把数据从设备传送到内存的起始目标地址;在输出时,它存放由内存到设备的内存源地址。
- (3) 数据寄存器(DR)。又称为数据缓冲寄存器或数据缓冲区,用于暂存从设备到内存,或从内存到设备的数据。
- (4) 数据计数器(DC)。存放本次 CPU 要读或写的字(节)数。
还需要:
- 设备地址寄存器:存放 I/O 信息的地址;
- 中断机制和控制逻辑:用于向 CPU 提出 I/O 中断请求,及保存 CPU 发来的 I/O 命令,管理 DMA 的传送过程。
DMA 与主存之间采用子传送,DMA 与设备之间可能是字位或字节传送,所以,DMA 中要设置数据移位寄存器、字节计数器等硬件逻辑,不仅如此还需要内部缓冲区。其原因是, DMA与主存,DMA 与设备之间的数据传输速率是不匹配的,而且传输总线可能被争用,因此缓冲区是必不可少的。
DMA 不仅设有中断机制,还增加了 DMA传输控制机制。若出现 DMA 与 CPU 同时经总线访问主存储器的情况,CPU 总是把总线占有权让给 DMA,DMA的这种占有称为周期窃用
。
窃取时间通常为一个存取周期,让设备和主存储器之间交换数据,而不再需要 CPU 干预。每次传输数据时,不必进入中断系统。从而提高了 CPU 资源利用率和数据传输效率。
通道方式
DMA 方式与程序中断方式相比,进一步减少了 CPU 对 I/O 操作的干预,但每发出一次 I/O 指令,只能读写一个数据块,用户希望一次能够读写多个离散的数据块,并把它们 传送到不同的主存区域内或相反,则需要由 CPU 发出多条启动 I/O 指令及多次 I/O 中断处理才能完成。
通道方式是 DMA 方式的发展,能够再次减少 CPU 对 I/O 操作的干预。同时,为了发挥 CPU 和设备之间的并行工作能力,也为了让种类繁多且物理特性各异的设备能够以标准 的接口连接到系统中,计算机系统引入自成体系的通道结构。
通道又称为I/O 处理器
,能完成主存储器和设备之间的信息传送,与 CPU 并行地执行操作。采用通道技术主要解决 I/O 操作的独立性和硬部件工作的并行性,实现了
设备和 CPU 并行操作、通道之间并行操作、设备之间并行操作,提高了整个系统的效率。
通道计算机的体系结构:
具有通道装置的计算机系统的主机、通道、控制器和设备之间采用四级连接,实施三级控制。一个 CPU 通常可以连接若干通道,一个通道可以连接若干控制器,一个控制器 可以连接若干台设备。CPU 通过执行 I/O 指令对通道实施控制,通道通过执行通道命令对控制器实施控制,控制器发出动作序列对设备实施控制,设备执行相应的 I/O 操作。
I/O 操作过程:
采用 I/O 通道设计后,I/O 操作的过程如下:CPU 在执行主程序时遇到 I/O 请求,启动在指定通道上选址的设备,一旦启动成功,通道开始控制设备进行操作,这是 CPU 就 可以执行其他任务并与通道并行工作,知道 I/O 操作完成;当通道发出 I/O 操作结束中断时,处理器才响应并停止当前工作,转而处理 I/O 操作结束事件。
通道程序:
通道是通过执行通道程序,并与设备控制器共同实现对I/O设备的控制的。通道程序是由一系列通道指令(或称为通道命令)所构成的。通道指令与一般的机器指令不同, 在它的每条指令中都包含下列诸信息:
- (1) 操作码。操作码规定了指令所执行的操作,如读、写、控制等操作。
- (2) 内存地址。内存地址标明字符送入内存(读操作)和从内存取出(写操作)时的内存首址。
- (3) 计数。该信息表示本条指令所要读(或写)数据的字节数。
- (4) 通道程序结束位 P。该位用于表示通道程序是否结束。P=1 表示本条指令是通道程序的最后一条指令。
- (5) 记录结束标志 R。R=0 表示本通道指令与下一条指令所处理的数据是同属于一个记录;R=1 表示这是处理某记录的最后一条指令。
缓冲管理
为了缓和 CPU 与 I/O 设备速度不匹配的矛盾,提高 CPU 和 I/O 设备的并行性,在现代操作系统中,几乎所有的 I/O 设备在与处理机交换数据时都用了缓冲区。 缓冲管理的主要职责是组织好这些缓冲区,并提供获得和释放缓冲区的手段。
缓冲引入的原因
在设备管理中,引入缓冲区的主要原因可归结为以下几点:
- 减少读块设备的次数:
当进程在块设备上进行读操作(输入)时,通过系统调用,由操作系统将数据读入缓冲器,然后送入相应进程的数据存储区,由进程处理。缓冲器内保存的信息可能会被再次 读到,因此,当再次对此块设备今次那个读操作时,可以先查看一下所读信息是否在缓冲器内,若在缓冲器内,则可以直接从缓冲器内读取,而不必从设备读取。这种场合 要求对缓冲器的读取速度快于块设备的读取速度,则当命中率(所读数据在缓冲器内的次数与读操作的总次数的比值)较高时,会明显提高读操作的速度。CPU 和内存之间 的告诉缓存的作用就是这样的。
- 缓和 CPU 与 I/O 设备间速度不匹配的矛盾:
事实上,凡在数据到达速率与其离去速率不同的地方,都可设置缓冲区,以缓和它们之间速率不匹配的矛盾。
- 减少对CPU的中断频率,放宽对CPU中断响应时间的限制;
- 作为无法直接通信的设备间的中转站;
- 解决程序锁清秋的逻辑记录大小和设设备的物理记录大小不匹配的问题;
- 加快进程的推进速度,提高CPU和I/O设备之间的并行性。
单缓冲
在单缓冲情况下,每当用户进程发出一 I/O 请求时,操作系统便在主存中为之分配一缓冲区。
单缓冲方式由于只有一个缓冲区,这一缓冲区在某个时候能存放输入数据或输出数据,但不能既是输入数据又是输出数据,否则缓冲区中的数据会引起混乱。因此,单缓冲方式 不能使外部设备并行工作。
双缓冲
为了加快输入和输出速度,提高设备利用率,人们又引入了双缓冲区机制,也称为缓冲对换(Buffer Swapping)。为每台 I/O 设备分配两个缓冲区。
双缓冲方式可以实现外部设备并行工作。先将数据输入到缓冲区 buffer1,然后进程从 buffer1 中取出数据进行计算,将输出的数据送往 buffer2,然后将 buffer2 中的数据 送往输出设备,与此同时,从输入设备将数据输入到 buffer1,如此继续进行,使得输入设备和输出设备并行工作。
循环缓冲
当输入与输出或生产者与消费者的速度基本相匹配时,采用双缓冲能获得较好的效果,可使生产者和消费者基本上能并行操作。但若两者的速度相差甚远, 双缓冲的效果则不够理想,不过可以随着缓冲区数量的增加,使情况有所改善。因此,又引入了多缓冲机制。可将多个缓冲组织成循环缓冲形式。对于用作输入的循环缓冲, 通常是提供给输入进程或计算进程使用,输入进程不断向空缓冲区输入数据,而计算进程则从中提取数据进行计算。
循环缓冲技术是在内存中分配大小相等的存储区作为缓冲区,并将这些缓冲区连接起来,每个缓冲区中有一个指向下一个缓冲区的指针,最后一个缓冲区的指针指向第一个 缓冲区。
使用虚幻缓冲结构需要两个指针,IN 指针指示可输入数据的第一个空缓冲区,OUT 指针指示可提取数据的第一个满缓冲区。系统初启时,这两个指针被初始化为 IN = OUT, 在系统工作过程中,两个指针向同一个方向移动。输入时,数据输入到 IN 指针指示的缓冲区,输入完毕,IN 指针后移指向下一个可用的空缓冲区;当进程从循环缓冲结构 提取数据时,提取 OUT 指针指示的缓冲区中的内容,提取完毕,OUT 指针后移指向下一个满缓冲区。指针移动时检测空/满缓冲区的数量,当空缓冲区数量为 0 时,IN 指针 不能再后移动,此时 IN = OUT;满缓冲区数量为 0 时,OUT 指针不能再后移,此时 OUT = IN。所以必须注意进程同步问题。
缓冲池
上述的缓冲区仅适用于某特定的I/O进程和计算进程,因而它们属于专用缓冲。当系统较大时,将会有许多这样的循环缓冲,这不仅要消耗大量的内存空间, 而且其利用率不高。为了提高缓冲区的利用率,目前广泛流行公用缓冲池(Buffer Pool),在池中设置了多个可供若干个进程共享的缓冲区。
缓冲池组成
对于既可用于输入又可用于输出的公用缓冲池,其中至少应含有以下三种类型的缓 冲区:
- 空(闲)缓冲区;
- 装满输入数据的缓冲区;
- 装满输出数据的缓冲区。
为了管理上的方便,可将相同类型的缓冲区链成一个队列,于是可形成以下三个队列(队列的相关知识请参考“数据结构”):
- 空缓冲队列;
- 输入队列;
- 输出队列。
除了上述三个队列外,还应具有以下四种工作缓冲区:
- 用于收容输入数据的工作缓冲区;
- 用于提取输入数据的工作缓冲区;
- 用于收容输出数据的工作缓冲区;
- 用于提取输出数据的工作缓冲区。
缓冲池中的队列本身是临界资源,多个进程在访问一个队列时,既应互斥,又须同步(详见“临界支援”和“进程同步”)。
缓冲池工作方式
缓冲区可以工作在收容输入、提取输入、收容输出和提取输出四种工作方式下,如下图所示:
I/O 软件
I/O 软件的总体设计目标是高效率和通用性。前者是要确保 I/O 设备与 CPU 的并发性,以提高资源的利用率;后者则是指尽可能地提供简单抽象、清晰而统一的接口, 采用统一标准的方法,来管理所有的设备以及所需的I/O操作。为了达到这一目标,通常将 I/O 软件组织成一种层次结构,
- 低层软件用于实现与硬件相关的操作,并可屏蔽硬件的具体细节;
- 高层软件则主要向用户提供一个简洁、友好和规范的接口。
- 每一层具有一个要执行的定义明确的功能和一个与邻近层次定义明确的接口,各层的功能与接口随系统的不同而异。
I/O软件的设计目标和原则
计算机系统中包含了众多的 I/O 设备,其种类繁多,硬件构造复杂,物理特性各异,速度慢,与 CPU 速度不匹配,并涉及到大量专用 CPU 及数字逻辑运算等细节,如寄存器、 中断、控制字符和设备字符集等,造成对设备的操作和管理非常复杂和琐碎。因此,
-
从系统的观点出发,采用多种技术和措施,解决由于外部设备与 CPU 速度不匹配所引起的问题,提高主机和外设的并行工作能力,提高系统效率, 成为操作系统的一个重要目标。
-
另一方面,对设备的操作和管理的复杂性,也给用户的使用带来了极大的困难。用户必须掌握 I/O 系统的原理,对接口和控制器及设备的物理特性要有深入了解, 这就使计算机的推广应用受到很大限制。所以,设法消除或屏蔽设备硬件内部的低级处理过程,为用户提供一个简便、易用、抽象的逻辑设备接口,保证用户安全、 方便地使用各类设备,也是 I/O 软件设计的一个重要原则。
具体而言,I/O软件应达到下面的几个目标:
- 1) 与具体设备无关
对于I/O系统中许多种类不同的设备,作为程序员,只需要知道如何使用这些资源来完成所需要的操作,而无需了解设备的有关具体实现细节。
为了提高OS的可移植性和易适应性,I/O软件应负责屏蔽设备的具体细节,向高层软件提供抽象的逻辑设备,并完成逻辑设备与具体物理设备的映射。
对于操作系统本身而言,应允许在不需要将整个操作系统进行重新编译的情况下,增添新的设备驱动程序,以方便新的 I/O 设备的安装。
- 2) 统一命名
要实现上述的设备无关性,其中一项重要的工作就是如何给I/O设备命名。不同的操作系统有不同的命名规则,一般而言,是在系统中对各类设备采取预先设计的、 统一的逻辑名称进行命名,所有软件都以逻辑名称访问设备。这种统一命名与具体设备无关,换言之,同一个逻辑设备的名称,在不同的情况下可能对应于不同的物理设备。
- 3) 对错误的处理
一般而言,错误多数是与设备紧密相关的,因此对于错误的处理,应该尽可能在接近硬件的层面处理,在低层软件能够解决的错误就不让高层软件感知, 只有低层软件解决不了的错误才通知高层软件解决。许多情况下,错误恢复可以在低层得到解决,而高层软件不需要知道。
- 4) 缓冲技术
由于CPU与设备之间的速度差异,无论是块设备还是字符设备,都需要使用缓冲技术。对于不同类型的设备,其 缓冲区(块)的大小是不一样的, 块设备的缓冲是以数据块为单位的,而字符设备的缓冲则以字节为单位。就是同类型的设备,其缓冲区(块)的大小也是存在差异的, 因此,I/O软件应能屏蔽这种差异, 向高层软件提供统一大小的数据块或字符单元,使得高层软件能够只与逻辑块大小一致的抽象设备进行交互。
- 5) 设备的分配和释放
- 6) I/O控制方式
针对具有不同传输速率的设备,综合系统效率和系统代价等因素,合理选择I/O控制方式,如像打印机等低速设备应采用中断驱动方式, 而对磁盘等高速设备则采用DMA控制方式等,以提高系统的利用率。为方便用户,I/O软件也应屏蔽这种差异,向高层软件提供统一的操作接口。
I/O 分层结构:
目前在I/O软件中已普遍采用了层次式结构,将系统中的设备操作和管理软件分为若干个层次,每一层都利用其下层提供的服务,完成输入、输出功能中的某些子功能, 并屏蔽这些功能实现的细节,向高层提供服务。
在层次式结构的 I/O 软件中,只要层次间的接口不变,对每个层次中的软件进行的修改都不会引起其下层或高层代码的变更,仅最低层才会涉及到硬件的具体特性。 通常把 I/O 软件组织成四个层次,如下图所示(图中的箭头表示 I/O 的控制流)。各层次及其功能如下所述:
(1) 用户层软件:实现与用户交互的接口,用户可直接调用在用户层提供的、与 I/O 操作有关的库函数,对设备进行操作。
(2) 设备独立性软件:负责实现与设备驱动器的统一接口、设备命名、设备的保护以及设备的分配与释放等,同时为设备管理和数据传送提供必要的存储空间。
(3) 设备驱动程序:与硬件直接相关,负责具体实现系统对设备发出的操作指令,驱动I/O设备工作的驱动程序。
(4) 中断处理程序:用于保存被中断进程的 CPU 环境,转入相应的中断处理程序进行处理,处理完后再恢复被中断进程的现场后返回到被中断进程。
实际上,在不同的操作系统中,这种层次的划分并不是固定的,主要是随系统具体情况的不同,而在层次的划分以及各层的功能和接口上存在一定的差异。 下面我们将从低到高地对每个层次进行讨论。
中断处理程序
中断处理层的主要工作有:进行进程上下文的切换,对处理中断信号源进行测试,读取设备状态和修改进程状态等。由于中断处理与硬件紧密相关,对用户及用户程序而言, 应该尽量加以屏蔽,故应该放在操作系统的底层进行中断处理,系统的其余部分尽可能少地与之发生联系。
当一个进程请求I/O 操作时,该进程将被挂起,直到 I/O设备完成I/O操作后,设备控制器便向CPU发送一中断请求,CPU响应后便转向中断处理程序, 中断处理程序执行相应的处理,处理完后解除相应进程的阻塞状态。
对于为每一类设备设置一个I/O进程的设备处理方式,其中断处理程序的处理过程分成以下几个步骤:
- 唤醒被阻塞的驱动(程序)进程
当中断处理程序开始执行时,首先去唤醒处于阻塞状态的驱动(程序)进程。
- 保护被中断进程的CPU环境
通常由硬件自动将处理机状态字PSW和程序计数器(PC)中的内容,保存在中断保留区(栈)中,然后把被中断进程的CPU现场信息(即包括所有的CPU寄存器,如通用寄存器、 段寄存器等内容)都压入中断栈中,因为在中断处理时可能会用到这些寄存器。
- 转入相应的设备处理程序
由处理机对各个中断源进行测试,以确定引起本次中断的I/O设备,并发送一应答信号给发出中断请求的进程,使之消除该中断请求信号, 然后将相应的设备中断处理程序的入口地址装入到程序计数器中,使处理机转向中断处理程序。
- 中断处理
对于不同的设备,有不同的中断处理程序。该程序首先从设备控制器中读出设备状态,以判别本次中断是正常完成中断,还是异常结束中断。 若是前者,中断程序便进行结束处理;若还有命令,可再向控制器发送新的命令,进行新一轮的数据传送。 若是异常结束中断,则根据发生异常的原因做相应的处理。
- 恢复被中断进程的现场
当中断处理完成以后,便可将保存在中断栈中的被中断进程的现场信息取出,并装入到相应的寄存器中,其中包括该程序下一次要执行的指令的地址 N+1、 处理机状态字 PSW,以及各通用寄存器和段寄存器的内容。这样,当处理机再执行本程序时,便从 N+1 处开始,最终返回到被中断的程序。
I/O 操作完成后,驱动程序必须检查本次 I/O 操作中是否发生了错误,并向上层软件报告,最终向调用者报告本次 I/O 的执行情况。除了上述的第 4 步外, 其它各步骤对所有 I/O 设备都是相同的,因而对于某种操作系统,例如 UNIX 系统,是把这些共同的部分集中 起来,形成中断总控程序。每当要进行中断处理时, 都要首先进入中断总控程序。而对于 第 4 步,则对不同设备须采用不同的设备中断处理程序继续执行。
设备驱动
设备驱动程序通常又称为设备处理程序,它是 I/O 进程与设备控制器之间的通信程序,又由于它常以进程的形式存在,故以后就简称之为设备驱动进程。 其主要任务是接收上层软件发来的抽象I/O要求,如 read 或 write 命令,在把它转换为具体要求后,发送给设备控制器,启动设备去执行;此外, 它也将由设备控制器发来的信号传送给上层软件。由于驱动程序与硬件密切相关,故应为每一类设备配置一种驱动程序;有时也可为非常类似的两类设备配置一个驱动程序。
例如,打印机和显示器需要不同的驱动程序,但 SCSI 磁盘驱动程序通常可以处理不同大小和不同速度的多个 SCSI 磁盘,甚至还可以处理 SCSI CD-ROM。
设备驱动程序的功能
为了实现I/O进程与设备控制器之间的通信,设备驱动程序应具有以下功能:
(1) 接收由设备独立性软件发来的命令和参数,并将命令中的抽象要求转换为具体要求,例如,将磁盘块号转换为磁盘的盘面、磁道号及扇区号。
(2) 检查用户I/O请求的合法性,了解I/O设备的状态,传递有关参数,设置设备的工作方式。
(3) 发出 I/O 命令。如果设备空闲,便立即启动 I/O设备去完成指定的 I/O 操作;如果设备处于忙碌状态,则将请求者的请求块挂在设备队列上等待。
(4) 及时响应由控制器或通道发来的中断请求,并根据其中断类型调用相应的中断处理程序进行处理。
(5) 对于设置有通道的计算机系统,驱动程序还应能够根据用户的I/O请求,自动地构成通道程序。
设备处理方式
在不同的操作系统中所采用的设备处理方式并不完全相同。根据在设备处理时是否设置进程,以及设置什么样的进程而把设备处理方式分成以下三类:
(1) 为每一类设备设置一个进程,专门用于执行这类设备的I/O操作。比如,为所有的交互式终端设置一个交互式终端进程;又如,为同一类型的打印机设置一个打印进程。
(2) 在整个系统中设置一个 I/O进程,专门用于执行系统中所有各类设备的 I/O操作。也可以设置一个输入进程和一个输出进程, 分别处理系统中所有各类设备的输入或输出操作。
(3) 不设置专门的设备处理进程,而只为各类设备设置相应的设备处理程序(模块),供用户进程或系统进程调用。
设备驱动程序的特点
设备驱动程序属于低级的系统例程,它与一般的应用程序及系统程序之间有下述明显差异:
(1) 驱动程序主要是指在请求 I/O 的进程与设备控制器之间的一个通信和转换程序。它将进程的 I/O 请求经过转换后,传送给控制器; 又把控制器中所记录的设备状态和 I/O 操作完成情况及时地反映给请求 I/O 的进程。
(2) 驱动程序与设备控制器和 I/O 设备的硬件特性紧密相关,因而对不同类型的设备应配置不同的驱动程序。例如,可以为相同的多个终端设置一个终端驱动程序, 但有时即使是同一类型的设备,由于其生产厂家不同,它们也可能并不完全兼容,此时也须为它们配置不同的驱动程序。
(3) 驱动程序与 I/O 设备所采用的 I/O 控制方式紧密相关。常用的 I/O 控制方式是中断驱动和 DMA 方式,这两种方式的驱动程序明显不同, 因为后者应按数组方式启动设备及进行中断处理。
(4) 由于驱动程序与硬件紧密相关,因而其中的一部分必须用汇编语言书写。目前有很 多驱动程序的基本部分,已经固化在 ROM 中。
(5) 驱动程序应允许可重入。一个正在运行的驱动程序常会在一次调用完成前被再次调用。例如,网络驱动程序正在处理一个到来的数据包时,另一个数据包可能到达。
(6) 驱动程序不允许系统调用。但是为了满足其与内核其它部分的交互,可以允许对某些内核过程的调用,如通过调用内核过程来分配和释放内存页面作为缓冲区, 以及调用其它过程来管理 MMU 定时器、DMA 控制器、中断控制器等。
设备驱动程序的处理过程
不同类型的设备应有不同的设备驱动程序,但大体上它们都可以分成两部分,其中,除了要有能够驱动 I/O 设备工作的驱动程序外,还需要有设备中断处理程序, 以处理 I/O 完成后的工作。
设备驱动程序的主要任务是启动指定设备。但在启动之前,还必须完成必要的准备工作,如检测设备状态是否为“忙”等。在完成所有的准备工作后, 才最后向设备控制器发送一条启动命令。
以下是设备驱动程序的处理过程:
- 1) 将抽象要求转换为具体要求
通常在每个设备控制器中都含有若干个寄存器,分别用于暂存命令、数据和参数等。由于用户及上层软件对设备控制器的具体情况毫无了解, 因而只能向它发出抽象的要求(命令),但这些命令无法传送给设备控制器。因此,就需要将这些抽象要求转换为具体要求。
例如,将抽象要求中的盘块号转换为磁盘的盘面、 磁道号及扇区。这一转换工作只能由驱动程序来完成, 因为在 OS中只有驱动程序才同时了解抽象要求和设备控制器中的寄存器情况;也只有它才知道命令、 数据和参数应分别送往哪个寄存器。
- 2) 检查I/O请求的合法性
对于任何输入设备,都是只能完成一组特定的功能,若该设备不支持这次的 I/O 请求或没有权限,则认为这次 I/O 请求非法。
例如,用户试图请求从打印机输入数据,显然系统应予以拒绝。此外,还有些设备如磁盘和终端,它们虽然都是既可读又可写的,但若在打开这些设备时规定的是读, 则用户的写请求必然被拒绝。
- 3) 读出和检查设备的状态
在启动某个设备进行I/O操作时,其前提条件应是该设备正处于空闲状态。因此在启动设备之前,要从设备控制器的状态寄存器中,读出设备的状态。
例如,为了向某设备写入数据,此前应先检查该设备是否处于接收就绪状态,仅当它处于接收就绪状态时,才能启动其设备控制器,否则只能等待。
- 4) 传送必要的参数
对于许多设备,特别是块设备,除必须向其控制器发出启动命令外,还需传送必要的参数。例如在启动磁盘进行读/写之前, 应先将本次要传送的字节数和数据应到达的主存始址,送入控制器的相应寄存器中。
- 5) 工作方式的设置
有些设备可具有多种工作方式,典型情况是利用RS-232接口进行异步通信。在启动该接口之前,应先按通信规程设定参数:波特率、奇偶校验方式、 停止位数目及数据字节长度等。
- 6) 启动I/O设备
在完成上述各项准备工作之后,驱动程序可以向控制器中的命令寄存器传送相应的控制命令。对于字符设备,若发出的是写命令,驱动程序将把一个数据传送给控制器; 若发出的是读命令,则驱动程序等待接收数据,并通过从控制器中的状态寄存器读入状态字的方法,来确定数据是否到达。
驱动程序发出 I/O 命令后,基本的I/O操作是在设备控制器的控制下进行的。通常,I/O 操作所要完成的工作较多,需要一定的时间,如读/写一个盘块中的数据, 此时驱动(程序)进程把自己阻塞起来,直到中断到来时才将它唤醒。
设备独立性软件
为了提高 OS 的可适应性和可扩展性,在现代 OS 中都毫无例外地实现了设备独立性(Device Independence),也称为设备无关性
。
设备独立性的含义:应用程序独立于具体使用的物理设备。为了实现设备独立性而引入了逻辑设备和物理设备这两个概念。在应用程序中, 使用逻辑设备名称来请求使用某类设备;而系统在实际执行时,还必须使用物理设备名称。
因此,系统须具有将逻辑设备名称转换为某物理设备名称的功能,这非常类似于存储器管理中所介绍的逻辑地址和物理地址的概念。在应用程序中所使用的是逻辑地址, 而系统在分配和使用内存时,必须使用物理地址。在实现了设备独立性的功能后,可带来以下两方面的好处:
- 1) 设备分配时的灵活性
当应用程序(进程)以物理设备名称来请求使用指定的某台设备时,如果该设备已经分配给其他进程或正在检修,而此时尽管还有几台其它的相同设备正在空闲, 该进程却仍阻塞。但若进程能以逻辑设备名称来请求某类设备时,系统可立即将该类设备中的任一台分配给进程,仅当所有此类设备已全部分配完毕时,进程才会阻塞。
- 2) 易于实现 I/O 重定向
所谓 I/O 重定向,是指用于 I/O 操作的设备可以更换(即重定向),而不必改变应用程序。例如,我们在调试一个应用程序时,可将程序的所有输出送往屏幕显示; 而在程序调试完后,如需正式将程序的运行结果打印出来,此时便须将 I/O 重定向的数据结构——逻辑设备表中的显示终端改为打印机, 而不必修改应用程序。I/O 重定向功能具有很大的实用价值,现已被广泛地引入到各类OS中。
设备独立性软件
驱动程序是一个与硬件(或设备)紧密相关的软件。为了实现设备独立性,必须再在驱动程序之上设置一层软件,称为设备独立性软件。 至于设备独立性软件和设备驱动程序之间的界限,根据不同的操作系统和设备有所差异,主要取决于操作系统、设备独立性和设备驱动程序的运行效率等多方面因素的权衡, 因为对于一些本应由设备独立性软件实现的功能,可能由于效率等诸多因素,实际上设计在设备驱动程序中。总的来说,设备独立性软件的主要功能可分为以下两个方面:
- (1) 执行所有设备的公有操作。这些公有操作包括:
① 对独立设备的分配与回收;
② 将逻辑设备名映射为物理设备名,进一步可以找到相应物理设备的驱动程序;
③ 对设备进行保护,禁止用户直接访问设备;
④ 缓冲管理,即对字符设备和块设备的缓冲区进行有效的管理,以提高I/O的效率;
⑤ 差错控制,由于在I/O操作中的绝大多数错误都与设备无关,故主要由设备驱动程序处理,而设备独立性软件只处理那些设备驱动程序无法处理的错误;
⑥ 提供独立于设备的逻辑块,不同类型的设备信息交换单位是不同的,读取和传输速率也各不相同,如字符型设备以单个字符为单位,块设备是以一个数据块为单位, 即使同一类型的设备,其信息交换单位大小也是有差异的,如不同磁盘由于扇区大小的不同,可能造成数据块大小的不一致,因此设备独立性软件应负责隐藏这些差异, 对逻辑设备使用并向高层软件提供大小统一的逻辑数据块。
- (2) 向用户层(或文件层)软件提供统一接口。无论何种设备,它们向用户所提供的接口应该是相同的。 例如,对各种设备的读操作,在应用程序中都使用 read;而对各种设备的写操作,也都使用 write。
逻辑设备名到物理设备名映射的实现
- 1) 逻辑设备表
为了实现设备的独立性,系统必须设置一张逻辑设备表(LUT,Logical Unit Table),用于将应用程序中所使用的逻辑设备名映射为物理设备名。 在该表的每个表目中包含了三项:逻辑设备名、物理设备名和设备驱动程序的入口地址,如下图所示。当进程用逻辑设备名请求分配 I/O 设备时, 系统为它分配相应的物理设备,并在 LUT 上建立一个表目,填上应用程序中使用的逻辑设备名和系统分配的物理设备名,以及该设备驱动程序的入口地址。 当以后进程再利用该逻辑设备名请求 I/O 操作时,系统通过查找 LUT,便可找到物理设备和驱动程序。
- 2) LUT的设置问题
LUT的设置可采取两种方式:
第一种方式是在整个系统中只设置一张 LUT。由于系统中所有进程的设备分配情况都记录在同一张 LUT 中,因而不允许在 LUT 中具有相同的逻辑设备名, 这就要求所有用户都不使用相同的逻辑设备名。在多用户环境下这通常是难以做到的,因而这种方式主要用于单用户系统中。
第二种方式是为每个用户设置一张 LUT。每当用户登录时,便为该用户建立一个进程,同时也为之建立一张 LUT,并将该表放入进程的 PCB 中。由于通常在多用户系统中, 都配置了系统设备表,故此时的逻辑设备表可以采用上图(b)中的格式。
用户层的 I/O 软件
一般而言,大部分的 I/O 软件都在操作系统内部,但仍有一小部分在用户层,包括与用户程序链接在一起的库函数,以及完全运行于内核之外的一些程序。
用户层软件必须通过一组系统调用来取得操作系统服务。在现代的高级语言以及 C 语言中,通常提供了与各系统调用一一对应的库函数, 用户程序通过调用对应的库函数使用系统调用。这些库函数与调用程序连接在一起,包含在运行时装入在内存的二进制程序中,如 C 语言中的库函数 write 等, 显然这些库函数的集合也是 I/O 系统的组成部分。但在许多现代操作系统中,系统调用本身已经采用 C 语言编写,并以函数形式提供, 所以在使用 C 语言编写的用户程序中,可以直接使用这些系统调用。
另外,在操作系统中还有一些程序,如下面章节我们将要论述的 Spooling 系统以及在网络传输文件时常使用的守护进程等,就是完全运行在内核之外的程序, 但它们仍归属于 I/O 系统。
设备分配
在多道程序环境下,系统中的设备供所有进程共享。为防止诸进程对系统资源的无序竞争,特规定系统设备不允许用户自行使用,必须由系统统一分配。 每当进程向系统提出 I/O 请求时,只要是可能和安全的,设备分配程序便按照一定的策略,把设备分配给请求用户(进程)。在有的系统中, 为了确保在 CPU 与设备之间能进行通信,还应分配相应的控制器和通道。为了实现设备分配,必须在系统中设置相应的数据结构。
设备分配中的数据结构
在进行设备分配时,通常都需要借助于一些表格的帮助。在表格中记录了相应设备或控制器的状态及对设备或控制器进行控制所需的信息。 在进行设备分配时所需的数据结构(表格)有:设备控制表、控制器控制表、通道控制表和系统设备表等。
设备控制表(DCT)
系统为每一个设备都配置了一张设备控制表,用于记录本设备的情况,如下图:
设备控制表中,除了有用于指示设备类型的字段 type 和设备标识字段 deviceid 外,还应含有下列字段:
(1) 设备队列队首指针。凡因请求本设备而未得到满足的进程,其 PCB 都应按照一定的策略排成一个队列,称该队列为设备请求队列或简称设备队列。 其队首指针指向队首 PCB。在有的系统中还设置了队尾指针。
(2) 设备状态。当设备自身正处于使用状态时,应将设备的忙/闲标志置“1”。若与该设备相连接的控制器或通道正忙,也不能启动该设备,此时则应将设备的等待标志置“1”。
(3) 与设备连接的控制器表指针。该指针指向该设备所连接的控制器的控制表。在设备到主机之间具有多条通路的情况下,一个设备将与多个控制器相连接。 此时,在 DCT 中还应设置多个控制器表指针。
(4) 重复执行次数。由于外部设备在传送数据时,较易发生数据传送错误,因而在许多系统中,如果发生传送错误,并不立即认为传送失败,而是令它重新传送, 并由系统规定设备在工作中发生错误时应重复执行的次数。在重复执行时,若能恢复正常传送,则仍认为传送成功。仅当屡次失败, 致使重复执行次数达到规定值而传送仍不成功时,才认为传送失败。
控制器控制表、通道控制表和系统设备表
(1) 控制器控制表(COCT)。系统为每一个控制器都设置了一张用于记录本控制器情况 的控制器控制表,如下图(a)所示。
(2) 通道控制表(CHCT)。每个通道都配有一张通道控制表,如下图(b)所示。
(3) 系统设备表(SDT)。这是系统范围的数据结构,其中记录了系统中全部设备的情况。每个设备占一个表目,其中包括有设备类型、设备标识符、 设备控制表及设备驱动程序的入口等项,如上图(c)所示。
设备分配时应考虑的因素
为了使系统有条不紊地工作,系统在分配设备时,应考虑这样几个因素:
- ① 设备的固有属性;
- ② 设备分配算法;
- ③ 设备分配时的安全性;
- ④ 设备独立性。
本小节介绍前三个问题,下一小节专门介绍设备独立性问题。
设备的固有属性
在分配设备时,首先应考虑与设备分配有关的设备属性。设备的固有属性可分成三种:
- 第一种是独占性,
独占性是指这种设备在一段时间内只允许一个进程独占,此即第二章所说的“临界资源”;
对于独占设备,应采用独享分配策略,即将一个设备分配给某进程后,便由该进程独占,直至该进程完成或释放该设备,然后,系统才能再将该设备分配给其他进程使用。 这种分配策略的缺点是,设备得不到充分利用,而且还可能引起死锁。
- 第二种是共享性,
共享性指这种设备允许多个进程同时共享;
对于共享设备,可同时分配给多个进程使用,此时须注意对这些进程访问该设备的先后次序进行合理的调度。
- 第三种是可虚拟设备,
可虚拟设备指设备本身虽是独占设备,但经过某种技术处理,可以把它改造成虚拟设备。
由于可虚拟设备是指一台物理设备在采用虚拟技术后,可变成多台逻辑上的所谓虚拟设备,因而说,一台可虚拟设备是可共享的设备,可以将它同时分配给多个进程使用, 并对这些访问该(物理)设备的先后次序进行控制。
设备分配算法
对设备进行分配的算法,与进程调度的算法有些相似之处,但前者相对简单,通常只采用以下两种分配算法:
(1) 先来先服务。当有多个进程对同一设备提出 I/O 请求时,该算法是根据诸进程对某设备提出请求的先后次序,将这些进程排成一个设备请求队列, 设备分配程序总是把设备首先分配给队首进程。
(2) 优先级高者优先。在进程调度中的这种策略,是优先权高的进程优先获得处理机。如果对这种高优先权进程所提出的 I/O 请求也赋予高优先权, 显然有助于这种进程尽快完成。在利用该算法形成设备队列时,将优先权高的进程排在设备队列前面,而对于优先级相同的 I/O 请求,则按先来先服务原则排队。
设备分配中的安全性
从进程运行的安全性考虑,设备分配有以下两种方式。
- 1) 安全分配方式
在这种分配方式中,每当进程发出 I/O 请求后,便进入阻塞状态,直到其 I/O 操作完成时才被唤醒。在采用这种分配策略时,一旦进程已经获得某种设备(资源)后便阻塞, 使该进程不可能再请求任何资源,而在它运行时又不保持任何资源。因此,这种分配方式已经摒弃了造成死锁的四个必要条件之一的“请求和保持”条件, 从而使设备分配是安全的。其缺点是进程进展缓慢,即 CPU 与 I/O 设备是串行工作的。
- 2) 不安全分配方式
在这种分配方式中,进程在发出 I/O 请求后仍继续运行,需要时又发出第二个I/O请求、 第三个 I/O 请求等。仅当进程所请求的设备已被另一进程占用时, 请求进程才进入阻塞状态。这种分配方式的优点是,一个进程可同时操作多个设备,使进程推进迅速。其缺点是分配不安全,因为它可能具备“请求和保持”条件, 从而可能造成死锁。因此,在设备分配程序中,还应再增加一个功能,以用于对本次的设备分配是否会发生死锁进行安全性计算, 仅当计算结果说明分配是安全的情况下才进行设备分配。
独占设备的分配程序
基本的设备分配程序
下面我们通过一个具有I/O通道的系统案例,来介绍设备分配过程。当某进程提出 I/O 请求后,系统的设备分配程序可按下述步骤进行设备分配。
- 1) 分配设备
首先根据 I/O 请求中的物理设备名,查找系统设备表(SDT),从中找出该设备的 DCT,再根据 DCT 中的设备状态字段,可知该设备是否正忙。若忙, 便将请求 I/O 进程的 PCB 挂在设备队列上;否则,便按照一定的算法来计算本次设备分配的安全性。如果不会导致系统进入不安全状态,便将设备分配给请求进程; 否则,仍将其 PCB 插入设备等待 队列。
- 2) 分配控制器
在系统把设备分配给请求 I/O 的进程后,再到其 DCT 中找出与该设备连接的控制器的 COCT,从 COCT 的状态字段中可知该控制器是否忙碌。 若忙,便将请求 I/O 进程的 PCB 挂在该控制器的等待队列上;否则,便将该控制器分配给进程。
- 3) 分配通道
在该 COCT 中又可找到与该控制器连接的通道的 CHCT,再根据 CHCT 内的状态信息,可知该通道是否忙碌。若忙,便将请求 I/O 的进程挂在该通道的等待队列上; 否则,将该通道分配给进程。只有在设备、 控制器和通道三者都分配成功时,这次的设备分配才算成功。然后,便可启动该 I/O 设备进行数据传送。
设备分配程序的改进
仔细研究上述基本的设备分配程序后可以发现:
- ① 进程是以物理设备名来提出 I/O 请求的;
- ② 采用的是单通路的 I/O 系统结构,容易产生“瓶颈”现象。
为此,应从以下两方面对基本的设备分配程序加以改进,以使独占设备的分配程序具有更强的灵活性,并提高分配的成功率。
- 1) 增加设备的独立性
为了获得设备的独立性,进程应使用逻辑设备名请求 I/O。这样,系统首先从 SDT 中找出第一个该类设备的 DCT。若该设备忙,又查找第二个该类设备的 DCT, 仅当所有该类设备都忙时,才把进程挂在该类设备的等待队列上;而只要有一个该类设备可用,系统便进一步计算分配该设备的安全性。
- 2) 考虑多通路情况
为了防止在 I/O 系统中出现“瓶颈”现象,通常都采用多通路的 I/O 系统结构。此时对控制器和通道的分配同样要经过几次反复, 即若设备(控制器)所连接的第一个控制器(通道)忙时,应查看其所连接的第二个控制器(通道),仅当所有的控制器(通道)都忙时,此次的控制器(通道)分配才算失败, 才把进程挂在控制器(通道)的等待队列上。而只要有一个控制器(通道)可用,系统便可将它分配给进程。
SPOOLing 技术
如前所述,虚拟性是 OS 的四大特征之一。如果说可以通过多道程序技术将一台物理 CPU 虚拟为多台逻辑 CPU,从而允许多个用户共享一台主机,那么, 通过 SPOOLing 技术便可将一台物理 I/O 设备虚拟为多台逻辑 I/O 设备,同样允许多个用户共享一台物理 I/O 设备。
什么是SPOOLing
为了缓和 CPU 的高速性与 I/O 设备低速性间的矛盾而引入了脱机输入、脱机输出技术。该技术是利用专门的外围控制机,将低速 I/O 设备上的数据传送到高速磁盘上;
或者相反。事实上,当系统中引入了多道程序技术后,完全可以利用其中的一道程序,来模拟脱机输入时的外围控制机功能,把低速 I/O 设备上的数据传送到高速磁盘上;
再用另一道程序来模拟脱机输出时外围控制机的功能,把数据从磁盘传送到低速输出设备上。这样,便可在主机的直接控制下,实现脱机输入、输出功能。
此时的外围操作与CPU对数据的处理同时进行,我们把这种在联机情况下实现的同时外围操作称为 SPOOLing(Simultaneaus Periphernal Operating On Line),
或称为假脱机操作
。
SPOOLing系统的组成
由上所述得知,SPOOLing 技术是对脱机输入、输出系统的模拟。相应地,SPOOLing系统必须建立在具有多道程序功能的操作系统上,而且还应有高速随机外存的支持, 这通常是采用磁盘存储技术。
SPOOLing 系统主要有以下三部分:
(1) 输入井和输出井。这是在磁盘上开辟的两个大存储空间。输入井是模拟脱机输入时的磁盘设备,用于暂存 I/O 设备输入的数据;输出井是模拟脱机输出时的磁盘, 用于暂存用户程序的输出数据。
(2) 输入缓冲区和输出缓冲区。为了缓和 CPU 和磁盘之间速度不匹配的矛盾,在内存中要开辟两个缓冲区:输入缓冲区和输出缓冲区。 输入缓冲区用于暂存由输入设备送来的数据,以后再传送到输入井。输出缓冲区用于暂存从输出井送来的数据,以后再传送给输出设备。
(3) 输入进程 SPi 和输出进程 SPo。这里利用两个进程来模拟脱机 I/O 时的外围控制机。其中,进程 SPi 模拟脱机输入时的外围控制机, 将用户要求的数据从输入机通过输入缓冲区再送到输入井,当 CPU 需要输入数据时,直接从输入井读入内存;进程 SPo 模拟脱机输出时的外围控制机, 把用户要求输出的数据先从内存送到输出井,待输出设备空闲时,再将输出井中的数据经过输出缓冲区送到输出设备上。
共享打印机
打印机是经常要用到的输出设备,属于独占设备。利用 SPOOLing 技术,可将之改造为一台可供多个用户共享的设备,从而提高设备的利用率,也方便了用户。 共享打印机技术已被广泛地用于多用户系统和局域网络中。当用户进程请求打印输出时,SPOOLing 系统同意为它打印输出,但并不真正立即把打印机分配给该用户进程, 而只为它做两件事 :
- ①由输出进程在输出井中为之申请一个空闲磁盘块区,并将要打印的数据送入其中;
- ②输出进程再为用户进程申请一张空白的用户请求打印表,并将用户的打印要求填入其中,再将该表挂到请求打印队列上。 如果还有进程要求打印输出,系统仍可接受该请求,也同样为该进程做上述两件事。
如果打印机空闲,输出进程将从请求打印队列的队首取出一张请求打印表,根据表中的要求将要打印的数据,从输出井传送到内存缓冲区,再由打印机进行打印。 打印完后,输出进程再查看请求打印队列中是否还有等待打印的请求表。若有,又取出队列中的第一张表,并根据其中的要求进行打印,如此下去, 直至请求打印队列为空,输出进程才将自己阻塞起来。仅当下次再有打印请求时,输出进程才被唤醒。
SPOOLing系统的特点
SPOOLing系统具有如下主要特点:
(1) 提高了 I/O 的速度。这里,对数据所进行的 I/O 操作,已从对低速 I/O 设备进行的 I/O 操作,演变为对输入井或输出井中数据的存取,如同脱机输入输出一样, 提高了 I/O 速度,缓和了 CPU 与低速 I/O 设备之间速度不匹配的矛盾。
(2) 将独占设备改造为共享设备。因为在 SPOOLing 系统中,实际上并没为任何进程分配设备,而只是在输入井或输出井中为进程分配一个存储区和建立一张I/O请求表。 这样,便把独占设备改造为共享设备。
(3) 实现了虚拟设备功能。宏观上,虽然是多个进程在同时使用一台独占设备,而对于每一个进程而言,他们都会认为自己是独占了一个设备。当然, 该设备只是逻辑上的设备。SPOOLing 系统实现了将独占设备变换为若干台对应的逻辑设备的功能。
磁盘存储器的管理
磁盘存储器不仅容量大,存取速度快,而且可以实现随机存取,是当前存放大量程序和数据的理想设备,故在现代计算机系统中,都配置了磁盘存储器, 并以它为主来存放文件。这样,对文件的操作,都将涉及到对磁盘的访问。磁盘I/O速度的高低和磁盘系统的可靠性,都将直接影响到系统性能。因此, 设法改善磁盘系统的性能,已成为现代操作系统的重要任务之一。
磁盘性能概述
磁盘设备是一种相当复杂的机电设备。
数据的组织和格式
一个物理记录存储在一个扇区上,磁盘上存储的物理记录块数目是由扇区数、磁道数以及磁盘面数所决定的。
为了提高磁盘的存储容量,充分利用磁盘外面磁道的存储能力,现代磁盘不再把内外磁道划分为相同数目的扇区,而是利用外层磁道容量较内层磁道大的特点, 将盘面划分成若干条环带,使得同一环带内的所有磁道具有相同的扇区数。显然,外层环带的磁道拥有较内层环带的磁道更多的扇区。 为了减少这种磁道和扇区在盘面分布的几何形式变化对驱动程序的影响,大多数现代磁盘都隐藏了这些细节,向操作系统提供虚拟几何的磁盘规格, 而不是实际的物理几何规格。
磁盘格式化:
为了在磁盘上存储数据,必须先将磁盘低级格式化。下图示出了一种温盘(温切斯特盘)中一条磁道格式化的情况。
每个扇区包括两个字段:
(1) 标识符字段,其中一个字节的 SYNCH 具有特定的位图像,作为该字段的定界符,利用磁道号、 磁头号及扇区号三者来标识一个扇区;CRC 字段用于段校验。
(2) 数据字段,其中可存放 512 个字节的数据。
格式化还对性能产生影响。上一次数据传送完之后,要通过计算等才能继续进行下一块数据的传送,此期间磁头将继续旋转, 可能将越过相邻的下一个数据块,如果要读取该块数据,只能等到转一圈再回来。通过在格式化磁盘时以交错方式对扇区进行编号可以消除这一问题。 如下图:
磁盘格式化完成后,一般要对磁盘分区。在逻辑上,每个分区就是一个独立的逻辑磁盘。 每个分区的起始扇区和大小都记录在磁盘 0 扇区的主引导记录分区表所包含的分区表中。在这个分区表中必须有一个分区被标记成活动的, 以保证能够从硬盘引导系统。 但是,在真正可以使用磁盘前,还需要对磁盘进行一次高级格式化,即设置一个引导块、空闲存储管理、根目录和一个空文件系统, 同时在分区表中标记该分区所使用的文件系统。
磁盘的类型
对磁盘,可以从不同的角度进行分类。最常见的有:将磁盘分成硬盘和软盘、单片盘和多片盘、固定头磁盘和活动头(移动头)磁盘等。 下面仅对固定头磁盘和移动头磁盘做些介绍。
- 1) 固定头磁盘
这种磁盘在每条磁道上都有一读/写磁头,所有的磁头都被装在一刚性磁臂中。通过这些磁头可访问所有各磁道,并进行并行读/写,有效地提高了磁盘的 I/O 速度。 这种结构的磁盘主要用于大容量磁盘上。
- 2) 移动头磁盘
每一个盘面仅配有一个磁头,也被装入磁臂中。为能访问该盘面上的所有磁道,该磁头必须能移动以进行寻道。可见,移动磁头仅能以串行方式读/写, 致使其 I/O 速度较慢;但由于其结构简单,故仍广泛应用于中小型磁盘设备中。在微型机上配置的温盘和软盘都采用移动磁头结构, 故本节主要针对这类磁盘的 I/O 进行讨论。
磁盘访问时间
磁盘设备在工作时以恒定速率旋转。为了读或写,磁头必须能移动到所要求的磁道上,并等待所要求的扇区的开始位置旋转到磁头下,然后再开始读或写数据。 故可把对磁盘的访问时间分成以下三部分。
- 1)寻道时间 Ts
这是指把磁臂(磁头)移动到指定磁道上所经历的时间。该时间是启动磁臂的时间 s 与磁头移动n条磁道所花费的时间之和,即
Ts = m ×n + s
其中,m 是一常数,与磁盘驱动器的速度有关。磁臂的启动时间约为 2 ms。这样,对于一般的温盘,其寻道时间将随寻道距离的增加而增大, 大体上是 5~30 ms。
- 2) 旋转延迟时间 Tr
这是指定扇区移动到磁头下面所经历的时间。不同的磁盘类型中,旋转速度至少相差一个数量级。
- 3) 传输时间 Tt
这是指把数据从磁盘读出或向磁盘写入数据所经历的时间。在访问时间中,寻道时间和旋转延迟时间基本上都与所读/写数据的多少无关, 而且它通常占据了访问时间中的大头。适当地集中数据(不要太零散)传输,将有利于提高传输效率。
坏扇区的处理
制道时的瑕疵引入坏扇区,也就是说,扇区不能正确地读回写到其上的值。如果瑕疵非常小,比如说只有几位,那么使用坏扇区并且每次只是让 ECC 矫正错误是可能的。 如果瑕疵较大,那么错误就不能被掩盖。
对于坏块存在两种一般的处理方法:在控制器中对它们进行处理或者在操作系统中对它们进行处理。在前一种方法中,磁盘在从工厂出厂之前要进行测试,并且将一个坏扇区 列表写在磁盘上。对于每个坏扇区,用一个备用扇区替换它。
有两种方法进行这样的替换:
- 控制器对坏扇区进行重映射到一个备用的好扇区;
- 另一种方法是将所有扇区向上移动一个扇区。
在这两种情况下,控制器都必须知道哪个扇区是坏扇区。它可以通过内部的表来跟踪这一信息(每个磁道一张表),或者通过重写前导码来给出重映射的扇区号。如果是重写 前导码,将会移动所有扇区以回避坏扇区。
稳定存储器
磁盘调度
磁盘是可供多个进程共享的设备,当有多个进程都要求访问磁盘时,应采用一种最佳调度算法,以使各进程对磁盘的平均访问时间最小。 由于在访问磁盘的时间中,主要是寻道时间,因此,磁盘调度的目标是使磁盘的平均寻道时间最少。目前常用的磁盘调度算法有先来先服务、 最短寻道时间优先及扫描等算法。下面逐一介绍。
先来先服务
这是一种最简单的磁盘调度算法。它根据进程请求访问磁盘的先后次序进行调度。此算法的优点是公平、简单,且每个进程的请求都能依次地得到处理, 不会出现某一进程的请求长期得不到满足的情况。但此算法由于未对寻道进行优化,致使平均寻道时间可能较长。FCFS 算法仅适用于请求磁 盘 I/O 的进程数目较少(这样优化的余地本来就很少了)的场合。
最短寻道时间优先
该算法选择这样的进程:其要求访问的磁道与当前磁头所在的磁道距离最近,以使每次的寻道时间最短。但这种算法不能保证平均寻道时间最短。 SSTF 算法的平均每次磁头移动距离明显低于 FCFS 的距离,因而 SSTF 较之 FCFS 有更好的寻道性能,故过去曾一度被广泛采用。不过该方法存在一个 问题:局部的(不论什么时候到达该区域)集中访问,远离该部分(即使先来的,而且等待了很久)长久得不到访问。
扫描(SCAN)算法
SSTF 算法虽然能获得较好的寻道性能,但却可能导致某个进程发生“饥饿”(Starvation)现象。因为只要不断有新进程的请求到达, 且其所要访问的磁道与磁头当前所在磁道的距离较近,这种新进程的I/O请求必然优先满足。对 SSTF算法略加修改后所形成的 SCAN 算法, 即可防止老进程出现“饥饿”现象。
SCAN 算法:
该算法不仅考虑到欲访问的磁道与当前磁道间的距离,更优先考虑的是磁头当前的移动方向。 由于在这种算法中磁头移动的规律颇似电梯的运行,
因而又常称之为电梯调度算法
。
循环扫描(CSCAN)算法
SCAN 算法既能获得较好的寻道性能,又能防止“饥饿”现象,故被广泛用于大、中、小型机器和网络中的磁盘调度。但 SCAN 也存在这样的问题: 当磁头刚从里向外移动而越过了某一磁道时,恰好又有一进程请求访问此磁道,这时,该进程必须等待,待磁头继续从里向外, 然后再从外向里扫描完所有要访问的磁道后,才处理该进程的请求,致使该进程的请求被大大地推迟。为了减少这种延迟,CSCAN 算法规定磁头单向移动,
NStepSCAN 和 FSCAN 调度算法
NStepSCAN 算法
在 SSTF、SCAN 及 CSCAN 几种调度算法中,都可能会出现磁臂停留在某处不动的情况,例如,有一个或几个进程对某一磁道有较高的访问频率,
即这个(些)进程反复请求对某一磁道的 I/O 操作,从而垄断了整个磁盘设备。我们把这一现象称为“磁臂粘着
”(Armstickiness)。
在高密度磁盘上容易出现此情况。
N步SCAN 算法是将磁盘请求队列分成若干个长度为 N 的子队列,磁盘调度将按 FCFS 算法依次处理这些子队列。而每处理一个队列时又是按 SCAN 算法, 对一个队列处理完后,再处理其他队列。当正在处理某子队列时,如果又出现新的磁盘 I/O 请求,便将新请求进程放入其他队列, 这样就可避免出现粘着现象。
- 当 N 值取得很大时,会使 N 步扫描法的性能接近于 SCAN 算法的性能;
- 当 N=1 时,N 步 SCAN 算法便蜕化为 FCFS 算法。
FSCAN算法
FSCAN 算法实质上是 N 步 SCAN 算法的简化,即 FSCAN 只将磁盘请求队列分成两个子队列。一个是由当前所有请求磁盘 I/O 的进程形成的队列, 由磁盘调度按 SCAN 算法进行处理。在扫描期间,将新出现的所有请求磁盘 I/O 的进程,放入另一个等待处理的请求队列。这样, 所有的新请求都将被推迟到下一次扫描时处理。
磁盘 I/O 速度的提高
目前,磁盘的 I/O 速度远低于对内存的访问速度,通常要低上 4~6 个数量级。因此,磁盘的 I/O 已成为计算机系统的瓶颈。 于是,人们便千方百计地去提高磁盘 I/O 的速度,其中最主要的技术便是采用磁盘高速缓存(Disk Cache)。
磁盘高速缓存
这里所说的磁盘高速缓存,并非通常意义下的内存和CPU之间所增设的一个小容量高速存储器, 而是指利用内存中的存储空间来暂存从磁盘中读出的一系列盘块中的信息。因此,这里的高速缓存是一组在逻辑上属于磁盘, 而物理上是驻留在内存中的盘块。
高速缓存在内存中可分成两种形式:
-
第一种是在内存中开辟一个单独的存储空间来作为磁盘高速缓存,其大小是固定的,不会受应用程序多少的影响;
-
第二种是把所有未利用的内存空间变为一个缓冲池,供请求分页系统和磁盘I/O时(作为磁盘高速缓存)共享。此时,高速缓存的大小显然不再是固定的。 当磁盘I/O的频繁程度较高时,该缓冲池可能包含更多的内存空间;而在应用程序运行得较多时,该缓冲池可能只剩下较少的内存空间。
数据交付方式
数据交付(Data Delivery)是指将磁盘高速缓存中的数据传送给请求者进程。当有一进程请求访问某个盘块中的数据时,由核心先去查看磁盘高速缓冲器, 看其中是否存在进程所需访问的盘块数据的拷贝。若有其拷贝,便直接从高速缓存中提取数据交付给请求者进程,这样,就避免了访盘操作, 从而使本次访问速度提高4~6个数量级;否则,应先从磁盘中将所要访问的数据读入并交付给请求者进程,同时也将数据送高速缓存。 当以后又需要访问该盘块的数据时,便可直接从高速缓存中提取。
系统可以采取两种方式将数据交付给请求进程:
- (1) 数据交付。这是直接将高速缓存中的数据,传送到请求者进程的内存工作区中。
- (2) 指针交付。这是只将指向高速缓存中某区域的指针交付给请求者进程。
后一种方式由于所传送的数据量少,因而节省了数据从磁盘高速缓存到进程的内存工作区的时间。
置换算法
如同请求调页(段)一样,在将磁盘中的盘块数据读入高速缓存时,同样会出现因高速缓存中已装满盘块数据而需要将该数据先换出的问题。相应地, 也必然存在着采用哪种置换算法的问题。较常用的置换算法仍然是最近最久未使用算法 LRU、最近未使用算法 NRU 及最少使用算法 LFU 等。
由于请求调页中的联想存储器与高速缓存(磁盘I/O中)的工作情况不同,因而使得在置换算法中所应考虑的问题也有所差异。因此, 现在不少系统在设计其高速缓存的置换算法时,除了考虑到最近最久未使用这一原则外,还考虑了以下几点:
- 1) 访问频率
通常,每执行一条指令时,便可能访问一次联想存储器,亦即联想存储器的访问频率,基本上与指令执行的频率相当。而对高速缓存的访问频率, 则与磁盘I/O的频率相当。因此 ,对联想存储器的访问频率远远高于对高速缓存的访问频率。
- 2) 可预见性
在高速缓存中的各盘块数据,有哪些数据可能在较长时间内不会再被访问,又有哪些数据可能很快就再被访问,会有相当一部分是可预知的。 例如,对二次地址及目录块等,在它被访问后,可能会很久都不再被访问。又如,正在写入数据的未满盘块,可能会很快又被访问。
- 3) 数据的一致性
由于高速缓存是做在内存中的,而内存一般又是一种易失性的存储器,一旦系统发生故障,存放在高速缓存中的数据将会丢失; 而其中有些盘块(如索引结点盘块)中的数据已被修改,但尚未拷回磁盘,因此,当系统发生故障后,可能会造成数据的不一致性。
基于上述考虑,在有的系统中便将高速缓存中的所有盘块数据拉成一条LRU链。对于那些会严重影响到数据一致性的盘块数据和很久都可能不再使用的盘块数据, 都放在 LRU链的头部,使它们能被优先写回磁盘,以减少发生数据不一致性的概率,或者可以尽早地腾出高速缓存的空间。对于那些可能在不久之后便要再使用的盘块数据, 应挂在 LRU链的尾部,以便在不久以后需要时,只要该数据块尚未从链中移至链首而被写回磁盘,便可直接到高速缓存中(即LRU链中)去找到它们。
周期性地写回磁盘
还有一种情况值得注意: 那就是根据 LRU 算法,那些经常要被访问的盘块数据,可能会一直保留在高速缓存中,长期不会被写回磁盘。 (注意,LRU 链意味着链中任一元素在被访问之后,总是又被挂到链尾而不被写回磁盘;只是一直未被访问的元素,才有可能移到链首,而被写回磁盘。) 例如,一位学者一上班便开始撰写论文,并边写边修改,他正在写作的论文就一直保存在高速缓存的LRU链中。如果在快下班时,系统突然发生故障,这样, 存放在高速缓存中的已写论文将随之消失,致使他枉费了一天的劳动。
为了解决这一问题,在 UNIX 系统中专门增设了一个修改(update)程序,使之在后台运行,该程序周期性地调用一个系统调用 SYNC。 该调用的主要功能是强制性地将所有在高速缓存中已修改的盘块数据写回磁盘。一般是把两次调用SYNC的时间间隔定为30 s。这样, 因系统故障所造成的工作损失不会超过30 s的劳动量。而在MS-DOS中所采用的方法是: 只要高速缓存中的某盘块数据被修改,便立即将它写回磁盘, 并将这种高速缓存称为“写穿透、高速缓存”(write-through cache)。MS-DOS所采用的写回方式,几乎不会造成数据的丢失,但须频繁地启动磁盘。
提高磁盘I/O速度的其它方法
在系统中设置了磁盘高速缓存后,能显著地减少等待磁盘I/O的时间。本小节再介绍几种能有效地提高磁盘I/O速度的方法,这些方法已被许多系统采用。
提前读(Read-ahead)
用户(进程)对文件进行访问时,经常采用顺序访问方式,即顺序地访问文件各盘块的数据。在这种情况下,在读当前块时可以预知下一次要读的盘块。因此, 可以采取预先读方式,即在读当前块的同时,还要求将下一个盘块(提前读的块)中的数据也读入缓冲区。这样,当下一次要读该盘块中的数据时, 由于该数据已被提前读入缓冲区,因而此时便可直接从缓冲区中取得下一盘块的数据,而不需再去启动磁盘I/O,从而大大减少了读数据的时间。 这也就等效于提高了磁盘 I/O 的速度。“提前读”功 能 已 被广泛采用,如在UNIX系统、OS/2,以及在 3 Plus和Netware等的网络 OS 中,都已采用该功能。
延迟写
延迟写是指在缓冲区A中的数据,本应立即写回磁盘,但考虑到该缓冲区中的数据在不久之后可能还会再被本进程或其它进程访问(共享资源), 因而并不立即将该缓冲区 A 中的数据写入磁盘,而是将它挂在空闲缓冲区队列的末尾。随着空闲缓冲区的使用,缓冲区也缓缓往前移动,直至移到空闲缓冲队列之首。 当再有进程申请到该缓冲区时,才将该缓冲区中的数据写入磁盘,而把该缓冲区作为空闲缓冲区分配出去。当该缓冲区A仍在队列中时,任何访问该数据的进程, 都可直接读出其中的数据而不必去访问磁盘。这样,又可进一步减小等效的磁盘I/O时间。同样,“延迟写”功能已在UNIX系统、OS/2等OS中被广泛采用。
优化物理块的分布
另一种提高磁盘 I/O 速度的重要措施是优化文件物理块的分布,使磁头的移动距离最小。 虽然链接分配和索引分配方式都允许将一个文件的物理块分散在磁盘的任意位置,但如果将一个文件的多个物理块安排得过于分散,会增加磁头的移动距离。 例如,将文件的第一个盘块安排在最里的一条磁道上,而把第二个盘块安排在最外的一条磁道上,这样,在读完第一个盘块后转去读第二个盘块时, 磁头要从最里的磁道移到最外的磁道上。如果我们将这两个数据块安排在属于同一条磁道的两个盘块上,显然会由于消除了磁头在磁道间的移动, 而大大提高对这两个盘块的访问速度。
对文件盘块位置的优化,应在为文件分配盘块时进行。如果系统中的空白存储空间是采用位示图方式表示的, 则要将同属于一个文件的盘块安排在同一条磁道上或相邻的磁道上是十分容易的事。这时,只要从位示图中找到一片相邻接的多个空闲盘块即可。 但当系统采用线性表(链)法来组织空闲存储空间时,要为一文件分配多个相邻接的盘块,就要困难一些。此时,我们可以将在同一条磁道上的若干个盘块组成一簇, 例如,一簇包括 4 个盘块,在分配存储空间时,以簇为单位进行分配。这样就可以保证在访问这几个盘块时,不必移动磁头或者仅移动一条磁道的距离, 从而减少了磁头的平均移动距离。
虚拟盘
所谓虚拟盘,是指利用内存空间去仿真磁盘,又称为RAM盘。该盘的设备驱动程序也可以接受所有标准的磁盘操作,但这些操作的执行,不是在磁盘上而是在内存中。 这些对用户都是透明的。换言之,用户并不会发现这与真正的磁盘操作有什么不同,而仅仅是略微快些而已。虚拟盘的主要问题是:它是易失性存储器, 故一旦系统或电源发生故障,或系统再启动时,原来保存在虚拟盘中的数据将会丢失。因此,虚拟盘通常用于存放临时文件,如编译程序所产生的目标程序等。 虚拟盘与磁盘高速缓存的主要区别在于: 虚拟盘中的内容完全由用户控制,而高速磁盘缓存中的内容则是由 OS控制的。例如,RAM 盘在开始时是空的, 仅当用户(程序)在RAM盘中创建了文件后,RAM盘中才有内容。
廉价磁盘冗余阵列
廉价磁盘冗余阵列(RAID,Redundant Array of Inexpensive Disk),它是利用一台磁盘阵列控制器,来统一管理和控制一组(几台到几十台)磁盘驱动器, 组成一个高度可靠的、快速的大容量磁盘系统。
并行交叉存取
为了提高对磁盘的访问速度,已把在大、中型机中应用的交叉存取(Interleave)技术应用到了磁盘存储系统中。在该系统中,有多台磁盘驱动器, 系统将每一盘块中的数据分为若干个子盘块数据,再把每一个子盘块的数据分别存储到各个不同磁盘中的相同位置上。在以后,当要将一个盘块的数据传送到内存时, 采取并行传输方式,将各个盘块中的子盘块数据同时向内存中传输,从而使传输时间大大减少。例如,在存放一个文件时, 可将该文件中的第一个数据子块放在第一个磁盘驱动器上;将文件的第二个数据子块放在第二个磁盘上;……;将第 N 个数据子块,放在第N个驱动器上。 以后在读取数据时,采取并行读取方式,即同时从第1~N个数据子块读出数据,这样便把磁盘I/O的速度提高了 N-1 倍。
RAID的分级
RAID 在刚被推出时,是分成 6 级的,即 RAID 0 级至 RAID 5 级,后来又增加了 RAID 6级和 RAID 7 级。
(1) RAID 0级。本级仅提供了并行交叉存取。它虽能有效地提高磁盘 I/O 速度,但并无冗余校验功能,致使磁盘系统的可靠性不好。只要阵列中有一个磁盘损坏, 便会造成不可弥补的数据丢失,故较少使用。
(2) RAID 1级。它具有磁盘镜像功能,例如,当磁盘阵列中具有8个盘时,可利用其中4个作为数据盘,另外 4 个作为镜像盘,在每次访问磁盘时,可利用并行读、写特性, 将数据分块同时写入主盘和镜像盘。故其比传统的镜像盘速度快,但其磁盘容量的利用率只有50%,它是以牺牲磁盘容量为代价的。
(3) RAID 3级。这是具有并行传输功能的磁盘阵列。它利用一台奇偶校验盘来完成数据的校验功能,比起磁盘镜像,它减少了所需要的冗余磁盘数。例如, 当阵列中只有 7 个盘时,可利用 6 个盘作数据盘,一个盘作校验盘。磁盘的利用率为 6/7。RAID 3 级经常用于科学计算和图像处理。
(4) RAID 5级。这是一种具有独立传送功能的磁盘阵列。每个驱动器都各有自己独立的数据通路,独立地进行读/写,且无专门的校验盘。用来进行纠错的校验信息, 是以螺旋(Spiral)方式散布在所有数据盘上。RAID 5级常用于 I/O 较频繁的事务处理中。
(5) RAID 6 级和 RAID 7级。这是强化了的RAID。在RAID 6级的阵列中,设置了一个专用的、可快速访问的异步校验盘。该盘具有独立的数据访问通路, 具有比 RAID 3级及 RAID 5级更好的性能,但其性能改进得很有限,且价格昂贵。RAID 7 级是对 RAID 6 级的改进,在该阵列中的所有磁盘, 都具有较高的传输速率和优异的性能,是目前最高档次的磁盘阵列,但其价格也较高。
RAID的优点
RAID 自 1988 年问世后,便引起了人们的普遍关注,并很快地流行起来。这主要是因为 RAID 具有下述一系列明显的优点:
(1) 可靠性高。RAID 最大的特点就是它的高可靠性。除了 RAID 0级外,其余各级都采用了容错技术。当阵列中某一磁盘损坏时,并不会造成数据的丢失, 因为它既可实现磁盘镜像,又可实现磁盘双工,还可实现其它的冗余方式。所以此时可根据其它未损坏磁盘中的信息,来恢复已损坏的盘中的信息。 它与单台磁盘机相比,其可靠性高出了一个数量级。
(2) 磁盘 I/O 速度高。由于磁盘阵列可采取并行交叉存取方式,故可将磁盘 I/O 速度提高 N-1 倍(N 为磁盘数目)。或者说,磁盘阵列可将磁盘 I/O 速度提高数倍至数十倍。
(3) 性能/价格比高。利用 RAID 技术来实现大容量高速存储器时,其体积与具有相同容量和速度的大型磁盘系统相比,只是后者的 1/3,价格也只是后者的 1/3, 且可靠性高。换言之,它仅以牺牲 1/N 的容量为代价,换取了高可靠性;而不像磁盘镜像及磁盘双工那样,须付出 50% 容量的代价。
文件管理
文件系统是操作系统中负责存取和管理信息的模块,它采用统一方法管理用户信息和系统信息的存储、检索、更新、共享和保护,并为用户提供一整套行之有效的文件使用及 操作方法。“文件”这一术语不但反映用户概念中的逻辑结构,而且同存放它的辅助存储器的存储结构紧密相关。所以,必须从逻辑文件和物理文件两个侧面来观察文件。
-
对于用户而言,可按照需要并遵循文件系统的规则来定义文件信息的逻辑结构,由文件系统提供“按名存取”方式来实现对文件信息的存储和检索;
-
对于系统而言,必须采用特定的数据结构和有效算法,实现文件的逻辑结构到存储结构的映射,实现对文件存储空间和文件信息的管理,提供多种存取方法。
例如,用户希望与具体的存储硬件无关,使用路径名、文件名、文件内位移就可以执行数据的读、写、修改、删除操作;而作为实现这些功能的文件系统来说,其工作与存储 硬件紧密相关,是将用户的文件操作请求转化为对磁盘上的信息按照所在的物理位置进行寻址、读写和控制。所以,文件系统的功能就是在逻辑文件和物理文件、逻辑地址与 物理地址、逻辑结构与物理结构之间实现转换,使得存取速度快、存储空间利用率高、数据可共享、安全可靠性好。
文件系统的主要功能有:
- 文件的按名存取,实现从逻辑文件到物理文件的转换;
- 文件目录的建立和维护;
- 文件的查找和定位;
- 文件存储空间的分配和管理;
- 提供文件的存取方法和文件存储结构;
- 实现文件的共享、保护和保密;
- 提供一组易用的文件操作和命令;
- 提供与设备管理交互的统一接口。
文件
文件是记录在外存上的相关信息的具有名称的集合。从用户角度而言,文件是逻辑外存的最小分配单元,即数据除非在文件中,否则不能写到外存。通常,文件表示程序 (源形式和目标形式)和数据。数据文件可以是数字、字符、字符数字或二进制。文件可以是自由形式,如文本文件,也可以具有严格的格式。通常,文件由位、字节、行 或记录组成,其具体意义是由文件创建者和使用者来定义的。因此,文件的概念几位广泛。
文件应具有自己的属性,属性可以包括:
- 文件基本属相:文件名称和扩展名、文件属主 ID、文件所属组 ID 等
- 文件类型:可以从不同的角度来规定文件的类型,如源文件、目标文件及可执行文件等。
- 文件保护属性:规定谁能够访问文件,以何种方式(读、写、执行、更新、删除等)访问。
- 文件管理属性:如文件创建时间、最后访问时间、最后修改时间等。
- 文件控制信息:逻辑记录长、文件当前长、文件最大长、关键字位置、关键字长度、信息位置、文件打开次数等。。
文件概述
文件是进程创建的信息逻辑单元。文件不仅仅被用来对磁盘建模,以替代对随机存储器的建模,事实上,如果能把每个文件看成一种地址空间,那么就离理解文件的本质不远了。
进程可以读取已经存在的文件,并在需要时建立新的文件。存储在文件中的信息必须是持久的,也就是说,不会因为进程的创建与终止受到影响。一个文件应只在其所有者明确 删除它的情况下才会消失。
文件是由文件名锁标识的一组信息的集合,文件名是字母或数字组成的字母数字串,其格式和长度因系统而异。文件信息是由其创建者定义的。
文件可存储许多不同类型的信息:源程序、目标程序、可执行程序、数字数据、文本、工资记录、图像、声音记录等。文件根据其类型具有一定结构。文本文件
是由行(或页)组成,而行(或页)是由字符组成的。源文件
由子程序和函数组成。而他们有事由声明和执行语句组成的。目标文件
是一系列字节序列,
它们按目标系统链接器所能理解的方式组成。可执行文件
为一系列代码段,以供装入程序调入内存执行。
操作系统提供文件系统后,应具备以下功能:
-
首先,便于用户使用,无需记住信息存放在辅存中的物理位置,无需考虑如何将信息存放到介质上,只要知道文件名,给出有关的操作要求便可访问,实现了 “
按名存取
”。特别地,当文件存放位置发生改变,甚至更换文件的存储设备,对使用者也不会产生丝毫影响; -
其次,文件安全可靠,由于用户通过文件系统才能实现对文件的访问,而系统能提供各种安全、保密和保护措施,可防止对文件信息的有意或无意的破坏或窃用。
-
此外,系统能有效地利用存储空间,优化安排不同属主文件的位置;如果在文件使用过程中出现设备故障,系统可组织充执或回复,对于因硬件失效而可能造成的信息破坏, 可组织转储以加强可靠性。
-
最后,文件系统还能提供文件共享功能,不同的用户可使用同名或异名的同一个文件,这样,合理利用文件存储空间,缩短传输信息的交换时间,提高文件空间的利用率。
文件命名
文件是一种抽象机制。这种抽象性体现在,用户不必去关心具体的实现细节。对于任何一种抽象机制来说,可能最重要的特性是对管理对象的命名方式。当进程创建一个文件 时,必须给它指定一个名字;当进程终止时,这个文件继续存在,别的进程可以通过它的名字来访问它。
文件的具体命名规则并无统一的标准,不同的系统可能会有不同的要求。许多操作系统支持两部分组成的文件名。两部分之间用句点隔开,在句点后面的部分称为文件扩展名
,
它通常给出了与文件类型有关的一些信息。在有些系统中,文件扩展名仅仅是一种惯例,并不强迫使用。
文件类型
为了便于管理和控制文件而将文件分成若干种类型。由于不同系统对文件的管理方式不同,因而它们对文件的分类方法也有很大差异。 为了方便系统和用户了解文件的类型,在许多OS中都把文件类型作为扩展名而缀在文件名的后面,在文件名和扩展名之间用“.”号隔开。下面是常用的几种文件分类方法。
按用途分类
根据文件的性质和用途的不同,可将文件分为三类:
- (1) 系统文件。这是指由系统软件构成的文件。大多数的系统文件只允许用户调用,但不允许用户去读,更不允许修改;有的系统文件不直接对用户开放。
- (2) 用户文件。指由用户的源代码、目标文件、可执行文件或数据等所构成的文件。用户将这些文件委托给系统保管。
- (3) 库文件。这是由标准子例程及常用的例程等所构成的文件。这类文件允许用户调用,但不允许修改。
按文件中数据的形式分类
按这种方式分类,也可把文件分为三类:
- (1) 源文件。这是指由源程序和数据构成的文件。通常由终端或输入设备输入的源程序和数据所形成的文件都属于源文件。它通常是由ASCII码或汉字所组成的。
- (2) 目标文件。这是指把源程序经过相应语言的编译程序编译过,但尚未经过链接程序链接的目标代码所构成的文件。它属于二进制文件。通常, 目标文件所使用的后缀名是“.obj”。
- (3) 可执行文件。这是指把编译后所产生的目标代码再经过链接程序链接后所形成的文件。
按存取控制属性分类
根据系统管理员或用户所规定的存取控制属性,可将文件分为三类:
- (1) 只执行文件。该类文件只允许被核准的用户调用执行,既不允许读,更不允许写。
- (2) 只读文件。该类文件只允许文件主及被核准的用户去读,但不允许写。
- (3) 读写文件。这是指允许文件主和被核准的用户去读或写的文件。
按组织形式和处理方式分类
根据文件的组织形式和系统对其的处理方式,可将文件分为三类:
- (1) 普通文件:由 ASCII 码或二进制码组成的字符文件。一般用户建立的源程序文件、数据文件、目标代码文件及操作系统自身代码文件、库文件、 实用程序文件等都是普通文件,它们通常存储在外存储设备上。
- (2) 目录文件:由文件目录组成的,用来管理和实现文件系统功能的系统文件,通过目录文件可以对其它文件的信息进行检索。由于目录文件也是由字符序列构成, 因此对其可进行与普通文件一样的种种文件操作。
- (3) 特殊文件:
特殊文件特指系统中的各类 I/O 设备。为了便于统一管理,系统将所有的 输入/输出设备都视为文件,按文件方式提供给用户使用,如目录的检索、 权限的验证等都与普通文件相似,只是对这些文件的操作是和设备驱动程序紧密相连的,系统将这些操作转为对具体设备的操作。根据设备数据交换单位的不同, 又可将特殊文件分为块设备文件和字符设备文件。前者用于磁盘、光盘或磁带等块设备的 I/O 操作,而后者用于终端、打印机等字符设备的 I/O 操作。
文件操作
用户通过文件系统所提供的系统调用实施对文件的操作。最基本的文件操作有: 创建文件、删除文件、读文件、写文件、截断文件和设置文件的读/写位置。 但对于一个实际的OS,为了方便用户使用文件而提供了更多的对文件的操作,如打开和关闭一个文件及改变文件名等操作。
最基本的文件操作
(1) 创建文件。在创建一个新文件时,系统首先要为新文件分配必要的外存空间,并在文件系统的目录中,为之建立一个目录项。 目录项中应记录新文件的文件名及其在外存的地址等属性。
(2) 删除文件。当已不再需要某文件时,可将它从文件系统中删除。在删除时,系统应先从目录中找到要删除文件的目录项,使之成为空项, 然后回收该文件所占用的存储空间。
(3) 读文件。在读一个文件时,须在相应系统调用中给出文件名和应读入的内存目标地址。此时,系统同样要查找目录,找到指定的目录项, 从中得到被读文件在外存中的位置。在目录项中,还有一个指针用于对文件的读/写。
(4) 写文件。在写一个文件时,须在相应系统调用中给出该文件名及该文件在内存中的(源)地址。为此,也同样须先查找目录,找到指定文件的目录项, 再利用目录中的写指针进行写操作。
(5) 截断文件。如果一个文件的内容已经陈旧而需要全部更新时,一种方法是将此文件删除,再重新创建一个新文件。但如果文件名及其属性均无改变时, 则可采取另一种所谓的截断文件的方法,此即将原有文件的长度设置为0,或者说是放弃原有的文件内容。
(6) 设置文件的读/写位置。前述的文件读/写操作都只提供了对文件顺序存取的手段,即每次都是从文件的始端读或写。设置文件读/写位置的操作, 用于设置文件读/写指针的位置,以便每次读/写文件时,不是从其始端而是从所设置的位置开始操作。也正因如此,才能改顺序存取为随机存取。
文件的“打开”和“关闭”操作
所谓“打开”,是指系统将指名文件的属性(包括该文件在外存上的物理位置)从外存拷贝到内存打开文件表的一个表目中,并将该表目的编号(或称为索引)返回给用户。 以后,当用户再要求对该文件进行相应的操作时,便可利用系统所返回的索引号向系统提出操作请求。系统这时便可直接利用该索引号到打开文件表中去查找, 从而避免了对该文件的再次检索。这样不仅节省了大量的检索开销,也显著地提高了对文件的操作速度。如果用户已不再需要对该文件实施相应的操作时, 可利用“关闭”(close)系统调用来关闭此文件,OS将会把该文件从打开文件表中的表目上删除掉。
其他文件操作
为了方便用户使用文件,通常,OS都提供了数条有关文件操作的系统调用,可将这些调用分成若干类:
最常用的一类是有关对文件属性进行操作的,即允许用户直接设置和获得文件的属性,如改变已存文件的文件名、改变文件的拥有者(文件主)、改变对文件的访问权, 以及查询文件的状态(包括文件类型、大小和拥有者以及对文件的访问权等);
另一类是有关目录的,如创建一个目录,删除一个目录,改变当前目录和工作目录等;此外,还有用于实现文件共享的系统调用和用于对文件系统进行操作的系统调用等。
值得说明的是,有许多文件操作都可以利用上述基本操作加以组合来实现。例如,创建一个文件拷贝的操作,可利用两条基本操作来实现。 其第一步是利用创建文件的系统调用来创建一个新文件;第二步是将原有文件中的内容写入新文件中。
文件的逻辑结构
通常,文件是由一系列的记录组成的。文件系统设计的关键要素,是指将这些记录构成一个文件的方法,以及将一个文件存储到外存上的方法。 事实上,对于任何一个文件,都存在着以下两种形式的结构:
(1) 文件的逻辑结构(File Logical Structure)。这是从用户观点出发所观察到的文件组织形式,是 用 户 可以直接处理的数据及其结构,它独立于文件的物理特性, 又称为文件组织(File Organization)。
(2) 文件的物理结构,又称为文件的存储结构,是指文件在外存上的存储组织形式。这不仅与存储介质的存储性能有关,而且与所采用的外存分配方式有关。
无论是文件的逻辑结构,还是其物理结构,都会影响对文件的检索速度。对文件逻辑结构所提出的基本要求,首先是能提高检索速度,即在将大批记录组成文件时, 应有利于提高检索记录的速度和效率;其次是便于修改,即便于在文件中增加、删除和修改一个或多个记录;第三是降低文件的存储费用,即减少文件占用的存储空间, 不要求大片的连续存储空间。
文件逻辑结构的类型
文件的逻辑结构可分为两大类,一类是有结构文件,这是指由一个以上的记录构成的文件,故又把它称为记录式文件;其二是无结构文件,这是指由字符流构成的文件, 故又称为流式文件。
有结构文件
在记录式文件中,每个记录都用于描述实体集中的一个实体,各记录有着相同或不同数目的数据项。记录的长度可分为定长和不定长两类。
(1) 定长记录。这是指文件中所有记录的长度都是相同的,所有记录中的各数据项都处在记录中相同的位置,具有相同的顺序和长度。文件的长度用记录数目表示。 对定长记录的处理方便、开销小,所以这是目前较常用的一种记录格式,被广泛用于数据处理中。
(2) 变长记录。这是指文件中各记录的长度不相同。产生变长记录的原因,可能是由于一个记录中所包含的数据项数目并不相同,也可能是数据项本身的长度不定. 不论是哪一种,在处理前,每个记录的长度是可知的。
根据用户和系统管理上的需要,可采用多种方式来组织这些记录,形成下述的几种文件:
- (1) 顺序文件。这是由一系列记录按某种顺序排列所形成的文件。其中的记录通常是定长记录,因而能用较快的速度查找文件中的记录。
- (2) 索引文件。当记录为可变长度时,通常为之建立一张索引表,并为每个记录设置一个表项,以加快对记录检索的速度。
- (3) 索引顺序文件。这是上述两种文件构成方式的结合。它为文件建立一张索引表,为每一组记录中的第一个记录设置一个表项。
无结构文件
如果说大量的数据结构和数据库是采用有结构的文件形式的话,则大量的源程序、可执行文件、库函数等,所采用的就是无结构的文件形式,即流式文件。 其长度以字节为单位。对流式文件的访问,则是采用读/写指针来指出下一个要访问的字符。可以把流式文件看做是记录式文件的一个特例。在 UNIX 系统中, 所有的文件都被看做是流式文件,即使是有结构文件,也被视为流式文件,系统不对文件进行格式处理,所有的含义只能由用户层的程序来解释。
操作系统把文件看成是简单的字节流,这种方式提供了很大的灵活性。用户程序可以在文件中加入任何内容,并且以任何方便的形式命名。操作系统不会停帮助,但也不会 设置障碍。对于那些需要做特殊事情的用户来说,后者可能更为重要。
堆
堆(pile)是最简单的文件内容组织形式。数据按照它们到达的顺序被采集,每个记录由一串数据组成。堆得目的仅仅是积累大量的数据并保存数据。记录可以有不同的域, 或者域相似但顺序不同。因此,每个域应该是自描述的。包括域名和值。每个域的长度由划分符隐式地指定,或者明确地包含在一个子域中,或者是该域类型的默认长度。
由于堆文件没有结构,因而对记录的访问是通过穷军查找的方式,也就是说,如果想找到包括某一特定域且值为某一特定值得记录,则需要检查堆中的每一个记录,知道找到 想要的记录,或者查找完整个文件为止。如果想查找包括某一个特定的域,或者包含具有某一特定值的域的所有记录,则必须查找整个文件。
当数据在处理前采集并存储时,或者当数据难以组织时,会用到堆文件。当保存的数据大小和结构不同时,这种类型的文件空间使用情况很好,能较好地用于穷军查找,且 易于查找。但是,除了这些受限制的使用,这类文件岁大多数应用都是不使用的。
顺序文件
文件是记录的集合。文件中的记录可以是任意顺序的,因此,它可以按照各种不同的顺序进行排列。一般地,可归纳为以下两种情况:
第一种是串结构
,各记录之间的顺序与关键字无关。通常的办法是由时间来决定,即按存入时间的先后排列,最先存入的记录作为第一个记录,
其次存入的为第二个记录……, 依此类推。
第二种情况是顺序结构
,指文件中的所有记录按关键字(词)排列。可以按关键词的长短从小到大排序,也可以从大到小排序;或按其英文字母顺序排序。
对顺序结构文件可有更高的检索效率,因为在检索串结构文件时,每次都必须从头开始,逐个记录地查找,直至找到指定的记录,或查完所有的记录为止。 而对顺序结构文件,则可利用某种有效的查找算法,如折半查找法、插值查找法、跳步查找法等方法来提高检索效率。
对顺序文件(Sequential File)的读/写操作
顺序文件中的记录可以是定长的,也可以是变长的。对于定长记录的顺序文件,如果已知当前记录的逻辑地址,便很容易确定下一个记录的逻辑地址。边读边统计该记录的 长度(定长的话,就不用统计了,直接给出),读完一个记录之后,当前记录的首地址加上该记录的长度就得到了下一个待读记录的首地址了。
顺序文件的优缺点
顺序文件的最佳应用场合是在对诸记录进行批量存取时,即每次要读或写一大批记录时。此时,对顺序文件的存取效率是所有逻辑文件中最高的;此外, 也只有顺序文件才能存储在磁带上,并能有效地工作。
在交互应用的场合,如果用户(程序)要求查找或修改单个记录,为此系统便要去逐个地查找诸记录。这时,顺序文件所表现出来的性能就可能很差,尤其是当文件较大时, 情况更为严重。如果是可变长记录的顺序文件,则为查找一个记录所需付出的开销将更大,这就限制了顺序文件的长度。
顺序文件的另一个缺点是,如果想增加或删除一个记录都比较困难。为了解决这一问题, 可以为顺序文件配置一个运行记录文件(Log File),
或称为事务文件
(Transaction File),把试图增加、删除或修改的信息记录于其中,规定每隔一定时间,例如 4 小时,将运行记录文件与原来的主文件加以合并,
产生一个按关键字排序的新文件。
索引文件
对于定长记录,除了可以方便地实现顺序存取外,还可较方便地实现直接存取(如同操作数组一般)。然而,对于变长记录就较难实现直接存取了, 因为用直接存取方法来访问变长记录文件中的一个记录是十分低效的(需要读取前面的记录并统计长度,最后才能到达需要访问的位置),其检索时间也很难令人接受。 为了解决这一问题,可为变长记录文件建立一张索引表,对主文件中的每个记录,在索引表中设有一个相应的表项, 用于记录该记录的长度L及指向该记录的指针(指向该记录在逻辑地址空间的首址)。由于索引表是按记录键排序的,因此,索引表本身是一个定长记录的顺序文件, 从而也就可以方便地实现直接存取。
在对索引文件进行检索时,首先是根据用户(程序)提供的关键字,并利用折半查找法去检索索引表,从中找到相应的表项;再利用该表项中给出的指向记录的指针值, 去访问所需的记录。而每当要向索引文件中增加一个新记录时,便须对索引表进行修改。由于索引文件可有较快的检索速度, 故它主要用于对信息处理的及时性要求较高的场合,例如,飞机订票系统。使用索引文件的主要问题是,它除了有主文件外,还须配置一张索引表, 而且每个记录都要有一个索引项,因此提高了存储费用。
索引顺序文件
索引顺序文件(Index Sequential File)可能是最常见的一种逻辑文件形式。它有效地克服了变长记录文件不便于直接存取的缺点,而且所付出的代价也不算太大。 前已述及,它是顺序文件和索引文件相结合的产物。它将顺序文件中的所有记录分为若干个组(例如,50个记录为一个组);为顺序文件建立一张索引表, 在索引表中为每组中的第一个记录建立一个索引项,其中含有该记录的键值和指向该记录的指针。索引顺序文件如下图所示。
在对索引顺序文件进行检索时,首先也是利用用户(程序)所提供的关键字以及某种查 找算法去检索索引表,找到该记录所在记录组中第一个记录的表项, 从中得到该记录组第 一个记录在主文件中的位置;然后,再利用顺序查找法去查找主文件,从中找到所要求的 记录。
但对于一个非常大的文件,为找到一个记录而须查找的记录数目仍然很多,为了进一步提高检索效率,可以为顺序文件建立多级索引,即为索引文件再建立一张索引表, 从而形成两级索引表。
直接文件
采用前述几种文件结构对记录进行存取时,都须利用给定的记录键值,先对线性表或链表进行检索,以找到指定记录的物理地址。然而对于直接文件,则可根据给定的记录键值, 直接获得指定记录的物理地址。换言之,记录键值本身就决定了记录的物理地址。这种由记录键值到记录物理地址的转换被称为键值转换(Key to address transformation)。 组织直接文件的关键,在于用什么方法(如哈希方法)进行从记录值到物理地址的转换。
哈希(Hash)文件
这是目前应用最为广泛的一种直接文件。它利用 Hash 函数(或称散列函数),可将记录键值转换为相应记录的地址。但为了能实现文件存储空间的动态分配, 通常由 Hash 函数所求得的并非是相应记录的地址,而是指向一目录表相应表目的指针,该表目的内容指向相应记录所在的物理块。
外存分配方式
由于磁盘具有可直接访问的特性,故当利用磁盘来存放文件时,具有很大的灵活性。在为文件分配外存空间时所要考虑的主要问题是: 怎样才能有效地利用外存空间和如何提高对文件的访问速度。目前,常用的外存分配方法有连续分配、链接分配和索引分配三种。通常,在一个系统中, 仅采用其中的一种方法来为文件分配外存空间。 如前所述,文件的物理结构直接与外存分配方式有关。在采用不同的分配方式时,将形成不同的文件物理结构。 例如,在采用连续分配方式时的文件物理结构,将是顺序式的文件结构;链接分配方式将形成链接式文件结构;而索引分配方式则将形成索引式文件结构。
连续分配
连续分配(Continuous Allocation)要求为每一个文件分配一组相邻接的盘块(也可能是编号相邻而已)。一组盘块的地址定义了磁盘上的一段线性地址。 通常,它们都位于一条磁道上,在进行读/写时,不必移动磁头,仅当访问到一条磁道的最后一个盘块后,才需要移到下一条磁道,于是又去连续地读/写多个盘块。
在采用连续分配方式时,可把逻辑文件中的记录顺序地存储到邻接的各物理盘块中,这样所形成的文件结构称为顺序文件结构
,此时的物理文件称为顺序文件
。
这种分配方式保证了逻辑文件中的记录顺序与存储器中文件占用盘块的顺序的一致性。为使系统能找到文件存放的地址,应在目录项的“文件物理地址”字段中,
记录该文件第一个记录所在的盘块号和文件长度(以盘块数进行计量)。
如同内存的动态分区分配一样,随着文件建立时空间的分配和文件删除时空间的回收,将使磁盘空间被分割成许多小块,这些较小的连续区已难于用来存储文件, 此即外存的碎片。同样,我们也可以利用紧凑的方法,将盘上所有的文件紧靠在一起,把所有的碎片拼接成一大片连续的存储空间。 这种方法能将含有多个文件的盘上的所有空闲盘块都集中在一起,从而消除了外部碎片。但为了将外存上的空闲空间进行一次紧凑, 所花费的时间远比将内存紧凑一次所花费的时间多得多。
连续分配的主要优点:
(1) 顺序访问容易。访问一个占有连续空间的文件非常容易。系统可从目录中找到该顺序文件所在的第一个盘块号,从此开始顺序地、逐个盘块地往下读/写。 连续分配也支持直接存取。例如,要访问一个从 b 块开始存放的文件中的第 i 个盘块的内容,就可直接访问 b+i 号盘块。
(2) 顺序访问速度快。因为由连续分配所装入的文件,其所占用的盘块可能是位于一条或几条相邻的磁道上,这时,磁头的移动距离最少,因此, 这种对文件访问的速度是几种存储空间分配方式中最高的一种。
连续分配的主要缺点:
(1) 要求有连续的存储空间。要为每一个文件分配一段连续的存储空间,这样,便会产生出许多外部碎片,严重地降低了外存空间的利用率。 如果是定期地利用紧凑方法来消除碎片,则又需花费大量的机器时间。
(2) 必须事先知道文件的长度。要将一个文件装入一个连续的存储区中,必须事先知道文件的大小,然后根据其大小,在存储空间中找出一块其大小足够的存储区, 将文件装入。在有些情况下,知道文件的大小是件非常容易的事,如可拷贝一个已存文件。但有时却很难,在此情况下,只能靠估算。如果估计的文件大小比实际文件小, 就可能因存储空间不 足而中止文件的拷贝,须再要求用户重新估算,然后再次执行。这样,显然既费时又麻烦。这就促使用户往往将文件长度估得比实际的大, 甚至使所计算的文件长度比实际长度大得多,显然,这会严重地浪费外存空间。对于那些动态增长的文件,由于开始时文件很小,在运行中逐渐增大, 比如,这种增长要经历几天、几个月。在此情况下,即使事先知道文件的最终大小,在采用预分配存储空间的方法时,显然也将是很低效的, 即它使大量的存储空间长期地空闲着。
链接分配
如同内存管理一样,连续分配所存在的问题就在于: 必须为一个文件分配连续的磁盘空间。如果在将一个逻辑文件存储到外存上时,并不要求为整个文件分配一块连续的空间,
而是可以将文件装到多个离散的盘块中,这样也就可以消除上述缺点。在采用链接分配(Chained Allocation)方式时,可通过在每个盘块上的链接指针,
将同属于一个文件的多个离散的盘块链接成一个链表,把这样形成的物理文件称为链接文件
。
链接分配的优势:
由于链接分配是采取离散分配方式,消除了外部碎片,故而显著地提高了外存空间的利用率;又因为是根据文件的当前需要,为它分配必需的盘块,当文件动态增长时, 可动态地再为它分配盘块,故而无需事先知道文件的大小。此外,对文件的增、删、改也十分方便。
链接方式:
链接方式又可分为隐式链接和显式链接两种形式。
- 隐式链接
在采用隐式链接分配方式时,在文件目录的每个目录项中,都须含有指向链接文件第一个盘块和最后一个盘块的指针。
隐式链接分配方的主要问题在于:它只适合于顺序访问,它对随机访问是极其低效的。如果要访问文件所在的第i个盘块,则必须先读出文件的第一个盘块……, 就这样顺序地查找直至第i块。当i=100时,须启动100次磁盘去实现读盘块的操作,平均每次都要花费几十毫秒。可见,随机访问的速度相当低。此外, 只通过链接指针来将一大批离散的盘块链接起来,其可靠性较差,因为只要其中的任何一个指针出现问题,都会导致整个链的断开。
为了提高检索速度和减小指针所占用的存储空间,可以将几个盘块组成一个簇(cluster)。比如,一个簇可包含 4 个盘块,在进行盘块分配时,是以簇为单位进行的。 在链接文件中的每个元素也是以簇为单位的。这样将会成倍地减小查找指定块的时间,而且也可减小指针所占用的存储空间,但却增大了内部碎片, 而且这种改进也是非常有限的。
- 显式链接
这是指把用于链接文件各物理块的指针,显式地存放在内存的一张链接表中。该表在整个磁盘仅设置一张,在每个表项中存放链接指针,即下一个盘块号。在该表中,
凡是属于某一文件的第一个盘块号,或者说是每一条链的链首指针所对应的盘块号,均作为文件地址被填入相应文件的 FCB的“物理地址”字段中。
由于查找记录的过程是在内存中进行的,因而不仅显著地提高了检索速度,而且大大减少了访问磁盘的次数。由于分配给文件的所有盘块号都放在该表中,
故把该表称为文件分配表 FAT
(File Allocation Table)。
索引分配
链接分配方式虽然解决了连续分配方式所存在的问题,但又出现了下述另外两个问题:
(1) 不能支持高效的直接存取。要对一个较大的文件进行直接存取,须首先在 FAT 中顺序地查找许多盘块号。
(2) FAT 需占用较大的内存空间。由于一个文件所占用盘块的盘块号是随机地分布在 FAT 中的,因而只有将整个 FAT 调入内存, 才能保证在 FAT 中找到一个文件的所有盘块号。当磁盘容量较大时,FAT 可能要占用数兆字节以上的内存空间,这是令人难以接受的。
单级索引分配
事实上,在打开某个文件时,只需把该文件占用的盘块的编号调入内存即可,完全没有必要将整个 FAT 调入内存。为此,应将每个文件所对应的盘块号集中地放在一起。 索引分配方法就是基于这种想法所形成的一种分配方法。它为每个文件分配一个索引块(表),再把分配给该文件的所有盘块号都记录在该索引块中, 因而该索引块就是一个含有许多盘块号的数组。在建立一个文件时,只需在为之建立的目录项中填上指向该索引块的指针。
索引分配方式支持直接访问。当要读文件的第i个盘块时,可以方便地直接从索引块中找到第 i 个盘块的盘块号;此外,索引分配方式也不会产生外部碎片。当文件较大时, 索引分配方式无疑要优于链接分配方式。
索引分配方式的主要问题是:
可能要花费较多的外存空间。每当建立一个文件时,便须为之分配一个索引块,将分配给该文件的所有盘块号记录于其中。但在一般情况下,总是中、小型文件居多, 甚至有不少文件只需 1~2 个盘块,这时如果采用链接分配方式,只需设置 1~2 个指针。如果采用索引分配方式,则同样仍须为之分配一索引块。 通常是采用一个专门的盘块作为索引块,其中可存放成百个、甚至上千个盘块号。可见,对于小文件采用索引分配方式时,其索引块的利用率将是极低的。
多级索引分配
当 OS 为一个大文件分配磁盘空间时,如果所分配出去的盘块的盘块号已经装满一个索引块时,OS便为该文件分配另一个索引块, 用于将以后继续为之分配的盘块号记录于其中。依此类推,再通过链指针将各索引块按序链接起来。显然,当文件太大,其索引块太多时,这种方法是低效的。 此时,应为这些索引块再建立一级索引,称为第一级索引,即系统再分配一个索引块,作为第一级索引的索引块,将第一块、第二块……等索引块的盘块号填入到此索引表中, 这样便形成了两级索引分配方式。如果文件非常大时,还可用三级、四级索引分配方式。
混合索引分配方式
所谓混合索引分配方式,是指将多种索引分配方式相结合而形成的一种分配方式。
- 1) 直接地址
为了提高对文件的检索速度,在索引结点中可设置 10 个直接地址项。换言之,在这里的每项中所存放的是该文件数据所在盘块的盘块号。假如每个盘块的大小为 4 KB, 当文件不大于 40 KB 时,便可直接从索引结点中读出该文件的全部盘块号。
- 2) 一次间接地址
对于大、中型文件,只采用直接地址是不现实的。为此,可再利用索引结点中的地址项中的一次间接地址。这种方式的实质就是一级索引分配方式。图中的一次间 址块也就是索引块,系统将分配给文件的多个盘块号记入其中。在一次间址块中可存放 1 K个盘块号,因而允许文件长达 4 MB。
- 3) 多次间接地址
当文件长度大于4 MB + 40 KB时(一次间址与10个直接地址项),系统还须采用二次间址分配方式。该方式的实质是两级索引分配方式。 系统此时是在二次间址块中记入所有一次间址块的盘号。在采用二次间址方式时,文件最大长度可达 4 GB。同理,地址项三次间接地址,其所允许的文件最大长度可达 4 TB。
目录管理
通常,在现代计算机系统中,都要存储大量的文件。为了能对这些文件实施有效的管理,必须对它们加以妥善组织,这主要是通过文件目录实现的。 文件目录也是一种数据结构,用于标识系统中的文件及其物理地址,供检索时使用。对目录管理的要求如下:
(1) 实现“按名存取”,即用户只须向系统提供所需访问文件的名字,便能快速准确地找到指定文件在外存上的存储位置。这是目录管理中最基本的功能, 也是文件系统向用户提供的最基本的服务。
(2) 提高对目录的检索速度。通过合理地组织目录结构的方法,可加快对目录的检索速度,从而提高对文件的存取速度。这是在设计一个大、 中型文件系统时所追求的主要目标。
(3) 文件共享。在多用户系统中,应允许多个用户共享一个文件。这样就须在外存中只保留一份该文件的副本,供不同用户使用,以节省大量的存储空间, 并方便用户和提高文件利用率。
(4) 允许文件重名。系统应允许不同用户对不同文件采用相同的名字,以便于用户按照自己的习惯给文件命名和使用文件。
文件控制块
为了能对一个文件进行正确的存取,必须为文件设置用于描述和控制文件的数据结构,称之为“文件控制块
(FCB)”。文件管理程序可借助于文件控制块中的信息,
对文件施以各种操作。文件与文件控制块一一对应,而人们把文件控制块的有序集合称为文件目录
,即一个文件控制块就是一个文件目录项。
通常,一个文件目录也被看做是一个文件,称为目录文件
。
为了能对系统中的大量文件施以有效的管理,在文件控制块中,通常应含有三类信息,即基本信息、存取控制信息及使用信息。
1) 基本信息类
- 文件标识和控制信息:文件名、用户名、文件主存取权限、授权者存取权限、文件口令、文件类型等。
- 文件逻辑结构信息:文件的逻辑结构,如记录类型、记录个数、记录长度、成组银子数等。
- 文件物理结构信息:文件所在设备名、文件物理结构类型、记录存放在辅助存储器的盘块号或文件信息首块盘块号,也可指出文件索引所在的位置等。
- 文件使用信息:共享文件的进程数、文件修改情况、文件最大长度和当前大小等。
- 文件管理信息:文件建立日期、最近修改日期等。
2) 存取控制信息类
存取控制信息类包括:文件主的存取权限、核准用户的存取权限以及一般用户的存取 权限。
3) 使用信息类
使用信息类包括: 文件的建立日期和时间、文件上一次修改的日期和时间及当前使用信息(这项信息包括当前已打开该文件的进程数、是否被其它进程锁住、 文件在内存中是否已被修改但尚未拷贝到盘上)。应该说明,对于不同 OS 的文件系统,由于功能不同,可能只含有上述信息中的某些部分。
索引结点
文件目录通常是存放在磁盘上的,当文件很多时,文件目录可能要占用大量的盘块。在查找目录的过程中,先将存放目录文件的第一个盘块中的目录调入内存, 然后把用户所给定的文件名与目录项中的文件名逐一比较。若未找到指定文件,便再将下一个盘块中的目录项调入内存。
稍加分析可以发现,在检索目录文件的过程中,只用到了文件名,仅当找到一个目录项(即其中的文件名与指定要查找的文件名相匹配)时,
才需从该目录项中读出该文件的物理地址。而其它一些对该文件进行描述的信息,在检索目录时一概不用。显然,这些信息在检索目录时不需调入内存。为此,
在有的系统中,如 UNIX 系统,便采用了把文件名与文件描述信息分开的办法,亦即,使文件描述信息单独形成一个称为索引结点的数据结构,简称为i 结点
。
在文件目录中的每个目录项仅由文件名和指向该文件所对应的i结点的指针所构成。
磁盘索引结点
这是存放在磁盘上的索引结点。每个文件有惟一的一个磁盘索引结点,它主要包括以下内容:
- (1) 文件主标识符,即拥有该文件的个人或小组的标识符。
- (2) 文件类型,包括正规文件、目录文件或特别文件。
- (3) 文件存取权限,指各类用户对该文件的存取权限。
- (4) 文件物理地址,每一个索引结点中含有 13 个地址项,它们以直接或间接方式给出数据文件所在盘块的编号。
- (5) 文件长度,指以字节为单位的文件长度。
- (6) 文件连接计数,表明在本文件系统中所有指向该(文件的)文件名的指针计数。
- (7) 文件存取时间,指本文件最近被进程存取的时间、最近被修改的时间及索引结点最近被修改的时间。
内存索引结点
这是存放在内存中的索引结点。当文件被打开时,要将磁盘索引结点拷贝到内存的索引结点中,便于以后使用。在内存索引结点中又增加了以下内容:
- (1) 索引结点编号,用于标识内存索引结点。
- (2) 状态,指示 i 结点是否上锁或被修改。
- (3) 访问计数,每当有一进程要访问此 i 结点时,将该访问计数加 1,访问完再减 1。
- (4) 文件所属文件系统的逻辑设备号。
- (5) 链接指针。设置有分别指向空闲链表和散列队列的指针。
目录结构
目录结构的组织,关系到文件系统的存取速度,也关系到文件的共享性和安全性。因此,组织好文件的目录,是设计好文件系统的重要环节。 目前常用的目录结构形式有单级目录、两级目录和多级目录。
单级目录结构
这是最简单的目录结构。在整个文件系统中只建立一张目录表,每个文件占一个目录项,目录项中含文件名、文件扩展名、文件长度、文件类型、 文件物理地址以及其它文件属性。此外,为表明每个目录项是否空闲,又设置了一个状态位。
每当要建立一个新文件时,必须先检索所有的目录项,以保证新文件名在目录中是惟一的。然后再从目录表中找出一个空白目录项,填入新文件的文件名及其它说明信息, 并置状态位为 1。删除文件时,先从目录中找到该文件的目录项,回收该文件所占用的存储空间,然后再清除该目录项。
单级目录结构住哟啊用于单用户操作系统,它具有如下优点:
- 结构简单,通过管理其目录文件,便可实现文件信息的管理;
- 实现按名存取。
同时,单级目录结构具有以下缺点:
- 文件较多时,目录检索时间长;
- 有命名冲突:在躲到程序系统中,尤其是多用户的分时系统中,重名很难避免,这就很难准确地找到用户需要的文件。显然,如果人工管理文件名注册,以避免命名冲突非常麻烦。
- 不便于共享:
通常,每个用户都有自己的名字空间或命名习惯。因此,应当允许不同用户使用不同的文件名来访问同一个文件。 然而,该目录结构要求所有用户用相同的名字访问同一个文件。
两级目录
为了克服单级目录所存在的缺点,可以为每一个用户建立一个单独的用户文件目录UFD(User File Directory)。这些文件目录具有相似的结构, 它由用户所有文件的文件控制块组成。此外,在系统中再建立一个主文件目录MFD(Master File Directory);在 主 文件目录中,每个用户目录文件都占有一个目录项, 其目录项中包括用户名和指向该用户目录文件的指针。
两级目录结构基本克服了单级目录结构的缺点而具有以下优点:
- 提高了检索目录的速度;
- 在不同的用户目录中可以使用相同的文件名;
- 不同用户可使用不同的文件名来访问系统中的同一个共享文件。
采用两级目录结构也存在一些问题。该结构虽然能有效地将多个用户隔开,在各用户之间完全无关时,这种隔离是一个优点; 但当多个用户之间要相互合作去完成一个大任务,且一用户又需去访问其他用户的文件时,这种隔离便成为一个缺点,因为这种隔离会使诸用户之间不便于共享文件。
多级目录结构
对于大型文件系统,通常采用三级或三级以上的目录结构,以提高对目录的检索速度和文件系统的性能。多级目录结构又称为树型目录结构
,主目录在这里被称为根目录,
把数据文件称为树叶,其它的目录均作为树的结点。
路径名
在树形目录结构中,从根目录到任何数据文件,都只有一条惟一的通路。在该路径上从树的根(即主目录)开始,把全部目录文件名与数据文件名依次地用“/” (不同系统使用的分隔符可能不同)连接起来,即构成该数据文件的路径名(path name)。系统中的每一个文件都有惟一的路径名。
当前目录
当一个文件系统含有许多级时,每访问一个文件,都要使用从树根开始直到树叶(数据文件)为止的、包括各中间节点(目录)名的全路径名。这是相当麻烦的事, 同时由于一个进程运行时所访问的文件大多仅局限于某个范围,因而非常不便。基于这一点,可为每个进程设置一个“当前目录”,又称为“工作目录”。 进程对各文件的访问都相对于“当前目录”而进行。此时各文件所使用的路径名,只需从当前目录开始,逐级经过中间的目录文件,最后到达要访问的数据文件。 把这一路径上的全部目录文件名与数据文件名用“/”连接形成路径名。
多级目录的优点:
就多级目录较两级目录而言,其查询速度更快,同时层次结构更加清晰,能够更加有效地进行文件的管理和保护。在多级目录中,不同性质、 不同用户的文件可以构成不同的目录子树,不同层次、不同用户的文件分别呈现在系统目录树中的不同层次或不同子树中,可以容易地赋予不同的存取权限。
多及目录的缺点:
但是在多级目录中查找一个文件,需要按路径名逐级访问中间节点,这就增加了磁盘访问次数,无疑将影响查询速度。
目录查询技术
当用户要访问一个已存在文件时,系统首先利用用户提供的文件名对目录进行查询,找出该文件的文件控制块或对应索引结点;然后, 根据 FCB 或索引结点中所记录的文件物理地址(盘块号),换算出文件在磁盘上的物理位置;最后,再通过磁盘驱动程序,将所需文件读入内存。 目前对目录进行查询的方式有两种: 线性检索法和 Hash 方法。
线性检索法
在一级目录结构中,利用用户提供的文件名,用顺序查找的方法直接从文件目录表中找到指定文件的目录项。在树形目录结构中,用户提供的文件名是由多个文件分量名组成的 路径名,此时需要对多级目录进行查找,即系统先读入第一个文件分量名,用它和根目录文件或当前目录文件中各个目录项进行比较,若找到匹配者,便可找到匹配项的文件控制块 或索引结点,然后再读入路径名中的第二个分量名,用它和相应的第二级文件目录中各个文件目录项的文件名顺序比较,若找到匹配项,再取下一个分量名,直至全部差玩, 最后可得到数据文件控制块或索引结点。若在查找过程中发现一个分量名没有查找到,则应停止查找并返回“文件未找到”的信息。
Hash 方法
如果我们建立了一张Hash索引文件目录,便可利用Hash 方法进行查询,即系统利用用户提供的文件名并将它变换为文件目录的索引值,再利用该索引值到目录中去查找, 这将显著地提高检索速度。
在进行文件名的转换时,有可能把n个不同的文件名转换为相同的Hash值,即出现了所谓的“冲突”。一种处理此“冲突”的有效规则是:
- (1) 在利用 Hash 法索引查找目录时,如果目录表中相应的目录项是空的,则表示系统中并无指定文件。
- (2) 如果目录项中的文件名与指定文件名相匹配,则 表 示该目录项正是所要寻找的文件所对应的目录项,故而可从中找到该文件所在的物理地址。
- (3) 如果在目录表的相应目录项中的文件名与指定文件名并不匹配,则表示发生了“冲突”,此时须将其Hash值再加上一个常数(该常数应与目录的长度值互质), 形成新的索引值,
顺便指出,在现代操作系统中,通常都提供了模式匹配功能,即在文件名中使用了通配符“*”、“?”等。对于使用了通配符的文件名, 系统此时便无法利用 Hash 方法检索目录,因此,这时系统还是需要利用线性查找法查找目录。
文件存储空间的管理
文件管理要解决的重要问题之一是如何为新创建的文件分配存储空间。其分配方法与内存的分配有许多相似之处,即同样可采取连续分配方式或离散分配方式。 前者具有较高的文件访问速度,但可能产生较多的外存零头;后者能有效地利用外存空间,但访问速度较慢。不论哪种分配方式, 文件存储空间的基本分配单位都是磁盘块而非字节。
为了实现存储空间的分配,系统首先必须能记住存储空间的使用情况。为此,
- 系统应为分配存储空间而设置相应的数据结构;
- 系统应提供对存储空间进行分配和回收的手段。
下面介绍几种常用的文件存储空间的管理方法。
空闲表法
空闲表法属于连续分配方式,它与内存的动态分配方式雷同,它为每个文件分配一块连续的存储空间,即系统也为外存上的所有空闲区建立一张空闲表, 每个空闲区对应于一个空闲表项,其中包括表项序号、该空闲区的第一个盘块号、该区的空闲盘块数等信息。再将所有空闲区按其起始盘块号递增的次序排列。
存储空间的分配与回收
空闲盘区的分配与内存的动态分配类似,同样是采用首次适应算法、循环首次适应算法等。例如,在系统为某新创建的文件分配空闲盘块时,先顺序地检索空闲表的各表项, 直至找到第一个其大小能满足要求的空闲区,再将该盘区分配给用户(进程),同时修改空闲表。系统在对用户所释放的存储空间进行回收时,也采取类似于内存回收的方法, 即要考虑回收区是否与空闲表中插入点的前区和后区相邻接,对相邻接者应予以合并。
应该说明,在内存分配上,虽然很少采用连续分配方式,然而在外存的管理中,由于这种分配方式具有较高的分配速度,可减少访问磁盘的I/O频率, 故它在诸多分配方式中仍占有一席之地。例如,在前面所介绍的对换方式中,对对换空间一般都采用连续分配方式。对于文件系统,当文件较小(1~4个盘块)时, 仍采用连续分配方式,为文件分配相邻接的几个盘块;当文件较大时,便采用离散分配方式。
空闲链表法
空闲链表法是将所有空闲盘区拉成一条空闲链。根据构成链所用基本元素的不同,可把链表分成两种形式:空闲盘块链和空闲盘区链。
- (1) 空闲盘块链。
空闲盘块链是将磁盘上的所有空闲空间,以盘块为单位拉成一条链。当用户因创建文件而请求分配存储空间时,系统从链首开始,依次摘下适当数目的空闲盘块分配给用户。 当用户因删除文件而释放存储空间时,系统将回收的盘块依次插入空闲盘块链的末尾。这种方法的优点是用于分配和回收一个盘块的过程非常简单, 但在为一个文件分配盘块时,可能要重复操作多次。
- (2) 空闲盘区链。
空闲盘区链是将磁盘上的所有空闲盘区(每个盘区可包含若干个盘块)拉成一条链。在每个盘区上除含有用于指示下一个空闲盘区的指针外, 还应有能指明本盘区大小(盘块数)的信息。分配盘区的方法与内存的动态分区分配类似,通常采用首次适应算法。在回收 盘区时, 同样也要将回收区与相邻接的空闲盘区相合并。在采用首次适应算法时,为了提高对空闲盘区的检索速度,可以采用显式链接方法,亦即, 在内存中为空闲盘区建立一张链表。
位示图法
位示图是利用二进制的一位来表示磁盘中一个盘块的使用情况。当其值为“0”时,表示对应的盘块空闲;为“1”时,表示已分配。有的系统把“0”作为盘块已分配的标志, 把“1”作为空闲标志。磁盘上的所有盘块都有一个二进制位与之对应,这样,由所有盘块所对应的位构成一个集合,称为位示图。通常可用 m ×n 个位数来构成位示图, 并使 m ×n 等于磁盘的总块数,
盘块的分配
根据位示图进行盘块分配时,可分三步进行:
(1) 顺序扫描位示图,从中找出一个或一组其值为“0”的二进制位(“0”表示空闲时)。
(2) 将所找到的一个或一组二进制位转换成与之相应的盘块号。假定找到的其值为“0”的二进制位位于位示图的第i行、第j列,则其相应的盘块号应按下式计算:
b = n(i - 1) + j
式中,n代表每行的位数。
(3) 修改位示图,令 map[i,j]=1。
盘块的回收
盘块的回收分两步:
(1) 将回收盘块的盘块号转换成位示图中的行号和列号。转换公式为:
i = (b - 1) DIV n + 1
j = (b - 1) MOD n + 1
(2) 修改位示图。令 map[i,j] =0。
位图法评价
这种方法的主要优点是,从位示图中很容易找到一个或一组相邻接的空闲盘块。例如,我们需要找到6个相邻接的空闲盘块, 这只需在位示图中找出6个其值连续为“0”的位即可。此外,由于位示图很小,占用空间少,因而可将它保存在内存中,进而使在每次进行盘区分配时, 无需首先把盘区分配表读入内存,从而节省了许多磁盘的启动操作。因此,位示图常用于微型机和小型机中,如 CP/M、Apple-DOS 等 OS 中。
成组链接法
空闲表法和空闲链表法都不适用于大型文件系统,因为这会使空闲表或空闲链表太长。在 UNIX 系统中采用的是成组链接法, 这是将上述两种方法相结合而形成的一种空闲盘块管理方法,它兼备了上述两种方法的优点而克服了两种方法均有的表太长的缺点。
空闲盘块的组织
空闲盘块的分配与回收
当系统要为用户分配文件所需的盘块时,须调用盘块分配过程来完成。该过程首先检查空闲盘块号栈是否上锁,如未上锁,便从栈顶取出一空闲盘块号, 将与之对应的盘块分配给用户,然后将栈顶指针下移一格。若该盘块号已是栈底,即 S.free(0),这是当前栈中最后一个可分配的盘块号。 由于在该盘块号所对应的盘块中记有下一组可用的盘块号,因此,须调用磁盘读过程,将栈底盘块号所对应盘块的内容读入栈中,作为新的盘块号栈的内容, 并把原栈底对应的盘块分配出去(其中的有用数据已读入栈中)。然后,再分配一相应的缓冲区(作为该盘块的缓冲区)。最后,把栈中的空闲盘块数减1并返回。
在系统回收空闲盘块时,须调用盘块回收过程进行回收。它是将回收盘块的盘块号记入空闲盘块号栈的顶部,并执行空闲盘块数加 1 操作。 当栈中空闲盘块号数目已达 100 时,表示栈已满,便将现有栈中的100个盘块号记入新回收的盘块中,再将其盘块号作为新栈底。
文件共享
在现代计算机系统中,必须提供文件共享手段,即系统应允许多个用户(进程)共享同一份文件。这样,在系统中只需保留该共享文件的一份副本。 如果系统不能提供文件共享功能,就意味着凡是需要该文件的用户,都须各自备有此文件的副本,显然这会造成对存储空间的极大浪费。随着计算机技术的发展, 文件共享的范围也在不断扩大,从单机系统中的共享,扩展为多机系统的共享,进而又扩展为计算机网络范围的共享,甚至实现全世界的文件共享。
基于索引结点的共享方式
在树型结构的目录中,当有两个(或多个)用户要共享一个子目录或文件时,必须将共享文件或子目录链接到两个(或多个)用户的目录中,才能方便地找到该文件,
如下图所示。此时该文件系统的目录结构已不再是树型结构,而是个有向无环图 DAG
(Directed Acyclic Graph)。
可以通过将待共享的原始文件的文件信息拷贝到共享该文件的目录中,不过该方法如同通过复制文件一般(虽然没有这么消耗空间),很难保持共享文件的一致性, 行为一旦共享的文件被修改(特别是增删物理块),那么文件信息发生改变,但另一方的备份文件信息并没有改变,从而出现不一致的情况,除非重新拷贝文件信息 (当然有多少用户共享该文件,就要重新拷贝多少份)。
为了解决这个问题,可以引用索引结点,即诸如文件的物理地址及其它的文件属性等信息,不再是放在目录项中,而是放在索引结点中。 在文件目录中只设置文件名及指向相应索引结点的指针。只要该文件的首地址没有改变,那么共享仍然有效,而且总是一致的。
在索引结点中还应有一个链接计数 count,用于表示链接到本索引结点(亦即文件)上的用户目录项的数目。当 count=3 时,表示有三个用户目录项连接到本文件上, 或者说是有三个用户(包括文件的创建者或拥有者)共享此文件。对于共享文件,即使是创建者当 count > 1 时也不能删除该文件,只是让 count 减 1; 只有 count = 1 时才能删除该文件,
利用符号链实现文件共享
为使 B 能共享 C 的一个文件 F,可以由系统创建一个 LINK 类型的新文件,也取名为 F,并将 F 写入 B 的目录中,以实现 B 的目录与文件F的链接。
在新文件中只包含被链接文件 F 的路径名。这样的链接方法被称为符号链接
(Symbolic Linking)(类似 Windows 中的快捷方式)。
新文件中的路径名则只被看作是符号链(Symbolic Link),当 B 要访问被链接的文件 F 且正要读 LINK 类新文件时,此要求将被 OS 截获,
OS 根据新文件中的路径名去读该文件,于是就实现了用户 B 对文件 F的共享。
符号链实现共享方式的优点:
在利用符号链方式实现文件共享时,只是文件主才拥有指向其索引结点的指针;而共享该文件的其他用户则只有该文件的路径名,并不拥有指向其索引结点的指针。 这样,也就不会发生在文件主删除一共享文件后留下一悬空指针的情况。当文件的拥有者把一个共享文件删除后, 其他用户试图通过符号链去访问一个已被删除的共享文件时,会因系统找不到该文件而使访问失败,于是再将符号链删除,此时不会产生任何影响。
符号链方式有一个很大的优点,是它能够用于链接(通过计算机网络)世界上任何地方的计算机中的文件, 此时只需提供该文件所在机器的网络地址以及该机器中的文件路径即可。
符号链实现共享方式的优点:
然而符号链的共享方式也存在自己的问题: 当其他用户去读共享文件时,系统是根据给定的文件路径名,逐个分量(名)地去查找目录,直至找到该文件的索引结点。 因此,在每次访问共享文件时,都可能要多次地读盘。这使每次访问文件的开销甚大,且增加了启动磁盘的频率。此外,要为每个共享用户建立一条符号链, 而由于该链实际上是一个文件,尽管该文件非常简单,却仍要为它配置一个索引结点,这也要耗费一定的磁盘空间。
基于索引结点的共享方式和利用符号链实现文件共享方式都存在这样一个共同的问题,即每一个共享文件都有几个文件名。换言之,每增加一条链接, 就增加一个文件名。这在实质上就是每个用户都使用自己的路径名去访问共享文件。当我们试图去遍历(traverse)整个文件系统时,将会多次遍历到该共享文件。 例如,当有一个程序员要将一个目录中的所有文件都转储到磁带上去时,就可能对一个共享文件产生多个拷贝。
磁盘容错技术
磁盘容量越来越大,存储的文件也越来越多,一旦出错可能是毁灭性的,这也就带来了不安全性。影响文件安全性的主要因素有三:
- (1) 人为因素,即由于人们有意或无意的行为,而 使 文件系统中的数据遭到破坏或丢失。
- (2) 系统因素,即由于系统的某部分出现异常情况,而造成对数据的破坏或丢失。特别是作为数据存储介质的磁盘,在出现故障或损坏时, 会对文件系统的安全性造成影响;
- (3) 自然因素,即存放在磁盘上的数据,随着时间的推移将可能发生溢出或逐渐消失。
为了确保文件系统的安全性,可针对上述原因而采取以下措施:
- (1) 通过存取控制机制来防止由人为因素所造成的文件不安全性。
- (2) 通过磁盘容错技术来防止由磁盘部分的故障所造成的文件不安全性。
- (3) 通过“后备系统”来防止由自然因素所造成的不安全性。
本小节主要讨论磁盘容错技术。
容错技术
是通过在系统中设置冗余部件的办法,来提高系统可靠性的一种技术。磁盘容错技术则是通过增加冗余的磁盘驱动器、磁盘控制器等方法,
来提高磁盘系统可靠性的一种技术,即当磁盘系统的某部分出现缺陷或故障时,磁盘仍能正常工作,且不致造成数据的丢失或错误。
目前广泛采用磁盘容错技术来改善磁盘系统的可靠性。
磁盘容错技术往往也被人们称为系统容错技术 SFT。可把它分成三个级别:
- 第一级是低级磁盘容错技术;
- 第二级是中级磁盘容错技术;
- 第三级是系统容错技术,它基于集群技术实现容错。
第一级容错技术SFT-Ⅰ
第一级容错技术(SFT-I是最基本的一种磁盘容错技术,主要用于防止因磁盘表面缺陷所造成的数据丢失。它包含双份目录、双份文件分配表及写后读校验等措施。
双份目录和双份文件分配表
在磁盘上存放的文件目录和文件分配表 FAT,是文件管理所用的重要数据结构。为了防止这些表格被破坏,可在不同的磁盘上或在磁盘的不同区域中, 分别建立(双份)目录表和 FAT。其中一份为主目录及主 FAT;另一份为备份目录及备份 FAT。一旦由于磁盘表面缺陷而造成主文件目录或主 FAT 的损坏时, 系统便自动启用备份文件目录及备份 FAT,从而可以保证磁盘上的数据仍是可访问的。
热修复重定向和写后读校验
由于磁盘价格昂贵,当磁盘表面有少量缺陷时,则可采取某种补救措施后继续使用磁盘。一般主要采取以下两个补救措施:
(1) 热修复重定向:系统将磁盘容量的一部分(例如 2%~3%)作为热修复重定向区,用于存放当发现磁盘有缺陷时的待写数据,并对写入该区的所有数据进行登记, 以便于以后对数据进行访问。
(2) 写后读校验方式。为了保证所有写入磁盘的数据都能写入到完好的盘块中,应该在每次从内存缓冲区向磁盘中写入一个数据块后,又立即从磁盘上读出该数据块, 并送至另一缓冲区中,再将该缓冲区内容与内存缓冲区中在写后仍保留的数据进行比较。若两者一致,便认为此次写入成功,可继续写下一个盘块;否则,再重写。 若重写后两者仍不一致,则认为该盘块有缺陷,此时,便将应写入该盘块的数据,写入到热修复重定向区中。
第二级容错技术 SFT-II
第二级容错技术主要用于防止由磁盘驱动器和磁盘控制器故障所导致的系统不能正常工作,它具体又可分为磁盘镜像和磁盘双工。
- 1) 磁盘镜像(Disk Mirroring)
为了避免磁盘驱动器发生故障而丢失数据,便增设了磁盘镜像功能。为实现该功能,须在同一磁盘控制器下再增设一个完全相同的磁盘驱动器,当采用磁盘镜像方式时, 在每次向主磁盘写入数据后,都需要将数据再写到备份磁盘上,使两个磁盘上具有完全相同的位像图。
磁盘镜像虽然实现了容错功能,但未能使服务器的磁盘 I/O 速度得到提高,却使磁盘的利用率降至仅为 50%。
- 2) 磁盘双工(Disk Duplexing)
如果控制这两台磁盘驱动器的磁盘控制器发生故障,或主机到磁盘控制器之间的通道发生了故障,磁盘镜像功能便起不到数据保护的作用。因此,在第二级容错技术中, 又增加了磁盘双工功能,即将两台磁盘驱动器分别接到两个磁盘控制器上,同样使这两台磁盘机镜像成对。
基于集群技术的容错功能
所谓集群,是指由一组互连的自主计算机组成统一的计算机系统,给人们的感觉是,它们是一台机器。利用集群系统不仅可提高系统的并行处理能力, 还可用于提高系统的可用性,它们是当前使用最广泛的一类具有容错功能的集群系统。其主要工作模式有三种:
- ① 热备份模式;
- ② 互为备份模式;
- ③ 公用磁盘模式。
下面我们介绍如何利用集群系统来提高服务器的可用性。
1) 双机热备份模式
在这种模式的系统中,备有两台服务器,两者的处理能力通常是完全相同的,一台作为主服务器,另一台作为备份服务器。平时主服务器运行, 备份服务器则时刻监视着主服务器的运行,一旦主服务器出现故障,备份服务器便立即接替主服务器的工作而成为系统中的主服务器,修复后的服务器再作为备份服务器。
为使在这两台服务器间能保持镜像关系,应在这两台服务器上各装入一块网卡,并通过一条镜像服务器链路MSL(Mirrored Server Link)将两台服务器连接起来。 两台服务器之间保持一定的距离,其所允许的距离取决于所配置的网卡和传输介质。
此外,还必须在系统中设置某种机制,来检测主服务器中数据的改变。一旦该机制检测到主服务器中有数据变化, 便立即通过通信系统将修改后的数据传送到备份服务器的相应数据文件中。为了保证在两台服务器之间通信的高速性和安全性,通常都选用高速通信信道,并有备份线路。
在这种模式下,一旦主服务器发生故障,系统能自动地将主要业务用户切换到备份服务器上。为 保证切换时间足够快(通常为数分钟), 要求在系统中配置有切换硬件的开关设备,在备份服务器上事先建立好通信配置,并能迅速处理客户机的重新登录等事宜。
该模式是早期使用的一种集群技术,它的最大优点是提高了系统的可用性,易于实现,而且主、备份服务器完全独立,可支持远程热备份,从而能消除由于火灾、 爆炸等非计算机因素所造成的隐患。该模式的主要缺点是从服务器处于被动等待状态,整个系统的使用效率只有 50%。
2) 双机互为备份模式
在双机互为备份的模式中,平时,两台服务器均为在线服务器,它们各自完成自己的任务,例如,一台作为数据库服务器,另一台作为电子邮件服务器。 为了实现两者互为备份,在两台服务器之间,应通过某种专线连接起来。如果希望两台服务器之间能相距较远,最好利用 FDDI 单模光纤来连接两台服务器, 在此情况下,最好再通过路由器将两台服务器互连起来,作为备份通信线路。
在互为备份的模式中,最好在每台服务器内都配置两台硬盘,一个用于装载系统程序和应用程序,另一个用于接收由另一台服务器发来的备份数据,作为该服务器的镜像盘。 在正常运行时,镜像盘对本地用户是锁死的,这样就较易于保证在镜像盘中数据的正确性。如果仅有一个硬盘, 则可用建立虚拟盘的方式或分区方式来分别存放系统程序和应用程序,以及另一台服务器的备份数据。
如果通过专线链接检查到某台服务器发生了故障,此时,再通过路由器去验证这台服务器是否真的发生了故障。如果故障被证实, 则由正常服务器向故障服务器的客户机发出广播信息,表明要进行切换。连接到故障服务器上的客户机在切换过程中会感觉到网络服务器的短暂停顿。在切换成功后, 客户机无需重新登录便可继续使用网络提供的服务和访问服务器上的数据。而对于接在非故障服务器上的客户机,则只会感觉到网络服务稍有减慢而已,不会有任何影响。 当故障服务器修复并重新连到网上后,已被迁移到无故障服务器上的服务功能将被返回,恢复正常工作。
这种模式的优点是两台服务器都可用于处理任务,因而系统效率较高,现在已将这种模式从两台机器扩大到 4 台、8 台、16 台甚至更多。 系统中所有的机器都可用于处理任务,当其中一台发生故障时,系统可指定另一台机器来接替它的工作。
3) 公用磁盘模式
为了减少信息复制的开销,可以将多台计算机连接到一台公共的磁盘系统上去。该公共磁盘被划分为若干个卷。每台计算机使用一个卷。如果某台计算机发生故障, 此时系统将重新进行配置,根据某种调度策略来选择另一台替代机器,后者对发生故障的机器的卷拥有所有权,从而来接替故障计算机所承担的任务。这种模式的优点是: 消除了信息的复制时间,因而减少了网络和服务器的开销。
数据一致性控制
数据一致性,是数据应用中必须解决的一个重要问题。事实上,只要把一个数据分别存储到多个文件中时,便可能使数据一致性出现问题。
为了保证在不同文件中所存储的同一个数据相一致,在现代操作系统乃至数据库系统中,都配置了能保证数据一致性的软件,以及相应的支持硬件。
硬件支持主要是要求在系统中能配置一个高度可靠的存储器系统,或称之为稳定存储器
(Stable Storage)。实现一个稳定存储器的措施是采用冗余技术。亦即,
将一份信息同时存放在多个独立的、非易失性存储器(Nonvolatile Storage)上。目前,广泛采用磁盘双工方式来实现稳定存储器。
事务
事务是用于访问和修改各种数据项的一个程序单位。事务也可以被看做是一系列相关读和写操作。被访问的数据可以分散地存放在同一文件的不同记录中, 也可放在多个文件中。只有对分布在不同位置的同一数据所进行的读和写(含修改)操作全部完成时,才能再以托付操作(Commit Operation)来终止事务。只要有一个读、 写或修改操作失败,便须执行夭折操作(Abort Operation)。读或写操作的失败可能是由于逻辑错误,也可能是系统故障所导致的。
一个夭折的事务,通常已执行了一些操作,因而可能已对某些数据做了修改。为使夭折的事务不会引起数据的不一致性,须将该事务内刚被修改的数据项恢复成原来的情况, 使系统中各数据项与该事务未执行时的数据项内容完全相同。此时,可以说该事务“已被退回”(rolled back)。不难看出,一个事务在对一批数据执行修改操作时, 要么全部完成,并用修改后的数据去代替原来的数据,要么一个也不修改。事务操作所具有的这种特性,就是“原子性”。
事务记录
为了实现上述的原子修改,通常须借助于称为事务记录的数据结构来实现。这些数据结构被放在稳定存储器中,用来记录在事务运行时数据项修改的全部信息, 故又称为运行记录(Log)。该记录中包括有下列字段:
- 事务名:用于标识该事务的惟一名字;
- 数据项名:指被修改数据项的惟一名字;
- 旧值:修改前数据项的值;
- 新值:修改后数据项将具有的值。
在事务记录表中的每一记录,描述了在事务运行中的重要事务操作,如修改操作、开始事务、托付事务或夭折事务等。
恢复算法
由于一组被事务 Ti 修改的数据以及它们被修改前和修改后的值都能在事务记录表中找到,因此,利用事务记录表, 系统能处理任何故障而不致使故障造成非易失性存储器中信息的丢失。恢复算法可利用以下两个过程:
- (1) undo〈Ti〉。该过程把所有被事务 Ti 修改过的数据恢复为修改前的值。
- (2) redo〈Ti〉。该过程把所有被事务 Ti 修改过的数据设置为新值。
如果系统发生故障,系统应对以前所发生的事务进行清理。通过查找事务记录表,可以把尚未清理的事务分成两类。一类是其所包含的各类操作都已完成的事务。 确定为这一类事务的依据是,在事务记录表中,既包含了〈Ti 开始〉记录,又包含了〈Ti 托付〉记录。此时系统利用 redo〈Ti〉过程,把所有已被修改的数据设置成新值。 另一类是其所包含的 各个操作并未全部完成的事务。对于事务 Ti,如果在 Log 表中只有〈Ti 开始〉记录而无 〈 Ti托付〉记录,则此 Ti 便属于这类事务。 此时,系统便利用 undo〈Ti〉过程,将所有已被修改的数据,恢复为修改前的值。
检查点
如前所述,当系统发生故障时,必须去检查整个 Log 表,以确定哪些事务需要利用 redo〈Ti〉过程去设置新值,而哪些事务需要利用undo〈Ti〉过程去恢复数据的旧值。 由于在系统中可能存在着许多并发执行的事务,因而在事务记录表中就会有许多事务执行操作的记录。随着时间的推移,记录的数据也会愈来愈多。因此, 一旦系统发生故障,在事务记录表中的记录清理起来就非常费时。
引入检查点的主要目的,
是使对事务记录表中事务记录的清理工作经常化,即每隔一定时间便做一次下述工作: 首先是将驻留在易失性存储器(内存)中的当前事务记录表中的所有记录输出到稳定存储器中;其次是将驻留在易失性存储器中的所有已修改数据输出到稳定存储器中; 然后是将事务记录表中的〈检查点〉记录输出到稳定存储器中;最后是每当出现一个〈检查点〉记录时,系统便执行上小节所介绍的恢复操作, 利用 redo和 undo 过程实现恢复功能。
如果一个事务 Ti 在检查点前就做了托付,则在事务记录表中便会出现一个在检查点记录前的〈Ti托付〉记录。在这种情况下,所有被 Ti 修改过的数据, 或者是在检查点前已写入稳定存储器,或者是作为检查点记录自身的一部分写入稳定存储器中。因此,以后在系统出现故障时,就不必再执行 redo 操作了。
新的恢复算法
在引入检查点后,可以大大减少恢复处理的开销。因为在发生故障后,并不需要对事务记录表中的所有事务记录进行处理,而只需对最后一个检查点之后的事务记录进行处理。 因此,恢复例程首先查找事务记录表,确定在最近检查点以前开始执行的最后的事务 Ti。在找到这样的事务后,再返回去搜索事务记录表,便可找到第一个检查点记录, 恢复例 程便从该检查点开始,返回搜索各个事务的记录,并利用 redo 和 undo 过程对它们进行处理。
如果把所有在事务 Ti 以后开始执行的事务表示为事务集 T,则新的恢复操作要求是: 对所有在 T 中的事务 TK,如果在事务记录表中出现了〈TK 托付〉记录, 则执行 redo〈TK〉操作;反之,如果在事务记录表中并未出现〈TK托付〉记录,则执行undo〈TK〉操作。
并发控制
在多用户系统和计算机网络环境下,可能有多个用户在同时执行事务。由于事务具有原子性,这使各个事务的执行必然是按某种次序依次执行的,只有在一个事务执行完后,
才允许另一事务执行,即各事务对数据项的修改是互斥的。人们把这种特性称为顺序性
(Serializability)。
把用于实现事务顺序性的技术称为并发控制
(Concurrent Control)。
虽然可以利用信号量机制来保证事务处理的顺序性(例如,令所有的事务共享一互斥信号量,每当一事务开始执行时,便执行 wait(mutex)操作,在事务正常或异常结束时, 再执行 signal(mutex)操作),但在数据库系统和文件服务器中,应用得最多的还是较简单且较灵活的同步机制——锁。
利用互斥锁实现“顺序性”
实现顺序性的一种最简单的方法是,设置一种用于实现互斥的锁,简称为互斥锁
(Exclusive Lock)。在利用互斥锁实现顺序性时,应为每一个共享对象设置一把互斥锁。
当一事务 Ti要去访问某对象时,应先获得该对象的互斥锁。若成功,便用该锁将该对象锁住,于是事务 Ti 便可对该对象执行读或写操作; 而其它事务由于未能获得该锁而不能访问该对象。如果 Ti需要对一批对象进行访问,则为了保证事务操作的原子性,Ti 应先获得这一批对象的互斥锁, 以将这些对象全部锁住。如果成功,便可对这一批对象执行读或写操作;操作完成后又将所有这些锁释放。但如果在这一批对象中的某一个对象已被其它事物锁住, 则此时 Ti 应对此前已被 Ti 锁住的其它对象进行开锁,宣布此次事务运行失败,但不致引起数据的变化。
利用互斥锁和共享锁实现顺序性
利用互斥锁实现顺序性的方法简单易行。目前有不少系统都是采用这种方法来保证事务操作的顺序性,但这却存在着效率不高的问题。
因为一个共享文件虽然只允许一个事务去写,但却允许多个事务同时去读;而在利用互斥锁来锁住文件后,则只允许一个事务去读。
为了提高运行效率而又引入了另一种形式的锁——共享锁
(Shared Lock)。共享锁与互斥锁的区别在于: 互斥锁仅允许一个事务对相应对象执行读或写操作,
而共享锁则允许多个事务对相应对象执行读操作,不允许其中任何一个事务对对象执行写操作。
在为一个对象设置了互斥锁和共享锁的情况下,如果事务 Ti 要对 Q 执行读操作,则只需去获得对象 Q 的共享锁。如果对象 Q 已被互斥锁锁住,则 Ti 必须等待; 否则,便可获得共享锁而对 Q 执行读操作。如果 Ti 要对 Q 执行写操作,则 Ti 还须去获得 Q 的互斥锁。若失败,须等待;否则,可获得互斥锁而对 Q 执行写操作。 利用共享锁和互斥锁来实现顺序性的方法,非常类似于读者—写者问题的解法。
重复数据的数据一致性问题
为了保证数据的安全性,最常用的做法是把关键文件或数据结构复制多份,分别存储在不同的地方,当主文件(数据结构)失效时,还有备份文件(数据结构)可以使用, 不会造成数据丢失,也不会影响系统工作。显然,主文件(数据结构)中的数据应与各备份文件中的对应数据相一致。此外,还有些数据结构(如空闲盘块表)在系统运行过程中, 总是不断地对它进行修改,因此,同样应保证不同处的同一数据结构中数据的一致性。
重复文件的一致性
在有重复文件时,如果一个文件拷贝被修改,则必须也同时修改其它几个文件拷贝,以保证各相应文件中数据的一致性。这可采用两种方法来实现:
- 第一种方法是当一个文件被修改后,可查找文件目录,以得到其它几个拷贝的索引结点号,再从这些索引结点中找到各拷贝的物理位置,然后对这些拷贝做同样的修改;
- 第二种方法是为新修改的文件建立几个拷贝,并用新拷贝去取代原来的文件拷贝。
盘块号一致性的检查
如果正在修改时,机器突然发生故障,此时也会使盘块数据结构中的数据产生不一致性现象。因此,在每次启动机器时,都应该检查相应的多个数据结构, 看它们之间是否保持了数据的一致性。
为了保证盘块数据结构(中数据)的一致性,可利用软件方法构成一个计数器表,每个盘块号占一个表项,可有0,…,N-1 项,N 为盘块总数。每一个表项中包含两个计数器, 分别用作空闲盘块号计数器和数据盘块号计数器。分配时对空闲块号进行计数,写好数据后再对该块号进行数据块号计数,如此可以保证分配和写入过程的完整完成,如果 出现错误,这该过程将不会完成,那么这两个计数就不会都完成。
如果情况正常,则上述两组计数器中对应的一对(计数器中的)数据应该互补,亦即,若 某个盘块号在被第一组计数器进行计数后,使该盘块号计数器为 1, 则在第二组计数器中相应盘块号计数器中的计数必为 0;反之亦然。但如果情况并非如此,则说明发生了某种错误。
链接数一致性检查
在 UNIX 类型的文件目录中,其每个目录项内都含有一个索引结点号,用于指向该文件的索引结点。对于一个共享文件,其索引结点号会在目录中出现多次。 另一方面,在该共享文件的索引结点中有一个链接计数 count,用来指出共享本文件的用户(进程)数。在正常情况下这两个数据应该一致,否则就会出现数据不一致性差错。
为了检查这种数据不一致性差错,同样要配置一张计数器表,此时应是为每个文件而不是为每个盘块建立一个表项,其中含有该索引结点号的计数值。在进行检查时, 从根目录开始查找,每当在目录中遇到该索引结点号时,便在该计数器表中相应文件的表项上加1。当把所有目录都检查完后, 便可将该计数器表中每个表项中的索引结点号计数值与该文件索引结点中的链接计数count值加以比较,如果两者一致,表示是正确的;否则, 便是产生了链接数据不一致的错误。
如果索引结点中的链接计数 count 值大于计数器表中相应索引结点号的计数值,则 即使在所有共享此文件的用户都不再使用此文件时,其 count 值仍不为 0, 因而该文件不会被删除。这种错误的后果是使一些已无用户需要的文件仍驻留在磁盘上,浪费了存储空间。当然这种错误的性质并不严重。 解决的方法是用计数器表中的正确的计数值去为 count 重新赋值。
反之,如果出现 count 值小于计数器表中索引结点号计数值的情况时,就有潜在的危险。假如有两个用户共享一个文件,但是 count 值仍为 1,这样, 只要其中有一个用户不再需要此文件时,count 值就会减为 0,从而使系统将此文件删除,并释放其索引结点及文件所占用的盘块, 导致另一共享此文件的用户所对应的目录项指向了一个空索引结点,最终是使该用户再无法访问此文件。如果该索引结点很快又被分配给其它文件, 则又会带来潜在的危险。解决的方法是将 count 值置为正确值。
虚拟文件系统
操作系统接口
操作系统是用户与计算机硬件系统之间的接口,用户通过操作系统的帮助,可以快速、有效和安全、可靠地操纵计算机系统中的各类资源,以处理自己的程序。 为使用户能方便地使用操作系统,OS又向用户提供了如下两类接口:
(1) 用户接口:操作系统专门为用户提供了“用户与操作系统的接口”,通常称为用户接口。该接口支持用户与OS之间进行交互,即由用户向OS请求提供特定的服务, 而系统则把服务的结果返回给用户。
(2) 程序接口:操作系统向编程人员提供了“程序与操作系统的接口”,简称程序接口,又称应用程序接口API(Application Programming Interface)。 该接口是为程序员在编程时使用的,系统和应用程序通过这个接口,可在执行中访问系统中的资源和取得OS的服务,它也是程序能取得操作系统服务的惟一途径。 大多数操作系统的程序接口是由一组系统调用(system call)组成,每一个系统调用都是一个能完成特定功能的子程序。
值得说明的是,在计算机网络中,特别是在Internet广为流行的今天,又出现了一种面向网络的网络用户接口。
系统调用
程序接口是OS专门为用户程序设置的,也是用户程序取得OS服务的唯一途径。程序接口通常是由各种类型的系统调用所组成的,因而,也可以说,系统调用提供了用户程序 和操作系统之间的接口,应用程序通过系统调用实现其与OS的通信,并可取得它的服务。系统调用不仅可供所有的应用程序使用,而且也可供OS自身的其它部分, 尤其是命令处理程序使用。在每个系统中,通常都有几十条甚至上百条的系统调用,并可根据其功能而把它们划分成若干类。例如, 有用于进程控制(类)的系统调用和用于文件管理(类)、设备管理(类)及进程通信等类的系统调用。
系统态和用户态
在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态: 系统态(也称为管态或核心态)和用户态(也称为目态)。操作系统在系统态运行,而应用程序只能在用户态运行。在实际运行过程中,处理机会在系统态和用户态间切换。 相应地,现代多数操作系统将CPU的指令集分为特权指令和非特权指令两类。
- 1) 特权指令
所谓特权指令
,就是在系统态时运行的指令,是关系到系统全局的指令。其对内存空间的访问范围基本不受限制,不仅能访问用户存储空间,也能访问系统存储空间,
如启动各种外部设备、设置系统时钟时间、关中断、清主存、修改存储器管理寄存器、执行停机指令、转换执行状态等。特权指令只允许操作系统使用,
不允许应用程序使用,否则会引起系统混乱。
- 2) 非特权指令
非特权指令
是在用户态时运行的指令。一般应用程序所使用的都是非特权指令,它只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,
其对内存的访问范围也局限于用户空间。这样,可以防止应用程序的运行异常对系统造成的破坏。
这种限制是由硬件实现的,如果在应用程序中使用了特权指令,就会发出权限出错信号,操作系统捕获到这个信号后,将转入相应的错误处理程序, 并将停止该应用程序的运行,重新调度。
系统调用的基本概念
通常,在OS的核心中都设置了一组用于实现各种系统功能的子程序(过程),并将它们提供给应用程序调用。由于这些程序或过程是OS系统本身程序模块中的一部分, 为了保护操作系统程序不被用户程序破坏,一般都不允许用户程序访问操作系统的程序和数据,所以也不允许应用程序采用一般的过程调用方式来直接调用这些过程, 而是向应用程序提供了一系列的系统调用命令,让应用程序通过系统调用去调用所需的系统过程。
系统调用在本质上是应用程序请求OS内核完成某功能时的一种过程调用,但它是一种特殊的过程调用,它与一般的过程调用有下述几方面的明显差别:
(1) 运行在不同的系统状态。一般的过程调用,其调用程序和被调用程序都运行在相同的状态——系统态或用户态;而系统调用与一般调用的最大区别就在于: 调用程序是运行在用户态,而被调用程序是运行在系统态。
(2) 状态的转换通过软中断进入。由于一般的过程调用并不涉及到系统状态的转换,可直接由调用过程转向被调用过程。但在运行系统调用时, 由于调用和被调用过程是工作在不同的系统状态,因而不允许由调用过程直接转向被调用过程。通常都是通过软中断机制,先由用户态转换为系统态,经核心分析后, 才能转向相应的系统调用处理子程序。
(3) 返回问题。在采用了抢占式(剥夺)调度方式的系统中,在被调用过程执行完后,要对系统中所有要求运行的进程做优先权分析。当调用进程仍具有最高优先级时, 才返回到调用进程继续执行;否则,将引起重新调度,以便让优先权最高的进程优先执行。此时,将把调用进程放入就绪队列。
(4) 嵌套调用。像一般过程一样,系统调用也可以嵌套进行,即在一个被调用过程的执行期间,还可以利用系统调用命令去调用另一个系统调用。 当然,每个系统对嵌套调用的深度都有一定的限制,例如最大深度为6。但一般的过程对嵌套的深度则没有什么限制。
中断机制:
系统调用是通过中断机制实现的,并且一个操作系统的所有系统调用都通过同一个中断入口来实现。 应用程序通过该中断获取操作系统的服务。
对于拥有保护机制的操作系统来说,中断机制本身也是受保护的,只有授权给应用程序保护等级的中断号,才是可以被应用程序调用的。对于未被授权的中断号, 如果应用程序进行调用,同样会引起保护异常,而导致自己被操作系统停止。
系统调用的实现
通常,一个 OS所具有的许多功能,可以从其所提供的系统调用上表现出来。显然,由于各OS的性质不同,在不同的OS中所提供的系统调用之间也会有一定的差异。 对于一般通用的OS而言,可将其所提供的系统调用分为:进程控制、文件操纵、通信管理和系统维护等几大类。
系统调用的实现与一般过程调用的实现相比,两者间有很大差异。对于系统调用,控制是由原来的用户态转换为系统态,这是借助于中断和陷入机制来完成的, 在该机制中包括中断和陷入硬件机构及中断与陷入处理程序两部分。当应用程序使用OS的系统调用时,产生一条相应的指令,CPU 在执行这条指令时发生中断, 并将有关信号送给中断和陷入硬件机构,该机构收到信号后,启动相关的中断与陷入处理程序进行处理,实现该系统调用所需要的功能。
中断和陷入硬件机构
中断
是指 CPU 对系统发生某事件时的这样一种响应: CPU暂停正在执行的程序,在保留现场后自动地转去执行该事件的中断处理程序;执行完后,
再返回到原程序的断点处继续执行。
还可进一步把中断分为外中断和内中断。所谓外中断
,是指由于外部设备事件所引起的中断,如通常的磁盘中断、打印机中断等;
而内中断
则是指由于 CPU 内部事件所引起的中断,如程序出错(非法指令、地址越界)、电源故障等。内中断(trap)也被译为“捕获”或“陷入”。通常,
陷入是由于执行了现行指令所引起的;而中断则是由于系统中某事件引起的,该事件与现行指令无关。由于系统调用引起的中断属于内中断,
因此把由于系统调用引起中断的指令称为陷入指令
。
中断和陷入向量:
为了处理上的方便,通常都是针对不同的设备编制不同的中断处理程序,并把该程序的入口地址放在某特定的内存单元中。此外,
不同的设备也对应着不同的处理机状态字 PSW,且把它放在与中断处理程序入口指针相邻接的特定单元中。在进行中断处理时,只要有了这样两个字,
便可转入相应设备的中断处理程序,重新装配处理机的状态字和优先级,进行对该设备的处理。因此,我们把这两个字称为中断向量
。相应地,
把存放这两个字的单元称为中断向量单元。类似地,对于陷入,也有陷入向量,不同的系统调用对应不同的陷入向量,在进行陷入处理时,根据陷入指令中的陷入向量,
转入实现相应的系统调用功能的子程序,即陷入处理程序。由所有的中断向量和陷入向量构成了中断和陷入向量表。
系统调用的处理步骤
在设置了系统调用号和参数后,便可执行一条系统调用命令。不同的系统可采用不同的执行方式。在 UNIX系统中,是执行 CHMK 命令; 而在 MS-DOS 中则是执行 INT 21 软中断。
系统调用的处理过程可分成以下三步:
首先,将处理机状态由用户态转为系统态;之后,由硬件和内核程序进行系统调用的一般性处理,即首先保护被中断进程的CPU环境, 将处理机状态字 PSW、程序计数器 PC、系统调用号、用户栈指针以及通用寄存器内容等,压入堆栈;然后,将用户定义的参数传送到指定的地址保存起来。
其次,是分析系统调用类型,转入相应的系统调用处理子程序。为使不同的系统调用能方便地转向相应的系统调用处理子程序,在系统中配置了一张系统调用入口表。 表中的每个表目都对应一条系统调用,其中包含该系统调用自带参数的数目、系统调用处理子程序的入口地址等。因此,核心可利用系统调用号去查找该表, 即可找到相应处理子程序的入口地址而转去执行它。
最后,在系统调用处理子程序执行完后,应恢复被中断的或设置新进程的 CPU 现场,然后返回被中断进程或新进程,继续往下执行。
系统调用处理子程序的处理过程
系统调用的功能主要是由系统调用子程序来完成的。对于不同的系统调用,其处理程序将执行不同的功能。我们以一条在文件操纵中常用的Creat命令为例来说明之。
进入 Creat 的处理子程序后,核心将根据用户给定的文件路径名 Path,利用目录检索过程去查找指定文件的目录项。查找目录的方式可以用顺序查找法, 也可用 Hash 查找法。
- 如果在文件目录中找到了指定文件的目录项,表示用户要利用一个已有文件来建立一个新文件。
- 但如果在该已有(存)文件的属性中有不允许写属性,或者创建者不具有对该文件进行修改的权限,便认为是出错而做出错处理;
- 若不存在访问权限问题,便将已存文件的数据盘块释放掉,准备写入新的数据文件。
- 如未找到指名文件,则表示要创建一个新文件,核心便从其目录文件中找出一个空目录项,并初始化该目录项,包括填写文件名、文件属性、文件建立日期等,然后将新建文件打开。
【注意】本文属于作者原创,欢迎转载!转载时请注明以下内容:
(转载自)ShengChangJian's Blog编程技术文章地址:
https://ShengChangJian.github.io/2016/11/OS-basic.html
主页地址:https://shengchangjian.github.io/