操作系统

333 阅读1小时+

1. 操作系统概述

1.1 基本概念

  • 计算机系统自下而上可粗分为四个部分:硬件、操作系 统、应用程序和用户(这里的划分与计算机组成原理的分层不同)
  • 操作系统是介于应用和硬件之间的一层软件,其屏蔽硬件的操作细节,对上层软件提供硬件的抽象,以控制和管理整个计算架系统的硬件和软件资源,并合理地组织调度计算机的工作和资源的分配,以提供给用户和其他软件方便的接口和环境的程序集合
  • 分类:单道批处理系统、多道批处理系统、分时系统、实时系统、网络与分布式系统、多机系统

1.2 操作系统特征

  • 操作系统的基本特征为:并发、共享、虚拟、异步

并发(Concurrence)

  • 并发指的是同一段时间内多个程序交替执行
  • 注意区别并行和并发,前者是同一时刻的同时执行多个进程,后者是同一时间段内的多个程序交替执行
  • 并行性需要有相关硬件的支持,如多流水线或多处理机硬件环境,而并发性是操作系统提供的特性

共享(Sharing)

  • 共享即资源共享,是指系统中的资源可以被内存中多个并发执行的进线程共同使用
  • 共享分为互斥共享方式和同时访问方式
  • 互斥共享方式:规定在一段时间内只允许一个进程访问的资源,对于该类资源,使用前必须先提出请求,若资源空闲才允许使用,若资源已被其他进程占用,则进入阻塞状态,直至获取到对应资源才可重新进入就绪状态;计算机系统中的大多数物理设备,以及某些软件中所用的栈、变量和表格,都属于临界资源,它们都要求被互斥地共享
  • 同时访问方式:系统中还有另一类资源,允许在一段时间内由多个进程“同时”对它们进行访问,这里所谓的“同时”往往是宏观上的,而在微观上,这些进程可能是交替地对该资源进行访问即 “分时共享”,例如典型的磁盘设备

虛拟(Virtual)

  • 虚拟指的是把一个物理上的实体变为若干个逻辑上的对应物,物理实体是实际存在的,而后者是虚拟的,是用户感觉上的事物
  • 操作系统中利用了多种虚拟技术,分别用来实现虚拟处理器、虚拟内存和虚拟外部设备等
  • 虚拟处理器:
    • 利用多道程序设计技术,把一个物理上的 CPU 虚拟为多个逻辑上的 CPU,称为虚拟处理器
    • 虚拟处理器通过通过多道程序设计技术来让多道程序并发执行,以分时使用一个处理器,虽然只有一个处理器,但它能同时为多个用户服务,使每个终端用户都感觉有一个中央处理器(CPU)在专门为它服务
  • 虚拟存储器:将一台机器的物理存储器变为虚拟存储器,以便从逻辑上来扩充存储器的容量;当然这时用户所感觉到的内存容量是虚的,我们把用户所感觉到的存储器(实际是不存在的)称为虚拟存储器
  • 虚拟设备:将一台物理 I/O 设备虚拟为多台逻辑上的 I/O 设备,并允许每个用户占用一台逻辑上的 I/O 设备,这样便可以使原来仅允许在一段时间内由一个用户访问的设备(即临界资源),变为在一段时间内允许多个用户同时访问的共享设备
  • 操作系统的虚拟技术可归纳为:时分复用技术,如处理器的分时共享;空分复用技术,如虚拟存储器

异步(Asynchronism)

  • 异步:系统中的进程是以走走停停的方式执行的,且以一种不可预知的速度推进

1.3 操作系统的目标和功能

1.3.1 功能

  • 操作系统主要提供的功能有:处理机管理、 存储器管理、设备管理和文件管理

处理机管理(进程线程管理)

  • 主要任务:管理进程和/线程
  • 详细内容:管理进程何时创建、何时撤销、如何调度和管理、如何避免冲突、合理共享
  • 在多道程序环境下,处理机的分配和运行都以进程(或线程)为基本单位,因而对处理机的管理可归结为对进程的管理,因此处理机管理也可称作进程线程管理

存储器管理(内存管理)

  • 主要任务:管理内存,使得能给多道程序的运行提供良好的环境,方便用户使用以及提高内存的利用率
  • 详细内容:内存分配、地址映射、内存保护与共享和内存扩充

文件管理

  • 主要任务:计算机中的信息都是以文件的形式存在的,文件管理就负责管理这些文件
  • 详细内容:文件存储空间的管理、目录管理及文件读写管理和保护
  • 操作系统中负责文件管理的部分称为文件系统

设备管理

  • 主要任务:完成用户的 I/O 请求,方便用户使用各种设备,并提高设备的利用率
  • 详细内容:缓冲管理、设备分配、设备处理和虛拟设备

1.3.2 目标

  • 为了方便用户使用操作系统,还必须向用户提供接口
  • 同时操作系统可用来扩充机器,以提供更方便的服务、更高的资源利用率
  • 接口主要分为两类:一类是命令接口,给用户用的,例如 cmd; 另一类是程序接口,一般是给程序员用的,如 GUI

1.4 发展与分类

  • 手工操作阶段(此阶段无操作系统)
  • 批处理阶段(操作系统开始出现):单道批处理系统、多道批处理系统
  • 分时操作系统:时间片技术进行时分复用,给每个用户的感觉好像是自己独占一台计算机
  • 实时操作系统:在某个时间限制内完成某些紧急任务而不需时间片排队,诞生了实时操作系统
  • 网络操作系统和分布式计算机系统
  • 个人计算机操作系统:目前使用最广泛的操作系统,广泛应用于文字处理、电子表格、 游戏等
  • 此外还有嵌入式操作系统、服务器操作系统、多处理器操作系统等
  • 操作系统的发展历程图大致如图所示
    操作系统发展历程

1.5 操作系统的运行机制

  • 计算机系统中,通常 CPU 会执行两种不同性质的程序:一种是操作系统内核程序; 另一种是用户自编程序或系统外层的应用程序
  • 对操作系统而言,这两种程序的作用不同,内核程序往往需要执行一些特权指令,而用户自定义程序处于安全考虑不能执行这些指令
  • 所谓特权指令,是指计算机中不允许用户直接使用的指令,如 I/O 指令、置中断指令、存取用于内存保护的寄存器、送程序状态字到程序状态字寄存器等指令
  • 出于上述原因,操作系统严格划分了用户态和内核态,以严格区分这两类程序,只有工作在内核态的内核程序才能执行这些特权指令
  • 在软件工程思想和结构程序设计方法的影响下诞生的现代操作系统,几乎都是层次式的结构,操作系统的各项功能分别被设置在不同的层次上
  • 一些与硬件关联较紧密的模块,诸如时钟管理、中断处理、设备驱动等处于最底层
  • 其次是运行频率较髙的程序,诸如进程管理、存储器管理和设备管理等
  • 这两部分内容构成了操作系统的内核,这些内容都工作在 OS 的内核态
  • 内核是计算机上配置的底层软件,是计算机功能的延伸,不同的操作系统对内核的定义稍有区别,但大多操作系统采用微内核结构,内核大致包含 4 个方面的内容:时钟管理、中断机制、原语、系统控制的数据结构及处理

1.5.1 时钟管理

  • 在计算机的各种部件中,时钟是最关键的设备
  • 时钟的第一功能是计时,操作系统需要通过时钟管理,向用户提供标准的系统时间
  • 通过时钟中断的管理,可以实现进程的切换
  • 例如,在分时操作系统中,釆用时间片轮转调度的实现;在实时系统中,按截止时间控制运行的实现;在批处理系统中,通过时钟管理来衡量一个作业的运行程度等;因此,系统管理的方方面面无不依赖于时钟

1.5.2 中断机制

  • 引入中断技术的初衷是提高多道程序运行环境中 CPU 的利用率,而且主要是针对外部设备的,后来逐步得到发展,形成了多种类型,成为操作系统各项操作的基础
  • 例如,键盘或鼠标信息的输入、进程的管理和调度、系统功能的调用、设备驱动、文件访问等,无不依赖于中断机制,可以说,现代操作系统是靠中断驱动的软件
  • 中断机制中,只有一小部分功能属于内核,负责保护和恢复中断现场的信息,转移控制权到相关的处理程序,这样可以减少中断的处理时间,提高系统的并行处理能力

1.5.3 原语

  • 按层次结构设计的操作系统,底层必然是一些可被调用的公用小程序,它们各自完成一个规定的操作,其特点是 :
    • 它们处于操作系统的最底层,是最接近硬件的部分
    • 这些程序的运行具有原子性:其操作只能一气呵成(这主要是从系统的安全性和便于管理考虑的)
    • 这些程序的运行时间都较短,而且调用频繁
  • 通常把具有这些上述的程序称为原语(Atomic Operation),定义原语的直接方法是关闭中断,让它的所有动作不可分割地进行完再打开中断
  • 可以看到,原语中的指令不能被中断,必须一气呵成的执行
  • 系统中的设备驱动、CPU切换、进程通信等功能中的部分操作都可以定义为原语,使它们成为内核的组成部分

1.5.4 系统控制的数据结构及处理

  • 系统中用来登记状态信息的数据结构很多,比如作业控制块、进程控制块(PCB)、设备控制块、各类链表、消息队列、缓冲区、空闲区登记表、内存分配表等
  • 为了实现有效的管理,系统需要一些基本的操作,常见的操作有以下三种 :
    • 进程管理:进程状态管理、进程调度和分派、创建与撤销进程控制块等
    • 存储器管理:存储器的空间分配和回收、内存信息保护程序、代码对换程序等
    • 设备管理:缓冲区管理、设备分配和回收等

1.5.5 总结

  • 从上述内容可以看出,内核态指令实际上包括系统调用类指令和一些针对时钟、中断和原语的操作指令
  • 我们之后要学习的处理器管理、存储器管理、设备管理都工作在内核态(文件管理呢?)

1.6 中断和异常的概念

  • 在 OS 中引入内核态与用户态后,就需要考虑这两种状态间的切换
  • 操作系统的内核工作在内核态,而用户程序工作在用户态,OS 不允许用户实现内核态的功能,但用户程序又必须使用这些功能,因此 OS 需要提供一些方式使得能从用户态进入内核态
  • 在实际 OS 中,用户态程序进入内核态的途径就是通过中断异常
  • 当中断或异常发生时,运行用户态的 CPU 会立即进入核心态,这是通过硬件实现的(例如,用一个特殊寄存器的一位来表示CPU 所处的工作状态,0 表示核心态,1 表示用户态。若要进入核心态,只需将该位置 0 即可)
  • 中断是操作系统中非常重要的一个概念,对一个运行在计算机上的实用操作系统而言,缺少了中断机制,将是不可想象的

1.6.1 中断(Interruption)

  • 中断(Interruption),也称外中断,指来自 CPU 执行指令以外的事件的发生
  • 比如设备发出的 I/O 结束中断,表示设备输入/输出处理已经完成,希望处理机能够向设备发下一个输入/输出请求,同时让完成输入/输出后的程序继续运行
  • 再比如时钟中断,表示一个固定的时间片已到,让处理机处理计时、启动定时运行的任务等
  • 这一类中断通常是与当前正在运行程序无关的事件,即它们与当前处理机运行的程序无关

1.6.2 异常(Exception)

  • 异常(Exception),也称内中断、例外或陷入(Trap),指源自 CPU 执行指令内部的事件,如程序的非法操作码、地址越界、算术溢出、虚存系统的缺页以及专门的陷入指令等引起的事件
  • 对异常的处理一般要依赖于当前程序的运行现场,而且异常不能被屏蔽,一旦出现应立即处理
  • 内中断、外中断的区别与联系大致如图所示
    内中断与外中断

1.7 系统调用

  • 系统调用:用户在程序中调用操作系统所提供的一些子功能,系统调用可以被看做特殊的公共子程序
  • 系统中的各种共享资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作(如存储分配、进行 I/0 传输以及管理文件等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成
  • 通常,一个操作系统提供的系统调用命令有几十乃至上百条之多,这些系统调用按功能大致可分为如下几类 :
    • 设备管理:完成设备的请求或释放,以及设备启动等功能
    • 文件管理:完成文件的读、写、创建及删除等功能
    • 进程控制:完成进程的创建、撤销、阻塞及唤醒等功能
    • 进程通信:完成进程之间的消息传递或信号传递等功能
    • 内存管理:完成内存的分配、回收以及获取作业占用内存区大小及始址等功能
  • 系统调用运行在系统的核心态,通过系统调用的方式来使用系统功能,可以保证系统的稳定性和安全性,防止用户随意更改或访问系统的数据或命令
  • 系统调用命令是由操作系统提供的一个或多个子程序模块实现的
  • 操作系统的运行环境可以理解为:
    • 用户通过操作系统运行上层程序(如系统提供的命令解释程序或用户自编程序),而这个上层程序的运行依赖于操作系统的底层管理程序提供服务支持
    • 当需要管理程序服务时,系统则通过硬件中断机制进入核心态,运行管理程序;也可能是程序运行出现异常情况,被动地需要管理程序的服务,这时就通过异常处理来进入核心态
    • 当管理程序运行结束时,用户程序需要继续运行,则通过相应的保存的程序现场退出中断处理程序或异常处理程序,返回断点处继续执行
  • 在操作系统这一层面上,我们关心的是系统内核态和用户态的软件实现和切换,对于硬件层面的具体理解,可以结合“计算机组成原理”课程中有关中断的内容进行学习
  • 下面列举一些由用户态转向核心态的例子 :
    • 用户程序要求操作系统的服务,即系统调用
    • 发生一次中断
    • 用户程序中产生了一个错误状态
    • 用户程序中企图执行一条特权指令
    • 从内核态转向用户态由一条指令实现,这条指令也是特权命令,一般是中断返回指令
  • 注意,由用户态进入核心态,不仅仅是状态需要切换,所使用的堆栈也可能需要由用户堆栈切换为系统堆栈,但这个系统堆栈也是属于该进程的

1.8 操作系统的体系结构

  • 操作系统在内核态为应用程序提供公共的服务,那么操作系统在核心态应该提供什么服务、怎样提供服务?有关这个问题的回答形成了两种主要的体系结构:大内核和微内核
  • 大内核系统将操作系统的主要功能模块都作为一个紧密联系的整体运行在核心态,从而为应用提供高性能的系统服务,因为各管理模块之间共享信息,能有效利用相互之间的有效特性,所以具有无可比拟的性能优势
  • 但随着体系结构和应用需求的不断发展,需要操作系统提供的服务越来越多,而且接口形式越来越复杂,操作系统的设计规模也急剧增长,操作系统也面临着“软件危机”困境
  • 为解决操作系统的内核代码难以维护的问题,于是提出了微内核的体系结构,它将内核中最基本的功能(如进程管理等)保留在内核,而将那些不需要在核心态执行的功能移到用户态执行,从而降低了内核的设计复杂性
  • 而那些移出内核的操作系统代码根据分层的原则被划分成若干服务程序,它们的执行相互独立,交互则都借助于微内核进行通信
  • 微内核结构有效地分离了内核与服务、服务与服务,使得它们之间的接口更加清晰,维护的代价大大降低,各部分可以独立地优化和演进,从而保证了操作系统的可靠性
  • 微内核结构的最大问题是性能问题,因为需要频繁地在核心态和用户态之间进行切换,操作系统的执行开销偏大
  • 因此有的操作系统将那些频繁使用的系统服务又移回内核,从而保证系统性能
  • 但是有相当多的实验数据表明,体系结构不是引起性能下降的主要因素,体系结构带来的性能提升足以弥补切换开销带来的缺陷
  • 为减少切换开销,也有人提出将系统服务作为运行库链接到用户程序的一种解决方案,这样的体系结构称为库操作系统

1.9 本章疑难点

  • 并行性与并发性的区别和联系
  • 特权指令与非特权指令
  • 访管指令与访管中断

2. 进程和线程管理

2.1 进程的概念与特征

2.1.1 进程的概念

  • 在多道程序环境下,允许多个程序并发执行,此时它们将失去封闭性,并具有间断性及不可再现性的特征
  • 引入了进程(Process)的概念,以便更好地描述和控制程序的并发执行,实现操作系统的并发性和共享性
  • 为了使参与并发执行的程序(含数据)能独立地运行,必须为之配置一个专门的数据结构,称为进程控制块(Process Control Block, PCB)
  • 系统利用 PCB 来描述进程的基本情况和运行状态,进而控制和管理进程
  • 由程序段、相关数据段和PCB三部分构成了进程映像(进程实体)
  • 所谓创建进程,实质上是创建进程映像中的 PCB
  • 而撤销进程,实质上是撤销进程的 PCB
  • 值得注意的是,进程映像是静态的,进程则是动态的
  • 注意,PCB是进程存在的唯一标志
  • 从不同的角度,进程可以有不同的定义,比较典型的定义有 :
    • 进程是程序的一次执行过程
    • 进程是一个程序及其数据在处理机上顺序执行时所发生的活动
    • 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位
  • 在引入进程实体的概念后,我们可以把传统操作系统中的进程定义为:进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位

2.1.2 进程的特征

  • 进程是由多程序的并发执行而引出的,它和程序是两个截然不同的概念,进程的基本特征是对比单个程序的顺序执行提出的,也是对进程管理提出的基本要求
  • 动态性:进程是程序的一次执行,它有着创建、活动、暂停、终止等过程,具有一定的生命周期,是动态地产生、变化和消亡的,动态性是进程最基本的特征
  • 并发性:指多个进程实体,同存于内存中,能在一段时间内同时运行,并发性是进程的重要特征,同时也是操作系统的重要特征;引入进程的目的就是为了使程序能与其他进程的程序并发执行,以提高资源利用率
  • 独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位,凡未建立 PCB 的程序都不能作为一个独立的单位参与运行
  • 异步性:由于进程的相互制约,使进程具有执行的间断性,即进程按各自独立的、 不可预知的速度向前推进;异步性会导致执行结果的不可再现性,为此,在操作系统中必须配置相应的进程同步机制
  • 结构性:每个进程都配置一个 PCB 对其进行描述,从结构上看,进程实体是由程序段、数据段和进程控制段三部分组成的

2.2 进程的状态与转换

  • 进程在其生命周期内,由于系统中各进程之间的相互制约关系及系统的运行环境的变化,使得进程的状态也在不断地发生变化(一个进程会经历若干种不同状态)
  • 通常进程有以下五种状态,其中运行、就绪、阻塞是进程的基本状态
  • 运行状态:进程正在处理机上运行,在单处理机环境下,每一时刻最多只有一个进程处于运行状态
  • 就绪状态:进程已处于准备运行的状态,即进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行
  • 阻塞状态:又称等待状态,进程正在等待某一事件而暂停运行,如等待某资源为可用(不包括处理机)或等待输入/输出完成;即使处理机空闲,阻塞状态的进程也不能运行
  • 创建状态:进程正在被创建,尚未转到就绪状态;创建进程通常需要多个步骤:首先申请一个空白的 PCB,并向 PCB 中填写一些控制和管理进程的信息;然后由系统为该进程分配运行时所必需的资源;最后把该进程转入到就绪状态
  • 结束状态:进程正从系统中消失,这可能是进程正常结束或其他原因中断退出运行;当进程需要结束运行时,系统首先必须置该进程为结束状态,然后再进一步处理资源释放和回收等工作
  • 注意区别就绪状态和阻塞状态:
    • 就绪状态是指进程仅缺少处理机,只要获得处理机资源就立即执行,而阻塞状态是指进程需要其他资源(除了处理机)或等待某一事件
    • 之所以把处理机和其他资源划分开,是因为在分时系统的时间片轮转机制中,每个进程分到的时间片是若干毫秒,也就是说,进程得到处理机的时间很短且非常频繁,进程在运行过程中实际上是频繁地转换到就绪状态的(时间片用完)
    • 而其他资源(如外设)的使用和分配或者某一事件的发生(如I/O操作的完成)对应的时间相对来说很长,进程转换到等待状态的次数也相对较少
    • 因此就绪状态和阻塞状态是进程生命周期中两个完全不同的状态,很显然需要加以区分
  • 下图说明了进程个状态间的转换关系
    进程的状态转换

2.3 进程控制:进程的创建、终止、阻塞、唤醒和切换

  • 进程控制的主要功能是对系统中的所有进程实施有效的管理,主要包括进程的创建、终止、阻塞、唤醒和切换
  • 在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位
  • 上述进程控制的相关操作都是原语,即不可分割,是同步执行的
  • 对于通常的进程,其创建、撤销以及要求由系统设备完成的 I/O 操作都是利用系统调用而进入内核,再由内核中相应处理程序予以完成的

2.3.1 进程的创建

  • 允许一个进程创建另一个进程:
    • 此时创建者称为父进程,被创建的进程称为子进程
    • 子进程可以继承父进程所拥有的资源
    • 当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程
    • 此外,在撤销父进程时,也必须同时撤销其所有的子进程
  • 在操作系统中,终端用户登录系统、作业调度、系统提供服务、用户程序的应用请求等都会引起进程的创建
  • 操作系统创建一个新进程的过程如下(创建原语):
    • 为新进程分配一个唯一的进程标识号,并申请一个空白的 PCB(PCB 是有限的),若 PCB 申请失败则创建失败
    • 为新进程的程序和数据以及用户栈分配必要的内存空间(若内存不足?我猜是创建失败)
    • 初始化 PCB,主要包括初始化标志信息、初始化处理机状态信息和初始化处理机控制信息,以及设置进程的优先级等
    • 如果进程就绪队列能够接纳新进程,就将新进程插入到就绪队列,等待被调度运行

2.3.2 进程的终止

  • 引起进程终止的事件主要有:
    • 正常结束:表示进程的任务已经完成和准备退出运行
    • 异常结束:进程在运行时,发生了某种异常事件,使程序无法继续运行,如存储区越界、保护错、非法指令、特权指令错、I/O 故障等
    • 外界干预:进程应外界的请求而终止运行,如操作员或操作系统干预、父进程请求和父进程终止
  • 操作系统终止进程的过程如下(终止原语):
    • 根据被终止进程的标识符,检索 PCB,从中读出该进程的状态
    • 若被终止进程处于执行状态,立即终止该进程的执行,将处理机资源分配给其他进程
    • 若该进程还有子进程,则应将其所有子进程终止。
    • 将该进程所拥有的全部资源,或归还给其父进程或归还给操作系统
    • 将该 PCB 从所在队列(链表)中删除

2.3.3 进程的阻塞和唤醒

阻塞

  • 可能导致阻塞的事件:如请求系统资源失败、等待某种操作的完成、新数据尚未到达、等待新任务到达等
  • 正在执行的进程,由于遇到上述事件,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态
  • 可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得 CPU),才可能将其转为阻塞状态
  • 阻塞原语的执行过程是:
    • 找到将要被阻塞进程的标识号对应的 PCB
    • 若该进程为运行状态,则保护其现场,将其状态转为阻塞状态,停止运行,并将 PCB 中的进程状态由执行改为阻塞
    • 把该 PCB 插入到相应事件的等待队列中去

唤醒

  • 当被阻塞进程所期待的事件出现时,如它所启动的 I/O 操作已完成或其所期待的数据已到达,则由有关进程(比如,提供数据的进程)调用唤醒原语(Wakeup),将等待该事件的进程唤醒
  • 唤醒原语的执行过程是:
    • 在该事件的等待队列中找到相应进程的 PCB
    • 将其从等待队列中移出,并将其状态由阻塞变为就绪状态
    • 把该 PCB 插入就绪队列中,等待调度程序调度
  • 需要注意的是,Block 原语和 Wakeup 原语是一对作用刚好相反的原语,必须成对使用,Block 原语是由被阻塞进程自我调用实现的,而 Wakeup 原语则是由一个与被唤醒进程相合作或被其他相关的进程调用实现的,否则阻塞进程将会音不能被唤醒而永久地处于阻塞状态,再无机会继续执行

2.3.4 进程切换(调度新进程)

  • 进程切换是指处理机从一个进程的运行转到另一个进程上运行,这个过程中,进程的运行环境产生了实质性的变化,当一个进程时间片用完时、触发阻塞条件、主动挂起等就会进行进程切换,切换的过程会使用进程调度算法选择新进程
  • 进程切换同样是在内核的支持下实现的,因此可以说,任何进程控制操作都是在操作系统内核的支持下运行的,是与内核紧密相关的
  • 进程切换的过程如下:
    • 保存处理机上下文,包括程序计数器和其他寄存器
    • 更新旧进程 PCB 信息
    • 把进程的 PCB 移入相应的队列,如就绪、在某事件阻塞等队列
    • 利用进程调度算法选择另一个进程执行,并更新其 PCB
    • 更新内存管理的数据结构
    • 恢复处理机上下文

2.3.5 进程挂起与激活

  • 在许多操作系统中,进程除了就绪、执行、阻塞三种基本状态外,为了系统和用户观察和分析进程的需要,还引入了进程的挂起、激活操作
  • 挂起操作为主动操作,进程被挂起意味着进程处于静止状态,不会被调度,例如我们常用的 debug 模式
  • 引入挂起与激活后,进程的状态转换变为下图的 7 状态:
    带挂起的进程状态转换
  • 挂起操作的过程(Suspend 原语):
    • 首先检查被挂起进程的转态,若处于就绪转态,则改为禁止就绪状态;若处于阻塞转态,则改为静止阻塞状态
    • 同时放入对应队列
    • 为了方便用户或者父进程考查该进程的情况,将 PCB 复制到指定的内存区域
  • 激活操作的过程(Active 原语):
    • 将原进程从外存掉如内存,检查进程状态,若是静止就绪,则转为就绪转态;若是静止阻塞状态,则转为阻塞状态
    • 同时放入对应队列

2.4 进程的组成

  • 进程是操作系统的资源分配和独立运行的基本单位,其由三个部分组成:进程控制块 PCB、程序段、数据段

2.4.1 进程控制块 PCB

  • 进程创建时,操作系统就新建一个 PCB 结构,它之后就常驻内存,用于进程的调度、销毁等
  • PCB 是进程实体的一部分,是进程存在的唯一标志,操作系统通过 PCB 表来管理和控制进程
  • 当创建一个进程时,系统为该进程建立一个 PCB;当进程执行时,系统通过其 PCB 了解进程的现行状态信息,以便对其进行控制和管理;当进程结束时,系统收回其 PCB,该进程随之消亡

PCB 通常包含的内容

  • 进程标识符
    • 外部标识符:方便用户的访问
    • 内部标识符:方便系统使用
  • 处理机状态:即进程当前运行的上下文,用于进程重新执行时恢复现场
    • 通用寄存器
    • 指令计数器
    • 程序状态字 PSW:含有状态信息,如条件码、执行方式、中断屏蔽标志等
    • 用户栈指针:指向系统栈的栈顶,该栈保存系统调用参数及调用地址
  • 进程调度信息:
    • 进程状态
    • 进程优先级
    • 进程调度所需的其他信息,如以等待 CPU 时间总和、已执行时间总和等
    • 事件,即阻塞原因
  • 进程控制信息:
    • 程序段和数据段的地址
    • 进程同步和通信机制所需的内容,如消息队列指针、信号量等
    • 资源清单:包括程序运行期间所需的全部资源清单以及另一张已分配的资源清单
    • 链接指针,下一个 PCB 地址

PCB 的组织方式

  • 主要有线性方式、链接方式、索引方式
  • 链接方式即按链表结构组织,PCB 自己包含指向下一个 PCB 的指针,执行指针、就绪队列指针、阻塞队列指针、空闲队列指针则指向队列的首部
  • 索引方式通过建立对应的索引表来维护队列,索引表中的指针值指向具体的 PCB 地址,而索引表自身就表示对应的队列

2.4.2 程序段

  • 程序段就是能被进程调度程序调度到 CPU 执行的程序代码段
  • 程序段可以被多个进程共享,就是说多个进程可以运行同一个程序

2.4.3 数据段

  • 一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果

2.5 进程的通信

  • 进程通信是指进程之间的信息交换
  • 用于进程同步的 PV 操作是低级通信方式,髙级通信方式是指以较高的效率传输大量数据的通信方式,进程同步可以看做一种特殊的通信方式
  • 高级通信方法主要有三类:共享区存储、管道通信、消息传递

2.5.1 共享存储

  • 在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读操作实现进程之间的信息交换
  • 在对共享空间进行写/读操作时,需要使用同步互斥工具(如 P操作、V操作),对共享空间的写/读进行控制
  • 共享存储又分为两种:低级方式的共享是基于数据结构的共享;高级方式则是基于存储区的共享
  • 操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读/写指令完成
  • 需要注意的是,用户进程空间一般都是独立的,要想让两个用户进程共享空间必须通过特殊的系统调用实现,而进程内的线程是自然共享进程空间的

2.5.2 管道通信

  • 管道通信是消息传递的一种特殊方式
  • 所谓“管道”,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名 pipe 文件
  • 向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入(写)管道;而接收管道输出的接收进程(即读进程),则从管道中接收(读)数据
  • 为了协调双方的通信,管道机制必须提供以下三方面的协调能力:互斥、同步和确定对方的存在

2.5.3 消息传递

  • 在消息传递系统中,进程间的数据交换是以格式化的消息(Message)为单位的
  • 若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信,进程通过系统提供的发送消息和接收消息两个原语进行数据交换
  • 直接通信方式:发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息
  • 间接通信方式:发送进程把消息发送到某个中间实体中,接收进程从中间实体中取得消息;这种中间实体一般称为信箱,这种通信方式又称为信箱通信方式;该通信方式广泛应用于计算机网络中,相应的通信系统称为电子邮件系统

2.6 线程的概念和多线程模型

2.6.1 线程的基本概念

  • 引入进程的目的,是为了使多道程序并发执行,以提高资源利用率和系统吞吐量;而引入线程,则是为了减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能
  • 线程最直接的理解就是“轻量级进程”,它是一个基本的 CPU 执行单元,也是程序执行流的最小单元,由线程 ID、程序计数器、寄存器集合和堆栈组成
  • 线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源
  • 一个线程可以创建和撤销另一个线程,同一进程中的多个线程之间可以并发执行
  • 由于线程之间的相互制约,致使线程在运行中呈现出间断性
  • 线程也有就绪、阻塞和运行三种基本状态
  • 引入线程后,进程的内涵发生了改变,进程只作为除 CPU 以外系统资源的分配单元,线程则作为处理机的分配单元

2.6.2 线程与进程的比较

调度

  • 在传统的操作系统中,拥有资源和独立调度的基本单位都是进程
  • 在引入线程的操作系统中,线程是独立调度的基本单位,进程是资源拥有的基本单位
  • 在同一进程中,线程的切换不会引起进程切换,在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换

拥有资源

  • 不论是传统操作系统还是设有线程的操作系统,进程都是拥有资源的基本单位,而线程不拥有系统资源(也有一点必不可少的资源),但线程可以访问其隶属进程的系统资源

并发性

  • 在引入线程的操作系统中,不仅进程之间可以并发执行,而且多个线程之间也可以并发执行,从而使操作系统具有更好的并发性,提高了系统的吞吐量

系统开销

  • 由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、 I/O 设备等,因此操作系统所付出的开销远大于创建或撤销线程时的开销
  • 在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度到进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小
  • 由于同一进程内的多个线程共享进程的地址空间,因此这些线程之间的同步与通信非常容易实现,甚至无需操作系统的干预

地址空间和其他资源(如打开的文件)

  • 进程的地址空间之间互相独立,同一进程的各线程间共享进程的资源,某进程内的线程对于其他进程不可见

通信方面

  • 进程间通信(IPC)需要进程同步和互斥手段的辅助,以保证数据的一致性
  • 而线程间可以直接读/写进程数据段(如全局变量)来进行通信

2.6.3 线程的属性

  • 在多线程操作系统中,把线程作为独立运行(或调度)的基本单位,此时的进程,已不再是一个基本的可执行实体
  • 但进程仍具有与执行相关的状态,所谓进程处于“执行”状态,实际上是指该进程中某线程正在执行
  • 线程的主要属性如下:
    • 线程是一个轻型实体,它不拥有系统资源,但每个线程都应有一个唯一的标识符和一个线程控制块,线程控制块记录了线程执行的寄存器和栈等现场状态
    • 不同的线程可以执行相同的程序,即同一个服务程序被不同的用户调用时,操作系统为它们创建成不同的线程
    • 同一进程中的各个线程共享该进程所拥有的资源
    • 线程是处理机的独立调度单位,多个线程是可以并发执行的,在单 CPU 的计算机系统中,各线程可交替地占用 CPU;在多 CPU 的计算机系统中,各线程可同时占用不同的 CPU,若各个 CPU 同时为一个进程内的各线程服务则可缩短进程的处理时间
    • 线程被创建后便开始了它的生命周期,直至终止,线程在生命周期内会经历阻塞态、就绪态和运行态等各种状态变化

2.6.4 线程的实现方式

  • 线程的实现可以分为两类:用户级线程(User-LevelThread, ULT)和内核级线程(Kemel-LevelThread, KLT),内核级线程又称为内核支持的线程
  • 在用户级线程中,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在
  • 应用程序可以通过使用线程库设计成多线程程序,通常,应用程序从单线程起始,在该线程中开始运行,在其运行的任何时刻,可以通过调用线程库中的派生例程创建一个在相同进程中运行的新线程
  • 在内核级线程中,线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口
  • 内核为进程及其内部的每个线程维护上下文信息,调度也是在内核基于线程架构的基础上完成
  • 在一些系统中,使用组合方式的多线程实现:线程创建完全在用户空间中完成,线程的调度和同步也在应用程序中进行;一个应用程序中的多个用户级线程被映射到一些(小于或等于用户级线程的数目)内核级线程上
    线程

2.6.5 多线程模型

  • 有些系统同时支持用户线程和内核线程由此产生了不同的多线程模型,即实现用户级线程和内核级线程的连接方式

多对一模型

  • 将多个用户级线程映射到一个内核级线程,线程管理在用户空间完成
  • 此模式中,用户级线程对操作系统不可见(即透明)
  • 优点:线程管理是在用户空间进行的,因而效率比较高
  • 缺点:当一个线程在使用内核服务时被阻塞,那么整个进程都会被阻塞;多个线程不能并行地运行在多处理机上

一对一模型

  • 将每个用户级线程映射到一个内核级线程。
  • 优点:当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强
  • 缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能

多对多模型

  • 将 n 个用户级线程映射到 m 个内核级线程上,要求 m <= n。
  • 特点:在多对一模型和一对一模型中取了个折中,克服了多对一模型的并发度不高的缺点,又克服了一对一模型的一个用户进程占用太多内核级线程,开销太大的缺点。又拥有多对一模型和一对一模型各自的优点,可谓集两者之所长

2.7 处理机调度

2.7.1 调度的概念

  • 在多道程序系统中,进程的数量往往多于处理机的个数,进程争用处理机的情况就在所难免 -处理机调度是对处理机进行分配,就是从就绪队列中,按照一定的算法(公平、髙效)选择一个进程并将处理机分配给它运行,以实现进程并发地执行
  • 处理机调度是多道程序操作系统的基础,它是操作系统设计的核心问题
  • 一个作业从提交开始直到完成,往往要经历三级调度:作业调度、中级调度、进程调度
  • 作业调度:
    • 又称高级调度,其主要任务是按一定的原则从外存上处于后备状态的作业中挑选一个(或多个)作业,给它(们)分配内存、输入/输出设备等必要的资源,并建立相应的进程,以使它(们)获得竞争处理机的权利
    • 简言之,就是内存与辅存之间的调度
    • 对于每个作业只调入一次、调出一次
    • 多道批处理系统中大多配有作业调度,而其他系统中通常不需要配置作业调度
    • 作业调度的执行频率较低,通常为几分钟一次
  • 中级调度:
    • 又称内存调度,引入中级调度是为了提高内存利用率和系统吞吐量,为此,应使那些暂时不能运行的进程,调至外存等待,把此时的进程状态称为挂起状态
    • 当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定,把外存上的那些已具备运行条件的就绪进程,再重新调入内存,并修改其状态为就绪状态,挂在就绪队列上等待
  • 进程调度:
    • 又称为低级调度,其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它
    • 进程调度是操作系统中最基本的一种调度,在一般操作系统中都必须配置进程调度;进程调度的频率很高,一般几十毫秒一次
  • 各调度关系如下图所示
    调度关系

三级调度的联系

  • 作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就绪队列
  • 进程调度从就绪队列中选出一个进程,并把其状态改为运行状态,把 CPU 分配给它
  • 中级调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来,当内存空间宽松时,通过中级调度选择具备运行条件的进程,将其唤醒
  • 作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起,中级调度处于作业调度和进程调度之间
  • 作业调度次数少,中级调度次数略多,进程调度频率最高。
  • 进程调度是最基本的,不可或缺

2.7.2 调度的时机、切换与过程

  • 进程调度和切换程序是操作系统内核程序,当请求调度的事件发生后,才可能会运行进程调度程序,当调度了新的就绪进程后,才会去进行进程间的切换
  • 理论上这三件事情应该顺序执行,但在实际设计中,在操作系统内核程序运行时,如果某时发生了引起进程调度的因素,并不一定能够马上进行调度与切换

不能进行进程的调度与切换的情况

  • 在处理中断的过程中:中断处理过程复杂,在实现上很难做到进程切换,而且中断处理是系统工作的一部分,逻辑上不属于某一进程,不应被剥夺处理机资源。
  • 进程在操作系统内核程序临界区中:进入临界区后,需要独占式地访问共享数据,理论上必须加锁,以防止其他并行程序进入,在解锁前不应切换到其他进程运行,以加快该共享数据的释放。
  • 其他需要完全屏蔽中断的原子操作过程中:如加锁、解锁、中断现场保护、恢复等原子操作;在原子过程中,连中断都要屏蔽,更不应该进行进程调度与切换
  • 如果在上述过程中发生了引起调度的条件,并不能马上进行调度和切换,而是置系统的请求调度标志,直到上述过程结束后才进行相应的调度与切换

进行进程调度与切换的情况

  • 当发生引起调度条件,且当前进程无法继续运行下去时,可以马上进行调度与切换。如果操作系统只在这种情况下进行进程调度,就是非剥夺调度
  • 当中断处理结束或自陷处理结束后,返回被中断进程的用户态程序执行现场前,若置上请求调度标志,即可马上进行进程调度与切换。如果操作系统支持这种情况下的运行调度程序,就实现了剥夺方式的调度

补充

  • 进程切换往往在调度完成后立刻发生,它要求保存原进程当前切换点的现场信息,恢复被调度进程的现场信息
  • 现场切换时,操作系统内核将原进程的现场信息推入到当前进程的内核堆栈来保存它们,并更新堆栈指针
  • 内核完成从新进程的内核栈中装入新进程的现场信息、更新当前运行进程空间指针、重设 PC 寄存器等相关工作之后,开始运行新的进程

2.7.3 进程调度方式

  • 所谓进程调度方式是指当某一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即有优先权更髙的进程进入就绪队列,此时应如何分配处理机
  • 通常有非剥夺调度和剥夺调度两种方式
  • 非剥夺调度方式:又称非抢占方式,是指当一个进程正在处理机上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程完成或发生某种事件而进入阻塞状态时,才把处理机分配给更为重要或紧迫的进程
  • 在非剥夺调度方式下,一旦把 CPU 分配给一个进程,那么该进程就会保持 CPU 直到终止或转换到等待状态,这种方式的优点是实现简单、系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统
  • 剥夺调度方式:又称抢占方式,是指当一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程
  • 釆用剥夺式的调度,对提高系统吞吐率和响应效率都有明显的好处,但“剥夺”不是一种任意性行为,必须遵循一定的原则,主要有:优先权、短进程优先和时间片原则等

2.7.4 调度的基本准则

  • 不同的调度算法具有不同的特性,在选择调度算法时,必须考虑算法所具有的特性。为了比较处理机调度算法的性能,人们提出很多评价准则,下面介绍主要的几种:
  • CPU 利用率:CPU 是计算机系统中最重要和昂贵的资源之一,所以应尽可能使 CPU 保持“忙”状态,使这一资源利用率最髙。
  • 系统吞吐量:表示单位时间内 CPU 完成作业的数量,长作业需要消耗较长的处理机时间,因此会降低系统的吞吐量,而对于短作业,它们所需要消耗的处理机时间较短,因此能提高系统的吞吐量,调度算法和方式的不同,也会对系统的吞吐量产生较大的影响。
  • 周转时间:是指从作业提交到作业完成所经历的时间,包括作业等待、在就绪队列中排队、在处迤机上运行以及进行输入/输出操作所花费时间的总和
    • 周转时间 = 作业完成时间 - 作业提交时间
    • 平均周转时间 = (作业1的周转时间 + … + 作业 n 的周转时间) / n
    • 带权周转时间 = 作业周转时间 / 作业实际运行时间
  • 等待时间:是指进程处于等处理机状态时间之和,等待时间越长,用户满意度越低;处理机调度算法实际上并不影响作业执行或输入/输出操作的时间,只影响作业在就绪队列中等待所花的时间,因此,衡量一个调度算法优劣常常只需简单地考察等待时间
  • 响应时间:是指从用户提交请求到系统首次产生响应所用的时间,在交互式系统中,周转时间不可能是最好的评价准则,一般釆用响应时间作为衡量调度算法的重要准则之一,从用户角度看,调度策略应尽量降低响应时间,使响应时间处在用户能接受的范围之内
  • 要想得到一个满足所有用户和系统要求的算法几乎是不可能的,设计调度程序,一方面要满足特定系统用户的要求(如某些实时和交互进程快速响应要求),另一方面要考虑系统整体效率(如减少整个系统进程平均周转时间),同时还要考虑调度算法的开销

2.8 操作系统典型调度算法

  • 在操作系统中存在多种调度算法,其中有的调度算法适用于作业调度,有的调度算法适用于进程调度,有的调度算法两者都适用

2.8.1 先来先服务(FCFS)

  • FCFS 调度算法是一种最简单的调度算法,该调度算法既可以用于作业调度也可以用于进程调度
  • 在作业调度中,算法每次从后备作业队列中选择最先进入该队列的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列
  • 在进程调度中,FCFS 调度算法每次从就绪队列中选择最先进入该队列的进程,将处理机分配给它,使之投入运行,直到完成或因某种原因而阻塞时才释放处理机

2.8.2 短作业优先(SJF)

  • 短作业(进程)优先调度算法是指对短作业(进程)优先调度的算法,可用于作业调度和进程调度
  • 短作业优先(SJF)调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行
  • 而短进程优先(SPF)调度算法,则是从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或发生某事件而阻塞时,才释放处理机
  • SJF 调度算法也存在不容忽视的缺点:
    • 该算法对长作业不利,SJF 调度算法中长作业的周转时间会增加;更严重的是,如果有一长作业进入系统的后备队列,由于调度程序总是优先调度那些 (即使是后进来的)短作业,将导致长作业长期不被调度(“饥饿”现象,注意区分“死锁”。后者是系统环形等待,前者是调度策略问题)。
    • 该算法完全未考虑作业的紧迫程度,因而不能保证紧迫性作业会被及时处理。
    • 由于作业的长短只是根据用户所提供的估计执行时间而定的,而用户又可能会有意或无意地缩短其作业的估计运行时间,致使该算法不一定能真正做到短作业优先调度
  • 注意,SJF 调度算法的平均等待时间、平均周转时间最少

2.8.3 优先级调度算法(PSA)

  • 优先级调度算法又称优先权调度算法,该算法既可以用于作业调度,也可以用于进程调度,该算法中的优先级用于描述作业运行的紧迫程度
  • 在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列
  • 在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行
  • 根据新的更高优先级进程能否抢占正在执行的进程,可将该调度算法分为:
    • 非剥夺式优先级调度算法:当某一个进程正在处理机上运行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在运行的进程继续运行,直到由于其自身的原因而主动让出处理机时(任务完成或等待事件),才把处理机分配给更为重要或紧迫的进程。
    • 剥夺式优先级调度算法:当一个进程正在处理机上运行时,若有某个更为重要或紧迫的进程进入就绪队列,则立即暂停正在运行的进程,将处理机分配给更重要或紧迫的进程
  • 而根据进程创建后其优先级是否可以改变,可以将进程优先级分为以下两种:
    • 静态优先级:优先级是在创建进程时确定的,且在进程的整个运行期间保持不变;确定静态优先级的主要依据有进程类型、进程对资源的要求、用户要求
    • 动态优先级:在进程运行过程中,根据进程情况的变化动态调整优先级;动态调整优先级的主要依据为进程占有 CPU 时间的长短、就绪进程等待 CPU 时间的长短

2.8.4 高响应比优先调度算法(HRRN)

  • 高响应比优先调度算法主要用于作业调度,该算法是对 FCFS 调度算法和 SJF 调度算法的一种综合平衡,同时考虑每个作业的等待时间和估计的运行时间
  • 在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行
  • 响应比计算公式为:响应比\ R_p = \frac{等待时间+要求服务时间}{要求服务时间}
  • 根据公式可知:
    • 当作业的等待时间相同时,则要求服务时间越短,其响应比越高,有利于短作业
    • 当要求服务时间相同时,作业的响应比由其等待时间决定,等待时间越长,其响应比越高,因而它实现的是先来先服务
    • 对于长作业,作业的响应比可以随等待时间的增加而提高,当其等待时间足够长时,其响应比便可升到很高,从而也可获得处理机,克服了饥饿状态,兼顾了长作业

2.8.5 时间片轮转调度算法

  • 时间片轮转调度算法主要适用于分时系统
  • 在这种算法中,系统将所有就绪进程按到达时间的先后次序排成一个队列,进程调度程序总是选择就绪队列中第一个进程执行,即先来先服务的原则,但仅能运行一个时间片,如 100 ms
  • 在使用完一个时间片后,即使进程并未完成其运行,它也必须释放出(被剥夺)处理机给下一个就绪的进程,而被剥夺的进程返回到就绪队列的末尾重新排队,等候再次运行
  • 在时间片轮转调度算法中,时间片的大小对系统性能的影响很大:如果时间片足够大,以至于所有进程都能在一个时间片内执行完毕,则时间片轮转调度算法就退化为先来先服务调度算法;如果时间片很小,那么处理机将在进程间过于频繁切换,使处理机的开销增大,而真正用于运行用户进程的时间将减少;因此时间片的大小应选择适当
  • 时间片的长短通常由系统的响应时间、就绪队列中的进程数目、系统的处理能力等确定

2.8.6 多级反馈队列调度算法(集合了前几种算法的优点)

  • 多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合和发展,通过动态调整进程优先级和时间片大小,多级反馈队列调度算法可以兼顾多方面的系统目标
  • 例如,为提高系统吞吐量和缩短平均周转时间而照顾短进程;为获得较好的 I/O 设备利用率和缩短响应时间而照顾 I/O 型进程;同时,也不必事先估计进程的执行时间
  • 算法调度图大致如下图所示
    多级反馈队列调度算法
  • 多级反馈队列调度算法的实现思想如下:
    • 应设置多个就绪队列,并为各个队列赋予不同的优先级,第 1 级队列的优先级最高,第 2 级队列次之,其余队列的优先级逐次降低
    • 赋予各个队列中进程执行时间片的大小也各不相同,在优先级越高的队列中,每个进程的运行时间片就越小;例如,第 2 级队列的时间片要比第 1 级队列的时间片长一倍,第 i+1 级队列的时间片要比第 i 级队列的时间片长一倍。
    • 当一个新进程进入内存后,首先将它放入第 1 级队列的末尾,按 FCFS 原则排队等待调度;当轮到该进程执行时,如它能在该时间片内完成,便可准备撤离系统;如果它在一个时间片结束时尚未完成,调度程序便将该进程转入第 2 级队列的末尾,再同样地按FCFS 原则等待调度执行;如果它在第 2 级队列中运行一个时间片后仍未完成,再以同样的方法放入第 3 级队列……如此下去,当一个长进程从第 1 级队列依次降到第 n 级队列后,在第 n 级队列中便釆用时间片轮转的方式运行
    • 仅当第 1 级队列为空时,调度程序才调度第 2 级队列中的进程运行;仅当第 1 ~ (i-1) 级队列均为空时,才会调度第 i 级队列中的进程运行;如果处理机正在执行第i级队列中的某进程时,又有新进程进入优先级较高的队列(第 1 ~ (i-1) 中的任何一个队列),则此时新进程将抢占正在运行进程的处理机,即由调度程序把正在运行的进程放回到第 i 级队列的末尾,把处理机分配给新到的更高优先级的进程
  • 多级反馈队列的优势有:
    • 终端型作业用户:短作业优先
    • 短批处理作业用户:周转时间较短
    • 长批处理作业用户:经过前面几个队列得到部分执行,不会长期得不到处理

2.9 进程同步

  • 在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系,为了协调进程之间的相互制约关系,引入了进程同步的概念

2.9.1 临界资源

  • 虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所使用,我们把一次仅允许一个进程使用的资源称为临界资源
  • 许多物理设备都属于临界资源,如打印机等,此外还有许多变量、数据等都可以被若干进程共享,也属于临界资源
  • 对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区
  • 为了保证临界资源的正确使用,可以把临界资源的访问过程分成四个部分:
    • 进入区:为了进入临界区使用临界资源,在进入区要检查可否进入临界区,如果可以进入临界区,则应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
    • 临界区:进程中访问临界资源的那段代码,又称临界段
    • 退出区:将正在访问临界区的标志清除
    • 剩余区:代码中的其余部分
do {
    entry section;  //进入区
    critical section;  //临界区
    exit section;  //退出区
    remainder section;  //剩余区
} while (true)

2.9.2 同步

  • 同步亦称直接制约关系(一个进程受另一个进程制约),它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系,进程间的直接制约关系就是源于它们之间的相互合作
  • 例如,输入进程 A 通过单缓冲向进程 B 提供数据,当该缓冲区空时,进程 B 不能获得所需数据而阻塞,一旦进程 A 将数据送入缓冲区,进程 B 被唤醒;反之,当缓冲区满时,进程 A 被阻塞,仅当进程 B 取走缓冲数据时,才唤醒进程 A

2.9.3 互斥

  • 互斥亦称间接制约关系,当一个进程进入临界区使用临界资源时,另一个进程必须等待, 当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源
  • 例如,在仅有一台打印机的系统中,有两个进程A和进程B,如果进程A需要打印时, 系统已将打印机分配给进程B,则进程A必须阻塞。一旦进程B将打印机释放,系统便将进程A唤醒,并将其由阻塞状态变为就绪状态
  • Java 中用于多线程同步的关键字 synchronize 虽然译作同步,但其语义更像是此处的互斥,而且其实现本质是一个管程,管程在语言层面屏蔽了同步的细节
  • 为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
    • 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
    • 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
    • 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区(避免死锁)
    • 让权等待:当进程不能进入临界区时,应立即释放处理器,防止进程忙等待(进入阻塞状态)

2.10 实现临界区互斥的基本方法

  • 各个进程的临界区必须互斥的访问同一临界资源,因此必须要有一定的机制来编写临界区的代码以避免同时访问临界资源(同时还要考虑涉及多个临界资源时避免死锁),主要有软件实现、硬件实现两种
  • 此外,还有一种经典的进程同步方法:信号量机制

2.10.1 软件实现办法

  • 主要通过标记来实现互斥访问临界区

  • 在进入区设置和检查一些标志来标明是否有进程在临界区中,如果已有进程在临界区,则在进入区通过循环检查进行等待(阻塞当前进程直到可进入),进程离开临界区后则在退出区修改标志

  • 单标志法违背空闲让进,两个进程必须交替地访问

  • 双标志法先检查违背忙则等待,两个进程会同时进入临界区,并不互斥

  • 双标志法后检查会导致导致“饥饿”现象

彼得森算法

  • 是对双标志法后检查的改进,在 flag 的基础上额外引入 turn 变量指示不允许进入临界区的进程编号,以避免两个进程同时死等
  • 每个进程在先设置自己标志后再设置 turn 标志,不允许另一个进程进入
  • 这时,再同时检测另一个进程状态标志和不允许进入标志,这样可以保证当两个进程同时要求进入临界区,只允许一个进程进入临界区 由于 turn == i 与 turn == j 不会同时成立,因此不会互相死等

2.10.2 硬件实现办法(略)

  • 中断屏蔽方法
  • 硬件指令方法

2.11 信号量

  • 信号量机构是一种功能较强的机制,可用来解决互斥与同步的问题,它只能被两个标准的原语 wait(S) 和 signal(S) 来访问,也可以记为“P 操作”和“V 操作”

2.11.1 信号量描述

整型信号量

  • 信号量为一个整数,一般记为 S,用于描述资源数目
  • 则对应的 wait(P) 和 signal(V) 操作可描述为:
// P
wait(S){
    while (S<=0);
    S=S-1;
}
// V
signal(S){
    S=S+1;
}
  • 可以看到,只要信号量S <= 0 就会不断循环,因此该机制未遵循��让权等待”,而是使进程处于“忙等”状态

记录型信号量

  • 记录型信号量为为一个结构体,描述如下:
typedef struct{
    int value; // 可用资源数目
    struct process *L; // 等待该资源的进程列表
} semaphore;
  • 对应的 P,V 操作如下
// P 操作,即申请资源
void wait (semaphore S) { 
    S.value--; // 可用资源数 - 1
    if(S.value<0) { // -1 后若可用资源数小于 0,说明当前没有拿到资源,需要阻塞
        add this process to S.L; // 添加到需要该资源的程序链表
        block(S.L); // 阻塞该链表(主要阻塞当前进程或线程)
    }
    // 如果 - 1 后等于 0 或者大于 0 说明分配到资源,无需阻塞
}

// V 操作,释放资源
void signal (semaphore S) {  
    S.value++; // 释放了一个资源
    if(S.value<=0) { // 由于可能连续 wait,因此此处可能小于 0,但这个此次释放的资源必须要挑选一个可用的进程分配出去
        remove a process P from S.L; // 取出一个进程
        wakeup(P); // 分配资源后唤醒,即进入就绪状态
    }

    // 如果S.value > 0 说明资源原本就有剩下,该资源的阻塞队列中没有对应内容
}

2.11.2 利用信号量实现同步

  • 信号量机制能用于解决进程间各种同步问题(同步指的是一个进程依赖另一个进程的预先操作,例如读写问题)
  • 设 S 为实现进程 P1、P2 同步的公共信号量,初值为 0,表示无可用资源
  • 进程 P2 中的语句 y 要使用进程 P1 中语句 x 的运行结果,所以只有当语句 x 执行完成之后语句 y 才可以执行(类似读写问题),因此其实现进程同步的算法如下:
semaphore S = 0;  // 初始化信号量,注意读写问题这里为 0,表示初始无资源,而互斥访问资源时往往为 1
P1 ( ) {
    // …
    x;  //语句 x
    V(S);  //告诉进程 P2,语句乂已经完成
}
P2()){
    // …
    P(S) ;  //检查语句 x 是否运行完成
    y;  // 检查无误,运行 y 语句
    // …
}

2.11.3 利用信号量实现进程互斥

  • 信号量机制也能很方便地解决进程互斥问题
  • 设 S 为实现进程 Pl、P2 互斥的信号量,由于每次只允许一个进程进入临界区,所以 S 的初值应为 1(即可用资源数为 1)
semaphore S = 1;  // 初化信号量,可用资源数 = 1
P1 () {
    // …
    P(S);  // 准备开始访问临界资源,加锁
    // 进程P1的临界区
    V(S);  // 访问结束,解锁
    // …
}
P2() {
    // …
    P(S); //准备开始访问临界资源,加锁
    // 进程P2的临界区;
    V(S);  // 访问结束,解锁
    // …
}
  • 互斥的实现是不同进程对同一信号量进行 P、V 操作,一个进程在成功地对信号量执行了 P 操作后进入临界区,并在退出临界区后,由该进程本身对该信号量执行 V 操作,表示当前没有进程进入临界区,可以让其他进程进入

2.11.4 利用信号量实现前驱关系

  • 信号量也可以用来描述程序之间或者语句之间的前驱关系
  • 如下图所示的前驱图,其中 S1, S2, S3, ..., S6 是最简单的程序段(只有一条语句)
    前驱图
  • 为使各程序段能正确执行,应设置若干个初始值为“0”的信号量,每一条边要有一个信号量
  • 例如,为保证 S1 -> S2、S1 -> S3的前驱关系,应分别设置信号量a1、a2,同样,为了保证 S2 -> S4、S2 -> S5、S3 -> S6、S4 -> S6、S5 -> S6,应设置信号量 bl、b2、c、d、e
  • 根据上述信号量,可以得到下述实现前驱关系的算法:
semaphore  al=a2=bl=b2=c=d=e=0;  //初始化信号量
S1() {
    // 无前驱,则无 P 操作
    V(al);  V(a2) ;  //S1已经运行完成
}
S2() {
    P(a1);  // 通过信号量 a1 检查前驱 S1 是否运行完成
    // …
    V(bl); V(b2); // S2 已经运行完成
}
S3() {
    P(a2);  //通过信号量 a2 检查检查前驱 S1 是否已经运行完成
    // …
    V(c);  // S3 已经运行完成
}
S4() {
    P(b1);  //检查S2是否已经运行完成
    // …
    V(d);  //S4已经运行完成
}
S5() {
    P(b2);  //检查S2是否已经运行完成
    // …
    V(e);  // S5已经运行完成
}
S6() {
    P(c);  //检查S3是否已经运行完成
    P(d);  //检查S4是否已经运行完成
    P(e);  //检查S5是否已经运行完成
    // …;
}

2.11.5 分析进程同步和互斥问题的方法步骤

  • 关系分析:找出问题中的进程数,并且分析它们之间的同步和互斥关系,同步、互斥、前驱关系直接按照上面例子中的经典范式改写
  • 整理思路:找出解决问题的关键点,并且根据做过的题目找出解决的思路,根据进程的操作流程确定 P 操作、V 操作的大致顺序
  • 设置信号量:根据上面两步,设置需要的信号量,确定初值,完善整理

2.12 管程

引入原因与定义

  • 信号量机制的缺点:进程自备同步操作,P(S) 和 V(S) 操作大量分散在各个进程中,不易管理,易发生死锁
  • 引入管程机制的目的:
    • 把分散在各进程中的临界区集中起来进行管理
    • 防止进程有意或无意的违法同步操作
    • 便于用高级语言来书写程序,也便于程序正确性验证
  • 管程特点:管程封装了同步操作,对进程隐蔽了同步细节,简化了同步功能的调用界面,用户编写并发程序如同编写顺序(串行)程序
  • 管程:由一组数据以及定义在这组数据之上的对这组数据的操作组成的软件模块,这组操作能初始化并改变管程中的数据和同步进程

管程的组成

  • 局部于管程的共享结构数据说明
  • 对该数据结构进行操作的一组过程
  • 对局部于管程的共享数据设置初始值的语句

管程的基本特性

  • 局部于管程的数据只能被局部于管程内的过程所访问。
  • 一个进程只有通过调用管程内的过程才能进入管程访问共享数据。
  • 每次仅允许一个进程在管程内执行某个内部过程

补充

  • 由于管程是一个语言成分,所以管程的互斥访问完全由编译程序在编译时自动添加,无需程序员关注,而且保证正确
  • 例如 Java 的 synchronize 关键字本质就是管程

2.13 生产者-消费者问题

问题描述

  • 多个生产者进程和多个消费者进程共享一个初始为空、大小为 n 的缓冲区
  • 只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待
  • 只有缓冲区不空时,消费者才能从中取出消息,否则必须等待
  • 由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息

问题分析

  • 关系分析:
    • 生产者和消费者对缓冲区的访问是互斥的,因此他们为互斥关系
    • 同时生产者和消费者又是一个写作关系,只有生产者生产后,消费者才能消费,因此他们又是同步关系(一个进程依赖于另一个进程或者互相依赖)
  • 整理思路:该问题较为简单,只有生产者和消费者两个进程对象,这两个进程存在着互斥关系和同步关系,因此只需要设置合适的信号量并合理放置信号量的 P, V 操作的位置即可
  • 信号量设置:
    • 设 mutex 为互斥信号量,用于控制互斥访问缓冲区,即初值为 1
    • 信号量 full 表示缓冲区可用数据量,信号量 empty 表示缓冲区空闲单元数量,即 empty + full = n,且初始值 empty = n, full = 0
  • 则该问题的进程描述如下:
semaphore mutex = 1; // 临界区互斥信号量
semaphore empty = n;  // 缓冲区空闲单元数量
semaphore full = 0;  // 缓冲区可用数据量
producer () { //生产者进程
    while(1){
        produce an item in nextp;  //生产数据
        P(empty);  // 获取空缓冲区单元(即有空闲单元才能进入缓冲区)
        P(mutex);  // 互斥进入临界区
        add nextp to buffer;  // 将数据放入缓冲区
        V(mutex);  // 离开临界区,释放互斥信号量
        V(full);  // 缓冲区可用数据量 +1,让消费者可以取
    }
}
consumer () {  //消费者进程
    while(1){
        P(full);  // 同步缓冲区可用数据量(即有数据才能取)
        P(mutex);  // 互斥进入临界区
        remove an item from buffer;  //从缓冲区中取出数据
        V (mutex);  // 离开临界区,释放互斥信号量
        V (empty) ;  //  缓冲区空闲单元数量 +1,让生产者能继续生产
        consume the item;  //消费数据
    }
}
  • 该类问题要注意对缓冲区大小为 n 的处理,当缓冲区中有空时便可对 empty 变量执行 P 操作,一旦取走一个产品便要执行 V 操作以释放一个空闲区
  • 注意对 empty 和 full 变量的 P 操作必须放在对 mutex 的 P 操作之前,否则会导致死锁,
  • 假设先执行 P(mutex) 再执行 P(empty)/P(full)
  • 假设生产者进程已经将缓冲区放满,消费者进程并没有取产品,即empty = 0
  • 当下次仍然是生产者进程运行时,它先执行 P(mutex) 互斥进入临界区,再执行 P(empty) 时将被阻塞,因为此时 empty = 0 缓冲区已经满了无法继续,需要等待消费者消费后增加空闲缓冲区后才可以继续
  • 轮到消费者进程运行时,它先执行 P(mutex) 试图互斥进入临界区,然而由于前面生产者进程已经封锁 mutex 信号量,消费者进程也会被阻塞,这样一来生产者、消费者进程都将阻塞,都指望对方唤醒自己,陷入了无休止的等待
  • 同理,如果消费者进程已经将缓冲区取空,即 full = 0,下次如果还是消费者先运行,也会出现类似的死锁
  • 不过生产者释放信号量时,mutex、full 先释放哪一个无所谓,消费者先释放 mutex 还是 empty 都可以

较为复杂的生产者消费者问题:问题描述

  • 桌子上有一只盘子,每次只能向其中放入一个水果
  • 爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等吃盘子中的橘子,女儿专等吃盘子中的苹果
  • 只有盘子为空时,爸爸或妈妈就可向盘子中放一个水果
  • 仅当盘子中有自己需要的水果时,儿子或女儿才可以从盘子中取出

问题分析

  • 关系分析:
    • 首先由每次只能向其中放入一只水果可知爸爸和妈妈是互斥关系
    • 爸爸和女儿、妈妈和儿子是同步关系,而且这两对进程必须连起来,因为盘子容量为 1,不连起来不能继续放置
    • 儿子和女儿之间没有互斥和同步关系,因为他们是选择条件执行,不可能并发
  • 整理思路:这里有 4 个进程,实际上可以抽象为两个生产者和两个消费者被连接到大小为 1 的缓冲区上
  • 信号量设置:
    • 首先设置信号量 plate 为互斥信号量,表示是否允许向盘子放入水果,初值为 1,表示允许放入,且只允许放入一个
    • 信号量 apple 表示盘子中是否有苹果,初值为 0,表示盘子为空,女儿不许取,若 apple = l 可以取
    • 信号量 orange 表示盘子中是否有橘子,初值为 0,表示盘子为空,儿子不许取,若 orange=l 可以取
  • 该问题的代码如下:
semaphore plate=l, apple=0, orange=0;
dad() {  // 父亲进程
    while (1) {
        prepare an apple;
        P(plate) ;  // 互斥向盘中放水果
        put the apple on the plate;  // 向盘中放苹果
        V(apple);  // 允许取苹果
    }
}
mom() {  // 母亲进程
    while(1) {
        prepare an orange;
        P(plate);  // 互斥向盘中放水果
        put the orange on the plate;  // 向盘中放橘子
        V(orange); // 允许取橘子
    }
}
son(){  // 儿子进程
    while(1){
        P(orange) ;  // 同步取橘子(有橘子才能取)
        take an orange from the plate;
        V(plate);  // 取完后释放盘子,允许向盘中取、放水果
        eat the orange;
    }
}
daughter () {  // 女儿进程
    while(1) {
        P(apple);  // 同步取苹果(有苹果才能取)
        take an apple from the plate;
        V(plate);  // 取完后释放盘子,允许向盘中取、放水果
        eat the apple;
    }
}

2.14 读者-写者问题

问题描述

  • 有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误,因此有如下访问条件:
    • 允许多个读者可以同时对文件执行读操作
    • 只允许一个写者往文件中写信息
    • 任一写者在完成写操作之前不允许其他读者或写者工作
    • 写者执行写操作前,应让已有的读者和写者全部退出(即没有其他读者或者写者占用着资源)

问题分析

  • 关系分析:由题目分析读者和写者是互斥的,写者和写者也是互斥的,而读者和读者不存在互斥问题
  • 整理思路:
    • 两个进程,即读者和写者
    • 写者是比较简单的,它和任何进程互斥,用互斥信号量的 P 操作、V 操作即可解决
    • 读者的问题比较复杂,它必须实现与写者互斥的同时还要实现与其他读者的同步,因此,仅仅简单的一对 P 操作、V 操作是无法解决的,需要使用一个计数器,用它来判断当前是否有读者读文件
    • 当有读者的时候写者是无法写文件的,此时读者会一直占用文件,当没有读者的时候写者才可以写文件
    • 同时这里不同读者对计数器的访问也应该是互斥的
  • 信号量设置:
    • 首先设置信号量 count 为计数器,用来记录当前读者数量,初值为 0
    • 设置 mutex 为互斥信号量,用于保护更新 count 变量时的互斥
    • 设置互斥信号量 rw 用于保证读者和写者的互斥访问
  • 该问题的实现代码如下:
int count = 0;  // 用于记录当前的读者数量,count 应当作为临界资源互斥访问
semaphore mutex = 1;  // 用于互斥访问 count
semaphore rw = 1;  // 用户写者和其他读者/写者互斥访问资源
writer () {  // 写者进程
    while (1){
        P(rw); // 互斥访问共享文件
        Writing;  // 写入
        V(rw) ;  // 释放共享文件
    }
}
reader () {  // 读者进程
    while(1){
        P (mutex) ;  // 互斥访问 count 变量
        if (count==0)  // 当第一个读进程读共享文件时
            P(rw);  // 阻止写进程写
        count++;  // 读者计数器加 1
        V (mutex) ;  // 释放互斥变量 count
        reading;  // 读取
        P (mutex) ;  //互斥访问 count 变量
        count--; // 读者计数器减 1
        if (count==0)  // 当最后一个读进程读完共享文件
            V(rw) ;  // 允许写进程写
        V (mutex) ;  // 释放互斥变量 count
    }
}
  • 在上面的算法中,读进程是优先的,也就是说,当存在读进程时,写操作将被延迟,并且只要有一个读进程活跃,随后而来的读进程都将被允许访问文件
  • 这样的方式下,会导致写进程可能长时间等待,且存在写进程“饿死”的情况
  • 如果希望写进程优先,即当有读进程正在读共享文件时,有写进程请求访问,这时应禁止后续读进程的请求,等待到已在共享文件的读进程执行完毕则立即让写进程执行,只有在无写进程执行的情况下才允许读进程再次运行
  • 为此,增加一个信号量并且在上面的程序中 writer() 和 reader() 函数中各增加一对 PV 操作,就可以得到写进程优先的解决程序
int count = 0;  //用于记录当前的读者数量
semaphore mutex = 1;  // 用于互斥访问 count
semaphore rw=1;  // 用户写者和其他读者/写者互斥访问资源
semaphore w=1;  // 标记是否已经有写进程在写或者在等待,有则后续的读进程不允许读,用于实现“写优先”
writer(){
    while(1){
        P(w);  // 在无写进程请求时进入
        P(rw);  // 互斥访问共享文件
        writing;  // 写入
        V(rw);  // 释放共享文件
        V(w) ;  // 恢复对共享支件的访问
    }
}
reader () {  //读者进程
    while (1){
        P(w) ;  // 在无写进程请求时进入
        P(mutex);  // 互斥访问 count 变量
        if (count==0)  // 当第一个读进程读共享文件时
            P(rw);  // 阻止写进程写
        count++;  // 读者计数器加 1
        V(mutex) ;  // 释放互斥变量 count
        V(w);  // 恢复对共享文件的访问
        reading;  // 读取
        P(mutex) ; // 互斥访问count变量
        count--;  // 读者计数器减 1
        if (count==0)  // 当最后一个读进程读完共享文件
            V(rw);  // 允许写进程写
        V(mutex);  // 释放互斥变量 count
    }
}

2.15 哲学家进餐问题

问题描述

  • 一张圆桌上坐着 5 名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭
  • 哲学家们倾注毕生精力用于思考和进餐,哲学家在思考时,并不影响他人
  • 只有当哲学家饥饿的时候,才试图拿起左、 右两根筷子(一根一根地拿起)
  • 如果筷子已在他人手上,则需等待
  • 饥饿的哲学家只有同时拿到了两根筷子才可以开始进餐,当进餐完毕后,放下筷子继续思考
  • 5 个哲学家,共 5 只筷子,每个哲学家左右手各一只筷子
    哲学家进餐

问题分析

  • 关系分析:5 名哲学家与左右邻居对其中间筷子的访问是互斥关系
  • 整理思路:
    • 显然这里有五个进程
    • 本题的关键是如何让一个哲学家拿到左右两个筷子而不造成死锁或者饥饿现象
    • 那么解决方法有两个,一个是让他们同时拿两个筷子
    • 二是对每个哲学家的动作制定规则,避免饥饿或者死锁现象的发生
  • 信号量设置:定义互斥信号量数组 chopstick[5] = {l, 1, 1, 1, 1} 用于对 5 个筷子的互斥访问
  • 对哲学家按顺序从 0~4 编号,哲学家 i 左边的筷子的编号为 i,哲学家右边的筷子的编号为 (i+l) % 5,我们可以得到下述代码(会死锁):
semaphore chopstick[5] = {1,1,1,1,1}; //定义信号量数组chopstick[5],并初始化

Pi(){  // i 号哲学家的进程
    do{
        P(chopstick[i]); // 互斥访问左边筷子
        P(chopstick[(i+1) %5]); //互斥访问右边篌子
        eat;  // 进餐
        V(chopstick[i]) ; // 放回左边筷子
        V(chopstick[(i+l)%5]);  // 放回右边筷子
        think;  // 思考
    } while (1);
}
  • 上述代码会导致死锁:当五个哲学家都想要进餐,分别拿起他们左边筷子的时候(都恰好执行完 wait(chopstick[i]);)筷子已经被拿光了,等到他们再想拿右边的筷子的时候(执行 wait(chopstick[(i+l)%5]);)就全被阻塞了,这就出现了死锁
  • 为了防止死锁的发生,可以对哲学家进程施加一些限制条件:
    • 比如至多允许四个哲学家同时进餐
    • 仅当一个哲学家左右两边的筷子都可用时才允许他抓起筷子
    • 对哲学家顺序编号,要求奇数号哲学家先抓左边的筷子,然后再转他右边的筷子,而偶数号哲学家刚好相反
  • 假设我们釆用第二种方法,当一个哲学家左右两边的筷子都可用时,才允许他抓起筷子,则有下述代码:
semaphore chopstick[5] = {1,1,1,1,1}; // 初始化信号量
semaphore mutex = 1;  // 设置取筷子的信号量
Pi(){ // i号哲学家的进程
    do{
        P(mutex) ; // 互斥地取筷子
        P(chopstick[i]) ; // 取左边筷子
        P(chopstick[(i+1) %5]) ;  // 取右边筷子
        V(mutex) ; // 释放互斥取筷子的信号量
        eat;  // 进餐
        V(chopstick[i] ) ;  // 放回左边筷子
        V(chopstick[(i+l)%5]) ;  // 放回右边筷子
        think;  // 思考
    }while(1);
}
  • 由于取筷子的过程是互斥的,因此不可能出现各个哲学家同时拿了左边筷子的情况
  • 此外还可以釆用 AND 型信号量机制来解决哲学家进餐问题

2.16 吸烟者问题

问题描述

  • 假设一个系统有三个抽烟者进程和一个供应者进程
  • 每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水
  • 三个抽烟者中,第一个拥有烟草、第二个拥有纸,第三个拥有胶水
  • 供应者进程无限地提供三种材料,供应者每次将两种材料放到桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者一个信号告诉完成了,供应者就会放另外两种材料在桌上,这种过程一直重复(让三个抽烟者轮流地抽烟)

问题分析

  • 关系分析:
    • 供应者与三个抽烟者分别是同步关系
    • 由于供应者无法同时满足两个或以上的抽烟者,三个抽烟者对抽烟这个动作互斥(或由三个抽烟者轮流抽烟得知)
  • 整理思路:显然这里有四个进程,供应者作为生产者向三个抽烟者提供材料
  • 信号量设置:
    • 信号量 offer1、offer2、offer3 分别表示烟草和纸组合的资源、烟草和胶水组合的资源、纸和胶水组合的资源,以控制是哪个吸烟者可以卷烟吸烟
    • 信号量 finish 用于互斥进行抽烟动作
  • 该问题的解决代码如下:
int random; // 存储随机数,用于随机生产
semaphore offer1=0; // 定义信号量对应烟草和纸组合的资源
semaphore offer2=0; // 定义信号量对应烟草和胶水组合的资源
semaphore offer3=0; // 定义信号量对应纸和胶水组合的资源
semaphore finish=0; // 定义信号量表示抽烟是否完成

//供应者
while(1){
    random = 任意一个整数随机数;
    random = random% 3;
    if(random==0)
        V(offerl) ; // 提供烟草和纸
    else if(random==l) 
        V(offer2);  // 提供烟草和胶水
    else
        V(offer3)  // 提供纸和胶水
    // 任意两种材料放在桌子上;
    P(finish); // 表示有原料,用于同步给其他吸烟者
}

// 拥有烟草者
while(1){
    P(offer3);
    // 拿纸和胶水,卷成烟,抽掉;
    V(finish);
}

// 拥有纸者
while(1){
    P(offer2);
    // 烟草和胶水,卷成烟,抽掉;
    V(finish);
}

// 拥有胶水者
while(1){
    P(offer1);
    // 拿烟草和纸,卷成烟,抽掉;
    v(finish);
}

2.17 死锁的概念以及产生死锁的原因

2.17.1 死锁的定义

  • 在多道程序系统中,由于多个进程的并发执行,改善了系统资源的利用率并提高了系统的处理能力,带也可能带来新的问题——死锁
  • 所谓死锁是指多个进 程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进
  • 一般涉及多个资源的竞争,且各进程的资源占用顺序不同,它们都占用着以申请的且互相等着对方释放,从而导致死锁
  • 比如进程 A 需要 r1, r2,进程 B 需要 r2, r1,A 申请了 r1,时间片到了,B 并发执行并申请了 r2,然后申请 r1 发现已被占用,阻塞自己并等待释放,从而导致 A B 都互相等待对方释放资源而一直阻塞着,即发生了死锁
  • 再具体一点,某计算机系统中只有一台打印机和一台输入设备,进程P1 正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程 P2 所占用,而 P2 在未释放打印机之前,又提出请求使用正被P1 占用着的输入设备;这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态

2.17.2 死锁产生的原因

  • 系统资源的竞争:通常涉及多个需要互斥访问且不可剥夺的资源的竞争
  • 进程推进顺序非法:一般是请求和释放资源的顺序不当导致的,此外信号量使用不当也会造成死锁
  • 死锁产生的必要条件:
    • 互斥条件:对资源互斥访问,即一段之间内只允许一个进程访问
    • 不可剥夺条件:进程获得的资源未使用完毕之前,不能被其他进程强行夺走
    • 请求和保持条件:进程至少已经持有一个资源,但又提出新的资源请求,而该资源已经被其他进程占有,则此时该进程阻塞,但不释放已持有的资源
    • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求;例如哲学家进餐问题中的所有哲学家都拿了左边的筷子,从而他们的右边筷子都被下一个哲学家占用了,形成了资源的循环等待链

2.18 死锁的处理策略

  • 为使系统不发生死锁,必须设法破坏产生死锁的四个必要条件之一,或者允许死锁产生,但当死锁发生时能检测出死锁,并有能力实现恢复
  • 预防死锁:设置某些限制条件,破坏产生死锁的四个必要条件中的一个或几个,以防止发生死锁
  • 避免死锁:在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而避免死锁
  • 死锁的检测及解除:无需釆取任何限制性措施,允许进程在运行过程中发生死锁;通过系统的检测机构及时 地检测出死锁的发生,然后釆取某种措施解除死锁
  • 预防死锁和避免死锁都属于事先预防策略,但预防死锁的限制条件比较严格,实现起来较为简单,但往往导致系统的效率低,资源利用率低;避免死锁的限制条件相对宽松,资源分配后需要通过算法来判断是否进入不安全状态,实现起来较为复杂(如经典的银行家算法属于死锁避免)

2.19 死锁预防和死锁避免

2.19.1 死锁预防

  • 死锁预防通过破坏死锁产生的四个必要条件的其中一个来达到目的
  • 破坏互斥条件:
    • 如果允许系统资源都能共享使用,则系统不会进入死锁状态
    • 但有些资源根本不能同时访问,如打印机等临界资源必须互斥使用,因此破坏互斥条件来预防死锁的方法不太可行,而且在有的场合应该保护这种互斥性
  • 破坏不剥夺条件:
    • 当一个已保持了某些不可剥夺资源的进程,请求新的资源而得不到满足时,它必须释放已经保持的所有资源,待以后需要时再重新申请
    • 这意味着,一个进程已占有的资源会被暂时释放,或者说是被剥夺了,或从而破坏了不可剥夺条件。
    • 该策略实现起来比较复杂,释放已获得的资源可能造成前一阶段工作的失效,反复地申请和释放资源会增加系统开销,降低系统吞吐量
    • 这种方法常用于状态易于保存和恢复的资源,如 CPU 的寄存器及内存资源,一般不能用于打印机之类的资源
  • 破坏请求和保持条件:
    • 釆用预先静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不把它投入运行
    • 一旦投入运行后,这些资源就一直归它所有,也不再提出其他资源请求,这样就可以保证系统不会发生死锁
    • 这种方式实现简单,但缺点也显而易见,系统资源被严重浪费,其中有些资源可能仅在运行初期或运行快结束时才使用,甚至根本不使用
    • 而且还会导致“饥饿”现象,当由于个别资源长期被其他进程占用时,将致使等待该资源的进程迟迟不能开始运行
  • 破坏循环等待条件:
    • 为了破坏循环等待条件,可釆用顺序资源分配法
    • 首先给系统中的资源编号,规定每个进程,必须按编号递增的顺序请求资源,同类资源一次申请完
    • 也就是说,只要进程提出申请分配资源 Ri,则该进程在以后的资源申请中,只能申请编号大于 Ri 的资源
    • 这种方法存在的问题是,编号必须相对稳定,这就限制了新类型设备的增加
    • 尽管在为资源编号时已考虑到大多数作业实际使用这些资源的顺序,但也经常会发生作业使用资源的顺序与系统规定顺序不同的情况,造成资源的浪费
    • 此外,这种按规定次序申请资源的方法,也必然会给用户的编程带来麻烦

2.19.2 死锁避免

  • 避免死锁同样是属于事先预防的策略,但并不是事先釆取某种限制措施破坏死锁的必要条件,而是在资源动态分配过程中,防止系统进入不安全状态,以避免发生死锁
  • 这种方法所施加的限制条件较弱,可以获得较好的系统性能

系统安全状态

  • 避免死锁的方法中,允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性
  • 若此次分配不会导致系统进入不安全状态,则将资源分配给进程; 否则,让进程等待
  • 所谓安全状态,是指系统能按某种进程推进顺序 P1, P2, ..., Pn,为每个进程 Pi 分配其所需资源,直至满足每个进程对资源的最大需求,使每个进程都可顺序地完成
  • 此时称 P1, P2, ..., Pn 为安全序列,如果系统无法找到一个安全序列,则称系统处于不安全状态
  • 假设系统中有三个进程 P1、P2 和 P3,共有 12 台磁带机;进程 P1 总共需要 10 台磁带机,P2 和 P3 分别需要 4 台和 9 台;假设在 T0 时刻,进程 P1、P2 和 P3 已分别获得 5 台、2 台 和 2 台,尚有 3 台未分配
  • 则在 T0 时刻是安全的,因为存在一个安全序列 P2、P1、P3,即只要系统按此进程序列分配资源,则每个进程都能顺利完成
  • 若在 T0 时刻后,系统分配1台磁带机给 P3,则此时系统便进入不安全状态,因为此时已无法再找到一个安全序列
  • 并非所有的不安全状态都是死锁状态,但当系统进入不安全状态后,便可能进入死锁状态;反之,只要系统处于安全状态,系统便可以避免进入死锁状态

2.19.3 银行家算法(死锁避免)

  • 银行家算法是最著名的死锁避免算法
  • 它提出的思想是:把操作系统看做是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款
  • 操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配
  • 当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量
  • 若超过则拒绝分配资源,若没有超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配

数据结构描述

  • 可用资源数组:Available[j] = K 表示第 j 类资源的可用数量为 K,长度为 m 即共 m 类资源
  • 最大需求矩阵:Max[i][j] = K 表进程 i 最多需要 K 个第 j 类资源,n * m 即 n 个进程,m 类资源
  • 分配矩阵:Allocation[i][j] = K 表示进程 i 已占用 K 个第 j 类资源
  • 需求矩阵:Need[i][j] = K 表示进程 i 还需要 K 个第 j 类资源
  • 显然,Need[i][j] = Max[i][j] - Allocation[i][j]

银行家算法描述

  • Requesti[j] = K 为进程 i 请求 K 个第 j 类资源
  • 当 Pi 发出资源请求后,系统按照下述步骤进行检查:
  • 如果 Requesti[j] <= Need[i][j],则继续,否则出错,因为他需要的资源已经超过其所宣布的最大值
  • 如果 Requesti[j] <= Available[j],则继续下述步骤,否则表示此时无足够资源,Pi 必须等待有足够资源才继续
  • 系统试探性地把申请的资源分配给 Pi,即执行下述代码:
Available[j] = Available[j] - Requesti[j]; // 可用资源减少
Allocation[i, j] = Allocation[i, j] + Requesti[ j]; // 已占用资源增加
Need[i, j] = Need[i, j] - Requesti[j]; // 所需资源减少对应值
  • 执行安全性算法,检查此次资源分配后,系统是否处于安全状态
    • 若安全,才真正地将资源分配给 Pi,以完成本次分配
    • 若不安全,将本次的试探分配作废,恢复原来的资源分配状态(上述代码的逆操作),让进程 Pi 等待

安全性算法

  • 安全性算法基于两个数组:Work[m], Finish[n]
  • Work[j] 表示算法执行过程系统的第 j 类资源的可用数量,初始时 Work = Available
  • Finish[i] 表示进程 Pi 是否已经被选取运行并释放,初始时 Finish[i] = false for all i
  • 算法执行步骤如下:
  • 步骤 1:初始化上述两个数组 Work = Available, Finish = false
  • 步骤 2:程序循环判断,每次在未选取过的进程(Finish[i] = false)中找到满足条件 Need[i][j] <= Work[i][j] for all j 的进程,即该进程的所有所需资源都小于等于当前的系统剩余资源,若能找到这样执行步骤 3,否则执行步骤 4
  • 步骤 3:由于找到了当前进程 Pi 的所有所需资源小于系统剩余资源,即将资源分配给 Pi 然后假装 Pi 执行完毕释放所有资源
// Pi 获取全部所需资源并执行完毕后释放自己已占用的资源
Work[j] = Work[j]+ Allocation[i, j]; // Pi 释放自己已占用的资源
Finish[i] = true; // 标记 Pi 已经处理过
go to step <2>; // 继续选取直至所有进程已被选取或者找不到符合条件的进程
  • 步骤 4:
    • 如果到该步骤时所有的 Finish[i] = true 则说明所有进程都得到处理,系统处于安全状态
    • 否则说明存在进程无法被处理,系统处于不安全状态,校验不同过,需要将该次分配作废

举例

  • 举例参考书上例子,此处略

2.20 死锁的检测和解除

  • 前面绍的死锁预防和避免算法,都是在为进程分配资源时施加限制条件或进行检测,若系统为进程分配资源时不釆取任何措施,则应该提供死锁检测和解除的手段

资源分配图

  • 系统死锁,可利用资源分配图来描述:
    • 用圆圈代表一个进程
    • 用矩形代表一类资源
    • 由于一种类型的资源可能有多个,用矩形中的一个点代表一类资源中的一个资源
    • 从进程到资源的有向边叫请求边,表示该进程申请一个单位的该类资源;注意可能有多条边表示申请多个
    • 从资源到进程的边叫分配边,表示该类资源已经有一个资源被分配给了该进程;同理可能有多个
  • 如下图所示的资源分配图中,进程 P1 已经分得了两个 R1 资源,并又请求一个 R2 资源;进程 P2 分得了一个 R1 和一个 R2 资源,并又请求一个 R1 资源
    资源分配图

死锁定理

  • 可以通过将资源分配图简化的方法来检测系统状态 S 是否为死锁状态,简化方法如下:

  • 在资源分配图中,找出既不阻塞又不是孤点的进程 Pi 并释放它

    • 即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量
    • 若所有的连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然后释放它所占有的所有资源
    • 消去它所有的请求边和分配边,使之成为孤立的结点,例如下图的 P1 满足这一条件的进程结点,将 P1 的所有边消去
  • 进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程

  • 因此继续挑选非阻塞进程执行上述简化方法,例如在 P1 消除后进程 P2 就满足这样的条件,继续消除

  • 若能消去图中所有的边,则称该图是可完全简化的

    资源分配图简化

  • S 为死锁的条件是当且仅当 S 状态的资源分配图是不可完全简化的,该条件为死锁定理

死锁的解除

  • 一旦检测出死锁,就应立即釆取相应的措施,以解除死锁
  • 死锁解除的主要方法有:
    • 资源剥夺法:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程;但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态
    • 撤销进程法:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源;撤销的原则可以按进程优先级和撤销进程代价的高低进行
    • 进程回退法:让一(多)个进程回退到足以回避死锁的地步,进程回退时自愿释放资源而不是被剥夺;要求系统保持进程的历史信息,设置还原点

2.21 关于进程和线程的知识点汇总

进程与程序的区别与联系

  • 进程是程序及其数据在计算机上的一次运行活动,是一个动态的概念;进程的运行实体是程序,离开程序的进程没有存在的意义;从静态角度看,进程是由程序、数据和进程控制块(PCB)三部分组成的;而程序是一组有序的指令集合,是一种静态的概念
  • 进程是程序的一次执行过程,它是动态地创建和消亡的,具有一定的生命周期,是暂时存在的;而程序则是一组代码的集合,它是永久存在的,可长期保存
  • 一个进程可以执行一个或几个程序,一个程序也可以构成多个进程;进程可创建进程,而程序不可能形成新的程序
  • 进程与程序的组成不同,进程的组成包括程序、数据和 PCB

死锁与饥饿

  • 死锁:涉及多个进程对多个资源的争用时,不合理的资源申请顺序导致的双方互相等待对方释放资源而循环等待下去
  • 说一组进程处于死锁状态是指:组内的每个进程都等待一个事件,而该事件只可能由组内的另一个进程产生,这里所关心的主要是事件是资源的获取和释放
  • 饥饿:
    • 对于每类系统资源,操作系统需要确定一个分配策略,当多个进程同时申请某类资源时,由分配策略确定资源分配给进程的次序
    • 有时资源分配策略可能是不公平的,在这种情况下,即使系统没有发生死锁,某些进程也可能会长时间等待(例如 SJF 中长作业可能一直无法得到调用)
    • 当等待时间给进程推进和响应带来明显影响时,称发生了进程“饥饿”,当“饥饿”到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被“饿死”
  • 饥饿”并不表示系统一定死锁,但至少有一个进程的执行被无限期推迟,“饥饿”与死锁的主要差别有:
    • 进入“饥饿”状态的进程可以只有一个,而由于循环等待条件而进入死锁状态的进程却必须大于或等于两个
    • 处于“饥饿”状态的进程可以是一个就绪进程,如静态优先权调度算法时的低优先权进程,而处于死锁状态的进程则必定是阻塞进程

银行家算法的工作原理

  • 银行家算法的主要思想是避免系统进入不安全状态
  • 在每次进行资源分配时,它首先检查系统是否有足够的资源满足要求,如果有,则先进行分配,并对分配后的新状态进行安全性检查
  • 如果新状态安全,则正式分配上述资源,否则就拒绝分配上述资源,这样,它保证系统始终处于安全状态,从而避免死锁现象的发生

进程同步、互斥的区别和联系

  • 并发进程的执行会产生相互制约的关系:
    • 一种是进程之间竞争使用临界资源,只能让它们逐个使用,这种现象称为互斥,是一种竞争关系
    • 另一种是进程之间协同完成任务,在关键点上等待另一个进程发来的消息,以便协同一致,是一种协作关系

作业和进程的关系

  • 进程是系统资源的使用者,系统的资源大部分都是以进程为单位分配的,而用户使用计算机是为了实现一串相关的任务,通常把用户要求计算机完成的这一串任务称为作业
  • 批处理系统中作业与进程的关系(进程组织)
  • 分时系统中作业与进程的关系
  • 交互地提交批作业

3. 内存管理

3.1 内存管理的概念

  • 内存管理(Memory Management)是操作系统设计中最重要和最复杂的内容之一
  • 虽然计算机硬件一直在飞速发展,内存容量也在不断增长,但是仍然不可能将所有用户进程和系统所需要的全部程序和数据放入主存中,所以操作系统必须将内存空间进行合理地划分和有效地动态分配
  • 操作系统对内存的划分和动态分配,就是内存管理的概念
  • 有效的内存管理在多道程序设计中非常重要,不仅方便用户使用存储器、提高内存利用率,还可以通过虚拟技术从逻辑上扩充存储器
  • 内存管理的功能有:
    • 内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率
    • 地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址
    • 内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存
    • 存储保护:保证各道作业在各自的存储空间内运行,互不干扰
  • 在进行具体的内存管理之前,需要了解进程运行的基本原理和要求

程序装入和链接

  • 创建进程首先要将程序和数据装入内存
  • 将用户源程序变为可在内存中执行的程序,通常需要以下几个步骤:
    • 编译:由编译程序将用户源代码编译成若干个目标模块
    • 链接:由链接程序将编译后形成的一组目标模块,以及所需库函数链接在一起,形成一个完整的装入模块
    • 装入:由装入程序将装入模块装入内存运行
      程序装入步骤
  • 程序的链接有以下三种方式:
    • 静态链接:在程序运行之前,先将各目标模块及它们所需的库函数链接成一个完整的可执行程序,以后不再拆开
    • 装入时动态链接:将用户源程序编译后所得到的一组目标模块,在装入内存时,釆用边装入边链接的链接方式
    • 运行时动态链接:对某些目标模块的链接,是在程序执行中需要该目标模块时,才对它进行的链接;其优点是便于修改和更新,便于实现对目标模块的共享
  • 内存的装入模块在装入内存时,同样有三种方式:绝对装入、可重定位装入、动态运行时装入
  • 绝对装入:
    • 在编译时,如果知道程序将驻留在内存的某个位置,编译程序将产生绝对地址的目标代码
    • 绝对装入程序按照装入模块中的地址,将程序和数据装入内存
    • 由于程序中的逻辑地址与实际内存地址完全相同,故不需对程序和数据的地址进行修改
    • 绝对装入方式只适用于单道程序环境
    • 另外,程序中所使用的绝对地址,可在编译或汇编时给出,也可由程序员直接赋予
    • 而通常情况下在程序中釆用的是符号地址,编译或汇编时再转换为绝对地址
  • 可重定位装入:
    • 在多道程序环境下,多个目标模块的起始地址通常都是从 0 开始,程序中的其他地址都是相对于起始地址的,此时应釆用可重定位装入方式
    • 根据内存的当前情况,将装入模块装入到内存的适当位置
    • 装入时对目标程序中指令和数据的修改过程称为重定位,地址变换通常是在装入时一次完成的,所以又称为静态重定位
    • 静态重定位的特点是在一个作业装入内存时,必须分配其要求的全部内存空间,如果没有足够的内存,就不能装入该作业
    • 此外,作业一旦进入内存后,在整个运行期间不能在内存中移动,也不能再申请内存空间
      静态重定位
  • 动态运行时装入:
    • 也称为动态重定位,程序在内存中如果发生移动,就需要釆用动态的装入方式
    • 装入程序在把装入模块装入内存后,并不立即把装入模块中的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时才进行
    • 因此,装入内存后的所有地址均为相对地址
    • 这种方式需要一个重定位寄存器的支持
    • 动态重定位的特点是可以将程序分配到不连续的存储区中,在程序运行之前可以只装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存,便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间

逻辑地址空间与物理地址空间

  • 编译后,每个目标模块都是从 0 号单元开始编址,称为该目标模块的相对地址(或逻辑地址)
  • 当链接程序将各个模块链接成一个完整的可执行目标程序时,链接程序顺序依次按各个模块的相对地址构成统一的从 0 号单元开始编址的逻辑地址空间
  • 用户程序和程序员只需知道逻辑地址,而内存管理的具体机制则是完全透明的,它们只有系统编程人员才会涉及
  • 不同进程可以有相同的逻辑地址,因为这些相同的逻辑地址可以映射到主存的不同位置

内存保护(不太理解)

  • 内存分配前,需要保护操作系统不受用户进程的影响,同时保护用户进程不受其他用户进程的影响
  • 通过釆用重定位寄存器和界地址寄存器来实现这种保护
  • 重定位寄存器含最小的物理地址值,界地址寄存器含逻辑地址值
  • 每个逻辑地址值必须小于界地址寄存器;内存管理机构动态地将逻辑地址与界地址寄存器进行比较,如果未发生地址越界,则加上重定位寄存器的值后映射成物理地址,再送交内存单元
  • 当 CPU 调度程序选择进程执行时,派遣程序会初始化重定位寄存器和界地址寄存器
  • 每一个逻辑地址都需要与这两个寄存器进行核对,以保证操作系统和其他用户程序及数据不被该进程的运行所影响
    内存保护

3.2 内存覆盖与内存交换

  • 覆盖与交换技术是在多道程序环境下用来扩充内存的两种方法

3.2.1 内存覆盖

  • 早期的计算机系统中,主存容量很小,虽然主存中仅存放一道用户程序,但是存储空间放不下用户进程的现象也经常发生,这一矛盾可以用覆盖技术来解决
  • 覆盖的基本思想是:
    • 由于程序运行时并非任何时候都要访问程序及数据的各个部分(尤其是大程序),因此可以把用户空间分成一个固定区和若干个覆盖区
    • 将经常活跃的部分放在固定区,其余部分按调用关系分段
    • 首先将那些即将要访问的段放入覆盖区,其他段放在外存中,在需要调用前,系统再将其调入覆盖区,替换覆盖区中原有的段
  • 覆盖技术的特点是打破了必须将一个进程的全部信息装入主存后才能运行的限制,但当同时运行程序的代码量大于主存时仍不能运行

3.2.2 内存交换

  • 交换(对换)的基本思想是:
    • 把处于等待状态(或在 CPU 调度原则下被剥夺运行权利) 的程序从内存移到辅存,把内存空间腾出来,这一过程又叫换出
    • 把准备好竞争 CPU 运行的程序从辅存移到内存,这一过程又称为换入
  • 第 2 章介绍的中级调度/内存调度就是釆用交换技术
  • 例如,有一个 CPU 釆用时间片轮转调度算法的多道程序环境,时间片到,内存管理器将刚刚执行过的进程换出,将另一进程换入到刚刚释放的内存空间中,同时,CPU 调度器可以将时间片分配给其他已在内存中的进程,每个进程用完时间片都与另一进程交换
  • 理想情况下,内存管理器的交换过程速度足够快,总有进程在内存中可以执行
  • 有关交换需要注意以下几个问题:
    • 交换需要备份存储,通常是快速磁盘,它必须足够大,并且提供对这些内存映像的直接访问
    • 为了有效使用 CPU,需要每个进程的执行时间比交换时间长,而影响交换时间的主要是转移时间,转移时间与所交换的内存空间成正比
    • 如果换出进程,必须确保该进程是完全处于空闲状态
    • 交换空间通常作为磁盘的一整块,且独立于文件系统(装系统时会划分虚存),因此使用就可能很快
    • 交换通常在有许多进程运行且内存空间吃紧时开始启动,而系统负荷降低就暂停
    • 普通的交换使用不多,但交换策略的某些变种在许多系统中(如 UNIX 系统)仍发挥作用
  • 交换技术主要是在不同进程(或作业)之间进行,而覆盖则用于同一个程序或进程中
  • 由于覆盖技术要求给出程序段之间的覆盖结构,使得其对用户和程序员不透明,所以对于主存无法存放用户程序的矛盾,现代操作系统是通过虚拟内存技术来解决的,覆盖技术则已成为历史
  • 而交换技术在现代操作系统中仍具有较强的生命力

3.3 内存连续分配管理方式

  • 连续分配方式,是指为一个用户程序分配一个连续的内存空间
  • 主要包括单一连续分配、固定分区分配和动态分区分配

3.3.1 单一连续分配

  • 内存在此方式下分为系统区和用户区
  • 系统区仅提供给操作系统使用,通常在低地址部分
  • 用户区是为用户提供的、除系统区之外的内存空间
  • 这种方式无需进行内存保护
  • 这种方式的优点是简单、无外部碎片,可以釆用覆盖技术,不需要额外的技术支持
  • 缺点是只能用于单用户、单任务的操作系统中,有内部碎片,存储器的利用率极低

3.3.2 固定分区分配

  • 固定分区分配是最简单的一种多道程序存储管理方式,它将用户内存空间划分为若干个固定大小的区域,每个分区只装入一道作业
  • 当有空闲分区时,便可以再从外存的后备作业队列中,选择适当大小的作业装入该分区,如此循环
  • 固定分区分配在划分分区时,有两种不同的方法:
    • 分区大小相等:用于利用一台计算机去控制多个相同对象的场合,缺乏灵活性
    • 分区大小不等:划分为含有多个较小的分区、适量的中等分区及少量的大分区
      固定分区分配
  • 为便于内存分配,通常将分区按大小排队,并为之建立一张分区说明表,其中各表项包括每个分区的起始地址、大小及状态(是否已分配)
    分区说明表
  • 当有用户程序要装入时,便检索该表,以找到合适的分区给予分配并将其状态置为”已分配”;未找到合适分区则拒绝为该用户程序分配内存
  • 这种分区方式存在两个问题:
    • 一是程序可能太大而放不进任何一个分区中,这时用户不得不使用覆盖技术来使用内存空间
    • 二是主存利用率低,当程序小于固定分区大小时,也占用了一个完整的内存分区空间,这样分区内部有空间浪费,这种现象称为内部碎片
  • 固定分区是可用于多道程序设计最简单的存储分配,无外部碎片,但不能实现多进程共享一个主存区,所以存储空间利用率低
  • 固定分区分配很少用于现在通用的操作系统中,但在某些用于控制多个相同对象的控制系统中仍发挥着一定的作用

3.3.3 动态分区分配

  • 动态分区分配又称为可变分区分配,是一种动态划分内存的分区方法
  • 这种分区方法不预先将内存划分,而是在进程装入内存时,根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要,因此系统中分区的大小和数目是可变的
    动态分区分配
  • 如上图所示:
    • 系统有 64MB 内存空间,其中低 8MB 固定分配给操作系统,其余为用户可用内存
    • 开始时装入前三个进程,在它们分别分配到所需空间后,内存只剩下 4MB,进程 4 无法装入
    • 在某个时刻,内存中没有一个就绪进程,CPU 出现空闲,操作系统就换出进程 2,换入进程 4
    • 由于进程 4 比进程 2 小,这样在主存中就产生了一个 6MB 的内存块
    • 之后 CPU 又出现空闲,而主存无法容纳进程 2,操作系统就换出进程 1,换入进程 2
  • 动态分区在开始分配时是很好的,但是之后会导致内存中出现许多小的内存块,随着时间的推移,内存中会产生越来越多的碎片,内存的利用率随之下降
  • 这些小的内存块称为外部碎片,指在所有分区外的存储空间会变成越来越多的碎片,这与固定分区中的内部碎片正好相对
  • 克服外部碎片可以通过紧凑(Compaction) 技术来解决,就是操作系统不时地对进程进行移动和整理,但是这需要动态重定位寄存器的支持,且相对费时
  • 紧凑的过程实际上类似于 Windows 系统中的磁盘整理程序,只不过后者是对外存空间的紧凑
  • 在进程装入或换入主存时,如果内存中有多个足够大的空闲块,操作系统必须确定分配哪个内存块给进程使用,这就是动态分区的分配策略,考虑以下几种算法:
    • 首次适应(First Fit)算法:空闲分区以地址递增的次序链接,分配内存时顺序查找,找到大小能满足要求的第一个空闲分区
    • 最佳适应(Best Fit)算法:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区
    • 最坏适应(Worst Fit)算法:又称最大适应(Largest Fit)算法,空闲分区以容量递减的次序链接,找到第一个能满足要求的空闲分区,也就是挑选出最大的分区
    • 邻近适应(Next Fit)算法:又称循环首次适应算法,由首次适应算法演变而成,不同之处是分配内存时从上次查找结束的位置开始继续查找
  • 在这几种方法中,首次适应算法不仅是最简单的,而且通常也是最好和最快的
  • 在 UNIX 系统的最初版本中,就是使用首次适应算法为进程分配内存空间,其中使用数组的数据结构 (而非链表)来实现
  • 不过,首次适应算法会使得内存的低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些分区,因此也增加了查找的开销
  • 邻近适应算法试图解决这个问题,但实际上,它常常会导致在内存的末尾分配空间,分裂成小碎片,它通常比首次适应算法的结果要差(因为内存前面部分释放后,暂时不会参与分配,而是要等到下轮走到低地址处)
  • 最佳适应算法虽然称为“最佳”,但是性能通常很差,因为每次最佳的分配会留下很小的难以利用的内存块,它会产生最多的外部碎片
  • 最坏适应算法与最佳适应算法相反,选择最大的可用块,这看起来最不容易产生碎片,但是却把最大的连续内存划分开,会很快导致没有可用的大的内存块,因此性能也非常差
  • Kunth和Shore分别就前三种方法对内存空间的利用情况做了模拟实验,结果表明:首次适应算法可能比最佳适应法效果好,而它们两者一定比最大适应法效果好
  • 另外注意,在算法实现时,分配操作中最佳适应法和最大适应法需要对可用块进行排序或遍历查找,而首次适应法和邻近适应法只需要简单查找
  • 回收操作中,当回收的块与原来的空闲块相邻时(有三种相邻的情况,比较复杂),需要将这些块合并
  • 在算法实现时,使用数组或链表进行管理
  • 除了内存的利用率,这里的算法开销也是操作系统设计需要考虑的一个因素

3.4 内存非连续分配管理方式

  • 非连续分配允许一个程序分散地装入到不相邻的内存分区中,根据分区的大小是否固定分为分页存储管理方式和分段存储管理方式
  • 分页存储管理方式中,又根据运行作业时是否要把作业的所有页面都装入内存才能运行分为基本分页存储管理方式和请求分页存储管理方式

3.4.1 基本分页存储管理方式

  • 固定分区会产生内部碎片,动态分区会产生外部碎片,这两种技术对内存的利用率都比较低
  • 我们希望内存的使用能尽量避免碎片的产生,这就引入了分页的思想:
    • 把主存空间划分为大小相等且固定的块,块相对较小,作为主存的基本单位
    • 每个进程也以块为单位进行划分,进程在执行时,以块为单位逐个申请主存中的块空间
  • 分页的方法从形式上看,像分区相等的固定分区技术,分页管理不会产生外部碎片,但又有许多本质的不同:
    • 块的大小相对分区要小很多,而且进程也按照块进行划分,进程运行时按块申请主存可用空间并执行
    • 因此进程只会在为最后一个不完整的块申请一个主存块空间时,才产生主存碎片,所以尽管会产生内部碎片,但是这种碎片相对于进程来说也是很小的,每个进程平均只产生半个块大小的内部碎片(也称页内碎片)

分页存储的几个基本概念

  • 页面:
    • 进程中的块称为页(Page),内存中的块称为页框(Page Frame,或页帧)
    • 外存也以同样的单位进行划分,直接称为块(Block)
    • 进程在执行时需要申请主存空间,就是要为每个页面分配主存中的可用页框,这就产生了页和页框的一一对应
  • 页面大小:
    • 为方便地址转换,页面大小应是 2 的整数幂
    • 页面的大小应该适中,考虑到耷间效率和时间效率的权衡
    • 如果页面太小,会使进程的页面数过多,这样页表就过长,占用大量内存,而且也会增加硬件地址转换的开销,降低页面换入/换出的效率
    • 页面过大又会使页内碎片增大,降低内存的利用率
  • 地址结构:
    • 32 位系统,地址长度 32 位
    • 页号部分 P:20 位,故地址空间最多允许有 2^20 页
    • 页内偏移 W:12 位,故每页大小为 2^12 B = 4 KB,
      分页地址结构
  • 页表:
    • 为了便于在内存中找到进程的每个页面所对应的物理块,系统为每个进程建立一张页表,记录页面在内存中对应的物理块号
    • 页表一般存放在内存中
    • 类似一个 map 做映射(实现应该是数组):进程页号 -> 物理块号
      页表

基本地址变换机构

  • 地址变换机构的任务是将逻辑地址转换为内存中物理地址,地址变换是借助于页表实现的
  • 系统为每个进程都分配一个页表,其需求的部分一般长时间处于内存中,进程的页表起始地址和长度会存在 PCB 中
  • 在系统中通常设置一个页表寄存器(PTR),存放页表在内存的始址 F 和页表长度 M
  • 进程未执行时,页表的始址和长度存放在进程控制块中,当进程执行时,才将页表始址和长度存入页表寄存器,用于访问内存时计算真正的物理地址
  • 设页面大小为 L,逻辑地址 A 到物理地址 E 的变换过程如下:
    • 计算页号 P(P = A / L)和页内偏移量 W (W = A % L)
    • 比较页号 P 和页表长度 M,若 P >= M,则产生越界中断,否则继续执行
    • 页表中页号 P 对应的页表项地址 = 页表起始地址 F + 页号 P * 页表项长度,取出该页表项内容 b,即为物理块号
    • 计算 E = b * L + W,用得到的物理地址 E 去访问内存
    • 上述地址变换过程均是由硬件自动完成的
  • 例如,若页面大小 L 为 1 KB,页号 2 对应的物理块为 b=8,计算逻辑地址 A=2500 的物理地址 E 的过程如下:
    • 计算逻辑页号 P 以及业内便宜 W:P = 2500 / 1KB = 2,W = 2500 % 1KB = 452,
    • 查找页表得到逻辑页号 2 对应的物理块的块号为 8
    • 根据物理块好和业内便宜计算真实地址:E = 8 * 1024 + 452 = 8644
  • 分页管理方式存在的两个主要问题:
    • 每次访问内存操作都需要将逻辑地址转换为物理地址,因此地址转换过程必须足够快,否则访存速度会降低
    • 每个进程都有自己的页表,用于存储映射机制,因此页表(或页表项结构)不能太大,否则内存利用率会降低

具有快表的地址变换机构

  • 由上面介绍的地址变换过程可知,若页表全部放在内存中,则存取一个数据或一条指令至少要访问两次内存:
    • 一次是访问页表,确定所存取的数据或指令的物理地址,
    • 第二次才根据该地址存取数据或指令
    • 显然,这种方法比通常执行指令的速度慢了一半
  • 为此,在地址变换机构中增设了一个具有并行查找能力的高速缓冲存储器——快表(其实就是一个慢表的缓存)
  • 块表:
    • 又称联想寄存器(TLB),用来存放当前访问的若干页表项,以加速地址变换的过程
    • 与此对应,主存中的页表也常称为慢表,配有快表的地址变换机构如下图所示
      具有快表的地址变换机构
  • 在具有快表的分页机制中,地址的变换过程:
    • CPU 给出逻辑地址后,由硬件进行地址转换并将页号送入高速缓存寄存器,并将此页号与快表中的所有页号进行比较
    • 如果找到匹配的页号,说明所要访问的页表项在快表中,则直接从中取出该页对应的页框号,与页内偏移量拼接形成物理地址,这样,存取数据仅一次访存便可实现
    • 如果没有找到,则需要访问主存中的页表,在读出页表项后,应同时将其存入快表,以便后面可能的再次访问
    • 但若快表已满,则必须按照一定的算法对旧的页表项进行替换
  • 注意:有些处理机设计为快表和慢表同时查找,如果在快表中查找成功则终止慢表的查找
  • 一般快表的命中率可以达到 90% 以上,这样,分页带来的速度损失就降低到 10% 以下
  • 快表的有效性是基于著名的局部性原理,这在后面的虚拟内存中将会具体讨论

两级页表

  • 由于引入了分页管理,进程在执行时不需要将所有页调入内存页框中,而只要将保存有映射关系的页表调入内存中即可,但是我们仍然需要考虑页表的大小
  • 以 32 位地址,页面大小为 4 KB 为例,若要实现对所有逻辑地址的映射,则共需要 2^20 个页表项,假设每个页表项为 4 B 大小,则需要需要 1 MB 的大小来作为页表,这显然会造成浪费
  • 即使进程不要要映射全部的逻辑地址,但对于一个逻辑地址稍大的进程,其页表的大小也可能是过大的
  • 比如一个进程需要 40 MB 的内存(10 K 个页面),则根据页表大小 4 KB 我们共需要 10 K 个页表项,假设每个页表项为 4 B 大小,则该进程的页表大小为 40 KB(相当于额外花费了 10 个页的大小来存储页表项)
  • 从一方面讲,源程序 10 K 个页面,执行程序时一般只需要几十个页面进入内存即可,我们如果要求 10 个页表都进入内存,这相对于几十个页面来讲,是大大降低了内存的利用率
  • 其实,这 10 个页表并不需要同时保存在内存中,因为绝大多数情况下,映射所需的页表项都处于页表的集中相邻的区域(如果是二级页表即往往在同一个二级页表中)
  • 因此,将页表映射的思想进一步延伸,就可以得到二级页表:将页表的 10 页空间也进行地址映射,建立上一级页表,用于存储页表的映射关系
  • 原来的一级页表:最大 2^20 个页表项,每个页表项指向一块物理块
  • 二级页表:2^10 个顶级页表项,分别指向 2^10 个二级页表的物理地址,同时每个二级页表有 2^10 个页表项,因此总共还是有 2^20 个页表项,但引入二级页表后,我们需要常驻内存的只需要顶级页表,二级页表只在有需要的时候调入即可,大大增加了内存的利用率
  • 下图为 Intel 处理器 80x86 系列的硬件分页的地址转换过程
    • 在 32 位系统中,全部 32 位逻辑地址空间可以分为 2^20 (4GB/4KB) 个页面
    • 这些页面可以再进一步建立顶级页表,需要 2^10 个顶级页表项进行索引,这正好是一页的大小(4B * 2^10 项 = 4 KB),所以建立二级页表即可
      硬件分页地址转换
  • 举例 32 位系统中进程分页的工作过程:
    • 假定内核已经给一个正在运行的进程分配的逻辑地址空间是 0x200000000x2003FFFF,这个空间由 64 个页面组成
    • 在进程运行时,我们不需要知道全部这些页的页框的物理地址,很可能其中很多页还不在主存中
    • 这里我们只注意在进程运行到某一页时,硬件是如何计算得到这一页的页框的物理地址即可
    • 现在进程需要读逻辑地址 0x20021406 中的字节内容,这个逻辑地址按如下进行处理:
      • 逻辑地址: 0x20021406 (0010 0000 0000 0010 0001 0100 0000 0110 B)
      • 顶级页表字段:0x80 (00 1000 0000 B)
      • 二级页表字段:0x21 (00 0010 0001B)
      • 页内偏移量字段:0x406 (0100 0000 0110 B)
    • 顶级页表字段的 0x80 用于选择顶级页表(只有一页,共有 1024 项,分别指向 1024 个二级页表物理地址)的第 0x80 项,此项指向和该进程的页相关的二级页表
    • 二级页表字段 0x21 用于选择二级页表的第 0x21 表项,该项所指向的页框即为和该进程相关的页
    • 最后的页内偏移量字段 0x406 用于在目标页框中读取偏移量为 0x406 中的字节
  • 建立多级页表的目的在于建立索引,这样不用浪费主存空间去存储无用的页表项,也不用盲目地顺序式查找页表项,而建立索引的要求是最高一级页表项不超过一页的大小
  • 在 64 位操作系统中,页表的划分则需要重新考虑,设计为多级页表

3.4.2 基本分段存储管理方式

  • 分页管理方式是从计算机的角度考虑设计的,以提高内存的利用率,提升计算机的性能,且分页通过硬件机制实现,对用户完全透明
  • 而分段管理方式的提出则是考虑了用户和程序员,以满足方便编程、信息保护和共享、动态增长及动态链接等多方面的需要

分段

  • 段式管理方式按照用户进程中的自然段划分逻辑空间
  • 例如,用户进程由主程序、两个子程序、栈和一段数据组成,于是可以把这个用户进程划分为 5 个段,每段从 0 开始编址,并分配一段连续的地址空间(段内要求连续,段间不要求连续,因此整个作业的地址空间是二维的)
  • 其逻辑地址由段号 S 与段内偏移量 W 两部分组成
  • 如下图,段号为 16 位,段内偏移量为 16 位,则一个作业最多可有216=65536 个段,最大段长为 64KB
    分段系统中的逻辑地址结构
  • 在页式系统中,逻辑地址的页号和页内偏移量对用户是透明的,但在段式系统中,段号和段内偏移量必须由用户显示提供,在髙级程序设计语言中,这个工作由编译程序完成

段表

  • 每个进程都有一张逻辑空间与内存空间映射的段表,其中每一个段表项对应进程的一个段,段表项记录该段在内存中的起始地址和段的长度,段表的内容如下图所示:
    段表项
  • 段表用于实现从逻辑段到物理内存区的映射,在配置了段表后,执行中的进程可通过查找段表,找到每个段所对应的内存区:
    利用段表实现地址映射

地址变换机构

  • 分段系统的地址变换过程大致如下图所示:
    分段系统的地址变换过程
  • 为了实现进程从逻辑地址到物理地址的变换功能,在系统中设置了段表寄存器,用于存放段表始址 F 和段表长度 M
  • 其从逻辑地址 A 到物理地址 E 之间的地址变换过程如下:
    • 从逻辑地址A中取出前几位为段号 S,后几位为段内偏移量 W
    • 比较段号 S 和段表长度 M,若 S >= M,则产生越界中断,否则继续执行
    • 段表中段号 S 对应的段表项地址 = 段表起始地址 F + 段号 S * 段表项长度,取出该段表项的前几位得到段长 C
    • 若 段内偏移量 >= C,则产生越界中断,否则继续执行
    • 取出段表项中该段的起始地址 b,计算 E = b + W,用得到的物理地址 E 去访问内存

段的共享与保护

  • 在分段系统中,段的共享是通过两个作业的段表中相应的表项指向被共享的同一个个段来实现的
  • 当一个作业正从共享段中读取数据时,必须防止另一个作业修改此共享段中的数据
  • 不能修改的代码称为纯代码或可重入代码(它不属于临界资源),这样的代码和不能修改的数据是可以共享的,而可修改的代码和数据则不能共享
  • 与分页管理类似,分段管理的保护方法主要有两种:一种是存取控制保护,另一种是地址越界保护
  • 地址越界保护是利用段表寄存器中的段表长度与逻辑地址中的段号比较,若段号大于段表长度则产生越界中断;再利用段表项中的段长和逻辑地址中的段内位移进行比较,若段内位移大于段长,也会产生越界中断

3.4.3 段页式管理方式

  • 页式存储管理能有效地提高内存利用率,而分段存储管理能反映程序的逻辑结构并有利于段的共享。如果将这两种存储管理方法结合起来,就形成了段页式存储管理方式
  • 在段页式系统中,作业的地址空间首先被分成若干个逻辑段,每段都有自己的段号,然后再将每一段分成若干个大小固定的页(这些页构成页表)
  • 对内存空间的管理仍然和分页存储管理一样,将其分成若干个和页面大小相同的存储块,对内存的分配以存储块为单位
    段页式管理方式
  • 在段页式系统中,作业的逻辑地址分为三部分:段号、页号和页内偏移量
    段页式系统的逻辑地址
  • 为了实现地址变换,系统为每个进程建立一张段表,而每个分段有一张页表
  • 段表表项中至少包括段号、页表长度和页表起始地址,通过段表项可以找到该段中对应的页表
  • 页表表项中至少包括页号和块号,通过段对应的页表可以确定改段涉及的所有页,通过一个页表项确定一个具体的页
  • 系统中还应有一个段表寄存器,指出作业的段表起始地址和段表长度
  • 注意:在一个进程中,段表只有一个,而页表可能有多个
  • 在进行地址变换时,首先通过段表查到页表起始地址,然后通过页表找到页帧号,最后形成物理地址,大致访问顺序如下图所示
    段页式系统的地址变换机构
  • 可以看到,进行一次访问实际需要三次访问主存,这里同样可以使用快表以加快查找速度,其关键字由段号、页号组成,值是对应的页帧号和保护码

3.5 虚拟内存的概念、特征以及虚拟内存的实现

3.5.1 传统存储管理方式的特征

  • 我们前面所讨论的各种内存管理策略都是为了同时将多个进程保存在内存中以便允许多道程序设计,它们都具有以下两个共同的特征:

一次性

  • 作业必须一次性全部装入内存后,方能开始运行,这会导致两种情况发生:
  • 当作业很大,不能全部被装入内存时,将使该作业无法运行
  • 当大量作业要求运行时,由于内存不足以容纳所有作业,只能使少数作业先运行,导致多道程序度的下降

驻留性

  • 作业被装入内存后,就一直驻留在内存中,其任何部分都不会被换出,直至作业运行结束
  • 运行中的进程,会因等待 I/O 而被阻塞,可能处于长期等待状态
  • 由以上分析可知,许多在程序运行中不用或暂时不用的程序(数据)占据了大量的内存空间,而一些需要运行的作业又无法装入运行,显然浪费了宝贵的内存资源

3.5.2 局部性原理

  • 要真正理解虚拟内存技术的思想,首先必须了解计算机中著名的局部性原理
  • 著名的 Bill Joy (SUN 公司 CEO)说过:“在研究所的时候,我经常开玩笑地说高速缓存是计算机科学中唯一重要的思想。事实上,髙速缓存技术确实极大地影响了计算机系统的设计。”
  • 快表、 页高速缓存以及虚拟内存技术从广义上讲,都是属于高速缓存技术,这个技术所依赖的原理就是局部性原理
  • 局部性原理既适用于程序结构,也适用于数据结构(更远地讲,Dijkstra 著名的关于“goto 语句有害”的论文也是出于对程序局部性原理的深刻认识和理解)
  • 局部性原理表现在以下两个方面:
    • 时间局部性:如果程序中的某条指令一旦执行,不久以后该指令可能再次执行;如果某数据被访问过,不久以后该数据可能再次被访问。产生时间局部性的典型原因,是由于在程序中存在着大量的循环操作。
    • 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,即程序在一段时间内所访问的地址,可能集中在一定的范围之内,这是因为指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的
  • 时间局部性是通过将近来使用的指令和数据保存到高速缓存存储器中,并使用高速缓存的层次结构实现
  • 空间局部性通常是使用较大的高速缓存,并将预取机制集成到高速缓存控制逻辑中实现。虚拟内存技术实际上就是建立了 “内存一外存”的两级存储器的结构,利用局部性原理实现髙速缓存

3.5.3 虚拟存储器的定义和特征

  • 基于局部性原理,在程序装入时,可以将程序的一部分装入内存,而将其余部分留在外存,就可以启动程序执行
  • 在程序执行过程中,当所访问的信息不在内存时,由操作系统将所需要的部分调入内存,然后继续执行程序
  • 另一方面,操作系统将内存中暂时不使用的内容换出到外存上,从而腾出空间存放将要调入内存的信息
  • 这样,系统好像为用户提供了一个比实际内存大得多的存储器,称为虚拟存储器
  • 之所以将其称为虚拟存储器,是因为这种存储器实际上并不存在,只是由于系统提供了部分装入、请求调入和置换功能后(对用户完全透明),给用户的感觉是好像存在一个比实际物理内存大得多的存储器
  • 虚拟存储器的大小由计算机的地址结构决定,并非是内存和外存的简单相加。
  • 虚拟存储器有以下三个主要特征:
    • 多次性,是指无需在作业运行时一次性地全部装入内存,而是允许被分成多次调入内存运行。
    • 对换性,是指无需在作业运行时一直常驻内存,而是允许在作业的运行过程中,进行换进和换出。
    • 虚拟性,是指从逻辑上扩充内存的容量,使用户所看到的内存容量,远大于实际的内存容量

3.5.4 虚拟内存技术的实现

  • 虚拟内存中,允许将一个作业分多次调入内存
  • 釆用连续分配方式时,会使相当一部分内存空间都处于暂时或“永久”的空闲状态,造成内存资源的严重浪费,而且也无法从逻辑上扩大内存容量
  • 虚拟内存的实需要建立在离散分配的内存管理方式的基础上,虚拟内存的实现有以下三种方式:
    • 请求分页存储管理
    • 请求分段存储管理
    • 请求段页式存储管理
  • 不管哪种方式,都需要有一定的硬件支持。一般需要的支持有以下几个方面:
    • 一定容量的内存和外存
    • 页表机制(或段表机制),作为主要的数据结构
    • 中断机构,当用户程序要访问的部分尚未调入内存,则产生中断
    • 地址变换机构,逻辑地址到物理地址的变换

3.6 请求分页管理方式实现虚拟内存

  • 请求分页系统建立在基本分页系统基础之上,为了支持虚拟存储器功能而增加了请求调页功能和页面置换功能
  • 请求分页是目前最常用的一种实现虚拟存储器的方法
  • 在请求分页系统中,只要求将当前需要的一部分页面装入内存,便可以启动作业运行
  • 在作业执行过程中,当所要访问的页面不在内存时,再通过调页功能将其调入,同时还可以通过置换功能将暂时不用的页面换出到外存上,以便腾出内存空间
  • 为了实现请求分页,系统必须提供一定的硬件支持
  • 除了需要一定容量的内存及外存的计算机系统,还需要有页表机制、缺页中断机构和地址变换机构

页表机制

  • 请求分页系统的页表机制不同于基本分页系统,请求分页系统在一个作业运行之前不要求全部一次性调入内存,因此在作业的运行过程中,必然会出现要访问的页面不在内存的情况,如何发现和处理这种情况是请求分页系统必须解决的两个基本问题
  • 为此,如下图所示,在请求页表项中增加了四个字段
    请求分页系统中的页表项
  • 增加的四个字段说明如下:
    • 状态位 P:用于指示该页是否已调入内存,供程序访问时参考。
    • 访问字段 A:用于记录本页在一段时间内被访问的次数,或记录本页最近己有多长时间未被访问,供置换算法换出页面时参考。
    • 修改位 M:标识该页在调入内存后是否被修改过。
    • 外存地址:用于指出该页在外存上的地址,通常是物理块号,供调入该页时参考

缺页中断机构

  • 在请求分页系统中,每当所要访问的页面不在内存时,便产生一个缺页中断,请求操作系统将所缺的页调入内存
  • 此时应将缺页的进程阻塞(调页完成后唤醒),如果内存中有空闲块,则分配一个块,将要调入的页装入该块,并修改页表中相应页表项
  • 若此时内存中没有空闲块,则要淘汰某页(若被淘汰页在内存期间被修改过,则要将其写回外存)
  • 缺页中断作为中断同样要经历,诸如保护 CPU 环境、分析中断原因、转入缺页中断处理程序、恢复 CPU 环境等几个步骤
  • 但与一般的中断相比,它有以下两个明显的区别:
    • 在指令执行期间产生和处理中断信号,而非一条指令执行完后,属于内部中断
    • 一条指令在执行期间,可能产生多次缺页中断

地址变换机构

  • 请求分页系统中的地址变换机构,是在分页系统地址变换机构的基础上,为实现虚拟内存,又增加了某些功能而形成的
  • 请求分页中的地址变换过程大致如下图所示:
    请求分页中的地址变换过程
  • 在进行地址变换时,先检索快表:
    • 若找到要访问的页,便修改页表项中的访问位(写指令则还须重置修改位),然后利用页表项中给出的物理块号和页内地址形成物理地址
    • 若未找到该页的页表项,应到内存中去查找页表,再对比页表项中的状态位 P,看该页是否已调入内存,未调入则产生缺页中断,请求从外存把该页调入内存
  • 所有算法的举例请参考原书或网址

3.7 页面置换算法

  • 进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区
  • 选择调出页面的算法就称为页面置换算法
  • 好的页面置换算法应有较低的页面更换频率,也就是说,应将以后不会再访问或者以后较长时间内不会再访问的页面先调出
  • 常见的页面置换算法有四种:最佳置换算法(OPT)、先进先出页面置换算法(FIFO)、最近最久未使用页面置换算法(LRU)、时钟置换算法(CLOCK)

3.7.1 最佳置换算法(OPT)

  • 最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率
  • 但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现
  • 但最佳置换算法可以用来评价其他算法

3.7.2 先进先出页面置换算法(FIFO)

  • 优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面
  • 该算法实现简单,只需把调入内存的页面根据先后次序链接成队列,设置一个指针总指向最早的页面
  • 但该算法与进程实际运行时的规律不适应,因为在进程中,有的页面经常被访问
  • FIFO算法还会产生当所分配的物理块数增大而页故障数不减反增的异常现象,这是由 Belady 于 1969 年发现,故称为 Belady 异常
  • Belady 异常:分配的页面数越多,反而缺页率越高

3.7.3 最近最久未使用页面置换算法(LRU)

  • LRU 选择最近最长时间未访问过的页面予以淘汰,它认为过去一段时间内未访问过的页面,在最近的将来可能也不会被访问
  • 该算法为每个页面设置一个访问字段,来记录该页面自上次被访问以来所经历的时间,淘汰页面时选择现有页面中值最大的予以淘汰
  • LRU 性能较好,但需要寄存器和栈的硬件支持
  • LRU 是堆栈类的算法,理论上可以证明,堆栈类算法不可能出现Belady 异常
  • FIFO 算法基于队列实现,不是堆栈类算法

3.7.4 时钟置换算法(CLOCK)/最近未使用算法(NRU)

  • LRU 算法的性能接近于 OPT,但是实现起来比较困难,且开销大
  • FIFO 算法实现简单,但性能差
  • 所以操作系统的设计者尝试了很多算法,试图用比较小的开销接近 LRU 的性能,这类算法都是 CLOCK 算法的变体

简单的 CLOCK 算法

  • 简单的 CLOCK 算法是给每一页关联一个附加位,称为使用位
  • 当某一页首次装入主存时,标记该页的使用位为 1
  • 当该页随后再被访问到时,再次它的使用位为 1
  • 对于页替换算法,将所有可被替换的页集合看做一个循环缓冲区,并且有一个指针与之相关联
  • 当某一页被替换时,该指针被设置成指向缓冲区中的下一页
  • 当需要替换一页时,操作系统扫描缓冲区,从初始值很位置开始查找使用位被置为 0 的一帧
  • 在查找的过程,每当遇到一个使用位为 1 的帧时,操作系统就将该位重新置为 0
  • 如果在这个过程开始时,缓冲区中所有帧的使用位均为 0,则选择遇到的第一个帧替换
  • 如果所有帧的使用位均为 1,则指针在缓冲区中完整地循环一周,把所有使用位都置为 0,并且停留在最初的位置上,替换该帧中的页(有点像循环队列的遍历)
  • 由于该算法循环地检查各页面的情况,故称为 CLOCK 算法,又称为最近未用(Not Recently Used, NRU)算法
  • CLOCK 算法的性能比较接近 LRU

改进的 CLOCK 置换算法

  • CLOCK 算法还能继续改进:通过增加使用的位数目,可以使得 CLOCK 算法更加高效
  • 在使用位的基础上再增加一个修改位,则得到改进型的 CLOCK 置换算法
  • 引入访问位和修改位后,每一帧都处于以下四种情况之一:
    • 近未被访问,也未被修改(u=0, m=0)
    • 最近被访问,但未被修改(u=1, m=0)
    • 最近未被访问,但被修改(u=0, m=1)
    • 最近被访问,被修改(u=1, m=1)
  • 算法执行如下操作步骤:
    • 从指针的当前位置开始,扫描帧缓冲区
    • 第一步:首次扫描过程中,对使用位不做任何修改,选择遇到的第一个(u=0, m=0)帧用于替换
    • 若首次扫描获取失败,则执行第二次扫描,查找第一次遇到的(u=0, m=1)的帧,扫描的过程中同时将 u = 1 的帧设置为 u = 0
    • 若第二次扫描仍然失败,指针将回到它的最初位置,此时集合中所有帧的使用位均为 0,我们重复首次扫描操作,若仍未找到重复第二次扫描操作,必定会找到一个合适的帧用于替换
  • 改进型的 CLOCK 算法优于简单 CLOCK 算法之处在于替换时首选没有变化的页
  • 由于修改过的页在被替换之前必须写回,因而这样做会节省时间

3.8 页面分配策略:驻留集大小、调入页面的时机以及从何处调入页面

驻留集大小

  • 对于分页式的虚拟内存,在准备执行时,不需要也不可能把一个进程的所有页都读取到主存,因此,操作系统必须决定读取多少页
  • 也就是说,给特定的进程分配多大的主存空间,这需要考虑以下几点:
    • 分配给一个进程的存储量越小,在任何时候驻留在主存中的进程数就越多,从而可以提高处理机的时间利用效率
    • 如果一个进程在主存中的页数过少,尽管有局部性原理,页错误率仍然会相对较高
    • 如果页数过多,由于局部性原理,给特定的进程分配更多的主存空间对该进程的错误率没有明显的影响
  • 基于这些因素,现代操作系统通常釆用三种策略:
  • 固定分配局部置换:
    • 它为每个进程分配一定数目的物理块,在整个运行期间都不改变
    • 若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一页换出,然后再调入需要的页面
    • 实现这种策略难以确定为每个进程应分配的物理块数目:太少会频繁出现缺页中断,太多又会使 CPU 和其他资源利用率下降
  • 可变分配全局置换:
    • 这是最易于实现的物理块分配和置换策略,为系统中的每个进程分配一定数目的物理块,操作系统自身也保持一个空闲物理块队列
    • 当某进程发生缺页时,系统从空闲物理块队列中取出一个物理块分配给该进程,并将欲调入的页装入其中。
  • 可变分配局部置换:
    • 它为每个进程分配一定数目的物理块,当某进程发生缺页时,只允许从该进程在内存的页面中选出一页换出,这样就不会影响其他进程的运行
    • 如果进程在运行中频繁地缺页,系统再为该进程分配若干物理块,直至该进程缺页率趋于适当程度
    • 反之,若进程在运行中缺页率特别低,则可适当减少分配给该进程的物理块

调入页面的时机

  • 为确定系统将进程运行时所缺的页面调入内存的时机,可釆取以下两种调页策略:
  • 预调页策略:
    • 根据局部性原理,一次调入若干个相邻的页可能会比一次调入一页更高效
    • 但如果调入的一批页面中大多数都未被访问,则又是低效的
    • 所以就需要釆用以预测为基础的预调页策略,将预计在不久之后便会被访问的页面预先调入内存
    • 但目前预调页的成功率仅约 50%,故这种策略主要用于进程的首次调入时,由程序员指出应该先调入哪些页
  • 请求调页策略:
    • 进程在运行中需要访问的页面不在内存而提出请求,由系统将所需页面调入内存
    • 由这种策略调入的页一定会被访问,且这种策略比较易于实现,故在目前的虚拟存储器中大多釆用此策略
    • 它的缺点在于每次只调入一页,调入调出页面数多时会花费过多的 I/O 开销

从何处调入页面

  • 请求分页系统中的外存分为两部分:用于存放文件的文件区和用于存放对换页面的对换区
  • 对换区通常是釆用连续分配方式,而文件区釆用离散分配方式,故对换区的磁盘 I/O 速度比文件区的更快
  • 这样从何处调入页面有三种情况:
  • 系统拥有足够的对换区空间:
    • 可以全部从对换区调入所需页面,以提髙调页速度
    • 为此,在进程运行前,需将与该进程有关的文件从文件区复制到对换区
  • 系统缺少足够的对换区空间:
    • 凡不会被修改的文件都直接从文件区调入
    • 而当换出这些页面时,由于它们未被修改而不必再将它们换出
    • 但对于那些可能被修改的部分,在将它们换出时须调到对换区,以后需要时再从对换区调入
  • UNIX 方式:
    • 与进程有关的文件都放在文件区,故未运行过的页面,都应从文件区调入
    • 曾经运行过但又被换出的页面,由于是被放在对换区,因此下次调入时应从对换区调入
    • 进程请求的共享页面若被其他进程调入内存,则无需再从对换区调入

3.9 页面抖动(颠簸)和工作集(驻留集)

页面抖动(颠簸)

  • 在页面置换过程中的一种最糟糕的情形是,刚刚换出的页面马上又要换入主存,刚刚换入的页面马上就要换出主存,这种频繁的页面调度行为称为抖动,或颠簸
  • 如果一个进程在换页上用的时间多于执行时间,那么这个进程就在颠簸
  • 频繁的发生缺页中断(抖动),其主要原因是某个进程频繁访问的页面数目高于可用的物理页帧数目
  • 保留每个进程较少的页数(驻留集大小),虚拟内存技术可以在内存中保留更多的进程以提髙系统效率
  • 在稳定状态,几乎主存的所有空间都被进程块占据,处理机和操作系统可以直接访问到尽可能多的进程
  • 但如果管理不当,处理机的大部分时间都将用于交换块,即请求调入页面的操作,而不是执行进程的指令,这就会大大降低系统效率

工作集(驻留集)

  • 工作集(或驻留集)是指在某段时间间隔内,进程要访问的页面集合
  • 经常被使用的页面需要在工作集中,而长期不被使用的页面要从工作集中被丢弃
  • 为了防止系统出现抖动现象,需要选择合适的工作集大小
  • 工作集模型的原理是:
    • 让操作系统跟踪每个进程的工作集,并为进程分配大于其工作集的物理块
    • 如果还有空闲物理块,则可以再调一个进程到内存以增加多道程序数
    • 如果所有工作集之和增加以至于超过了可用物理块的总数,那么操作系统会暂停一个进程,将其页面调出并且将其物理块分配给其他进程,防止出现抖动现象
  • 正确选择工作集的大小,对存储器的利用率和系统吞吐量的提嵩,都将产生重要影响

3.10 内存管理知识点总结

4. 文件管理

5. I/O 管理

6. 试题

参 1.6