【王道考研操作系统】

微博大号–@王道咸鱼老师-计算机考研
微博小号–@王道楼楼老师-计算机考研

第一章 计算机系统概述

1.1.1 操作系统的概念、功能

  • 大家都熟悉的操作系统


  • 操作系统的概念(定义)

一台电脑的诞生~
Step 1:厂家组装一台裸机(裸机是没有配置操作系统和其他软件的电子计算机)
Step 2:出售前安装操作系统
Step 3:用户安装应用程序(eg:QQ)
Step 4:使用 QQ 聊天


操作系统(Operating System, OS)

  • 是指控制和管理整个计算机系统的硬件和软件①操作系统是系统资源的管理者)资源,并合理地组织调度计算机的工作和资源的分配;
  • 提供给用户和其他软件方便的接口和环境②向上层提供方便易用的服务);
  • 它是计算机系统中最基本的系统软件③是最接近硬件的一层软件)。

直观的例子:打开 Windows 操作系统的“任务管理器”(快捷键:Ctrl+Alt+Del)


  • 操作系统的功能和目标——作为系统资源的管理者
    • 提供的功能:
      • 处理机管理
      • 存储器管理
      • 文件管理
      • 设备管理
    • 目标:
      • 安全、高效

补充知识:执行一个程序前需要将该程序放到内存中,才能被CPU处理。

用QQ和朋友视频聊天的过程:
Step 1:在各个文件夹中找到 QQ 安装的位置(如 D:/Tencent/QQ/Bin)【逐层打开文件夹,找到QQ.exe 这个程序(可执行文件)的存放位置】
Step 2:双击打开 QQ.exe 【需要把该程序相关数据放入内存】
Step 3:QQ 程序正常运行 【对应的进程被处理机(CPU)处理】
Step 4:开始和朋友视频聊天 【需要将摄像头设备分配给进程】


  • 操作系统的功能和目标——向上层提供方便易用的服务

封装思想:操作系统把一些丑陋的硬件功能封装成简单易用的服务,使用户能更方便地使用计算机,用户无需关心底层硬件的原理,只需要对操作系统发出命令即可。

GUI:图形化用户接口(Graphical User Interface) 【很多现代操作系统都提供GUI】
用户可以使用形象的图形界面进行操作,而不再需要记忆复杂的命令、参数。
例子:在 Windows 操作系统中,删除一个文件只需要把文件“拖拽”到回收站即可。

联机命令接口实例(Windows系统) 联机命令接口=交互式命令接口
Step 1:win键+R
Step 2:输入cmd,按回车,打开命令解释器
Step 3:尝试使用 time 命令
【特点:用户说一句, 系统跟着做一句】

脱机命令接口实例(Windows系统) 脱机命令接口=批处理命令接口
使用windows系统的搜索功能,搜索C盘中的 *.bat文件,用记事本任意打开一个。
【特点:用户说一堆, 系统跟着做一堆】

程序接口:可以在程序中进行系统调用来使用程序接口。普通用户不能直接使用程序接口,只能通过程序代码间接使用。
如:写C语言“Hello world”程序时,在 printf 函数的底层就使用到了操作系统提供的显式相关的“系统调用”。

系统调用类似于函数调用,是应用程序请求操作系统服务的唯一方式。在有的教材中: 系统调用=广义指令


【有的教材中把命令接口和程序接口统称为“用户接口”,意思就是说狭义的用户接口不包括GUI。】


  • 操作系统的功能和目标——作为最接近硬件的层次

需要实现对硬件机器的拓展
没有任何软件支持的计算机成为裸机。在裸机上安装的操作系统,可以提供资源管理功能和方便用户的服务功能,将裸机改造成功能 更强、使用更方便的机器;
通常把覆盖了软件的机器成为扩充机器,又称之为虚拟机

类比汽车: 发动机——只会转;轮胎——只会滚;
在原始的硬件机器上覆盖一层传动系统——让发动机带着轮子转——使原始的硬件机器得到拓展

操作系统对硬件机器的拓展:将CPU、内存、磁盘、显示器、键盘等硬件合理地组织起来,让各种硬件能够相互协调配合,实现更多更复杂的功能普通用户无需关心这些硬件在底层是怎么组织起来工作的,只需直接使用操作系统提供的接口即可


1.1.2 操作系统的特征

  • 并发
  • 共享

并发和共享是两个最基本的特征,二者互为存在条件。

  • 虚拟
  • 异步

重要考点:
理解并发和并行的区别
并发和共享互为存在条件
没有并发和共享,就谈不上虚拟和异步,因此并发和共享是操作系统的两个最基本的特征

  • 操作系统的特征——并发

并发:指两个或多个事件在同一时间间隔内发生。这些事件宏观上是同时发生的,但微观上是交替发生的。
常考易混概念——并行:指两个或多个事件在同一时刻同时发生。

并发 VS 并行:
eg:假设小渣和老渣每人有两个女朋友。任务1:和一号约会;任务2:和二号约会…

操作系统的并发性指计算机系统中“同时”运行着多个程序,这些程序宏观上看是同时运行着的,而微观上看是交替运行的。
操作系统就是伴随着“多道程序技术”(后面介绍)而出现的。因此,操作系统和程序并发是一起诞生的

注意(重要考点):
单核CPU同一时刻只能执行一个程序,各个程序只能并发地执行
多核CPU同一时刻可以同时执行多个程序,多个程序可以并行地执行
比如Intel 的第八代 i3 处理器就是 4 核CPU,意味着可以并行地执行4个程序;【即使是对于4核CPU来说,只要有4个以 上的程序需要“同时”运行,那么并发性依然是必不可少的,因此并发性是操作系统一个最基本的特性


  • 操作系统的特征——共享

共享即资源共享,是指系统中的资源可供内存中多个并发执行的进程共同使用。

  • 两种资源共享方式

    • 互斥共享方式
      系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源

    • 同时共享方式

      系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问

所谓的“同时”往往是宏观上的,而在微观上,这些进程可能是交替地对该资源进行访问的(即分时共享)

生活实例:
互斥共享方式:使用QQ和微信视频。同一时间段内摄像头只能分配给其中一个进程。
同时共享方式:使用QQ发送文件A,同时使用微信发送文件B。宏观上看,两边都在同时读取并发送文件, 说明两个进程都在访问硬盘资源,从中读取数据。微观上看,两个进程是交替着访问硬盘的。


  • 操作系统的特征——并发和共享的关系

并发性指计算机系统中同时存在着多个运行着的程序。
共享性是指系统中的资源可供内存中多个并发执行的进程共同使用。

通过上述例子来看并发与共享的关系:
使用QQ发送文件A,同时使用微信发送文件B。
1.两个进程正在并发执行(并发性) 【如果失去并发性,则系统中只有一个程序正在运行,则共享性失去存在的意义】
2.需要共享地访问硬盘资源(共享性)【如果失去共享性,则QQ和微信不能同时访问硬盘资源,就无法实现同时发送文件,也就无法并发】

二者互为存在条件。


  • 操作系统的特征——虚拟

虚拟是指把一个物理上的实体变为若干个逻辑上的对应物。物理实体(前者)是实际存在的,而逻辑上对应物(后者)是用户感受到的。

Yo~用一个例子来理解
背景知识:一个程序需要放入内存并给它分配CPU才能执行

GTA5需要4GB的运行内存,
QQ 需要256MB的内存,
迅雷需要256MB的内存,
网易云音乐需要256MB的内存……
我的电脑:4GB内存
问题:这些程序同时运行需要的内存远大于4GB,那么为什么它们还可以在我的电脑上同时运行呢?
答:这是虚拟存储器技术。实际只有4GB的内存,在用户看来似乎远远大于4GB。
\Rightarrow 虚拟技术中的“空分复用技术” 。

某单核CPU的计算机中,用户打开了以下软件。。。

问题:既然一个程序需要被分配CPU才能正常执行,那么为什么单核CPU 的电脑中能同时运行这么多个程序呢? 答:这是虚拟处理器技术。实际上只有一个单核CPU,在用户看来似乎有 6个CPU在同时为自己服务。
\Rightarrow 虚拟技术中的“时分复用技术” 。微观上处理机在各个微小的时间段内交替着为各个进程服务。

  • 虚拟技术
    • 空分复用技术(如虚拟存储技术)
    • 时分复用技术(如虚拟处理器)

显然,如果失去了并发性,则一个时间段内系统中只需运行一道程序,那么就失去了实现虚拟性的意义了。因此,没有并发性,就谈不上虚拟性


  • 操作系统的特征——异步

异步是指,在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底的,而是走走停停,以不可预知的速度向前推进,这就是进程的异步性。

老渣要和两个女孩并发约会:

  • 第一道程序:
    • 一号的指令1:老渣陪我吃饭
    • 一号的指令2:老渣把心给我
  • 第二道程序:
    • 二号的指令1:老渣把心给我
    • 二号的指令2:老渣陪我吃饭

与一、二号的约会 = 两道并发执行的程序 老渣的心 = 有限的系统资源

由于并发运行的程序会争抢着使用系统资源,而系统中的资源有限,因此进程的执行不是一贯到底的,而是走走停停的,以不可预知的速度向前推进

如果失去了并发性,即系统只能串行地运行各个程序,那么每个程序的执行会一贯到底。只有系统拥有并发性,才有可能导致异步性


1.2 操作系统的发展与分类

  • 手工操作阶段


  • 批处理阶段——单道批处理系统

引入脱机输入/输出技术(用外围机+磁带完成),并由监督程序(操作系统的雏形)负责控制作业的输入、输出

主要优点:缓解了一定程度的人机速度矛盾,资源利用率有所提升。
主要缺点:内存中仅能有一道程序运行,只有该程序运行结束之后才 能调入下一道程序。CPU有大量的时间是在空闲等待I/O完成。资源利用率依然很低。

主要特征:自动性、顺序性、单道性


  • 批处理阶段——多道批处理系统

主要优点:多道程序并发执行,共享计算机资源。资源利用率大幅提升,CPU和其他资源更能保持“忙碌”状态,系统吞吐量增大
主要缺点:用户响应时间长,没有人机交互功能(用户提交自己的作业之后就只能等待 计算机处理完成,中间不能控制自己的作业 执行。eg:无法调试程序/无法在程序运行过 程中输入一些参数)

主要特征:多道、宏观并行、微观串行

多道程序的基本特征:共享性、间断性、制约性


  • 分时操作系统

分时操作系统:计算机以时间片为单位轮流为各个用户/作业服务,各个用户可通过终端与计算机进行交互。
主要优点:用户请求可以被即时响应,解决了人机交互问题。允许多个用户同时使用一台计算机,并且用户对计算机的操作相互独立,感受不到别人的存在。
主要缺点:不能优先处理一些紧急任务。操作系统对各个用户/作业都是完全公平的,循环地为每个用户/作业服务一个时间片,不区分任务的紧急性

主要特征:同时性、交互性、独立性、及时性


  • 实时操作系统
    • 硬实时系统
      必须在绝对严格的规定时间内完成处理
      如:导弹控制系统、自动驾驶系统
    • 软实时系统
      能接受偶尔违反时间规定
      如:12306火车订票系统

实时操作系统: 主要优点:能够优先响应一些紧急任务,某些紧急任务不需时间片排队。
在实时操作系统的控制下,计算机系统接收到外部信号后及时进行处理,并且要在严格的时限内处理完事件。实时操作系统的主要特点是及时性和可靠性


  • 其他几种操作系统

网络操作系统:是伴随着计算机网络的发展而诞生的,能把网络中各个计算机有机地结合起来,实现数据传送等功能,实现网络中各种资源的共享(如文件共享)和各台计算机之间的通信。(如:Windows NT 就是一种典型的网络操作系统,网站服务器就可以使用)

分布式操作系统:主要特点是分布性和并行性。系统中的各台计算机地位相同,任何工作都可以分布在这些计算机上,由它们并行、协同完成这个任务

个人计算机操作系统:如 Windows XP、MacOS,方便个人使用。


1.3.1 操作系统的运行机制

Tips:

  1. 都是高频考点,很重要
  2. 初学者不完全理解没关系, 放心大胆地往后学,随着后面章节的学习,理解会逐渐加深
  • 预备知识:程序是如何运行的?

程序运行的过程其实就 是CPU执行一条一条的机器指令的过程

“指令”就是处理器(CPU)能识别、执行的最基本命令

注:很多人习惯把 Linux、Windows、MacOS 的“小黑框”中使用的命令也 称为“指令”,其实这是“交互式命令接口”,注意与本节的“指令”区别 开。本节中的“指令”指二进制机器指令


  • 内核程序 v.s. 应用程序

我们普通程序员写的程序就是“应用程序

微软、苹果有一帮人负责实现操作系统,他们写的是“内核程序
由很多内核程序组成了“操作系统内核”,或简称“内核(Kernel) ” 内核是操作系统最重要最核心的部分,也是最接近硬件的部分甚至可以说,一个操作系统只要有内核就够了(eg:Docker—>仅需Linux内核) 操作系统的功能未必都在内核中,如图形化用户界面 GUI


  • 特权指令 v.s. 非特权指令

CPU设计和生产的时候就划分了特权指令和非特权指令,因此CPU 执行一条指令前就能判断出其类型

应用程序只能使用“非特权指令”,如: 加法指令、减法指令等,不允许用户直接使用

操作系统内核作为 “管理者”,有时会让CPU执行一些“特权指令”,如:内存清零指令。这些指令影响重大,只允许“管理者”——即操作系统内核来使用


  • 内核态 v.s. 用户态

\Rightarrow 问题:如何实现CPU状态的切换?

CPU 有两种状态,“内核态”和“用户态
处于内核态时,说明此时正在运行的是内核程序,此时可以执行特权指令
处于用户态时,说明此时正在运行的是应用程序,此时只能执行非特权指令

拓展:CPU 中有一个寄存器叫程序状态字寄存器(PSW) ,其中有个二进制位,1表示 “内核态”,0表示“用户态”

别名:内核态=核心态=管态;用户态=目态


  • 内核态、用户态 的切换
一个故事:
① 刚开机时,CPU 为“内核态”,操作系统内核程序先上CPU运行
② 开机完成后,用户可以启动某个应用程序
③ 操作系统内核程序在合适的时候主动让出 CPU,让该应用程序上CPU运行
【操作系统内核在让出CPU之前,会用一条特权指令把 PSW 的标志位设置为“用户态”
④ 应用程序运行在“用户态”
⑤ 此时,一位猥琐黑客在应用程序中植入了一条特权指令,企图破坏系统…
⑥ CPU发现接下来要执行的这条指令是特权指令,但是自己又处于“用户态”
⑦ 这个非法事件会引发一个中断信号
CPU检测到中断信号后,会立即变为“核心态”,并停止运行当前的应用程序,转而运行处理中断信号的内核程序】
⑧ “中断”使操作系统再次夺回CPU的控制权
⑨ 操作系统会对引发中断的事件进行处理,处理完了再把CPU使用权交给别的应用程序
内核态用户态执行一条特权指令——修改PSW的标志位为“用户态”,这个动作意味着操作系统 将主动让出CPU使用权 用户态内核态:由“中断”引发,硬件自动完成变态过程,触发中断信号意味着操作系统将强行夺回CPU的使用权 【除了非法使用特权指令之外,还有很多事件 会触发中断信号。一个共性是,但凡需要操作系统介入的地方,都会触发中断信号
  • 补充

时钟管理的功能:1、计时;2、通过时钟中断的管理,可以实现进程切换


1.3.2 中断和异常

  • 中断的作用
    “中断”会使CPU由用户态变为内核态,使操作系统重新夺回对CPU的控制权

CPU 上会运行两种程序,一种是操作系统内核程序(是整个系统的管理者),一种是应用程序

在合适的情况下,操作系统内核会把CPU的使用权主动让给应用程序(第二章进程管理相关内容)

“中断”是让操作系统内核夺回CPU使用权唯一途径

如果没有“中断”机制,那么一旦应用程序上CPU运行,CPU就会一直运行这个应用程序
没有中断机制,就不可能实现操作系统,不可能实现程序并发


  • 中断的类型
    • 内中断
      与当前执行的指令有关, 中断信号来源于CPU内部
    • 外中断
      与当前执行的指令无关, 中断信号来源于CPU外部

  • 内中断的例子

例子 1:试图在用户态下执行特权指令
例子 2:执行除法指令时发现除数为 0
【若当前执行的指令是非法的,则会引发一个中断信号】

例子 3:有时候应用程序想请求操作系统内核的服务,此时会执行一条特殊的指令——陷入指令,该指令会引发一个内部中断信号
【执行“陷入指令”,意味着应用程序主动地将CPU控制权还给操作系统内核。 “系统调用”就是通过陷入指令完成的


  • 外中断的例子

例子1:时钟中断——由时钟部件发来的中断信号

例子2:I/O中断——由输入/输出设备发来的中断信号


  • 中断的分类【广义的中断】
    • 内中断(也称异常、例外)
      • 陷阱、陷入(trap)【由陷入指令引发,是应用程序故意引发的】
      • 故障(fault)【由错误条件引起的,可能被内核程序修复。内核程序修复故障后会把 CPU使用权还给应用程序,让它继续执行下去。如:缺页中断。】
      • 终止(abort)【由致命错误引起,内核程序无法修复该错误,因此一般不再将CPU使用权还给引发终止的应用程序, 而是直接终止该应用程序。如: 整数除0、非法使用特权指令】
    • 外中断(也称中断)【狭义的中断】
      • 时钟中断
      • I/O中断

大多数的教材、试卷中,“中断”特指狭义的中断,即外中断。而内中断一般称为“异常“


  • 中断机制的基本原理

检测中断信号:

  • 内中断:CPU在执行指令时会检查是否有异常发生
  • 外中断:每个指令周期末尾,CPU都会检查是否有外中断信号需要处理

不同的中断信号,需要用不同的中断处理程序来处理。当CPU检测到中断信号后,会根据中断信号的类型去查询“中断向量表”,以此来找到相应的中断处理程序在内存中的存放位置

显然,中断处理程序一定是内核程序,需要运行在“内核态


1.3.3 系统调用

  • 什么是系统调用,有何作用?

知识点回顾:
操作系统作为用户和计算机硬件之间的接口,需要向上提供一些简单易用的服务。主要包括命令接口和程序接口。其中,程序接口由一组系统调用组成。

“系统调用”是操作系统提供给应用程序(程序员/编程人员)使用的接口,可以理解为一种可供应用程序调用的特殊函数,应用程序可以通过系统调用来请求获得操作系统内核的服务


  • 系统调用与库函数的区别
普通应用程序 可直接进行系统调用,也可使用库函数。有的库函数涉及系统调用,有的不涉及
编程语言 向上提供库函数。有时会将系统调用封装成库函数,以隐藏系统调用的一些细节,使程序员编程更加方便。
操作系统 向上提供系统调用,使得上层程序能请求内核的服务
裸机

不涉及系统调用的库函数:如的“取绝对值”的函数 涉及系统调用的库函数:如“创建一个新文件”的函数


  • 小例子:为什么系统调用是必须的?

生活场景:去学校打印店打印论文,你按下了 WPS 的“打印”选项,打印机开始工作。
你的论文打印到一半时,另一位同学按下了 Word 的“打印”按钮,开始打印他自己的论文。

思考:如果两个进程可以随意地、并发地共享打印机资源,会发生什么情况?

两个进程并发运行,打印机设备交替地收到 WPS 和 Word 两个进程发来的打印请求,结果两篇论文的内容混杂在一起了…

解决方法:由操作系统内核对共享资源进行统一的管理,并向上提供“系统调用” ,用户进程想要使用打印机这种共享资源,只能通过系统调用向操作系统内核发出请求。内核会对各个请求进行协调处理。


  • 什么功能要用到系统调用?

应用程序通过系统调用请求操作系统的服务。而系统中的各种共享资源都由操作系统内核统一掌管,因此凡是与共享资源有关的操作(如存储分配、I/O操作、文件管理等),都必须通过系统调用的方式向操作系统内核提出服务请求,由操作系统内核代为完成。这样可以保证系统的稳定性和安全性,防止用户进行非法操作。

  • 系统调用(按功能分类)
    • 设备管理:完成设备的 请求/释放/启动 等功能
    • 文件管理:完成文件的 读/写/创建/删除 等功能
    • 进程管理:完成进程的 创建/撤销/阻塞/唤醒 等功能
    • 进程通信:完成进程之间的 消息传递/信号传递 等功能
    • 内存管理:完成内存的 分配/回收 等功能

拓展:感兴趣的同学可以搜索“Linux 系统调用”,了解 Linux 操作系统提供了哪些系统调用


  • 系统调用的过程

传递系统调用参数 → 执行陷入指令(用户态,陷入指令是非特权指令) → 执行相应的内请求核程序处理系统调用(核心态) → 返回应用程序

注意:
1.陷入指令是在用户态执行的,执行陷入指令之后立即引发一个内中断,使CPU进入核心态
2.发出系统调用请求是在用户态,而对系统调用的相应处理在核心态下进行

注意别名: 陷入指令 = trap 指令 = 访管指令


1.4.1 操作系统的体系结构(上)

  • 操作系统的内核

内核是计算机上配置的底层软件,是操作系统最基本、最核心的部分。
实现操作系统内核功能的那些程序就是内核程序
原语是一种特殊的程序,具有原子性,也就是说这段程序的运行必须一气呵成,不可被“中断”

注意:
操作系统内核需要运行在内核态
操作系统的非内核功能运行在用户态

一个故事:现在,应用程序想要请求操作系统的服务,这个服务的处理同时涉及到进程管理、存储管理、设备管理

注意:变态的过程是有成本的,要消耗不少时间,频繁地变态会降低系统性能
【注意:“变态”在这里是口头表述,考试应该正规表述,例如“状态改变”】


  • 操作系统的体系结构
    • 大内核
      • 将操作系统的主要功能模块作为系统内核,运行在核心态
      • 优点:高性能
      • 缺点:内核代码庞大,结构混乱,难以维护
    • 微内核
      • 只把最基本功能保留在内核
      • 优点:内核功能少,结构清晰,方便维护
      • 缺点:需要频繁地在核心态和用户态之间切换,性能低

典型的大内核/宏内核/单内核 操作系统: Linux、UNIX
典型的 微内核 操作系统: Windows NT

类比:
操作系统的体系结构问题与企业的管理问题很相似。
内核就是企业的管理层,负责一些重要的工作。只有管理层才能执行特权指令,普通员工只能执行非特权指令用户态核心态之间的切换相当于普通员工和管理层之间的工作交接
大内核:企业初创时体量不大,管理层的人会负责大部分的事情。优点是效率高;缺点是组织结构混乱,难以维护。
微内核:随着企业体量越来越大,管理层只负责最核心的一些工作。优点是组织结构清晰,方便维护;缺点是效率低。


1.4.2 操作系统的体系结构(下)

  • 大内核(又名:宏内核/单内核)
  • 微内核
  • 分层结构(新增)
  • 模块化(新增)
  • 外核(新增)

Tips: 一定是考简单的选择题,了解各种体系结构的特性,了解各自的优缺点

  • 操作系统结构

【注:加星号是作者认为更容易考的,红色星号是全新的内容,黄色星号的是老内容(大内核、微内核)】


  • 操作系统结构——分层结构

最底层是硬件,最高层是用户接口
每层可调用更低一层【不能跨层调用】


  • 操作系统结构——模块化

模块化是将操作系统按功能划分为若干个具有一定独立性的模块。每个模块具有某方面的管理功能,并规定好各模块间的接口,使各模块之间能通过接口进行通信。还可以进一步将各模块细分为若干个具有一定功能的子模块,同样也规定好各子模块之间的接口。把这种设计方法称为模块-接口法,上图所示为由模块、子模块等组成的模块化操作系统结构。


  • 操作系统结构——外核(exokernel)


1.5 操作系统的引导

操作系统引导(boot)–开机的时候怎么让操作系统运行起来?

  • 一个刚买来的磁盘(硬盘)


  • 安装操作系统后,操作系统引导(开机过程)

【注释】
根目录:顾名思义,双击C盘看到的那些东西
分区表:实际上是一个数据结构,记录每个盘每个分区多大、地址范围这些信息
BIOS:Basic Input/Output System
ROM引导程序:ROM boot 程序
MBR:告诉CPU去硬件的哪个主分区找操作系统
PBR:寻找根目录下用于引导操作系统的程序(即启动管理器)

操作系统引导:
① CPU从一个特定主存地址开始,取指令,执行ROM中的引导程序 (先进行硬件自检,再开机)
② 将磁盘的第一块–主引导记录读入内存,执行磁盘引导程序,扫描分区表
③ 从活动分区 (又称主分区,即安装了操作系统的分区)读入分区引导记录,执行其中的程序
④ 从根目录下找到完整的操作系统初始化程序(即启动管理器)并执行,完成“开机”的一系列动作


  • 例: windows 操作系统的初始化程序

注:完整的操作系统初始化程序 (即 启动管理器) 可在根目录下找到
Eg:windows操作系统完整的开机初始化程序在“根目录/Windows/Boot”下


1.6 虚拟机

  • 传统计算机


  • 虚拟机

虚拟机:使用虚拟化技术,将一台物理机器虚拟化为多台虚拟机器(Virtual Machine,VM),每个虚拟机器都可以独立运行一个操作系统

同义术语:虚拟机管理程序/虚拟机监控程序/Virtual Machine Monitor(VMM)/Hypervisor


  • 两类虚拟机管理程序(VMM)的对比
第一类VMM 第二类VMM
对物理资源的控制权 直接运行在硬件上,能直接控制和分配物理资源 运行在Host OS之上,依赖Host OS为其分配物理资源
资源分配方式 在安装Guest OS时,VMM要在原本的硬盘上自行分配存储空间,类似于“外核”的分配方式,分配未经抽象的物理硬件 Guest OS拥有自己的虚拟磁盘,该盘实际上是Host OS文件系统中的一个大文件。Guest OS分配到的内存是虚拟内存
性能 性能更好 性能更差,需要Host OS作为“中介”
可支持虚拟机的数量 更多,不需要和Host OS竞争资源,相同的硬件资源可以支持更多的虚拟机 更少,Host OS本身也需要使用物力资源,Host OS上运行的其他进程也需要物理资源
虚拟机的可迁移性 更差 更好,只需要导出虚拟机镜像文件即可迁移到另一台Host OS上,商业化应用更广泛
运行模式 第一类VMM运行在最高权限级(Ring 0),可以执行最高权限指令 第二类VMM部分运行在用户态,部分运行在内核态。Guest OS发出的系统调用会被VMM截获,并转化为VMM对Host OS的系统调用

第二章 进程与线程

2.1.1 进程的概念、组成、特征

  • 进程的概念

程序:是静态的,就是个存放在磁盘里的 可执行文件,就是一系列的指令集合。
进程(Process):是动态的,是程序的一 次执行过程 同一个程序多次执行会对应多个进程;
动态性是进程最重要的特性。


  • 进程的组成——PCB

思考:操作系统是这些进程的管理者,它要怎么区分各个进程?
\Rightarrow 当进程被创建时,操作系统会为该进程分配一个唯一的不重复的“身份证号”—— PID(Process ID,进程ID)

  1. 操作系统要记录PID、进程所属用户ID(UID)
    【基本的进程描述信息,可以让操作系统区分各个进程】
  2. 还要记录给进程分配了哪些资源(如:分配了多少内存、正在使用哪些I/O设备、正在使用哪些文件)
    【可用于实现操作系统对资源的管理】
  3. 还要记录进程的运行情况(如:CPU使用时间、磁盘使用情况、网络流量使用情况等)
    【可用于实现操作系统对进程的控制、调度】
  4. 这些信息都被保存在一个数据结构PCB (Process Control Block)中,即进程控制块
    操作系统需要对各个并发运行的进程进行管理,但凡管理时所需要的信息,都会被放在PCB中

【总结】:

  • 进程控制块(PCB) :PCB是进程存在的唯一标志,当进程被创建时,操作系统为其创建PCB,当进程结束时,会回收其PCB。
    • 进程描述信息
      • 进程标识符PID
      • 用户标识符UID
    • 进程控制和管理信息
      • CPU、磁盘、网络流量使用情况统计…
      • 进程当前状态:就绪态 / 阻塞态 / 运行态…
    • 资源分配清单
      • 正在使用哪些文件
      • 正在使用哪些内存区域
      • 正在使用哪些I/O设备
    • 处理机相关信息
      • 如PSW,PC等等各种寄存器的值(用于实现进程切换)

操作系统对进程进行管理工作所需的信息都存在PCB中


  • 进程的组成——程序段、数据段
    • PCB
      • 进程描述信息
      • 进程控制和管理信息
      • 资源分配清单
      • 处理机相关信息
    • 程序段
      • 程序的代码(指令序列)
    • 数据段
      • 运行过程中产生的各种数据(如:程序中定义的变量)

PCB给操作系统用的
程序段、数据段给进程自己用的


  • 知识滚雪球:程序是如何运行的?

一条高级语言的代码翻译过来可能会对应多条机器指令
程序运行的过程其实就是CPU执行一条一条的机器指令的过程

一个进程实体(进程映像)PCB程序段数据段组成。 进程动态的,进程实体(进程映像)静态的。 进程实体反应了进程在某一时刻的状态(如:x++后,x=2)


  • 进程的组成【更确切的说,应该 是“进程实体(进程映像)的组成”】
    • PCB【PCB 是给操作系统用的】
      • 进程描述信息
      • 进程控制和管理信息
      • 资源分配清单
      • 处理机相关信息
    • 程序段
      • 程序的代码(指令序列)
    • 数据段
      • 运行过程中产生的各种数据(如:程序中定义的变量)

【程序段、数据段是给进程自己用的, 与进程自身的运行逻辑有关】
【同时挂三个QQ号,会对应三个QQ进程,它们的PCB、数据段各不相同,但程序段的内容都是相同的 (都是运行着相同的QQ程序)】

程序段、数据段、PCB三部分组成了进程实体(进程映像) 引入进程实体的概念后,
可把进程定义为: 进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
【一个进程被“调度”,就是指操作系统决定让这个进程上CPU运行】

“进程实体” 是操作系统中用于管理和存储关于一个进程的信息的数据结构,而 “进程” 是一个正在执行的程序实例,包含了执行所需的各种资源。

注意:PCB是进程存在的唯一标志!


  • 进程的特征

程序是静态的,进程是动态的,相比于程序,进程拥有以下特征:

  1. 动态性【动态性是进程最基本的特征】
    进程是程序的一次执行过程,是动态地产生、变化和消亡的
  2. 并发性
    内存中有多个实体进程,各进程可并发执行
  3. 独立性
    进程是能独立运行、独立获得资源、独立接受调度的基本单位
  4. 异步性
    各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制”来解决异步问题
  5. 结构性
    每个进程都会配置一个PCB。结构上看,进程由程序段、数据段、PCB组成

2.1.2 进程的状态与转换、进程的组织

  • 进程的状态——创建态、就绪态

进程正在被创建时,它的状态是“创建态”,在这个阶段操作系统会为进程分配资源、初始化PCB

当进程创建完成后,便进入“就绪态”, 处于就绪态的进程已经具备运行条件, 但由于没有空闲CPU,就暂时不能运行


  • 进程的状态——运行态

系统中可能会有很多个进程都处于就绪态;

当CPU空闲时,操作系统就会选择一个就绪进程, 让它上处理机运行;

如果一个进程此时在CPU上运行,那么这个进程处于“运行态”。 CPU会执行该进程对应的程序(执行指令序列)


  • 进程的状态——阻塞态

在进程运行的过程中,可能会请求等待某个事件的发生(如等待某种系统资源的分配,或者等待其他进程的响应)。

在这个事件发生之前,进程无法继续往下执行,此时操作系统会 让这个进程下CPU,并让它进入“阻塞态

当CPU空闲时,又会选择另一个“就绪态”进程上CPU运行


  • 进程的状态——终止态

一个进程可以执行 exit 系统调用,请求操作系统终止该进程。

此时该进程会进入“终止态”,操作系统会让该进程下CPU, 并回收内存空间等资源,最后还要回收该进程的PCB。

当终止进程的工作完成之后,这个进程就彻底消失了。


  • 进程状态的转换

注意:不能由阻塞态直接转换为运行态, 也不能由就绪态直接转换为阻塞态(因为 进入阻塞态是进程主动请求的,必然需要 进程在运行时才能发出这种请求)


  • 进程的状态
    • 三种基本状态【进程的整个生命周期 中,大部分时间都处 于三种基本状态】
      • 运行态(Running)【单CPU情况下,同一时刻只会有一 个进程处于运行态,多核CPU情况 下,可能有多个进程处于运行态】
        占有CPU,并在CPU上运行
      • 就绪态(Ready)
        已经具备运行条件,但由于没有空闲CPU,而暂时不能运行
      • 阻塞态(Waiting / Blocked,又称:等待态)
        因等待某一事件而暂时不能运行
    • 另外两种状态
      • 创建态(New,又称:新建态)
        进程正在被创建,操作系统为进程分配资源、初始化PCB
      • 终止态(Terminated,又称:结束态)
        进程正在从系统中撤销,操作系统会回收进程拥有的资源、撤销PCB

进程PCB中,会有一个变量 state 来表示进程的当前状态。如:1表示创建态、2表示就绪态、3表示运行态… 为了对同一个状态下的各个进程进行统一的管理,操作系统会将各个进程的PCB组织起来。


  • 进程的组织——链接方式


  • 进程的组织——索引方式


  • 进程的组织
    • 链接方式
      • 按照进程状态将PCB分为多个队列
      • 操作系统持有指向各个队列的指针
    • 索引方式
      • 根据进程状态不同,建立几张索引表
      • 操作系统持有指向各个索引表的指针

大多数操作系统使用链接方式


2.1.3 进程控制

  • 什么是进程控制?

进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。

简化理解:反正进程控制就是要实现进程状态转换


  • 如何实现进程控制?

用“原语”实现【第一章提到了】

原语的执行具有“原子性”,一气呵成

思考:为何进程控制(状态转换)的过程要“一气呵成”?
如果不能“一气呵成”,就有可能导致操作系统中的某些关键数据结构信息不统一的情况, 这会影响操作系统进行别的管理工作

Eg:假设PCB中的变量 state 表示进程当前所处状态,1表示就绪态,2表示阻塞态…
假设此时进程2等待的事件发生,则操作系统中,负责进程控制的内核程序至少需要做这样两件事:
①将PCB2的 state 设为 1
②将PCB2从阻塞队列放到就绪队列

但是完成了第一步后收到中断信号,CPU处理中断去了,那么这个时候PCB2 的state=1,但是它却还在阻塞队列里,这是不合理的。


  • 如何实现原语的“原子性”?

原语的执行具有原子性,即执行过程只能一气呵成,期间不允许被中断
可以用 “关中断指令”和“开中断指令”这两个特权指令实现原子性

如上图:
CPU执行了关中断指令之后,就不再例行检查中断信号,直到执行开中断指令之后才会恢复检查。

这样,关中断、开中断 之间的这些指令序列就是不可被中断的,这就实现了“原子性”

思考:如果这两个特权指令允许用户程序使用的话,会发生什么情况?
假设用户直接在程序开头设置关中断,在程序尾设置开中断,那么这个程序执行时将不能被打断,一直霸占资源,这是不合理的。


  • 进程控制相关的原语
  1. 进程的创建
    • 创建原语【操作系统创建一个进程时使用的原语】
      • 申请空白PCB
      • 为新的进程分配资源
      • 初始化PCB
      • 将PCB插入就绪队列【创建态 → 就绪态】
    • 引起进程创建的事件
      • 用户登录
        分时系统中,用户登录成功,系统会为其创建一个新的进程
      • 作业调度
        多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程
      • 提供服务
        用户向操作系统提出某些请求时,会新建一个进程处理该请求
      • 应用请求
        由用户进程主动请求创建一个子进程
  2. 进程的终止
    • 撤销原语【就绪态/阻塞态/运行态 → 终止态 → 无】
      • 从PCB集合中找到终止进程的PCB
      • 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程
      • 终止其所有子进程【进程间的关系是树形结构】
      • 将该进程拥有的所有资源归还给父进程或操作系统
      • 删除PCB
    • 引起进程终止的事件
      • 正常结束【进程自己请求终止(exit系统调用)】
      • 异常结束【整数除以0、非法使用特权指令, 然后被操作系统强行杀掉】
      • 外界干预【eg:Ctrl+Alt+delete,用户选择杀掉进程】

3.阻塞原语、4.唤醒原语必须成对使用

  1. 进程的阻塞
    • 阻塞原语【运行态 → 阻塞态】
      • 找到要阻塞的进程对应的PCB
      • 保护进程运行现场,将PCB状态信息设置为“阻塞态”,暂时停止进程运行
      • 将PCB插入相应事件的等待队列
    • 引起进程阻塞的事件
      • 需要等待系统分配某种资源
      • 需要等待相互合作的其他进程完成工作
  2. 进程的唤醒
    • 唤醒原语【阻塞态 → 就绪态】
      • 在事件等待队列找到PCB
      • 将PCB从等待队列移除,设置进程为就绪态
      • 将PCB插入就绪队列,等待被调度
    • 引起进程唤醒的事件
      • 等待的事件发生【因何事阻塞,就应由何事唤醒】
  3. 进程的切换
    • 切换原语【运行态 → 就绪态、就绪态 → 运行态】
      • 运行环境信息【进程上下文(Context )】存入PCB
      • PCB移入相应队列
      • 选择另一个进程执行,并更新其PCB
      • 根据PCB恢复新进程所需的运行环境
    • 引起进程切换的事件
      • 当前进程时间片到
      • 有更高优先级的进程到达
      • 当前进程主动阻塞
      • 当前进程终止

  • 知识滚雪球:程序是如何运行的?

【上接:2.1.1 进程的概念、组成、特征 中的知识滚雪球】

CPU中会设置很多 “寄存器”,用来存放程序运行过程中所需的某些数据。

寄存器 作用
PSW 程序状态字寄存器
PC 程序计数器,存放下一条指令的地址
IR 指令寄存器,存放当前正在执行的指令
通用寄存器 其他一些必 要信息
int x = 1;
x++;
……

指令1: 往内存中某个地方写入变量x的值
指令2: 把变量x的值放到某个寄存器中
指令3: 寄存器中的数值+1
指令4: 把寄存器的值写回变量x的存放位置
……

指令顺序执行的过程中,很多中间结果是放在各种寄存器中的

思考:执行完指令3后, 另一个进程开始上CPU运行。
注意:另一个进程在运行过程中也会使用各个寄存器

灵魂拷问:之后还怎么切换回之前的进程????
解决办法:在进程切换时先在PCB中保存这个进程的运行环境(保存一些必要的寄存器信息)
当原来的进程再次投入运行时,可以通过PCB恢复它的运行环境


  • 学习技巧

进程控制会导致进程状态的转换。无论哪个进程控制原语,要做的无非三类事情:

  1. 更新PCB中的信息
    • a. 所有的进程控制原语一定都会修改进程状态标志
    • b. 剥夺当前运行进程的CPU使用权必然需要保存其运行环境
    • c. 某进程开始运行前必然要恢复期运行环境
  2. 将PCB插入合适的队列
  3. 分配/回收资源

2.1.4 进程通信

  • 什么是进程通信?
    进程间通信(Inter-Process Communication,IPC)是指两个进程之间产生数据交互。

  • 为什么进程通信需要操作系统支持?

进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立

为了保证安全,一个进程不能直接访问另一个进程的地址空间。 但是进程之间的信息交换又是必须实现的。为了保证进程间的安全通信,操作系统提供了一些方法。

下面将介绍三种进程通信方式:1. 共享存储、2. 消息传递、3. 管道通信


  • 进程通信——共享存储

为避免出错,各个进程对共享空间的访问应该是互斥的。
各个进程可使用操作系统内核提供的同步互斥工具(如P、V操作【后面介绍】)

linux中,如何实现共享内存:

1
2
3
int shm_open(...)	// 通过 shm_open 系统调用,申请一片共享内存区
void * mmap(...) // 通过 mmap 系统调用,将共享内存区映射到进程自己的地址空间
// 注:通过“增加页表项/段表项”即可将同一片共享内存区映射到各个进程的地址空间中(第三章内容)

共享存储

  • 基于数据结构的共享:
    比如共享空间里只能放 一个长度为10的数组。这种共享方式速度慢、 限制多,是一种低级通信方式
  • 基于存储区的共享:
    在内存中画出一块共享存储区,数据的形式、存放位置都由进程控制, 而不是操作系统。相比之下,这种共享方式速度更快,是一种高级通信方式。

  • 进程通信——消息传递

进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。

消息头包括:发送进程ID、接受进程ID、消息类型、消息长度等格式化的信息(计算机网络中发送的“报文”其实就是一种格式化的消息)

消息传递

  • 直接通信方式
    消息直接挂到接收进程的消息缓冲队列上
  • 间接通信方式
    消息要先发送到中间实体(信箱)中,因此也称“信箱通信方式”。Eg:计网中的电子邮件系统

  • 进程通信——消息传递(直接通信方式)

点名道姓的消息传递


  • 进程通信——消息传递(间接通信方式)

以“信箱”作为中间实体进行消息传递


  • 进程通信——管道通信

那么管道通信和进程通信又有什么区别呢?
管道通信读写要求先进先出(循环队列);而共享存储存取没有要求,都行

  1. 管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道
  2. 各进程要互斥地访问管道。 (由操作系统实现)
  3. 管道写满时,写进程将阻塞,直到读进程将管道中的数据取走,即可唤醒写进程。
  4. 管道读空时,读进程将阻塞,直到写进程往管道中写入数据,即可唤醒读进程。
  5. 管道中的数据一旦被读出,就彻底消失。因此,当多个进程读同一个管道时,可能会错乱。对此,通常有两种解决方案:1、一个管道允许多个写进程,一个读进程 (2014年408真题高教社官方答案);2、允许有多个写进程,多个读进程,但系统会让各个读进程轮流从管道中读数据(Linux 的方案)。

  • 王道书修正


2.1.5 线程概念 多线程模型

  • 什么是线程,为什么要引入线程?

有的进程可能需要“同时”做很多事,而传统的进程只能串行地执行一系列程序。为此,引入了“线程”,来增加并发度。

传统的进程是程序执行流的最小单位。
引入线程后,线程成为了程序执行流的最小单位

可以把线程理解为“轻量级进程”。

线程是一个基本的CPU执行单元, 也是程序执行流的最小单位
引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内 也可以并发处理各种任务(如QQ 视频、文字聊天、传文件)

引入线程后,进程只作为除CPU之外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)
线程则作为处理机的分配单元


  • 引入线程机制后,有什么变化?
  1. 资源分配、调度
    • 传统进程机制中,进程是资源分配、调度的基本单位
    • 引入线程后,进程是资源分配的基本单位,线程是调度的基本单位
  2. 并发性
    • 传统进程机制中,只能进程间并发
    • 引入线程后,各线程间也能并发,提升了并发度
  3. 系统开销
    • 传统的进程间并发,需要切换进程的运行环境,系统开销很大
    • 线程间并发,如果是同一进程内的线程切换,则不需要切换进程环境,系统开销小
    • 引入线程后,并发所带来的系统开销减小

  • 线程的属性
  1. 线程是处理机调度的单位
  2. 多CPU计算机中,各个线程可占用不同的CPU
  3. 每个线程都有一个线程ID、线程控制块 (TCB)
  4. 线程也有就绪、阻塞、运行三种基本状态
  5. 线程几乎不拥有系统资源
  6. 同一进程的不同线程间共享进程的资源
  7. 由于共享内存地址空间,同一进程中的线程间通信甚至无需系统干预
  8. 同一进程中的线程切换,不会引起进程切换
  9. 不同进程中的线程切换,会引起进程切换
  10. 切换同进程内的线程,系统开销很小
  11. 切换进程,系统开销较大

2.1.6 线程的实现方式 多线程模型

  • 线程的实现方式——用户级线程(User-Level Thread, ULT)

历史背景:早期的操作系统(如:早期Unix)只支持进程, 不支持线程。当时的“线程”是由线程库实现的

eg:将下面进程描述成如下代码

1
2
3
4
5
6
7
8
9
int main() {
int i = 0;
while (true) {
if (i == 0) { 处理视频聊天的代码 }
if (i == 1) { 处理文字聊天的代码 }
if (i == 2) { 处理文件传输的代码 }
i = (i + 1) % 3 // i = 0, 1, 2, 0, 1, 2...
}
}

从代码的角度看,线程其实就是一段代码逻辑。 上述三段代码逻辑上可以看作三个“线程”。 while 循环就是一个最弱智的“线程库”,线程库完成了对线程的管理工作(如调度)。

很多编程语言提供了强大的线程库,可以实现线程的创建、销毁、调度等功能。

【问题:】

  1. 线程的管理工作由谁来完成?
  2. 线程切换是否需要CPU变态?
  3. 操作系统是否能意识到用户级线程的存在?
  4. 这种线程的实现方式有什么优点和缺点?

【答:】

  1. 用户级线程由应用程序通过线程库实现,所有的线程管理工作都由应用程序负责(包括线程切换)
  2. 用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。
  3. 在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。“用户级线程”就是“从用户视角看能看到的线程
  4. 优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高
    缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行。

  • 线程的实现方式——内核级线程(Kernel-Level Thread, KLT”)
    又称“内核支持的线程

由操作系统支持的线程,内核级线程才是处理机分配的单位

大多数现代操作系统都实现了内核级线程,如 Windows、Linux

【问题:】

  1. 线程的管理工作由谁来完成?
  2. 线程切换是否需要CPU变态?
  3. 操作系统是否能意识到用户级线程的存在?
  4. 这种线程的实现方式有什么优点和缺点?

【答:】

  1. 内核级线程的管理工作操作系统内核完成。
  2. 线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
  3. 操作系统会为每个内核级线程建立相应的 TCB(Thread Control Block,线程控制块), 通过TCB对线程进行管理。“内核级线程”就是“从操作系统内核视角看能看到的线程
  4. 优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。
    缺点:一个用户进程会占用多个内核级线程,内核级线程切换由操作系统内核完成,需要切换到核心态,因此线程管理的成本高,开销大。

  • 多线程模型
    在支持内核级线程的系统中,根据用户级线程和内核级线程的映射关 系,可以划分为几种多线程模型

重点重点重点: 操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位

一对一模型:一个用户级线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。

优点:当一个线程被阻塞后,别的线程还可以继续执行,并发能力强。多线程可在多核处理机上并行执行。

缺点:一个用户进程会占用多个内核级线程, 线程切换由操作系统内核完成,需要切换到 核心态,因此线程管理的成本高,开销大。

多对一模型:多个用户级线程映射到一个内核级线程。且一个进程只被分配一个内核级线程。

优点:用户级线程的切换在用户空间即可完成,不需要切换到核心态,线程管理的系统开销小,效率高

缺点:当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高。多个线程不可在多核处理机上并行运行

多对多模型:n 用户及线程映射到 m 个内核级 线程(n >= m)。每个用户进程对应 m 个内核 级线程。

克服了多对一模型并发度不高的缺点(一个阻塞全体阻塞),又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。

可以这么理解:
用户级线程是“代码逻辑”的载体
内核级线程是“运行机会”的载体
内核级线程才是处理机分配的单位。例如:多核 CPU环境下,上图这个进程最多能被分配两个核。】

一段“代码逻辑”只有获得了“运行机会”才能被 CPU执行
【内核级线程中可以运行任意一个有映射关系的用户级线程代码,如上图,只有两个内核级线程中正在运行的代码逻辑都阻塞时,这个进程才会阻塞】


2.1.7 线程的状态与转换

  • 线程的状态与转换

下面与进程完全一致


  • 线程的组织与控制

类似于进程

组织:


2.2.1 调度的概念、层次

  • 调度的概念

当有一堆任务要处理,但由于资源有限,这些事情没法同时处理。这就需要确定某种规则决定处理这些任务的顺序,这就是“调度”研究的问题。


  • 调度的三个层次——高级调度

作业:一个具体的任务
用户向系统提交一个作业 ≈ 用户让操作系统启动一个程序(来处理一个具体的任务)

内存空间有限,有时无法将用户提交的作业全部放入内存

\Rightarrow 高级调度(作业调度) —— 按一定的原则从外存的作业后备队列中挑选一个作业调入内存,并创建进程。每个作业只调入一次,调出一次。作业调入时会建立PCB,调出时才撤销PCB。
【简化理解:好几个程序需要启动,到底先启动哪个】


  • 调度的三个层次——低级调度

低级调度(进程调度/处理机调度) —— 按照某种策略从就绪队列中选取一个进程,将处理机分配给它。

进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。 进程调度的频率很高,一般几十毫秒一次。


  • 调度的三个层次——中级调度

内存不够时,可将某些进程的数据调出外存。等内存空闲或者进程需要运行时再重新调入内存。

暂时调到外存等待的进程状态为挂起状态。被挂起的进程PCB会被组织成挂起队列

中级调度(内存调度) —— 按照某种策略决定将哪个处于挂起状态的进程重新调入内存。
一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高


  • 补充知识:进程的挂起态与七状态模型

暂时调到外存等待的进程状态为挂起状态(挂起态,suspend)
挂起态又可以进一步细分为就绪挂起阻塞挂起两种状态

五状态模型→七状态模型:【看学校怎么考】

注意“挂起”和“阻塞”的区别,两种状态都是暂时不能获得CPU的服务,但挂起态是将进程映像调到外存去了,而阻塞态下进程映像还在内存中。
有的操作系统会把就绪挂起、阻塞挂起分为两个挂起队列,甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列。


  • 三层调度的联系、对比
要做什么 调度发生在… 发生频率 对进程状态的影响
高级调度
(作业调度)
按照某种规则,从后备队列中选择合适的作业将其调入内存,并为其创建进程 外存→内存
(面向作业)
最低 无→创建态→就绪态
中级调度
(内存调度)
按照某种规则,从挂起队列中选择合适的进程将其数据调回内存 外存→内存
(面向进程)
中等 挂起态→就绪态
(阻塞挂起→阻塞态)
低级调度
(进程调度)
按照某种规则,从就绪队列中选择一个进程为其分配处理机 内存\toCPU 最高 就绪态→运行态

2.2.2 进程调度的时机、切换与过程调度方式

  • 进程调度的时机

进程调度(低级调度),就是按照某种算法从就绪队列中选择一个进程为其分配处理机。

需要进行进程调度与切换的情况:

  • 当前运行的进程主动放弃处理机
    • 进程正常终止
    • 运行过程中发生异常而终止
    • 进程主动请求阻塞(如 等待I/O)
  • 当前运行的进程被动放弃处理机
    • 分给进程的时间片用完
    • 有更紧急的事需要处理(如 I/O中断)
    • 有更高优先级的进程进入就绪队列

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

  1. 处理中断的过程中。中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换。
  2. 进程在操作系统内核程序临界区中。
    【但是进程在普通临界区中是可以进行调度和切换的】
  3. 原子操作过程中(原语)。原子操作不可中断,要一气呵成(如之前讲过的修改PCB中进程状态标志,并把PCB放到相应队列)

【考题:】
【对】进程在操作系统内核程序临界区中不能进行调度与切换
【错】(2012年联考真题)进程处于临界区时不能进行处理机调度

首先要知道:
临界资源:一个时间段内只允许一个进程使用的资源。各进程需要互斥地访问临界资源。
临界区:访问临界资源的那段代码。
内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列(由各就绪进程的PCB组成)

【eg:】

如果还没退出临界区 (还没解锁 )就进行进程调度,但是进程调度相关的程序也需要访问就绪队列,但此时就绪队列被锁住了,因此又无法顺利进行进程调度
内核程序临界区访问的临界资源如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作。因此在访问内核程序临界区期间不能进行调度与切换

在打印机打印完成之前,进程一直处于临界区内,临界资源不会解锁。但打印机又是慢速设备,此时如果一直不允许进程调度的话就会导致CPU一直空闲;
普通临界区访问的临界资源不会直接影响操作系统内核的管理工作。因此在访问普通临界区时可以进行调度与切换。


  • 进程调度的方式

有的系统中,只允许进程主动放弃处理机
有的系统中,进程可以主动放弃处理机,当有更紧急的任务需要处理时,也会强行剥夺处理机(被动放弃)

  • 非剥夺调度方式,又称非抢占方式。即,只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态。
    【实现简单,系统开销小但是无法及时处理紧急任务,适合于早期的批处理系统】
  • 剥夺调度方式,又称抢占方式。当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程。
    【可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断)。适合于分时操作系统、实时操作系统】

  • 进程的切换与过程

“狭义的进程调度”与“进程切换”的区别:
狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程, 也可能是另一个进程,后一种情况就需要进程切换)
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程

广义的进程调度包含了选择一个进程和进程切换两个步骤。

进程切换的过程主要完成了:

  1. 对原来运行进程各种数据的保存
  2. 对新的进程各种数据的恢复 (如:程序计数器、程序状态字、各种数据寄存器等处理机现场信息,这些信息一般保存在进程控制块)

注意:进程切换是有代价的,因此如果过于频繁的进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。


2.2.3 调度器和闲逛程序

  • 调度器/调度程序(scheduler)

上图中,2和3由调度程序引起,调度程序决定:

让谁运行?——调度算法
运行多长时间?——时间片大小

调度时机——什么事件会触发“调度程序” ?

  • 创建新进程
  • 进程退出
  • 运行进程阻塞
  • l/O中断发生(可能唤醒某些阻塞进程)
  • 非抢占式调度策略,只有运行进程阻塞或退出才触发调度程序工作
  • 抢占式调度策略,每个时钟中断或k个时钟中断会触发调度程序工作

不支持内核级线程的操作系统,调度程序的处理对象是进程
支持内核级线程的操作系统,调度程序的处理对象是内核线程


  • 闲逛进程

调度程序永远的备胎,没有其他就绪进程时,运行闲逛进程(idle)

闲逛进程的特性:

  • 优先级最低
  • 可以是0地址指令,占一个完整的指令周期(指令周期末尾例行检查中断)
  • 能耗低

2.2.4 调度算法的评价指标

CPU利用率=忙碌的时间总时间系统吞吐率=总共完成了多少作业总共花了多少时间(作业)周转时间=作业完成时间作业提交时间平均周转时间=各作业周转时间之和作业数带权周转时间=作业周转时间作业实际运行时间平均带权周转时间=各作业带权周转时间之和作业数等待时间=周转时间运行时间平均等待时间=各作业等待时间之和作业数\begin{align} &CPU利用率 = \frac{忙碌的时间}{总时间}\nonumber\\\nonumber\\ &系统吞吐率 = \frac{总共完成了多少作业}{总共花了多少时间}\nonumber\\\nonumber\\ &(作业)周转时间=作业完成时间-作业提交时间\nonumber\\\nonumber\\ &平均周转时间 = \frac{各作业周转时间之和}{作业数}\nonumber\\\nonumber\\ &带权周转时间 = \frac{作业周转时间}{作业实际运行时间}\nonumber\\\nonumber\\ &平均带权周转时间 = \frac{各作业带权周转时间之和}{作业数}\nonumber\\\nonumber\\ &等待时间=周转时间-运行时间\nonumber\\\nonumber\\ &平均等待时间=\frac{各作业等待时间之和}{作业数}\nonumber \end{align}

  • CPU利用率

由于早期的CPU造价极其昂贵,因此人们会希望让CPU尽可能多地工作

CPU利用率:指CPU “忙碌”的时间占总时间的比例。

CPU利用率=忙碌的时间总时间CPU利用率 = \frac{忙碌的时间}{总时间}
【有的题目还会要求计 算某种设备的利用率】

Eg:某计算机只支持单道程序,某个作业刚开始需要在CPU上运行5秒, 再用打印机打印输出5秒,之后再执行5秒,才能结束。在此过程中, CPU利用率、打印机利用率分别是多少?

CPU利用率=5+55+5+5=66.66%CPU利用率=\frac{5+5}{5+5+5}=66.66\%
打印机利用率=515=33.33%打印机利用率=\frac{5}{15}=33.33\%
【通常会考察多道程序并发 执行的情况,可以用“甘特图”来辅助计算】


  • 系统吞吐量

对于计算机来说,希望能用尽可能少的时间处理完尽可能多的作业
系统吞吐量:单位时间内完成作业的数量

系统吞吐率=总共完成了多少作业总共花了多少时间系统吞吐率 = \frac{总共完成了多少作业}{总共花了多少时间}

Eg:某计算机系统处理完10道作业,共花费100秒,则系统吞吐量为?
10/100 = 0.1 道/秒


  • 周转时间

对于计算机的用户来说,他很关心自己的作业从提交到完成花了多少时间。
周转时间,是指从作业被提交给系统开始,到作业完成为止的这段时间间隔。
它包括四个部分:作业在外存后备队列上等待作业调度(高级调度)的时间、进程在就绪队列上等待进程调度(低级调度)的时间、进程在CPU上执行的时间、进程等待I/O操作完成的时间。后三项在一个作业的整个处理过程中,可能发生多次

(作业)周转时间=作业完成时间作业提交时间(作业)周转时间=作业完成时间-作业提交时间
【对于用户来说,更关心自己的单个作业的周转时间】

平均周转时间=各作业周转时间之和作业数平均周转时间 = \frac{各作业周转时间之和}{作业数}
【对于操作系统来说,更关心系统的整体表现, 因此更关心所有作业周转时间的平均值】

【思考】有的作业运行时间短,有的作业运行时间长,因此在周转时间相同的情况下,运行时间不同的作业,给用户的感觉肯定是不一样的
eg:等待1分钟,执行10分钟;等待10分钟,运行1分钟。

带权周转时间=作业周转时间作业实际运行时间带权周转时间 = \frac{作业周转时间}{作业实际运行时间}

平均带权周转时间=各作业带权周转时间之和作业数平均带权周转时间 = \frac{各作业带权周转时间之和}{作业数}


  • 等待时间

计算机的用户希望自己的作业尽可能少的等待处理机
等待时间,指进程/作业处于等待处理机状态时间之和,等待时间越长,用户满意度越低。

作业在后备队列等待被服务(调度)
作业调入内存后,建立对应的进程。这个进程会被CPU服务、会被I/O设备服务,当然也会有等待被服务的时候

对于进程来说,等待时间就是指进程建立后等待被服务的时间之和,在等待I/O完成的期间其实进程也是在被服务的,所以不计入等待时间。
对于作业来说,不仅要考虑建立进程后的等待时间,还要加上作业在外存后备队列中等待的时间

一个作业总共需要被CPU服务多久,被I/O设备服务多久一般是确定不变的,因此调度算法其实只会影响作业/进程的等待时间。当然,与前面指标类似,也有“平均等待时间”来评价整体性能。


  • 响应时间

对于计算机用户来说,会希望自己的提交的请求(比如通过键盘输入了一个调试命令)尽早地开始被系统服务、回应。

响应时间,指从用户提交请求首次产生响应所用的时间。


2.2.5 调度算法1:先来先服务 最短作业优先 最高响应比优先

Tips:各种调度算法的学习思路

  1. 算法思想
  2. 算法规则
  3. 这种调度算法是用于 作业调度 还是 进程调度?
  4. 抢占式?非抢占式?
  5. 优点和缺点
  6. 是否会导致饥饿【某进程/作业长期得不到服务】
  • 先来先服务(FCFS, First Come First Serve)
  1. 算法思想
    主要从“公平”的角度考虑(类似于我们生活中排队买东西的例子)
  2. 算法规则
    按照作业/进程到达的先后顺序进行服务
  3. 用于作业/进程调度
    用于作业调度时,考虑的是哪个作业先到达后备队列;
    用于进程调度时,考虑的是哪个进程先到达就绪队列
  4. 是否可抢占?
    非抢占式的算法
  5. 优缺点
    优点:公平、算法实现简单
    缺点:排在长作业(进程)后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好。即, FCFS算法对长作业有利,对短作业不利(Eg :排队买奶茶,前面某个人要买100杯…)
  6. 是否会导致饥饿
    不会

例题】各进程到达就绪队列的时间、需要的运行时间如下表所示。使用先来先服务调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间。

先来先服务调度算法:按照到达的先后顺序调度,事实上就 是等待时间越久的越优先得到服务。 因此,调度顺序为:P1 → P2 → P3 → P4

周转时间 = 完成时间 - 到达时间 P1=7-0=7;P2=11-2=9;P3=12-4=8;P4=16-5=11

带权周转时间 = 周转时间/运行时间 P1=7/7=1;P2=9/4=2.25;P3=8/1=8;P4=11/4=2.75

等待时间 = 周转时间 – 运行时间 P1=7-7=0;P2=9-4=5;P3=8-1=7;P4=11-4=7

平均周转时间 = (7+9+8+11)/4 = 8.75

平均带权周转时间 = (1+2.25+8+2.75)/4 = 3.5

平均等待时间 = (0+5+7+7)/4 = 4.75


  • 短作业优先(SJF, Shortest Job First)
  1. 算法思想
    追求最少的平均等待时间,最少的平均周转时间、最少的平均平均带权周转时间
  2. 算法规则
    最短的作业/进程优先得到服务(所谓“最短”,是指要求服务时间最短)
  3. 用于作业/进程调度
    即可用于作业调度,也可用于进程调度。用于进程调度时称为“短进程优先(SPF, Shortest Process First)算法”
  4. 是否可抢占?
    SJF和SPF是非抢占式的算法。但是也有抢占式的版本——最短剩余时间优先算法SRTN, Shortest Remaining Time Next)
  5. 优缺点
    优点:“最短的”平均等待时间、平均周转时间
    缺点:不公平。对短作业有利,对长作业不利。可能产生饥饿现象。另外,作业/进程的运行时间是由用户提供的, 并不一定真实,不一定能做到真正的短作业优先
  6. 是否会导致饥饿
    会。如果源源不断地有短作业/进程到来,可能使长作业/进 程长时间得不到服务,产生“饥饿”现象。如果一直得不到服务,则称为“饿死

例题1】各进程到达就绪队列的时间、需要的运行时间如下表所示。使用非抢占式短作业优先调度算法,计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周 转时间。

短作业/进程优先调度算法:每次调度时选择当前已到达且运行时间最短的作业/进程。 因此,调度顺序为:P1 → P3 → P2 → P4

周转时间 = 完成时间 - 到达时间 P1=7-0=7;P3=8-4=4;P2=12-2=10;P4=16-5=11

带权周转时间 = 周转时间/运行时间 P1=7/7=1;P3=4/1=4;P2=10/4=2.5;P4=11/4=2.75

等待时间 = 周转时间 – 运行时间 P1=7-7=0;P3=4-1=3;P2=10-4=6;P4=11-4=7

平均周转时间 = (7+4+10+11)/4 = 8

平均带权周转时间 = (1+4+2.5+2.75)/4 = 2.56

平均等待时间 = (0+3+6+7)/4 = 4

【对比FCFS算法的结果的平均周转时间8.75、平均带权周转时间3.5、平均等待时间4.75,显然SPF算法的 平均等待/周转/带权周转时间都要更低】

例题2】各进程到达就绪队列的时间、需要的运行时间如下表所示。使用抢占式短作业优先调度算法, 计算各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时 间。

最短剩余时间优先算法:每当有进程加入就绪队列改变时就需要调度,如果新到达的进程剩余时间比当前运行的进程剩余时间更短,则由新进程抢占处理机,当前运行进程重新回到就绪队列。另外,当一个进程完成时也需要调度

需要注意的是,当有新进程到达时就绪队列就会改变,就要按照上述规则进行检查。以下 Pn(m)P_n(m)表 示当前PnP_n进程剩余时间为 m。各个时刻的情况如下:
0时刻(P1到达) : P1(7)\mathbf{P_1(7)}
2时刻(P2到达):P1(5)P2(4)P_1(5)、\mathbf{P_2(4)}
4时刻(P3到达): P1(5)P2(2)P3(1)P_1(5)、P_2(2)、\mathbf{P_3(1)}
5时刻(P3完成且P4刚好到达):P1(5)P2(2)P4(4)P_1(5)、\mathbf{P_2(2)}、P_4(4)
7时刻(P2完成):P1(5)P4(4)P_1(5)、\mathbf{P_4(4)}
11时刻(P4完成) :P1(5)\mathbf{P_1(5)}

周转时间 = 完成时间 - 到达时间 P1=16-0=16;P2=7-2=5;P3=5-4=1;P4=11-5=6

带权周转时间 = 周转时间/运行时间 P1=16/7=2.28;P2=5/4=1.25;P3=1/1=1;P4=6/4=1.5

等待时间 = 周转时间 – 运行时间 P1=16-7=9;P2=5-4=1;P3=1-1=0;P4=6-4=2

平均周转时间 = (16+5+1+6)/4 = 7

平均带权周转时间 = (2.28+1.25+1+1.5)/4 = 1.50

平均等待时间 = (9+1+0+2)/4 = 3

【对比非抢占式的短作业优先算法的平均周转时间8、平均带权周转时间2.56、平均等待时间4,显 然抢占式的这几个指标又要更低】

  • 注意几个小细节:
  1. 如果题目中未特别说明,所提到的“短作业/进程优先算法”默认非抢占式
  2. 很多书上都会说“SJF 调度算法的平均等待时间、平均周转时间最少”
    • 严格来说,这个表述是错误的,不严谨的。之前的例子表明,最短剩余时间优先算法得到的平均等待时间、平均周转时间还要更少
    • 应该加上一个条件“在所有进程同时可运行时,采用SJF调度算法的平均等待时间、平均周转时间最少”;
    • 或者说“在所有进程都几乎同时到达时,采用SJF调度算法的平均等待时间、平均周转时间最少”;
    • 如果不加上述前提条件,则应该说“抢占式的短作业/进程优先调度算法(最短剩余时间优先, SRNT算 法)的平均等待时间、平均周转时间最少”
  3. 虽然严格来说,SJF的平均等待时间、平均周转时间并不一定最少,但相比于其他算法(如 FCFS), SJF依然可以获得较少的平均等待时间、平均周转时间
  4. 如果选择题中遇到“SJF 算法的平均等待时间、平均周转时间最少”的选项,那最好判断其他选项是不是有很明显的错误,如果没有更合适的选项,那也应该选择该选项

  • 对FCFS和SJF两种算法的思考…

FCFS 算法是在每次调度的时候选择一个等待时间最长的作业(进程)为其服务。但是没有考虑到作业的运行时间,因此导致了对短作业不友好的问题

SJF 算法是选择一个执行时间最短的作业为其服务。但是又完全不考虑各个作业的等待时间,因此导致了对长作业不友好的问题,甚至还会造成饥饿问题

能不能设计一个算法,即考虑到各个作业 的等待时间,也能兼顾运行时间呢?
\Rightarrow 高响应比优先算法


  • 高响应比优先(HRRN, Highest Response Ratio Next)
  1. 算法思想
    要综合考虑作业/进程的等待时间和要求服务的时间

  2. 算法规则
    在每次调度时先计算各个作业/进程的响应比,选择响应比最高的作业/进程为其服务

    响应比=等待时间+要求服务时间要求服务时间响应比=\frac{等待时间 + 要求服务时间}{要求服务时间}

  3. 用于作业/进程调度
    即可用于作业调度,也可用于进程调度

  4. 是否可抢占?
    非抢占式的算法。因此只有当前运行的作业/进程主动放弃处理机时,才需要调度,才需要计算响应比

  5. 优缺点
    综合考虑了等待时间和运行时间(要求服务时间)
    等待时间相同时,要求服务时间短的优先(SJF 的优点)
    要求服务时间相同时,等待时间长的优先(FCFS 的优点)
    对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题

  6. 是否会导致饥饿
    不会

例题】各进程到达就绪队列的时间、需要的运行时间如下表所示。使用高响应比优先调度算法,计算 各进程的等待时间、平均等待时间、周转时间、平均周转时间、带权周转时间、平均带权周转时间。

高响应比优先算法:非抢占式的调度算法,只有当前运行的进程主动放弃CPU时(正常/异常完成,或主动阻塞),才需要进 行调度,调度时计算所有就绪进程的响应比,选响应比最高的进程上处理机。

0时刻:只有 P1到达就绪队列,P1上处理机
7时刻(P1主动放弃CPU): 就绪队列中有 P2 (响应比=(5+4)/4=2.25)、 P3((3+1)/1=4)、 P4((2+4)/4=1.5),
8时刻(P3完成): P2(2.5)、 P4(1.75)
12时刻(P2完成):就绪队列中只剩下 P4

周转时间 = 完成时间 - 到达时间 P1=7-0=7;P3=8-4=4;P2=12-2=10;P4=16-5=11

带权周转时间 = 周转时间/运行时间 P1=7/7=1;P3=4/1=4;P2=10/4=2.5;P4=11/4=2.75

等待时间 = 周转时间 – 运行时间 P1=7-7=0;P3=4-1=3;P2=10-4=6;P4=11-4=7

平均周转时间 = (7+4+10+11)/4 = 8

平均带权周转时间 = (1+4+2.5+2.75)/4 = 2.56

平均等待时间 = (0+3+6+7)/4 = 4


  • 三种调度算法对比
算法 可抢占? 优点 缺点 考虑到等待时间&运行时间? 导致饥饿?
FCFS 非抢占式 公平;实现简单 对短作业不利 等待时间√
运行时间×
不会
SJF/SPF 默认为非抢占式,也有SJF的抢占式版本最短剩余时间优先算法(SRTN) “最短的”平均等待/周转时间; 对长作业不利,可能导致饥饿;难以做到真正的短作业优先 等待时间×
运行时间√
HRRN 非抢占式 上述两种算法的权衡 折中,综合考虑的等 待时间和运行时间 等待时间√
运行时间√
不会

注:这几种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。因此这三种算法一般适合用于早期的批处理系统,当然,FCFS算法也常结合其他的算法使用,在现在也扮演着很重要的角色。而适合用于交互式系统的调度算法将在下个小节介绍…

提示:一定要动手做课后习题!这些算法特性容易考小题,算法的使用常结合调度算法的评价指标在大题中考察。


2.2.6 调度算法2:时间片轮转 优先级调度 多级反馈队列

  • 时间片轮转(RR, Round-Robin)
  1. 算法思想
    公平地、轮流地为各个进程服务,让每个进程在一定时间间隔内都可以得到响应
  2. 算法规则
    按照各进程到达就绪队列的顺序,轮流让各个进程执行一个时间片(如 100ms)。若进程未在一个时间片内执行完,则剥夺处理机,将进程重新放到就绪队列队尾重新排队。
  3. 用于作业/进程调度
    用于进程调度(只有作业放入内存建立了相应的进程后, 才能被分配处理机时间片)
  4. 是否可抢占?
    若进程未能在时间片内运行完,将被强行剥夺处理机使用权,因此时间片轮转调度算法属于抢占式的算法。由时钟装置发出时钟中断来通知CPU时间片已到
  5. 优缺点
    优点:公平;响应快,适用于分时操作系统
    缺点:由于高频率的进程切换,因此有一定开销;不区分任务的紧急程度。
  6. 是否会导致饥饿
    不会
  7. 补充
    时间片太大或太小分别有什么影响?【下面例题中介绍】

例题】各进程到达就绪队列的时间、需要的运行时间如下表所示。使用时间片轮转调度算法,分析时间片大小分别是2、5时的进程运行情况。

时间片轮转调度算法:轮流让就绪队列中的进程依次执行一个时间片(每次选择的都是排在就绪队列队头的进程)

时间片大小为 2 (注:以下括号内表示当前时刻就绪队列中的进程、进程的剩余运行时间)

0时刻(P1(5) ):0时刻只有P1到达就绪队列,让P1上处理机运行一个时间片
2时刻(P2(4) → P1(3)):2时刻P2到达就绪队列,P1运行完一个时间片,被剥夺处理机,重新放到队尾。 此时P2排在队头,因此让P2上处理机。(注意: 2时刻,P1下处理机,同一时刻新进程P2到达,如果在 题目中遇到这种情况,默认新到达的进程先进入就绪队列
4时刻(P1(3) → P3(1) → P2(2)):4时刻,P3到达,先插到就绪队尾,紧接着,P2下处理机也插到队尾
5时刻(P3(1) → P2(2) → P4(6)):5时刻,P4到达插到就绪队尾(注意:由于P1的时间片还没用完,因此 暂时不调度。另外,此时P1处于运行态,并不在就绪队列中)
6时刻(P3(1) → P2(2) → P4(6) → P1(1)):6时刻,P1时间片用完,下处理机,重新放回就绪队尾,发生调度
7时刻(P2(2) → P4(6) → P1(1)):虽然P3的时间片没用完,但是由于P3只需运行1个单位的时间,运行完了会主动放弃处理机,因此也会发生调度。队头进程P2上处理机。
9时刻(P4(6) → P1(1)):进程P2时间片用完,并刚好运行完,发生调度,P4上处理机
11时刻(P1(1) → P4(4) ):P4时间片用完,重新回到就绪队列。P1上处理机
12时刻(P4(4) ):P1运行完,主动放弃处理机,此时就绪队列中只剩P4,P4上处理机
14时刻():就绪队列为空,因此让P4接着运行一个时间片。
16时刻:所有进程运行结束

时间片大小为 5

0时刻(P1(5) ):只有P1到达,P1上处理机。
2时刻(P2(4) ):P2到达,但P1时间片尚未结束,因此暂不调度
4时刻(P2(4) → P3(1)):P3到达,但P1时间片尚未结束,因此暂不调度
5时刻(P2(4) → P3(1) → P4(6) ):P4到达,同时,P1运行结束。发生调度,P2上处理机。
9时刻(P3(1) → P4(6) ):P2运行结束,虽然时间片没用完,但是会主动放弃处理机。发生调度。
10时刻(P4(6) ):P3运行结束,虽然时间片没用完,但是会主动放弃处理机。发生调度。
15时刻( ):P4时间片用完,但就绪队列为空,因此会让P4继续执行一个时间片。
16时刻( ):P4运行完,主动放弃处理机。所有进程运行完。

若按照先来先服务调度算法…

如果时间片太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法退化为先来先服务调度算法,并且会增大进程响应时间。因此时间片不能太大
【比如:系统中有10个进程在并发执行,如果时间片为1秒,则一个进程被响应 可能需要等9秒…也就是说,如果用户在自己进程的时间片外通过键盘发出调 试命令,可能需要等待9秒才能被系统响应】

另一方面,进程调度、切换是有时间代价的(保存、恢复运行环境),因此如果时间片太小,会导致进程切换过于频繁,系统会花大量的时间来处理%进程切换,从而导致实际用于进程执行的时间比例减小。可见时间片也不能太小
【一般来说,设计时间片时要让切换进程的开销占比不超过1%】


  • 优先级调度算法
  1. 算法思想
    随着计算机的发展,特别是实时操作系统的出现,越来越多的应用场景需要根据任务的紧急程度来决定处理顺序
  2. 算法规则
    每个作业/进程有各自的优先级,调度时选择优先级最高的作业/进程
  3. 用于作业/进程调度
    既可用于作业调度,也可用于进程调度。甚至,还会用于在之后会学习的I/O调度中
  4. 是否可抢占?
    抢占式、非抢占式都有。做题时的区别在于:非抢占式只需在进程主动放弃处理机时进行调度即可,而抢占式还需在就绪队列变化时,检查是否会发生抢占。
  5. 优缺点
    优点:用优先级区分紧急程度、重要程度,适用于实时操作系统。可灵活地调整对各种作业/进程的偏好程度。
    缺点:若源源不断地有高优先级进程到来,则可能导致饥饿
  6. 是否会导致饥饿
  7. 补充:
    就绪队列未必只有一个,可以按照不同优先级来组织。另外,也可以把优先级高的进程排在更靠近队头的位置
    根据优先级是否可以动态改变,可将优先级分为静态优先级和动态优先级两种。
    静态优先级:创建进程时确定,之后一直不变。
    动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整优先级。

思考

如何合理地设置各类进程的优先级?
通常:
系统进程优先级 高于 用户进程
前台进程优先级 高于 后台进程
操作系统更偏好 I/O型进程(或称 I/O繁忙型进程)
注:与I/O型进程相对的是计算型进程(或称 CPU繁忙型进程)
【I/O设备和CPU可以并行工作。如果优先让I/O繁忙型进程优先运行的话, 则越有可能让I/O设备尽早地投入工作,则资源利用率、系统吞吐量都会得到提升】

如果采用的是动态优先级,什么时候应该调整?
可以从追求公平、提升资源利用率等角度考虑
如果某进程在就绪队列中等待了很长时间,则可以适当提升其优先级
如果某进程占用处理机运行了很长时间,则可适当降低其优先级
如果发现一个进程频繁地进行I/O操作,则可适当提升其优先级

例题1

各进程到达就绪队列的时间、需要的运行时间、进程优先数如下表所示。使用非抢占式优先级调度算法,分析进程运行情况。(注:优先数越大,优先级越高)

非抢占式的优先级调度算法:每次调度时选择当前已到达优先级最高的进程。当前进程主动放弃处理机时发生调度。

注:以下括号内表示当前处于就绪队列的进程
0时刻(P1):只有P1到达,P1上处理机。
7时刻(P2、P3、P4):P1运行完成主动放弃处理机,其余进程都已到达,P3优先级最高,P3上处理机。
8时刻( P2、P4 ):P3完成,P2、P4优先级相同,由于P2先到达,因此P2优先上处理机
12时刻( P4 ):P2完成,就绪队列只剩P4,P4上处理机。
16时刻( ):P4完成,所有进程都结束

例题2

各进程到达就绪队列的时间、需要的运行时间、进程优先数如下表所示。使用抢占式优先级调度算法,分析进程运行情况。(注:优先数越大,优先级越高)

抢占式的优先级调度算法:每次调度时选择当前已到达优先级最高的进程。当前进程主动放弃处理机时发生调度。另 外,当就绪队列发生改变时也需要检查是会发生抢占。

注:以下括号内表示当前处于就绪队列的进程
0时刻(P1):只有P1到达,P1上处理机。
2时刻(P2):P2到达就绪队列,优先级比P1更高,发生抢占。P1回到就绪队列,P2上处理机。
4时刻(P1、P3):P3到达,优先级比P2更高,P2回到就绪队列,P3抢占处理机。
5时刻(P1、P2、P4):P3完成,主动释放处理机,同时,P4也到达,由于P2比P4更先进入就绪队列, 因此选择P2上处理机
7时刻(P1、P4):P2完成,就绪队列只剩P1、P4,P4上处理机。
11时刻(P1 ):P4完成,P1上处理机
16时刻():P1完成,所有进程均完成


  • 思考

FCFS算法的优点是公平

SJF 算法的优点是能尽快处理完短作业, 平均等待/周转时间等参数很优秀

时间片轮转调度算法可以让各个进程得到及时的响应

优先级调度算法可以灵活地调整各种进程被服务的机会

能否对其他算法做个折中权衡?得到一个综合表现优秀平衡的算法呢?

多级反馈队列调度算法


  • 多级反馈队列调度算法
  1. 算法思想
    对其他调度算法的折中权衡
  2. 算法规则
    1. 设置多级就绪队列,各级队列优先级从高到低,时间片从小到大
    2. 新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片,若用完时间片进程还未结束,则进程进入下一级队列队尾。 如果此时已经是在最下级的队列,则重新放回该队列队尾
    3. 只有第 k 级队列为空时,才会为 k+1 级队头的进程分配时间片
  3. 用于作业/进程调度
    用于进程调度
  4. 是否可抢占?
    抢占式的算法。在 k 级队列的进程运行过程中,若更上级的队列 (1~k-1级)中进入了一个新进程,则由于新进程处于优先级更高的 队列中,因此新进程会抢占处理机,原来运行的进程放回 k 级队列 队尾。
  5. 优缺点
    对各类型进程相对公平(FCFS的优点);
    每个新到达的进程都可以 很快就得到响应(RR的优点);
    短进程只用较少的时间就可完成 (SPF的优点);
    不必实现估计进程的运行时间(避免用户作假);
    可灵活地调整对各类进程的偏好程度,比如CPU密集型进程、I/O密 集型进程(拓展:可以将因I/O而阻塞的进程重新放回原队列,这样 I/O型进程就可以保持较高优先级)
  6. 是否会导致饥饿
    会【一直来短进程的话,有可能一直在高优先级队列处理,被降级的会饥饿】

例题】各进程到达就绪队列的时间、需要的运行时间如下表所示。使用多级反馈队列调度算法,分析进程运行的过程。

P1(1) —> P2(1) —> P1(2) —> P2(1)—> P3(1)—> P2(2) —> P1(4) —> P1(1)

设置多级就绪队列,各级队列优先级高到低时间片小到大新进程到达时先进入第1级队列,按FCFS原则排队等待被分配时间片。若用完时间片进程还未结束,则进程进入下一级队列队尾。如果此时已经在最下级的队列,则重新放回最下级队列队尾
只有第 k 级队列为空时,才会为 k+1 级队头的进程分配时间片
被抢占处理机的进程重新放回原队列队尾


  • 三种调度算法对比
算法 可抢占? 优点 缺点 导致饥饿? 补充
时间片轮转 抢占式 公平,适用于分时系统 频繁切换有开销,不区分优先级 不会 时间片太大或太小有何 影响?
优先级调度 有抢占式的,也有非抢占式的。注意做题时的区 区分优先级, 适用于实时系统 可能导致饥饿 动态/静态优先级。各类型进程如何设置优先级?如何调整优先级?
多级反馈队列 抢占式 平衡优秀 666 一般不说它有缺点,不过可能导致饥饿

注:比起早期的批处理操作系统来说,由于计算机造价大幅降低,因此之后出现的交互式操作系统(包括 分时操作系统、实时操作系统等)更注重系统的响应时间、公平性、平衡性等指标。而这几种算法恰好也能较好地满足交互式系统的需求。因此这三种算法适合用于交互式系统。(比如UNIX使用的就是多级反馈 队列调度算法)

提示:一定要动手做课后习题


2.2.5 调度算法3:多级队列

  • 多级队列调度算法

系统中按进程类型设置多个队列,进程创建成功后插入某个队列

队列之间可采取固定优先级,或时间片划分
固定优先级: 高优先级空时低优先级进程才能被调度
时间片划分:如三个队列分配时间50%、40%、10%

各队列可采用不同的调度策略,如:
系统进程队列采用 优先级调度
交互式队列采用 RR
批处理队列采用 FCFS


2.3.1 进程同步 进程互斥

  • 什么是进程同步

知识点回顾:进程具有异步性的特征。异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进。

例1】老渣要和两个女孩并发约会:

  • 第一道程序:
    • 一号的指令1:老渣陪我吃饭
    • 一号的指令2:老渣把心给我
  • 第二道程序:
    • 二号的指令1:老渣把心给我
    • 二号的指令2:老渣陪我吃饭

但是这个时候:
女一号只想做老渣的初恋
女二号只想交一个有恋爱经验的渣男

那么,老渣在并发执行这两个约会进程的时候,
就必须保证“一号的指令2” 一定要在“二号的指令1”之前执行。

操作系统要提供“进程同步机制”来实现上述需求。

例2】进程通信——管道通信

读进程和写进程并发地运行,由于并发必然导致异步性,因此“写数据”和“读数据”两个操作执行的先后顺序是不确定的。而实际应用中,又必须按照“写数据→读数据”的顺序来执行的。 如何解决这种异步问题,就是 “进程同步”所讨论的内容。

同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。


  • 什么是进程互斥

进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/O设备)

两种资源共享方式

  • 互斥共享方式
    系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源

  • 同时共享方式

    系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问

我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。

对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后, 另一个进程才能去访问临界资源。

  • 对临界资源的互斥访问,可以在逻辑上分为如下四个部分:
1
2
3
4
5
6
do {
entry section; // 进入区
critical section; // 临界区
exit section; // 退出区
remainder section; // 剩余区
} while(true)
功能
进入区 负责检查是否可进入临界区,若可进入,则应
设置正在访问临界资源的标志(可理解为“上锁”),
以阻止其他进程同时进入临界区
临界区 访问临界资源的那段代码
退出区 负责解除正在访问临界资源的标志(可理解为“解锁”)
剩余区 做其他处理

注意:
临界区是进程中访问临界资源的代码段。
进入区退出区负责实现互斥的代码段。
临界区也可称为“临界段”。

  • 为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则
  1. 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
  2. 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待;
  3. 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿);
  4. 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。

2.3.2 进程互斥的软件实现方法

学习提示:

  1. 理解各个算法的思想、原理
  2. 结合上小节学习的“实现互斥的四个逻辑部分”,重点理解各算法在进入区、退出区都做了什么
  3. 分析各算法存在的缺陷(结合“实现互斥要遵循的四个原则”进行分析)
  • 如果没有注意进程互斥?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 进程A、进程B在系统中并发地运行
进程A:
{
其他代码;
使用打印机;
其他代码;
}

进程B:
{
其他代码;
使用打印机;
其他代码;
}

先调度A上处理机运行
当A在使用打印机的过程中,分配给它的时间片用完了,接下来操作系统调度B让它上处理机运行
进程B也在使用打印机

结局:A、B 的打印内容混在一起了【错误的,不希望看到的】

如何实现进程互斥?


  • 单标志法

算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int turn = 0;    // 表示当前 允许进入 临界资源的进程号
// 【turn 变量背后的逻辑:表达“谦让”】

// ===P0进程===
while (turn != 0); // 1 进入区
critical section; // 2 临界区
turn = 1; // 3 退出区
remainder section; // 4 剩余区

// ===P1进程===
while (turn != 1); // 5 进入区
critical section; // 6 临界区
turn = 0; // 7 退出区
remainder section; // 8 剩余区

turn 的初值为 0,即刚开始只允许 0 号进程进入临界区。
若 P1 先上处理机运行,则会一直卡在5。直到 P1 的时间片用完,发生调度,切换 P0 上处理机运行。
代码1不会卡住 P0,P0 可以正常访问临界区,在 P0 访问临界区期间即时切换回 P1,P1依然会卡在5。
只有 P0 在退出区将 turn 改为 1 后,P1才能进入临界区。

因此,该算法可以实现“同一时刻最多只允许一个进程访问临界区”

只能按 P0 → P1 → P0 → P1 →……这样轮流访问。这种必须“轮流访问”带来的问题是,如果此时允许进入临界区的进程是 P0,而 P0 一直不访问临界区,那么虽然此时临界区空闲,但是并不允许 P1 访问。
因此,单标志法存在的主要问题是:违背“空闲让进”原则


  • 双标志先检查法

算法思想:设置一个布尔型数组 flag[],数组中各个元素用来标记各进程想进入临界区的意愿,比如 “flag[0] = ture”意味着 0 号进程 P0 现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志 flag[i] 设为 true,之后开始访问临界区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool flag[2];    // 表示进入临界区意愿的数组 
// 【理解背后的含义:“表达意愿"】
flag[0] = false;
flag[1] = false; // 刚开始设置为两个进程都不想进入临界区

// ===P0进程===
while (flag[1]); // 1 如果此时 P1 想进入临界区,P0 就一直循环等待 "检查"
flag[0] = true; // 2 标记为 P0 进程想要进入临界区 “上锁”
critical section; // 3 访问临界区
flag[0] = false; // 4 访问完临界区,修改标记为 P0 不想使用临界区 “解锁”
remainder section;

// ===P1进程===
while (flag[0]); // 5 如果此时 P0 想进入临界区,P1 就一直循环等待 "检查"
flag[1] = true; // 6 标记为 P1 进程想要进入临界区 “上锁”
critical section; // 7 访问临界区
flag[1] = false; // 8 访问完临界区,修改标记为 P1 不想使用临界区 “解锁”
remainder section;

若按照 1 5 2 6 3 7….的顺序执行,P0 和 P1 将会同时访问临界区。 因此,双标志先检查法主要问题是:违反“忙则等待”原则
原因在于,进入区的“检查”和“上锁” 两个处理不是一气呵成的。“检查”后,“上锁”前可能发生进程切换。


  • 双标志后检查法

算法思想:双标志先检查法的改版。前一个算法的问题是先“检查”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到先“上锁”后“检查” 的方法,来避免上述问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool flag[2l;    // 表示进入临界区意愿的数组 
// 【理解背后的含义:“表达意愿"】
flag[0] = false;
flag[1] = false; // 刚开始设置为两个进程都不想进入临界区

// ===P0进程===
flag[0] = true; // 1 标记为 P0 进程想要进入临界区 “上锁”
while (flag[1]); // 2 如果此时 P1 想进入临界区,P0 就一直循环等待 "检查"
critical section; // 3 访问临界区
flag[0] = false; // 4 访问完临界区,修改标记为 P0 不想使用临界区 “解锁”
remainder section;

// ===P1进程===
flag[1] = true; // 5 标记为 P1 进程想要进入临界区 “上锁”
while (flag[0]); // 6 如果此时 P0 想进入临界区,P1 就一直循环等待 "检查"
critical section; // 7 访问临界区
flag[1] = false; // 8 访问完临界区,修改标记为 P1 不想使用临界区 “解锁”
remainder section;

若按照 1 5 2 6….的顺序执行,P0 和 P1 将都无法进入临界区
因此,双标志后检查法虽然解决了“忙则等待”的问题,但是又违背了“空闲让进”和“有限等待” 原则,会因各进程都长期无法访问临界资源而产生 “饥饿” 现象。
两个进程都争着想进入临界区,但是谁也不让谁,最后谁都无法进入临界区。


  • Peterson 算法

算法思想:结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”(谦让)。做一个有礼貌的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int turn = 0;    // 表示当前允许进入临界资源的进程号
// 【turn 变量背后的逻辑:表达“谦让”】

bool flag[2l; // 表示进入临界区意愿的数组
// 【理解背后的含义:“表达意愿"】
flag[0] = false;
flag[1] = false; // 刚开始设置为两个进程都不想进入临界区

// ===P0进程===
flag[0] = true; // 1 标记为 P0 进程想要进入临界区 “上锁”
turn = 1; // 2 表示可以优先让对方进入临界区
while (flag[1] && turn == 1); // 3 "检查" 检查对方是否也想使用,且最后一次是不是自己“让梨”
critical section; // 4 访问临界区
flag[0] = false; // 5 访问完临界区,修改标记为 P0 不想使用临界区 “解锁”
remainder section;

// ===P1进程===
flag[1] = true; // 6 标记为 P1 进程想要进入临界区 “上锁”
turn = 0; // 7 表示可以优先让对方进入临界区
while (flag[0] && turn == 0); // 8 "检查" 检查对方是否也想使用,且最后一次是不是自己“让梨”
critical section; // 9 访问临界区
flag[1] = false; // 10 访问完临界区,修改标记为 P1 不想使用临界区 “解锁”
remainder section;

动手推导: 按不同的顺序穿插 执行会发生什么?
1 2 3 6 7 8…
1 6 2 3…
1 3 6 7 8…
1 6 2 7 8…

Peterson 算法用软件方法解决了进 程互斥问题,遵循了 空闲让进、忙则等待、有限等待 三个原则,但是 依然未遵循让权等待的原则。

Peterson 算法相较于之前三种软件解决方案来说,是最好的,但依然不够好。


2.3.3 进程互斥的硬件实现方法

  • 中断屏蔽方法

利用“开/关中断指令”实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)

1
2
3
4
5
...
关中断; // 关中断后即不允许当前进程被中断,也必然不会发生进程切换
临界区;
开中断; // 直到当前进程访问完临界区,再执行开中断指令,才有可能有别的进程上处理机并访问临界区
...

优点:简单、高效
缺点:不适用于多处理机【多个处理机同时访问临界区的情况,因为关中断只能关这个处理机】;
只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)


  • TestAndSet指令

简称 TS 指令,也有地方称为 TestAndSetLock 指令,或 TSL 指令
TSL 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。以下是用C语言描述的逻辑【只是逻辑】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 布尔型共享变量 lock 表示当前临界区是否被加锁
// true 表示已加锁,false 表示未加锁
bool TestAndSet (bool *lock){
bool old;
old = *lock; // old用来存放Lock 原来的值
*lock = true; // 无论之前是否已加锁,都将Lock设为true
return old; // 返回Lock原来的值
}

// ==============================

// 以下是使用 TSL 指今实现互斥的算法逻辑
while (TestAndSet (&lock)); // “检查”并“上锁”
临界区代码段...
lock = false; // “解锁”
剩余区代码段...

若刚开始 lock 是 false,则 TSL 返回的 old 值为 false,while 循环条件不满足,直接跳过循环,进入临界区。若刚开始 lock 是 true,则执行 TLS 后 old 返回的值为 true,while 循环条件满足,会一直循环,直到当前访问临界区的进程在退出区进行“解锁”。

相比软件实现方法,TSL 指令把“上锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作

优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境
缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。


  • Swap指令

有的地方也叫 Exchange 指令,或简称 XCHG 指令。
Swap 指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。以下是用C语言描述的逻辑【只是逻辑】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Swap 指令的作用是交换两个变量的值
Swap (bool *a,bool *b) {
bool temp;
temp = *a;
*a = *b;
*b = temp;
}

// ==============================

// 以下是用 Swap 指令实现互斥的算法逻辑
// Lock 表示当前临界区是否被加锁
bool old = true;
while (old == true)
Swap (&lock,&old);
临界区代码段...
lock = false;
剩余区代码段...

逻辑上来看 Swap 和 TSL 并无太大区别,都是先记录下此时临界区是否已经被上锁(记录在 old 变 量上),再将上锁标记 lock 设置为 true,最后检查 old,如果 old 为 false 则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。

优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境

缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致“忙等”。


  • TestAndSet指令和Swap指令的逻辑
  1. old记录是否已被上锁;
  2. 再将lock设为 true;
  3. 检查临界区是否已被上锁(若已上锁,则循环重复前几步)

总之就是为了:检查并上锁


2.3.4 互斥锁

  • 进程互斥:锁

解决临界区最简单的工具就是互斥锁 (mutex lock) 。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数 acquire() 获得锁,而函数 release() 释放锁。

每个互斥锁有一个布尔变量 available,表示锁是否可用。如果锁是可用的,调用 acqiure()会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻寒,直到锁被释放。

1
2
3
4
5
6
7
acquire(){
while(!available); // 忙等待
available = false; // 获得锁
}
release(){
available = true; // 释放锁
}

acquire()或release()的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。

互斥锁的主要缺点是忙等待【违反了“让权等待”】,当有一个进程在临界区中,任何其他进程在进入临界区时必约连续循环调用 acquire()。当多个进程共享同一 CPU 时,就浪费了 CPU 周期。因此,互斥锁通用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。

需要连续循环忙等的互斥锁,都可称为自旋锁 (spin lock) ,如TSL指令、swap指令、单标志法

特性】:

  • 需忙等,进程时间片用完才下处理机,违反“让权等待”
  • 优点:等待期间不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低
  • 常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区
  • 不太适用于单处理机系统,忙等的过程中不可能解锁

2.3.5 信号量机制

  • 复习回顾

复习回顾+思考:之前学习的这些进程互斥的解决方案分别存在哪些问题?

进程互斥的四种软件实现方式 (单标志法、双标志先检查、双标志后检查、Peterson算法)

进程互斥的三种硬件实现方式 (中断屏蔽方法、TS/TSL指令、Swap/XCHG指令)

  1. 在双标志先检查法中,进入区的“检查”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题:
  2. 所有的解决方案都无法实现“让权等待“

1965年,荷兰学者Dijkstra提出了一种卓有成效的实现进程互斥、同步的方法–信号量机制


  • 信号量机制

用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。

信号量其实就是一个变量 (可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如: 系统中只有一台打印机,就可以设置一个初值为1的信号量。

原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。

一对原语: wait(S) 原语和 signal(S) 原语,可以把原语理解为我们自己写的函数,函数名分别为 wait和 signal,括号里的信号量 S 其实就是函数调用时传入的一个参数。

wait、signal 原语常简称为 P、V操作 (来自荷兰语 proberen 和 verhogen)。因此,做题的时候把wait(S)、signal(S) 两个操作分别写为 P(S)V(S)


  • 信号量机制——整型信号量

用一个整数型的变量作为信号量,用来表示系统中某种资源的数量
【与普通整数变量的区别: 对信号量的操作只有三种, 即 初始化、P操作、V操作】

Eg :某计算机系统中有一台打印机…

1
2
3
4
5
6
7
8
9
10
11
12
int S = 1; // 初始化整型信号量s,表示当前系统中可用的打印机资源数

// --“检查”和“上锁”一气呵成,避免了并发、异步导致的问题--
void wait (int S) { // wait 原语,相当于“进入区”
// --存在的问题:不满足“让权等待”原则,会发生“忙等”--
while (S <= 0); // 如果资源数不够,就一直循环等待
S=S-1; // 如果资源数够,则占用一个资源
}

void signal (int s) { // signal 原语,相当于“退出区"
S=S+1; // 使用完资源后,在退出区释放资源
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
进程P0:
...
wait(S); // 进入区,申请资源
使用打印机资源... // 临界区,访问资源
signal(s); // 退出区,释放资源
...

进程P1:
...
wait(S); // 进入区,申请资源
使用打印机资源... // 临界区,访问资源
signal(s); // 退出区,释放资源
...

进程Pn:
...
wait(S); // 进入区,申请资源
使用打印机资源... // 临界区,访问资源
signal(s); // 退出区,释放资源
...

  • 信号量机制——记录型信号量

    大题、小题超高频出题点

整型信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构表 示的信号量。
为了解决上面"让权等待"的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 记录型信号量的定义 */
typedef struct {
int value; // 剩余资源数
struct process *L; // 等待队列
} semaphore;

/*某进程需要使用资源时,通过 wait 原语申请*/
void wait(semaphore S) {
S.value--;
if (S.value < 0 ) {
block(S.L);
}
}
// 【如果剩余资源数不够,使用block原语使进程从
// 运行态进入阻塞态,并把挂到信号量 S 的等待队列(即阻塞队列)中】

/*进程使用完资源后,通过 signal 原语释放*/
void signal(semaphore S) {
S.value++;
if (S.value <= 0) {
wakeup(S.L);
}
}
// 【释放资源后,若还有别的进程在等待这种资源,则使用
// wakeup 原语唤醒等待队列中的一个进程,该进程从阻塞态变为就绪态】

Eg:某计算机系统中有2台打印机…,
则可在初始化信号量 S 时将 S.value 的值设为 2,队列 S.L 设置为空

在考研题目中 wait(S)、signal(S) 也可以记为 P(S)、V(S), 这对原语可用于实现系统资源的“申请”和“释放”

S.value 的初值表示系统中某种资源的数目

对信号量 S 的一次 P 操作意味着进程请求一个单位的该类资源,因此需要执行 S.value--,表示资源数减1,当 S.value < 0 时表示该类资源已分配完毕,因此进程应调用 block 原语进行自我阻塞(当前运行的进程从运行态→阻塞态),主动放弃处理机,并插入该类资源的等待队列 S.L 中。可见,该机制遵循了“让权等待”原则" , 不会出现“忙等”现象。

对信号量 S 的一次 V 操作意味着进程释放一个单位的该类资源,因此需要执行 S.value++,表示资源数加1, 若加1后仍是 S.value <= 0,表示依然有进程在等待该类 资源,因此应调用 wakeup 原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态→就绪态)。

注:若考试中出现 P(S)、V(S) 的操作,除非特别说明,否则默认 S 为记录型信号量。


2.3.6 用信号量机制实现:进程互斥、同步、前驱关系

Tips:不要一头钻到代码里,要注意理解信号量背后的含义,一个信号量对应一种资源

信号量的值 = 这种资源的剩余数量(信号量的值如果小于0,说明此时有进程在等待这种资源)

P( S ) —— 申请一个资源S,如果资源不够就阻塞等待
V( S ) —— 释放一个资源S,如果有进程在等待该资源,则唤醒一个进程

  • 信号量机制实现进程互斥
  1. 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问就应放在临界区)
  2. 设置互斥信号量 mutex,初值为 1 【互斥访问临界资源】
    【理解:信号量 mutex 表示 “进入临界区的名额”】
  3. 在进入区 P(mutex)——申请资源
  4. 在退出区 V(mutex)——释放资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* 记录型信号量的定义 */
typedef struct {
int value; // 剩余资源数
struct process *L; // 等待队列
} semaphore;
// 这是上一小节信号量的定义,但是如果题目 没有特别说明 ,用下面方式定义即可

// 【要会自己定义记录型信号量,但如果题目中没特别说明,可以把信号量的声明简写成这种形式】
semphore muutex = 1; // 初始化信号量

P1() {
. . .
P(mutex); // 使用临界资源前需要加锁
临界区代码段...
V(mutex); // 使用临界资源后需要解锁
...
}

P2() {
. . .
P(mutex);
临界区代码段...
V(mutex);
...
}

注意:对不同的临界资源需要设置不同的互斥信号量

P、V操作必须成对出现。缺少P(mutex) 就不能保证临界资源的互斥访问。缺少 V(mutex) 会导致资源永不被释放,等待进程永不被唤醒。


  • 信号量机制实现进程同步

进程同步:要让各并发进程按要求有序地推进。

1
2
3
4
5
6
7
8
9
10
P1() {
代码1;
代码2;
代码3;
}
P2() {
代码4;
代码5;
代码6;
}

比如,P1、P2 并发执行,由于存在异步性,因此二者交替推进的次序是不确定的。

若 P2 的“代码4”要基于 P1 的“代码1”和“代码2”的运行结果才能执行,那么我们就必须保证“代码4”一定是在“代码2”之后才会执行。 这就是进程同步问题,让本来异步并发的进程互相配合,有序推进。

用信号量实现进程同步:

  1. 分析什么地方需要实现“同步关系”,即必须保证“一前一后”执行的两个操作(或两句代码)
  2. 设置同步信号量 S, 初始为 0
  3. 在“前操作”之后执行 V(S)
  4. 在“后操作”之前执行 P(S)

技巧口诀:前V后P

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 信号量机制实现进程同步 */
semaphore S = 0; // 初始化同步信号量,初值为0
// 【理解:信号量S代表“某种资源”,刚开始是没有这种资源的。
// P2需要使用这种资源,而又只能由P1产生这种资源】

P1() {
代码1;
代码2;
V(S); // 释放资源
代码3;
}
P2() {
P(S); // 保证了 代码4 一定是在 代码2 之后执行
代码4;
代码5;
代码6;
}

若先执行到 V(S) 操作,则 S++S=1。之后当执行到 P(S) 操作 时,由于 S=1,表示有可用资源,会执行 S--,S 的值变回 0, P2 进程不会执行 block 原语,而是继续往下执行代码4。

若先执行到 P(S) 操作,由于 S=0S--S=-1,表示此时没有可用资源,因此P操作中会执行 block 原语,主动请求阻塞。 之后当执行完代码2,继而执行 V(S) 操作, S++,使 S 变回 0, 由于此时有进程在该信号量对应的阻塞队列中,因此会在 V 操作中执行 wakeup 原语,唤醒 P2 进程。这样 P2 就可以继续 执行 代码4 了


  • 信号量机制实现前驱关系

进程 P1 中有句代码 S1,P2 中有句代码 S2 ,P3中有句代码S3 …… P6 中有句代码 S6。这些代码要求 按如下前驱图所示的顺序来执行:

其实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作) 因此,

  1. 为每一对前驱关系各设置一个同步信号量
  2. 在“前操作”之后对相应的同步信号量执行 V 操作
  3. 在“后操作”之前对相应的同步信号量执行 P 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
semaphore a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0;

P1() {
...
s1;
V(a);
V(b);
...
}

P2() {
...
P(a);
s2;
V(c);
V(d);
...
}

P3() {
...
P(b);
s3;
V(g);
...
}

P4() {
...
P(c);
s4;
V(e);
...
}

P5() {
...
P(d);
s5;
V(f);
...
}

P6() {
...
P(e);
P(f);
P(g);
s6;
...
}

  • 知识点和考点

除了互斥、同步问题外, 还会考察有多个资源的问题,有多少资源就把信号量初值设为多少。申请资源时进行P操作,释放资源时进行 V 操作即可

互斥问题,信 号量初值为1
同步问题,信 号量初值为0

前驱关系问题, 本质上就是多级同步问题


2.3.7 生产者消费者问题

  • 问题描述

  • 系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者 进程每次从缓冲区中取出一个产品并使用。(注:这里的“产品”理解为某种数据)

  • 生产者、消费者共享一个初始为空、大小为n的缓冲区。

  • 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待。 【缓冲区没满→生产者生产】

  • 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待。 【缓冲区没空→消费者消费】

  • 缓冲区是临界资源,各进程必须互斥地访问。【互斥关系】

  • 缓冲区满时,生产者必须等待。

  • 缓冲区空时,消费者必须等待。


  • 问题分析

PV操作题目分析步骤:

  1. 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
  2. 整理思路。根据各进程的操作流程确定P、V操作的大致顺序。
  3. 设置信号量。并根据题目条件确定信号量初值。(互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
semaphore mutex = 1;    // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0; // 同步信号量,表示产品的数量,也即非空缓冲区的数量

producer() {
while(1) {
生产一个产品;
P(empty); // 消耗一个空闲缓冲区
// --实现互斥是在同一进程中进行一对PV操作--
P(mutex);
把产品放入缓冲区;
V(mutex);
V(full); // 增加一个产品
}
}
// --实现两进程的同步关系,是在其中一个进程中执行P,另一进程中执行V--
consumer() {
while(1){
P(full); // 消耗一个产品(非空缓冲区)
P(mutex);
从缓冲区取出一个产品;
V(mutex);
V(empty); // 增加一个空闲缓冲区
使用产品;
}
}

【分析双方执行后,增加了什么,申请了什么资源
生产者增加了产品 V(full),消费者增加了缓冲区空白 V(empty)
生产者申请了空白 P(empty),消费者申请了产品 P(full)】


  • 思考:能否改变相邻P、V操作的顺序?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
producer() {
while(1) {
生产一个产品;
P(mutex); // ①
P(empty); // ②
把产品放入缓冲区;
V(mutex);
V(full);
}
}
consumer() {
while(1) {
P(mutex); // ③
P(full); // ④
从缓冲区取出一个产品;
V(mutex);
V(empty);
使用产品;
}
}

若此时缓冲区内已经放满产品,则 empty=0full=n
则生产者进程执行① 使mutex变为0,再执行②,由于已没有空闲缓冲区,因此生产者被阻塞。
由于生产者阻塞,因此切换回消费者进程。消费者进程执行③,由于mutex为0,即生产者还没释放对临界资源的“锁”,因此消费者也被阻塞。
这就造成了生产者等待消费者释放空闲缓冲区,而消费者又等待生产者释放临界区的情况,生产者和消费者循环等待被对方唤醒,出现“死锁”。

同样的,若缓冲区中没有产品,即full=0empty=n。按③④① 的顺序执行就会发生死锁。

因此,实现互斥的P操作一定要在实现同步的P操作之后
V操作不会导致进程阻塞,因此**两个V操作顺序可以交换**。

【思考】能否将“生产一个产品;”和“使用产品;”放到PV操作之间呢?

可以;但是不这么做,这样会使上锁和解锁之间的时间变长,实际应该让这个时间尽可能短。


  • 知识点和考点

生产者消费者问题是一个互斥、同步的综合问题。
对于初学者来说最难的是发现题目中隐含的两对同步关系。
有时候是消费者需要等待生产者生产,有时候是生产者要等待消费者消费,这是两个不同的“一前一后问题”,因此也需要设置两个同步信号量。


2.3.8 多生产者-多消费者

  • 问题描述

桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。 用PV操作实现上述过程。


  • 问题分析
  1. 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
  2. 整理思路。根据各进程的操作流程确定P、V操作的大致顺序。
    【互斥:临界区前后加P、V;同步:前V后P】
  3. 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为 1,同步信号量的初始值要看对应资源的初始值是多少)

互斥关系:mutex = 1
对缓冲区(盘子)的访问要互斥地进行

同步关系(一前一后):

  1. 父亲将苹果放入盘子后,女儿才能取苹果
  2. 母亲将橘子放入盘子后,儿子才能取橘子
  3. 只有盘子为空时,父亲或母亲才能放入水果
    【“盘子为空”这个事件可以由儿子或女儿触发,事件发生后才允许父亲或母亲放水果】


  • 如何实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
semaphore mutex = 1;    // 实现互斥访问盘子(缓冲区)
semaphore apple = 0; // 盘子中有几个苹果
semaphore orange = 0; // 盘子中有几个橘子
semaphore plate = 1; // 盘子中还可以放多少个水果

dad() {
while(1) {
准备一个苹果;
P(plate);
P(mutex);
把苹果放入盘子;
V(mutex);
V(apple);
}
}

mom() {
while(1) {
准备一个橘子;
P(plate);
P(mutex);
把橘子放入盘子;
V(mutex);
V(orange);
}
}

daughter() {
while(1) {
P(apple);
P(mutex);
从盘中取出苹果;
V(mutex);
V(plate);
吃掉苹果;
}
}

son(){
while(1) {
P(orange);
P(mutex);
从盘中取出橘子;
V(mutex);
V(plate);
吃掉橘子;
}
}

问题:可不可以不用互斥信号量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
semaphore apple = 0;    // 盘子中有几个苹果
semaphore orange = 0; // 盘子中有几个橘子
semaphore plate = 1; // 盘子中还可以放多少个水果

dad() {
while(1) {
准备一个苹果;
P(plate);
把苹果放入盘子;
V(apple);
}
}

mom() {
while(1) {
准备一个橘子;
P(plate);
把橘子放入盘子;
V(orange);
}
}

daughter() {
while(1) {
P(apple);
从盘中取出苹果;
V(plate);
吃掉苹果;
}
}

son() {
while(1) {
P(orange);
从盘中取出橘子;
V(plate);
吃掉橘子;
}
}

进程即使上处理机运行也会被阻塞。如果刚开始是父亲进程先上处理机运行,则: 父亲 P(plate),可以访问盘子→母亲 P(plate),阻塞等待盘子→父亲放入苹果 V(apple),女儿进程被唤醒,其他进程即使运行也都会阻塞,暂时不可能访问临界资源(盘子)→女儿 P(apple),访问盘子,V(plate),等待盘子的母亲进程被唤醒→母亲进程访问盘子(其他进程暂时都无法进入临界区)→……

结论:即使不设置专门的互斥变量mutex,也不会出现多个进程同时访问盘子的现象。

原因在于:本题中的缓冲区大小为1,在任何时刻,apple、orange、plate 三个同步信号量中最多只有一个是1。因此在任何时刻, 最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区…

如果盘子容量为2的话

1
semaphore plate = 2; // 盘子中还可以放多少个水果

父亲 P(plate),可以访问盘子→母亲 P(plate),可以访问盘子→父亲在往盘子里放苹果,同时母亲也可以往盘子里 放橘子。于是就出现了两个进程同时访问缓冲区的情况,有可能导致两个进程写入缓冲区的数据相互覆盖的情况。


  • 知识点与考点

总结:在生产者-消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现 互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析。

建议:在考试中如果来不及仔细分析,最好是可以加上互斥信号量,保证各进程一定会互斥地访问缓冲区。 但需要注意的是,实现互斥的P操作一定要在实现同步的P操作之后,否则可能引起“死锁”。

PV 操作题目的解题思路

  1. 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
  2. 整理思路。根据各进程的操作流程确定P、V操作的大致顺序。
    【互斥:临界区前后加P、V;同步:前V后P】
  3. 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为 1,同步信号量的初始值要看对应资源的初始值是多少)

解决“多生产者-多消费者问题”的关键在于理清复杂的同步关系。 在分析同步问题(一前一后问题)的时候不能从单个进程行为的角度来分析,要把“一前一后”发生 的事看做是两种“事件”的前后关系。
比如,如果从单个进程行为的角度来考虑的话,我们会有以下结论:
如果盘子里装有苹果,那么一定要女儿取走苹果后父亲或母亲才能再放入水果
如果盘子里装有橘子,那么一定要儿子取走橘子后父亲或母亲才能再放入水果
这么看是否就意味着要设置四个同步信号量分别实现这四个“一前一后”的关系了?

正确的分析方法应该从“事件”的角度来考虑,我们可以把上述四对“进程行为的前后关系”抽象为一对“事件的前后关系”
盘子变空事件→放入水果事件。“盘子变空事件”既可由儿子引发,也可由女儿引发;“放水果事件” 既可能是父亲执行,也可能是母亲执行。这样的话,就可以用一个同步信号量解决问题了


2.3.9 吸烟者问题

  • 问题描述

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


  • 问题分析

本质上这题也属于“生产者-消费者”问题,更详细的说应该是“可生产多种产品的单生产者-多消费者”。

  1. 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
  2. 整理思路。根据各进程的操作流程确定P、V操作的大致顺序
  3. 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为 1,同步信号量的初始值要看对应资源的初始值是多少)

组合一:纸+胶水
组合二:烟草+胶水
组合三:烟草+纸

互斥关系:桌子可以抽象为容量为1的缓冲区,要互斥访问

同步关系(从事件的角度来分析):
桌上有组合一 → 第一个抽烟者取走东西
桌上有组合二 → 第二个抽烟者取走东西
桌上有组合三 → 第三个抽烟者取走东西
发出完成信号 → 供应者将下一个组合放到桌上


  • 如何实现
1
2
3
4
5
semaphore offer1 = 0;    // 桌上组合一的数量
semaphore offer2 = 0; // 桌上组合二的数量
semaphore offer3 = 0; // 桌上组合三的数量
semaphore finish = 0; // 抽烟是否完成
int i = 0; // 用于实现“三个抽烟者轮流抽烟”

【思考】是否需要设置 一个专门的互斥信号量?
缓冲区大小为1,同一时 刻,四个同步信号量中至多有一个的值为1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
provider() {
while(1) {
if (i == 0) {
将组合一放桌上;
V(offer1);
} else if (i == 1) {
将组合二放桌上;
V(offer2);
} else if (i == 2) {
将组合三放桌上;
V(offer3);
}
i = (i + 1) % 3;
P(finish);
}
}

smoker1() {
while(1) {
P(offer1);
从桌上拿走组合一;卷烟;抽掉;
V(finish);
}
}

smoker2() {
while(1) {
P(offer2);
从桌上拿走组合二;卷烟;抽掉;
V(finish);
}
}

smoker3() {
while(1) {
P(offer3);
从桌上拿走组合三;卷烟;抽掉;
V(finish);
}
}

  • 知识点与考点

吸烟者问题可以为我们解决“可以生产多个产品的单生产者”问题提供一个思路。
值得吸取的精华是:“轮流让各个吸烟者吸烟”必然需要“轮流的在桌上放上组合一、二、三”,注 意体会我们是如何用一个整型变量 i 实现这个“轮流”过程的。

如果题目改为“每次随机地让一个吸烟者吸烟”,我们有应该如何用代码写出这个逻辑呢?

1
i = rand() % 3;    // 随机产生0/1/2

若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个V操作应该放在各自对应的 “事件”发生之后的位置。


2.3.10 读者-写者问题

  • 问题描述

有读者和写者两组并发进程,共享一个文件【不要被下面的图误导了,本问题只有一个已存在的文件】,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求
① 允许多个读者可以同时对文件执行读操作;
② 只允许一个写者往文件中写信息;
③ 任一写者在完成写操作之前不允许其他读者或写者工作;
④ 写者执行写操作前,应让已有的读者和写者全部退出。

  • 与消费者进程不同,读者进程在读数据后并不会将数据清空,并不会改变数据。 因此多个读者可同时访问共享数据
  • 两个写进程同时共享数据,可能导致数据错误覆盖的问题
  • 读进程与写进程同时共享数据,可能导致读出的数据不一致的问题

  • 问题分析
  1. 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
  2. 整理思路。根据各进程的操作流程确定P、V操作的大致顺序
  3. 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。(互斥信号量初值一般为 1,同步信号量的初始值要看对应资源的初始值是多少)

两类进程:写进程、读进程
互斥关系:写进程—写进程、写进程—读进程。读进程与读进程不存在互斥问题。


  • 如何实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
semaphore rw=1;    // 用于实现对共享文件的互斥访问
int count = 0; // 记录当前有几个读进程在访问文件

writer() {
while(1) {
P(rw); // 写之前“加锁”
写文件…
V(rw); // 写完了“解锁”
}
}

reader() {
while(1) {
if (count==0) // “加锁”由第一个读进程负责
P(rw); // 读之前“加锁”
count++; // 访问文件的读进程数+1
读文件…
count--; // 访问文件的读进程数-1
if (count==0) // “解锁”由最后一个读进程负责
V(rw); // 读完了“解锁”
}
}

【思考】:若两个读进程并发执行,则 count=0 时两个进程也许都能满足 if 条件,都会执行 P(rw),从而使第二个读进程阻塞的情况。 如何解决:出现上述问题的原因在于对 count 变量的检查和赋值无法一气呵成,因此可以设置另一个互斥信号量来保证各读进 程对count 的访问是互斥的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
semaphore rw=1;        // 用于实现对共享文件的互斥访问
int count = 0; // 记录当前有几个读进程在访问文件
semaphore mutex = 1; // 用于保证对count变量的互斥访问

writer() {
while(1) {
P(rw); // 写之前“加锁”
写文件…
V(rw); // 写完了“解锁”
}
}

reader() {
while(1) {
P(mutex); // 各读进程互斥访问count
if (count==0) // 由第一个读进程负责
P(rw); // 读之前“加锁”
count++; // 访问文件的读进程数+1
V(mutex);
读文件…
P(mutex); // 各读进程互斥访问count
count--; // 访问文件的读进程数-1
if (count==0) // 由最后一个读进程负责
V(rw); // 读完了“解锁”
V(mutex);
}
}

潜在的问题:只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”。 因此,这种算法中,读进程是优先的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
semaphore rw=1;       // 用于实现对共享文件的互斥访问
int count = 0; // 记录当前有几个读进程在访问文件
semaphore mutex = 1; // 用于保证对count变量的互斥访问
semaphore w = 1; // 用于实现“写优先”【其实不是写优先,是“读写公平法”】

writer() {
while(1) {
P(w);
P(rw); // 写之前“加锁”
写文件…
V(rw); // 写完了“解锁”
V(W);
}
}

reader() {
while(1) {
P(w);
P(mutex); // 各读进程互斥访问count
if (count==0) // 由第一个读进程负责
P(rw); // 读之前“加锁”
count++; // 访问文件的读进程数+1
V(mutex);
V(W);
读文件…
P(mutex); // 各读进程互斥访问count
count--; // 访问文件的读进程数-1
if (count==0) // 由最后一个读进程负责
V(rw); // 读完了“解锁”
V(mutex);
}
}

分析以下并发执行 P(w) 的情况:
(1)读者1\to读者2
(2)写者1\to写者2
(3)写者1\to读者1
(4)读者1\to写者1\to读者2
(5)写者1\to读者1\to写者2

结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正的“写优先” ,而是相对公平的先来先服务原则。 有的书上把这种算法称为“读写公平法”。

【其实是通过w实现了一个等待w的队列
对上面各情况来说,当第一个事件占用了“w资源”,等待队列如下:
(1)[w] → [读者2]
(2)[w] → [写者2]
(3)[w] → [读者1]
(4)[w] → [写者1] → [读者2]
(5)[w] → [读者1] → [写者2]
所以其实是先来先服务原则,不会导致写操作饿死,但也不是“写优先”,而是”读写公平法“


  • 知识点与考点

读者-写者问题为我们解决复杂的互斥问题ᨀ供了一个参考思路。

核心思想在于设置了一个计数器 count 用来记录当前正在访问共享文件的读进程数。我们可以用 count 的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。

另外,对 count 变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量

最后,还要认真体会我们是如何解决“写进程饥饿”问题的。

绝大多数的考研PV操作大题都可以用之前介绍的几种生产者-消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。


2.3.11 哲学家进餐问题

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

  1. 关系分析。系统中有5个哲学家进程,5位哲学 家与左右邻居对其中间筷子的访问是互斥关系。
  2. 整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的是,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象,是哲学家问题的精髓。
  3. 信号量设置。定义互斥信号量数组 chopstick[5]={1,1,1,1,1} 用于实现对5个筷子的互 斥访问。并对哲学家按0~4编号,哲学家 i 左边 的筷子编号为 i,右边的筷子编号为 (i+1)%5
  • 每个哲学家吃饭前依次拿起左、 右两支筷子
1
2
3
4
5
6
7
8
9
10
11
semaphore chopstick[5] = {1, 1, 1, 1, 1};
Pi() { // i号哲学家的进程
while(1) {
P(chopstick[i]); // 拿左
P(chopstick[(i+1)%5]); // 拿右
吃饭…
V(chopstick[i]); // 放左
V(chopstick[(i+1)%5]); // 放右
思考…
}
}

如果5个哲学家并发地拿起了自己左手边的筷子…
每位哲学家循环等待右边 的人放下筷子(阻塞)。 发生“死锁”

  • 如何防止死锁的发生呢

① 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 自己写的
semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore able2eat = 4; // 最多有4个人能吃饭
Pi() { // i号哲学家的进程
while(1) {
p(able2eat); // 申请吃饭
P(chopstick[i]); // 拿左
P(chopstick[(i+1)%5]); // 拿右
吃饭…
V(chopstick[i]); // 放左
V(chopstick[(i+1)%5]); // 放右
v(able2eat); // 吃完了,别人可以吃
思考…
}
}

② 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就 避免占有一支后再等待另一只的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
semaphore chopstick[5] = {1, 1, 1, 1, 1};
Pi() { // i号哲学家的进程
while(1) {
if (i % 2 == 1) { // 奇数号哲学家
P(chopstick[i]); // 拿左
P(chopstick[(i+1)%5]); // 拿右
}
else { // 偶数号哲学家
P(chopstick[(i+1)%5]); // 拿右
P(chopstick[i]); // 拿左
}
吃饭…
V(chopstick[i]); // 放左
V(chopstick[(i+1)%5]); // 放右
思考…
}
}

③ 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子。【下面的实现其实并不是这样】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1; // 互斥地取筷子
Pi() { // i号哲学家的进程
while(1) {
P(mutex);
P(chopstick[i]); // 拿左
P(chopstick[(i+1)%5]); // 拿右
V(mutex);
吃饭…
V(chopstick[i]); // 放左
V(chopstick[(i+1)%5]); // 放右
思考…
}
}

对于上面代码③分析下面的情况:
【哲学家0拿起来左边的筷子,然后进程切换到哲学家2】

【哲学家0吃饭,这个时候哲学家1想吃饭,然后哲学家2想吃饭】

【哲学家0吃饭,这个时候哲学家4想吃饭】


  • 知识点与考点

哲学家进餐问题的关键在于解决进程死锁。
这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。

如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。
可以参考哲学家就餐问题解决死锁的三种思路。


2.3.12 管程

  • 为什么要引入管程

信号量机制存在的问题:编写程序困难、易出错

能不能设计一种机制,让程序员写程序时不需要再关注复杂的PV操作,让写代码更轻松呢?

1973年,Brinch Hansen 首次在程序设计语言 (Pascal) 中引入了“管程”成分——一种高级同步机制


  • 管程的定义和基本特征

管程是一种特殊的软件模块,有这些部分组成:

  1. 局部于管程的共享数据结构说明;
  2. 对该数据结构进行操作的一组过程
  3. 对局部于管程的共享数据设置初始值的语句;
  4. 管程需要一个名字。

跨考Tips: “过程”其实就是“函数”

管程的基本特征:

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

  • 拓展1:用管程解决生产者消费者问题

以下当做伪代码来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 以下当做伪代码来理解
monitor ProcedureConsumer // 定义一个管程
管程内容...
end monitor

// 开始
monitor ProcedureConsumer
condition full, empty; // 条件变量用来实现同步
int count = 0; // 缓冲区中的产品
void insert (Item item) { // 把产品放入缓冲区
if (count == N) // 是否满
wait(empty); // 相当于P操作
count++;
insert_item(item); // 放入缓冲区
if (count == 1) // 我是第一个,空->非空,那么我会唤醒等待队列的进程
signal(full); // 相当于V操作
}
Item remove() { // 从缓冲区取出一个产品
if (count == 0) // 是否空
wait(full); // 相当于P操作
count--;
if (count == N - 1) // 我是最后一个,满->不满,那么我会唤醒等待队列的进程
signal(empty); // 相当于V操作
return remove_item(); // 取出一个产品
}
end monitor

// 生产者进程
producer() {
while(1) {
item = 生产一个产品;
ProcedureConsumer.insert(item);
}
}

// 消费者进程
consumer() {
while(1) {
item = ProcedureConsumer.remove();
消费产品item;
}
}

由编译器负责实现 各进程互斥地进入管程中的过程

每次仅允许一个进程在管程内执行某个内部过程。
例1:两个生产者进程并发执行,依次调用了 insert 过程…
例2:两个消费者进程先执行,生产者进程后执行…

引入管程的目的无非就是要更方便地实现进程互斥和同步。

  1. 需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
  2. 需要在管程中定义用于访问这些共享数据的“入口”——其实就是一些函数(如生产者消费者 问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
  3. 只有通过这些特定的“入口”才能访问共享数据
  4. 管程中有很多“入口”,但是每次只能开放其中一个“入口” ,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一 个时间段内最多只会有一个进程在访问缓冲区。注意:这种互斥特性是由编译器负责实现的, 程序员不用关心
  5. 可在管程中设置条件变量等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口” );可以通过唤醒操作将 等待在条件变量上的进程或线程唤醒。

程序员可以用某种特殊的语法定义一个管程(比如: monitor ProducerConsumer …… end monitor;), 之后其他程序员就可以使用这个管程提供的特定“入口”【“封装”思想】很方便地使用实现进程同步/互斥了。


  • 拓展2:Java 中类似于管程的机制

Java 中,如果用关键字 synchronized 来描述一个函数,那么这个函数同一时间段内只能被一个线程调用

1
2
3
4
5
6
7
8
9
static class monitor {
private Item buffer[] = new Item[N];
private int count = 0;

// 每次只能有一个线程进入insert 函数,如果多个线程同时调用 insert 函数,则后来者需要排队等待
public synchronized viod insert(Item item) {
...
}
}

Tips:不熟悉 Java 的同学看不懂也没关系,不会考,仅作为思维拓展。 熟悉 Java 的同学在时间充裕的情况下可以动手尝试用 synchronized 实现生产者消费者问题的“管程”


2.4.1 死锁的概念

  • 什么是死锁

每位哲学家都在等待自己右边的人放下筷 子,这些哲学家进程都因等待筷子资源而被阻塞。即发生“死锁”

在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推 进的现象,就是“死锁”。 发生死锁后若无外力干涉, 这些进程都将无法向前推进。


  • 死锁、饥饿、死循环的区别

死锁各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象

饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。

死循环:某进程执行过程中一直跳不出某个循环的现象。有时是因为程序逻辑 bug 导致的,有时是程序员故意设计的。

共同点:都是进程无法顺利向前推进的现象 (故意设计的死循环除 外)

现象 区别
死锁 死锁一定是“循环等待对方手里的资源”导致的,因此如果有死锁现象,那至少有两个或两个以上的进程同时发生死锁。另外,发生死锁的进程一定处于阻塞态。
饥饿 可能只有一个进程发生饥饿。发生饥饿的进程既可能是阻塞态(如长期得不到需要的I/O设备),也可能是就绪态(长期得不到处理机)
死循环 可能只有一个进程发生死循环。死循环的进程可以上处理机运行(可以是运行态),只不过无法像期待的那样顺利推进。死锁和饥饿问题是由于操作系统分配资源的策略不合理导致的,而死循环是由代码逻辑的错误导致的。死锁和饥饿是管理者(操作系统)的问题,死循环是被管理者(程序员)的问题

  • 死锁产生的必要条件

产生死锁必须同时满足一下四个条件,只要其中任一条件不成立,死锁就不会发生。

  1. 互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)。 像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待 这种资源)。
  2. 不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
  3. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
  4. 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

注意!发生死锁时一定有循环等待,但是发生循环等待时未必死锁(循环等待是死锁的必要不充分条件)【选择题】

如果同类资源数大于1,则即使有循环等待,也未必发生死锁。但如果系统中每类资源都只有一个,那循环等待就是死锁的充分必要条件了。


  • 什么时候会发生死锁
  1. 对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的。
  2. 进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程P1、 P2 分别申请并占有了资源 R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1, 两者会因为申请的资源被对方占有而阻塞,从而发生死锁。
  3. 信号量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的P操作之前,就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资 源)

总之,对不可剥夺资源的不合理分配,可能导致死锁。


  • 死锁的处理策略
  1. 预防死锁。破坏死锁产生的四个必要条件中的一个或几个。
  2. 避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)
  3. 死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。

2.4.2 死锁的处理策略——预防死锁

  • 破坏互斥条件

互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁。

如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如: SPOOLing技术。 操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备…

【进程1还没用完打印机之前,进程2申请使用打印机会阻塞】

【使用了SPOOLing技术后,在各进程看来,自己对打印机资源的使用请求立即就被接收处理 了,不需要再阻塞等待】

该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件


  • 破坏不剥夺条件

不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。

破坏不剥夺条件:
方案一:当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。

方案二:当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)

该策略的缺点

  1. 实现起来比较复杂。
  2. 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。
  3. 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
  4. 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重 新申请。如果一直发生这样的情况,就会导致进程饥饿

  • 破坏请求和保持条件

请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。

可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前, 不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源 了。

该策略实现起来简单,但也有明显的缺点
有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造 成严重的资源浪费,资源利用率极低

另外,该策略也有可能导致某些进程饥饿。eg:
【如下图:假设A类进程结束后,又来了A类进程,这样C类进程一直的怒道资源1,发生了饥饿】


  • 破坏循环等待条件

循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。

可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源, 同类资源(即编号相同的资源)一次申请完。

原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。

eg:假设系统中共有10个资源,编号为 1, 2, …… 10

在任何一个时刻,总有一个进程拥有的资源编号是最大的,那这个进程申请之后的资源必然畅通无阻。 因此,不可能出现所有进程都阻塞的死锁现象
我的疑问,假设现在有P1,P2,P3,P4,P5,他们需要任意三个资源才能运行,这个时候他们几乎同时进行申请资源,第一轮P1[1],p2[2],p3[3],p4[4],p5[5],第二轮P1[1,6],p2[2,7],p3[3,8],p4[4,9],p5[5,10],这不是死锁了吗?】
【上面的问题,首先资源不同的编号的资源不相同,错啦】
【证明:反证法,假设资源编完号了(10个资源,10个进程),有以下死锁情况:(资源<a,b,…,j>,已经按<1, 2, …,10>有序编号了)
P1需要a资源,但是a资源被P2占有了;
P2需要b资源,但是b资源被P3占有了;

p10需要j资源,但是j资源被P1占有了。
那么这里对于P1进程,拥有了j(10),资源,但是却还申请a(1)资源,矛盾
大概是这个意思。】

该策略的缺点: 1. 不方便增加新的设备,因为可能需要重新分配所有的编号; 2. 进程实际使用资源的顺序可能和 编号递增顺序不一致,会导致资源浪费; 3. 必须按规定次序申请资源,用户编程麻烦。


2.4.3 死锁的处理策略——避免死锁

  • 什么是安全序列

你是一位成功的银行家,手里掌握着100个亿的资金…
有三个企业想找你贷款,分别是 企业B、企业A、企业T,为描述方便,简称BAT。
B 表示:“大哥,我最多会跟你借70亿…”
A 表示:“大哥,我最多会跟你借40亿…”
T 表示:“大哥,我最多会跟你借50亿…”
然而…江湖中有个不成文的规矩:如果你借给企业的钱总数达不到企业提出的最大要求,那么不管你之前给企业借了多少钱,那些钱都拿不回来了

刚开始,BAT三个企业分别从你这儿借了 20、10、30 亿 …

手里还有:40亿
此时… B 还想借 30 亿,你敢借吗? 假如答应了B的请求……

手里还有:10亿
只剩下10亿,如果BAT都提出再借20亿的请求,那么任何一个企业的需求都得不到满足…
【借30亿是不安全的】

手里还有:40亿
此时… A 还想借 20 亿,你敢借吗?假如答应了A的请求……

手里还有:20亿
可以先把20亿全部借给T,等T把钱全部还回来了,手里就会有20+30=50亿,再把这些钱全借给B,B还钱后总共有 50+20=70亿,最后再借给A【之后按T → B → A的顺序借钱是OK的】

或者,先借给A 10亿,等A还钱了手里就 有 20+30 = 50 亿,再给 T 20亿,等T还钱 了就有 50+30 = 80 亿,最后再给 B 借…【按A → T → B的顺序借钱也是OK的】


  • 安全序列、不安全状态、死锁的联系

给B借30亿是不安全的…之后手里只剩10亿,如果BAT都提出再借20亿的请求,那么任何一个企 业的需求都得不到满足…

给A借 20 亿是安全的,因为存在 T → B → A 这样的安全序列

所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态【如下解释】,不过我们在分配资源之前总是要考虑到最坏的情况。
【系统也有可能重新回到安全状态】:手里还有10亿,比如A先归还了10亿,那么就有安全序列 B → A → T

如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。


  • 银行家算法

银行家算法是荷兰学者 Dijkstra【还提出了信号量机制】 为银行系统设计的,以确保银行在发放现金贷款时,不会发生不能 满足所有客户需要的情况。后来该算法被用在操作系统中,用于避免死锁

核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待。

思考:BAT 的例子中,只有一种类型的资源——钱,但是在计算机系统中会有多种多样的资源,应该怎么把算法拓展为多种资源的情况呢?

可以把单维的数字拓展为多维的向量。比如:系统中有5个进程P0~P4,3 种资源 R0~R2,初始数量为 (10, 5, 7),则某一时刻的情况可表示如下:

【查看上表的情况是否安全】:

依次检查剩余可用资源 (3, 3, 2) 是否能满足各进程的需求
可满足P1需求,将 P1 加入安全序列,并更新剩余可用资源值为 (5, 3, 2)
依次检查剩余可用资源 (5, 3, 2) 是否能满足剩余进程(不包括已加入安全序列的进程)的需求
可满足P3需求,将 P3 加入安全序列,并更新剩余可用资源值为 (7, 4, 3)
依次检查剩余可用资源 (7, 4, 3) 是否能满足剩余进程(不包括已加入安全序列的进程)的需求……
。。。
以此类推,共五次循环检查即可将5个进程都加入安全序列中,最终可得一个安全序列(P1,P3,P0,02,P4)
该算法称为安全性算法。可以很方便地用代码实现以上流程,每一轮检查都从编号较小的进程开始检查。 实际做题时可以更快速的得到安全序列。

实际做题(手算)时可用更快速的方法找到一个安全序列: 经对比发现,(3, 3, 2)可满足 P1、P3,说明无论如何,这两个进程的资源需求一定是可以依次被满足的,因此P1、P3 一定可以顺利的执行完,并归还资源。 可把 P1、P3 先加入安全序列。 (2, 0, 0) + (2, 1, 1) + (3, 3, 2) = (7, 4, 3) 剩下的 P0、P2、P4 都可被满足。同理,这些进程都可以加入安全序列。

于是,5个进程全部加入安全序列,说明此时系统处于安全状态,暂不可能发生死锁

【不安全序列的例子】资源总数 (10, 5, 7) ,剩余可用资源 (3,3,2)

无法找到任何一个安全序列,说明此时系统处于不安全状态有可能发生死锁
经对比发现, (3,3,2)可满足 P1、P3,说明无论如何,这两个进程的资源需求一定是可以依次被满足的,因此P1、P3 一定可以顺利的执行完,并归还资源。 可把 P1、P3 先加入安全序列。
(2, 0, 0) +(2, 1, 1) +(3,3, 2) =(7, 4, 3)
剩下的 P0 需要(8,4,3),P2需要(6,5,0),P4 需要(4,3,4)
任何一个进程都不能被完全满足。

代码实现

假设系统中有 n 个进程,m 种资源
每个进程在运行前先声明对各种资源的最大需求数, 则可用一个 n*m 的矩阵(可用二维数组实现)表示所有进程对各种资源的最大需求数。不妨称为最大需求 矩阵 Max,Max[i, j]=K 表示进程 Pi 最多需要 K 个资源 Rj。同理,系统可以用一个 n*m 的分配矩阵 Allocation 表示对所有进程的资源分配情况。Max – Allocation = Need 矩阵,表示各进程最多还需要多少各类资源。 另外,还要用一个长度为m的一维数组 Available 表示当前系统中还有多少可用资源。 某进程Pi向系统申请资源,可用一个长度为m的一维数组 Requesti\mathsf{Request_i} 表示本次申请的各种资源量。

可用银行家算法预判本次分配是否会导致系统进入不安全状态:
① 如果 Requesti\mathsf{Request_i}[j] ≤ Need[i, j] (0≤j≤m)便转向②;否则认为出错。
② 如果 Requesti\mathsf{Request_i}[j] ≤ Available[j] (0≤j≤m),便转向③ ;否则表示尚无足够资源,Pi必须等待。
③ 系统试探着把资源分配给进程Pi【代码如下】,并修改相应的数据(并非真的分配,修改数值只是为了做预判
④ 操作系统执行安全性算法,检查此次资源分配后,系统是否处于安全状态。若安全,才正式分配;否则,恢复相应数据,让进程阻塞等待。

1
2
3
4
// 分配资源
Available = Available - Request_i;
Allocation[i, j] = Allocation[i, j] + Request_i[j];
Need[i, j] = Need[i, j] – Request_i[j]

  • 知识点与考点

数据结构:
长度为 m 的一维数组 Available 表示还有多少可用资源
n*m 矩阵 Max 表示各进程对资源的最大需求数
n*m 矩阵 Allocation 表示已经给各进程分配了多少资源
Max – Allocation = Need 矩阵表示各进程最多还需要多少资源
用长度为 m 的一位数组 Request 表示进程此次申请的各种资源

银行家算法步骤:
① 检查此次申请是否超过了之前声明的最大需求数
② 检查此时系统剩余的可用资源是否还能满足这次请求
③ 试探着分配,更改各数据结构
④ 用安全性算法检查此次分配是否会导致系统进入不安全状态

安全性算法步骤:
检查当前的剩余可用资源是否能满足某个进程的最大需求,如果可以,就把该进程加入安全序列,
并把该进程持有的资源全部回收。
不断重复上述过程,看最终是否能让所有进程都加入安全序列。


2.4.4 死锁的处理策略——检测和解除

如果系统中既不采取预防死锁的措施,也不采取避免死锁的措施,系统就很可能发生死锁。在这种情况下,系统应当提供两个算法:
① 死锁检测算法:用于检测系统状态,以确定系统中是否发生了死锁。
② 死锁解除算法:当认定系统中已经发生了死锁,利用该算法可将系统从死锁状态中解脱出来。

  • 死锁的检测

为了能对系统是否已发生了死锁进行检测,必须:
① 用某种数据结构来保存资源的请求和分配信息;
② 提供一种算法,利用上述信息来检测系统是否已进入死锁状态。

数据结构:资源分配图

  • 两种结点
    • 进程结点
      对应一个进程
    • 资源结点
      对应一类资源
  • 两种边
    • 进程结点→资源结点
      表示进程想申请几个资源 (每条边代表一个)
    • 资源结点→进程结点
      表示已经为进程分配了几个资源 (每条边代表一个)

一般用矩形表示资源结点,矩形中的小圆代表该类资源的数量

如果系统中剩余的可用资源数足够满足进程的需求,那 么这个进程暂时是不会阻塞的,可以顺利地执行下去。 如果这个进程执行结束了把资源归还系统,就可能使某些正在等待资源的进程被激活,并顺利地执行下去。 相应的,这些被激活的进程执行完了之后又会归还一些资源,这样可能又会激活另外一些阻塞的进程…

如果按上述过程分析,最终能消除所有边,就称这个图是可完全简化的。此时一定没有发生死锁(相当于能找到一个安全序列)

如果最终不能消除所有边,那么此时就是发生了死锁
最终还连着边的那些进程就是处于死锁状态的进程

检测死锁的算法
1)在资源分配图中,找出既不阻塞又不是孤点的进程 Pi(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。如下图中,R1没有空闲资源,R2有一个空闲资源。若所有的连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然 后释放它所占有的所有资源)。消去它所有的请求边和分配变,使之称为孤立的结点。在下图中, P1 是满足这一条件的进程结点,于是将P1的所有边消去。

2)进程 Pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变 为非阻塞进程。在下图中,P2 就满足这样的条件。根据 1)中的方法进行一系列简化后,若能消去途中所有的边,则称该图是可完全简化的

死锁定理:如果某时刻系统的资源分配图是不可完全简化的,那么此时系统死锁


  • 死锁的解除

一旦检测出死锁的发生,就应该立即解除死锁。

补充:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程

解除死锁的主要方法有:

  1. 资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
  2. 撤销进程法(或称终止进程法)。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行 了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
  3. 进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。

如何决定“对谁动手”

  1. 进程优先级
  2. 已执行多长时间
  3. 还要多久能完成
  4. 进程已经使用了多少资源
  5. 进程是交互式的还是批处理式的

第三章 内存管理

3.1.1 内存的基础知识

  • 什么是内存?有何作用?

内存可存放数据。程序执行前需要先放到内存中才能被CPU处理——缓和CPU与硬盘之间的速度矛盾

思考:在多道程序环境下,系统中会有多个程序并发执行,也就是说会有多个程序的数据需要同时放到内存中。那么,如何区分各个程序的数据是放在什么地方的呢?

方案:给内存的存储单元编地址

内存地址从0 开始,每个地址对应一个存储单元

内存中也有一个一个的“小房间”,每个小房间就是一 个“存储单元

如果计算机“按字节编址”, 则每个存储单元大小1字节,即 1B,即 8个二进制位

如果字长为16位的计算机 “按字编址”,则每个存储单元大小1个字;每个字的大小为 16 个二进制位


  • 补充知识:几个常用的数量单位

一台手机/电脑 有 4GB 内存,是什么意思?
是指该内存中可以存放 42304*2^{30}个字节。如果是按字节编址的 话,也就是有 4230=2324*2^{30} = 2^{32}个“小房间”

补充知识:
210=1K(千)2^{10} = 1K (千)
220=1M(兆,百万)2^{20} = 1M (兆,百万)
230=1G(十亿,千兆)2^{30} = 1G (十亿,千兆)

注:有的题目会告诉我们内存的大小,让我们确定地址长度应该是多少(即要多少个二进制位才能表示相应数目的存储单元)


  • 知识滚雪球:指令的工作原理

3.1_1_内存的基础知识_哔哩哔哩_bilibili

07:57-18:31

程序经过编译、链接 后生成的指令中指明的是逻辑地址(相对地址),即:相对于 进程的起始地址而言 的地址

指令使用的是逻辑地址(相对地址),那么如何装入内存无法确定。

接下来解决的就是如何把逻辑地址转化为最终的物理地址


  • 装入的三种方式——绝对装入

绝对装入:在编译时,如果知道程序将放到内存中的哪个位置,编译程序将产生绝对地址的目标代码。 装入程序按照装入模块中的地址,将程序和数据装入内存。

Eg:如果知道装入模块要从地址为100 的地方开始存放…

绝对装入只适用于单道程序环境【此时还没有产生操作系统】。

程序中使用的绝对地址,可在编译或汇编时给出,也可由程序员直接赋予。通常情况下都是编译或汇编时再转换为绝对地址。

【灵活性很低,换个电脑就无法执行了】


  • 装入的三种方式——可重定位装入

静态重定位:又称可重定位装入。编译、链接后的装入模块的地址都是从0开始的,指令中使用的地址、数据存放的地址都是相对于起始地址而言的逻辑地址。可根据内存的当前情况,将装入模块装入到内存的适当位置。装入时对地址进行“重定位”,将逻辑地址变换为物理地址(地址变换是在装入时一次完成的)。

静态重定位的特点是在一个作业装入内存时,必须分配其要求的全部内存空间,如果没有足够的内存,就不能装入该作业。

作业一旦进入内存后,在运行期间就不能再移动,也不能再申请内存空间。

用于早期的多道批处理操作系统


  • 装入的三种方式——动态运行时装入

动态重定位:又称动态运行时装入。编译、链接后的装入模块的地址都是从0开始的。装入程序把装入模块装入内存后,并不会立即把逻辑地址转换为物理地址,而是把地址转换推迟到程序真正要执行时才进行。因此装入内存后所有的地址依然是逻辑地址。这种方式需要一个重定位寄存器的支持。

采用动态重定位时允许程序在内存中发生移动

并且可将程序分配到不连续的存储区中;在程序运行前只需装入它的部分代码即可投入运行,然后在程序运行期间,根据需要动态申请分配内存;便于程序段的共享,可以向用户提供一个比存储空间大得多的地址空间。

用于现代操作系统


  • 从写程序到程序运行

编译:由编译程序将用户源代码编译成若干个目标模块(编译就是把高级语言翻译为机器语言

链接:由链接程序将编译后形成的一组目标模块,以及所需库函数链接在一起,形成一个完整的装入模块

装入(装载)由装入程序将装入模块装入内存运行


  • 链接的三种方式

静态链接:在程序运行之前, 先将各目标模块及它们所需的库函数连接成一个完整的可执行文件(装入模块), 之后不再拆开。

装入时动态链接:将各目标模块装入内存时,边装入边链接的链接方式。

运行时动态链接:在程序执行中需要该目标模块时,才对它进行链接。其优点是便于修改和更新,便于实现对目标模块的共享。


3.1.2 内存管理的概念

  • 内存空间的分配与回收

操作系统作为系统资源的管理者,当然也需要对内存进行管理,要管些什么呢?

很多位置都可以放, 那应该放在哪里?
操作系统要怎么记录哪些内存区域已经被分配出去了, 哪些又还空闲?
当进程运行结束之后,如何将进程占用的内存空间回收?

  1. 操作系统负责内存空间的分配与回收

  • 内存空间的扩展

游戏 GTA 的大小超过 60GB,按理来说这个游戏程序运行之前需要把 60GB 数据全部放入内存。然而,实际我的电脑内存才 4GB,但为什么这个游戏可以顺利运行呢?——虚拟技术(操作系统的虚拟性)

  1. 操作系统需要提供某种技术从逻辑上对内存空间进行扩充

  • 地址转换

为了使编程更方便,程序员写程序时应该只需要关注指令、数据的逻辑地址。而逻辑地址到物理地址的转换(这个过程称为地址重定位)应该由操作系统负责,这样就保证了程序员写程序时不需要关注物理内存的实际情况

  1. 操作系统需要提供地址转换功能,负责程序的逻辑地址与物理地址的转换

三种装入方式

  • 绝对装入【单道程序阶段,此时还没产生操作系统】
    编译时产生绝对地址
  • 可重定位装入【用于早期的多道批处理操作系统】
    装入时将逻辑地址转化为物理地址
  • 动态运行时装入【现代操作系统】
    运行时将逻辑地址转化为物理地址,需设计重定位寄存器

  • 内存保护
  1. 操作系统需要提供内存保护功能。保证各进程在各自存储空间内运行,互不干扰

内存保护可采取两种方法:
方法一:在CPU中设置一对上、下限寄存器,存放进程的上、下限地址。进程的指令要访问某个地址时,CPU检查是否越界。

方法二:采用重定位寄存器(又称基址寄存器)和界地址寄存器(又称限长寄存器)进行越界检查。重定 位寄存器中存放的是进程的起始物理地址。界地址寄存器中存放的是进程的最大逻辑地址


3.1.3 覆盖与交换

内存空间的扩充

  • 覆盖技术
  • 交换技术
  • 虚拟存储技术
  • 覆盖技术

早期的计算机内存很小,比如 IBM 推出的第一台PC机最大只支持 1MB 大小的内存。因此经常会出现内存大小不够的情况。

后来人们引入了覆盖技术,用来解决“程序大小超过物理内存总和”的问题

覆盖技术的思想:将程序分为多个段(多个模块)。 常用的段常驻内存,不常用的段在需要时调入内存。

内存中分为一个“固定区”若干个“覆盖区” 。 需要常驻内存的段放在“固定区”中,调入后就不再调出(除非运行结束)

不常用的段放在“覆盖区”,需要用到时调入内存, 用不到时调出内存

按照自身逻辑结构,让那些不可能同时被访问的程序段共享同一 个覆盖区

必须由程序员声明覆盖结构,操作系统完成自动覆盖。
缺点:对用户不透明,增加了用户编程负担。 覆盖技术只用于早期的操作系统中,现在已成为历史。


  • 交换技术

交换(对换)技术的设计思想:内存空间紧张时,系统将内存中某些进程暂时换出外存,把外存中某些已具备运行条件的进程换入内存(进程在内存与磁盘间动态调度)

中级调度(内存调度) ,就是要决定将哪个处于挂起状态的进程重新调入内存。

暂时换出外存等待的进程状态为挂起状态(挂起态,suspend)
挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态

  1. 应该在外存(磁盘)的什么位置保存被换出的进程?

具有对换功能的操作系统中,通常把磁盘空间分为文件区对换区两部分。文件区主要用于存放文件,主要追求存储空间的利用率,因此对文件区空间的管理采用离散分配方式对换区空间只占磁盘空间的小部分,被换出的进程数据就存放在对换区。由于对换的速度直接影响到系统的整体速度,因此对换区空间的管理主要追求换入换出速度,因此通常对换区采用连续分配方式(学过文件管理章节 后即可理解)。总之,对换区的I/O速度比文件区的更快

  1. 什么时候应该交换?

交换通常在许多进程运行且内存吃紧时进行,而系统负荷降低就暂停。例如:在发现许多进程运行时经常发生缺页,就说明内存紧张,此时可以换出一些进程; 如果缺页率明显下降,就可以暂停换出。

  1. 应该换出哪些进程?

可优先换出阻塞进程;可换出优先级低的进程;为了防止优先级低的进程在被调入内存后很快又被换出,有的系统还会考虑进程在内存的驻留时间… (注意:PCB 会常驻内存,不会被换出外存)


3.1.4 连续分配管理方式

连续分配:指为用户进程分配的必须是一个连续的内存空间

  • 单一连续分配

在单一连续分配方式中,内存被分为系统区用户区

系统区通常位于内存的低地址部分,用于存放操作系统
相关数据;用户区用于存放用户进程相关数据。

内存中只能有一道用户程序,用户程序独占整个用户区 空间。

优点:实现简单;无外部碎片【后面说】;可以采用覆盖技术扩充
内存;不一定需要采取内存保护(eg:早期的 PC 操作 系统 MS-DOS)。

缺点:只能用于单用户、单任务的操作系统中;有内部碎片【分配给某进程的内存区域中,如果有些部分没有用 上,就是“内部碎片”】;存储器利用率极低。


  • 固定分区分配
    • 分区大小相等
    • 分区大小不相等

20世纪60年代出现了支持多道程序的系统,为了能在内存中装入多道程序,且这些程序之间又不会相互干扰, 于是将整个用户空间划分为若干个固定大小的分区,在每个分区中只装入一道作业,这样就形成了最早的、最 简单的一种可运行多道程序的内存管理方式。

分区大小相等:缺乏灵活性,但是很适合用于用一台计算机控制多个相同对象的场合(比如:钢铁厂有n个相同的炼钢炉,就可把内存分为n个大小相等的区域存放 n个炼钢炉控制程序)

分区大小不等:增加了灵活性,可以满足不同大小的进程需求。根据常在系统中运行的作业大小情况进行划分 (比如:划分多个小分区、适量中等分区、少量大分区)

操作系统需要建立一个数据结构——分区说明表,来实现各个分区的分配与回收。每个表项对应一个分区,通常按分区大小排列。每个表项包括对应分区的大小、起始地址、状态(是否已分配)。

当某用户程序要装入内存时,由操作系统内核程序根据用户程序大小检索该表, 从中找到一个能满足大小的、未分配的分区,将之分配给该程序,然后修改状 态为“已分配”。

优点:实现简单,无外部碎片

缺点:a. 当用户程序太大时,可能所有的分区都不能满足需求,此时不得不采用覆盖技术来解决,但这又会降低性能;b. 会产生内部碎片,内存利用率低


  • 动态分区分配

动态分区分配又称为可变分区分配。这种分配方式不会预先划分内存分区,而是在进程装入内存时, 根据进程的大小动态地建立分区,并使分区的大小正好适合进程的需要。因此系统分区的大小和数目是可变的。(eg:假设某计算机内存大小为 64MB,系统区 8MB,用户区共 56 MB…)

  1. 系统要用什么样的数据结构记录内存的使用情况?

两种数据结构:

  • 空闲分区表
    每个空闲分区对应 一个表项。表项中包含分区号、 分区大小、分区起始地址等信息
  • 空闲分区链
    每个分区的起始部分和末尾部分分别设置前向指针和后向指针。起始部分处还可记录分区大小等信息

  1. 当很多个空闲分区都能满足需求时, 应该选择哪个分区进行分配?

上图中有三个分区,现有进程5 (4MB),应该用最大的分区进行分配?还是用最小的分区进行分配?又或是用地址最低的部分进行分配?

把一个新作业装入内存时,须按照一定的动态分区分配算法,从空闲分区表(或空闲分区链)中选出一个分区分配给该作业。由于分配算法算法对系统性能有很大的影响,因此人们对它进行了广泛的研究。

下个小节会介绍四种动态分区分配算法

  1. 如何进行分区的分配与回收操作?

分配情况1:

分配情况2:

回收情况1:回收区的后(前)面有一个相邻的空闲分区
两个相邻的空闲分区合并为一个

回收情况2:回收区的前、后各有一个相邻的空闲分区
三个相邻的空闲分区合并为一个

回收情况3:回收区的前、后都没有相邻的空闲分区

新增一个表项
注:各表项的顺序不一定按照地址递增顺序排列,具体的排列方式需要依据动态分区分配算法来确定。

动态分区分配没有内部碎片,但是有外部碎片

内部碎片,分配给某进程的内存区域中,如果有些部分没有用上。

外部碎片,是指内存中的某些空闲分区由于太小而难以利用。

如果内存中空闲空间的总和本来可以满足某进程的要求, 但由于进程需要的是一整块连续的内存空间,因此这些“碎片”不能满足进程的需求。

可以通过紧凑(拼凑,Compaction) 技术来解决外部碎片。

思考动态分区分配应使用哪种装方式?“紧凑”之后需要做什么处理?
【动态运行时装入;需要修改重定位寄存器的起始地址】


3.1.5 动态分区分配算法

动态分区分配算法:在动态分区分配方式中, 当很多个空闲分区都能满足需求时,应该选择哪个分区进行分配?

  • 首次适应算法(First Fit)
  • 最佳适应算法(Best Fit)
  • 最坏适应算法(Worst Fit)
  • 临近适应算法(Next Fit)
  • 首次适应算法(First Fit)

算法思想:每次都从低地址开始查找,找到第一个能满足大小的空闲分区。

如何实现:空闲分区以 地址 递增 的次序排列。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。


  • 最佳适应算法(Best Fit)

算法思想:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域。因此为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区, 即,优先使用更小的空闲区。

如何实现:空闲分区容量 递增 次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。

缺点:每次都选最小的分区进行分配,会留下越来越多的、很小的、难以利用的内存块。因此这种方法会产生很多的外部碎片。


  • 最坏适应算法(Worst Fit)

又称 最大适应算法(Largest Fit)

算法思想:为了解决最佳适应算法的问题——即留下太多难以利用的小碎片,可以在每次分配时 优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用。

如何实现:空闲分区容量 递减 次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。

缺点:每次都选最大的分区进行分配,虽然可以让分配后留下的 空闲区更大,更可用,但是这种方式会导致较大的连续空闲区被 迅速用完。如果之后有“大进程”到达,就没有内存分区可用了。


  • 邻近适应算法(Next Fit)

算法思想:首次适应算法每次都从链头开始查找的。这可能会导致低地址部分出现很多小的空闲分区,而每次分配查找时,都要经过这些分区,因此也增加了查找的开销。如果每次都从上次查找结束的位置开始检索,就能解决上述问题。

如何实现:空闲分区地址 递增 的顺序排列(可排成一个循环链表)。每次分配内存时从上次查找结束的位置开始查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。

首次适应算法每次都要从头查找,每次都需要检索低地址的小分区。 但是这种规则也决定了当低地址部分有更小的分区可以满足需求时, 会更有可能用到低地址部分的小分区,也会更有可能把高地址部分的大分区保留下来(最佳适应算法的优点)

邻近适应算法的规则可能会导致无论低地址、高地址部分的空闲分区 都有相同的概率被使用,也就导致了高地址部分的大分区更可能被使用,划分为小分区,最后导致无大分区可用(最大适应算法的缺点)

综合来看,四种算法中,首次适应算法的效果反而更好


  • 知识点与考点
算法 算法思想 分区排列顺序 优点 缺点
首次适应 从头到尾找适合的分区 空闲分区以地址递增次序排列 综合看性能最好。算法开销小,回收分区后一般不需要对空闲分区队列重新排序
最佳适应 优先使用更小的分区,以保留更多大分区 空闲分区以容量 递增次序排列 会有更多的大分区被保留下来,更能满足大进程需求 会产生很多太小的、难以利用的碎片;算法开销大, 回收分区后可能需要对空闲分区队列重新排序
最坏适应 优先使用更大的分区,以防止产生太小的不可用的碎片 空闲分区以容量 递减次序排列 可以减少难以利用的小碎片 大分区容易被用完,不利于大进程;算法开销大 (原因同上)
邻近适应 由首次适应演变而来,每次从上次查找结束位置开始查找 空闲分区以地址 递增次序排列 (可排列成循环 链表) 不用每次都从低地址的小分区开始检索。 算法开销小(原因同首次适应算法) 会使高地址的大分区也被用完

3.1.6 基本分页存储管理的基本概念

连续分配:为用户进程分配的必须是一个连续的内存空间

非连续分配:为用户进程分配的可以是一些分散的内存空间

  • 什么是分页存储

将内存空间分为一个个大小相等的分区(比如:每个分区4KB),每个分区就是一个“页框”(页框=页帧=内存块=物理块=物理页面)。每个页框有一个编号,即“页框号”(页框号=页帧号=内存块号=物理块号=物理页号),页框号从0开始

将进程的逻辑地址空间也分为与页框大小相等的一个个部分, 每个部分称为一个“”或“页面” 。每个页面也有一个编号, 即“页号”,页号也是从0开始

Tips:初学易混——页、页面 vs 页框、页帧、物理页

操作系统以页框为单位为各个进程分配内存空间。进程的每个页面分别放入一个页框中。也就是说,进程的页面与内存的页框一一对应的关系。

各个页面不必连续存放,可以放到不相邻的各个页框中。

(注:进程的最后一个页面可能没有一个页框那么大。也就是说,分页存储有可能产生内部碎片,因此页框不能太大,否则可能产生过大的内部碎片造成浪费)


  • 重要的数据结构——页表

为了能知道进程的每个页面在内存中存放的位置,操作系统要为每个进程建立一张页表。 注:页表通常存在PCB(进程控制块)中
【答题推荐使用内存块号页框号】

  1. 一个进程对应一张页表
  2. 进程的每个页面对应一个页表项
  3. 每个页表项由“页号”和“块号”组成
  4. 页表记录进程页面和实际存放的内存块之间的映射关系
  5. 每个页表项的长度是相同的

  • 问题一:每个页表项占多少字节?

每个页表项多大?占几个字节?
如何通过页表实现逻辑地址到物理地址的转换?

Eg:假设某系统物理内存大小为 4GB,页面大小为 4KB,则 每个页表项至少应该为多少字节?

内存块大小=页面大小=4KB= 2122^{12}B
→4GB 的内存总共会被分为 232/212=2202^{32} / 2^{12} = 2^{20}个内存块
→内存块号的范围应该是 0 ~2202^{20} -1
→内存块号至少要用 20 bit 来表示
→至少要用3B来表示块号(3*8=24bit)【注意:页表记录的只是内存块号,而不是内存块的起始地址! J 号内存块的起始地址 = J*内存块大小】
→由于页号隐含的,因此每个页表项占3B,存储整个页表至少需要 3*(n+1)B【0~n页】

页表项连续存放,因此页号可以是隐含的,不占存储空间(类比数组)
假设页表中的各页表项从内存地址为 X 的地方开始连续存放…
如何找到页号为 i 的页表项?
i 号页表项的存放地址 = X + 3*i 因此,页表中的页号可以是隐含的,即页号不占用存储空间


  • 问题二:如何实现地址的转换

进程在内存中连续存放时,操作系统是如何实现逻辑地址到物理地址的转换的?

将进程地址空间分页之后,操作系统该如何实现逻辑地址到物理地址的转换?

特点:虽然进程的各个页面是离散存放的,但是页面内部是连续存放的

如果要访问逻辑地址 A,则
① 确定逻辑地址A 对应的“页号” ?
②找到P号页面在内存中的起始地址(需要查页表) 【前面介绍了】
③确定逻辑地址A 的“页内偏移量” ?

逻辑地址A对应的物理地址 = P号页面在内存中的起始地址+页内偏移量W


  • 子问题:如何确定一个逻辑地址对应的页号、页内偏移量?

Eg:在某计算机系统中,页面大小是50B。某进程逻辑地址空间大小为200B,则逻辑地址 110 对应的页号、页内偏移量是多少?

如何计算:
页号 = 逻辑地址 / 页面长度 (取除法的整数部分)
页内偏移量 = 逻辑地址 % 页面长度(取除法的余数部分)

页号 = 110 / 50 = 2
页内偏移量 = 110 % 50 = 10

逻辑地址 可以拆分为(页号页内偏移量

通过页号查询页表,可知页面在内存中的起始地址
页面在内存中的起始地址+页内偏移量 = 实际的物理地址

在计算机内部,地址是用二进制表示的, 如果页面大小刚好是 2 的整数幂,则计算机硬件可以很快速的把逻辑地址拆分成(页号,页内偏移量)

假设某计算机用32 个二进制位表示逻辑地址,页面大小为 4KB = 212B = 4096B

0号页的逻辑地址范围应该是 0~4095,用二进制表示应该是:
00000000000000000000000000000000 ~
00000000000000000000111111111111

1号页的逻辑地址范围应该是 4096~8191,用二进制表示应该是:
00000000000000000001000000000000 ~
00000000000000000001111111111111

2号页的逻辑地址范围应该是 8192~12287,用二进制表示应该是:
00000000000000000010000000000000 ~
00000000000000000010111111111111

Eg:逻辑地址 2,用二进制表示应该是 00000000000000000000000000000010
页号 = 2/4096 = 0 = 00000000000000000000,
页内偏移量 = 2%4096 = 2 = 000000000010

Eg:逻辑地址 4097,用二进制表示应该是 00000000000000000001000000000001
页号 = 4097/4096 = 1 = 00000000000000000001,
页内偏移量 = 4097%4096 = 1 = 000000000001

结论:如果每个页面大小为 2K2^KB,用二进制数表示逻辑地址, 则末尾 K 位即为页内偏移量其余部分就是页号,计算机硬件就 可以很方便地得出一个逻辑地址对应的页号和页内偏移量,而无需进行除法运算,从而提升了运行速度。

假设物理地址也用32个二进制位表示,则由于内存块的大小=页面大小,因此:
0号内存块的起始物理地址是 00000000000000000000000000000000
1号内存块的起始物理地址是 00000000000000000001000000000000
2号内存块的起始物理地址是 00000000000000000010000000000000
3号内存块的起始物理地址是 00000000000000000011000000000000

根据页号可以查询页表,而页表中记录的只是内存块号,而不是内存块的起始地址!
J 号内存块的起始地址 = J * 内存块大小

假设通过查询页表得知1号页面存放的内存块号是9(1001),则 9号内存块的起始地址 = 9*4096(4KB) = 00000000000000001001000000000000
则逻辑地址4097对应的物理地址 = 页面在内存中存放的起始地址 + 页内偏移量 =(00000000000000001001000000000001

结论:如果页面大小刚好是2的整数幂,则只需把页表中记录的物理块号拼接上页内偏移量就能得到对应的物理地址


  • 逻辑地址结构

分页存储管理的逻辑地址结构如下所示:

地址结构包含两个部分:前一部分为页号,后一部分为页内偏移量 W。在上图所示的例子中,地址长度为 32 位,其中 0~11位 为“页内偏移量”,或称“页内地址”;12~31 位为“页号”。

如果有 K 位表示“页内偏移量”,则说明该系统中一个页面的大小是 2K2^K个内存单元
如果有 M 位表示“页号”,则说明在该系统中,一个进程最多允许有 2M2^M 个页面

重要重要重要!!!】 页面大小↔页内偏移量位数 → 逻辑地址结构

Tips:有些奇葩题目中页面大小有可能不是2的整数次幂,这种情况还是得用最原始的方法计算:
页号 = 逻辑地址 / 页面长度 (取除法的整数部分)
页内偏移量 = 逻辑地址 % 页面长度(取除法的余数部分)


3.1.7 基本地址变换机构

结合上一小节理解基本地址变换机构(用于实现逻辑地址到物理地址转换的一组硬件机构)的原理和流程

  • 基本地址变换机构

基本地址变换机构可以借助进程的页表将逻辑地址转换为物理地址。

通常会在系统中设置一个页表寄存器(PTR) ,存放页表在内存中的起始地址F和页表长度M。 进程未执行时,页表的始址 和 页表长度 放在进程控制块(PCB) 中,当进程被调度时,操作系统内核会把它们放到页表寄存器中。

注意:页面大小是2的整数幂
设页面大小为L,逻辑地址A到物理地址E的变换过程如下:

①计算页号 P 和页内偏移量W( 如果用十进制数手算,则 P=A/L,W=A%L;但是在计算机实际运行时,逻辑地址结构是固定不变的,因此计算机硬件可以更快地得到二进制表示的页号、页内偏移量)

②比较页号P 和页表长度M,若 P≥M,则产生越界中断,否则继续执行。(注意:页号是从0开 始的,而页表长度至少是1,因此 P=M 时也会越界

③页表中页号P对应的页表项地址 = 页表起始地址F + 页号P * 页表项长度,取出该页表项内容b, 即为内存块号。(注意区分页表项长度页表长度页面大小的区别。页表长度指的是这个页表中总共有几个页表项,即总共有几个页;页表项长度指的是每个页表项占多大的存储空间; 页面大小指的是一个页面占多大的存储空间)

④计算 E = b * L + W,用得到的物理地址E 去访存。(如果内存块号、页面偏移量是用二进制表 示的,那么把二者拼接起来就是最终的物理地址了)

⑤访问目标内存单元

:若页面大小L 为 1K 字节,页号2对应的内存块号 b = 8,将逻辑地址 A=2500 转换为物理地址E。

等价描述:某系统按字节寻址,逻辑地址结构中,页内偏移量占10(210B=1KB2^{10}B=1KB)位,页号2对应的内存块号 b = 8, 将逻辑地址 A=2500 转换为物理地址E。

①计算页号、页内偏移量
页号P = A/L = 2500/1024 = 2;
页内偏移量 W = A%L = 2500%1024 = 452

②根据题中条件可知,页号2没有越界,其存放的内存块号 b = 8

③物理地址 E = b * L + W = 8 * 1024 + 425 = 8644

在分页存储管理(页式管理)的系统中,只要确定了每个页面的大小,逻辑地址结构就确定了。因此,页式管理中地址是一维的即,只要给出一个逻辑地址,系统就可以自动地算出页号、页内偏移量 两个部分,并不需要显式地告诉系统这个逻辑地址中,页内偏移量占多少位。

一共两次访问内存的操作

  1. 查询页表
  2. 访问目标内存单元

  • 对页表项大小的进一步探讨

每个页表项的长度是相同的,页号是“隐含”的

Eg:假设某系统物理内存大小为 4GB,页面大小为 4KB的内存总共会被分为 232/212=2202^{32} / 2^{12} = 2^{20}个内存块,因此内存块号的范围应该是 0 ~2202^{20} -1,因此至少要 20 个二进制位才能表示这么多的内存块号,因此至少要 3个字节才够 (每个字节 8 个二进制位,3个字节共 24 个二进制位)

各页表项会按顺序连续地存放在内存中,如果该页表在内存中存放的起始地址为 X ,则 M 号页对应的页表项是存放在内存地址为 X + 3*M

一个页面为 4KB,则每个页框可以存放 4096/3 = 1365 个 页表项,但是这个页框会剩余 4096 % 3 = 1 B 页内碎片 因此,1365 号页表项存放的地址为 X + 3*1365 + 1

如果每个页表项占 4字节,则每个页框刚好可存放 1024 个页表项
1024 号页表项虽然是存放在下一个页框中的,但是它 的地址依然可以用 X + 4*1024 得出

【进程页表通常是装在连续的内存块中的】

结论:理论上,页表项长度为 3B 即可表示内存块号的范围,但是,为了方便页表的查询, 常常会让一个页表项占更多的字节,使得每个页面恰好可以装得下整数个页表项。


3.1.8 具有快表的地址变换机构

快表:是基本地址变换机构的改进版本

  • 什么是快表(TLB)

快表,又称联想寄存器TLB,translation lookaside buffer),是一种访问速度比内存快很多的高速缓存TLB不是内存!),用来存放最近访问的页表项的副本,可以加速地址变换的速度。 与此对应,内存中的页表常称为慢表

TLB 和 普通 Cache 的区别——TLB 中只有页表项的副本,而普通 Cache 中可能会有其他各种数据的副本


  • 思考:能否把整个页表都放在TLB中?

贵!


  • 引入快表后,地址的变换过程

① CPU给出逻辑地址,由某个硬件算得页号、页内偏移量,将页号与快表中的所有页号进行比较。

② 如果找到匹配的页号,说明要访问的页表项在快表中有副本,则直接从中取出该页对应的内存号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此, 若快表命中,则访问某个逻辑地址仅需一次访存【快表不是内存】即可。

③ 如果没有找到匹配的页号,则需要访问内存中的页表,找到对应页表项,得到页面存放的内存块 号,再将内存块号与页内偏移量拼接形成物理地址,最后,访问该物理地址对应的内存单元。因此, 若快表未命中,则访问某个逻辑地址需要两次访存注意:在找到页表项后,应同时将其存入快表, 以便后面可能的再次访问。但若快表已满,则必须按照一定的算法对旧的页表项进行替换)

由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。 因为局部性原理,一般来说快表的命中率可以达到 90% 以上。

:某系统使用基本分页存储管理,并采用了具有快表的地址变换机构。访问一次快表耗时 1us,访问一次内存耗时 100us。若快表的命中率为 90%,那么访问一个逻辑地址的平均耗时是多少?
(1+100) * 0.9 + (1+100+100) * 0.1 = 111 us
有的系统支持快表和慢表同时查找,如果是这样,平均耗时应该是 (1+100) * 0.9 + (100+100) * 0.1 = 110.9 us
若未采用快表机制,则访问一个逻辑地址需要 100+100 = 200us
显然,引入快表机制后,访问一个逻辑地址的速度快多了。

【快表和慢表同时查找】:


  • 局部性原理

时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再 次被访问。(因为程序中存在大量的循环)

空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的)

上小节介绍的基本地址变换机构中,每次要访问一个逻辑地址,都需要查询内存中的页表。由于局部性原理,可能连续很多次查到的都是同一个页表项


  • 知识点与考点
地址变换过程 访问一个逻辑地 址的访存次数
基本地址变换机构 ①:算页号、页内偏移量
②:检查页号合法性
③:查页表,找到页面存放的内存块号
④:根据内存块号与页内偏移量得到物理地址
⑤:访问目标内存单元
两次访存
具有快表的地址变换机构 ①:算页号、页内偏移量
②:检查页号合法性
③:查快表。若命中,即可知道页面存放的内存块号,可直接进行⑤; 若未命中则进行④
④:查页表,找到页面存放的内存块号,并且将页表项复制到快表中
⑤:根据内存块号与页内偏移量得到物理地址
⑥:访问目标内存单元
快表命中,只需 一次访存 快表
未命中,需要两次访存

TLB 和 普通 Cache 的区别——TLB 中只有页表项的副本,而普通 Cache 中可能会有其他各种数据的副本


3.1.9 两级页表

  • 单级页表存在的问题

某计算机系统按字节寻址,支持 32 位的逻辑地址,采用分页存储管理,页面大小为4KB,页表项长度为 4B。

4KB = 2122^{12}B,因此页内地址要用12位表示,剩余 20 位表示页号。
因此,该系统中用户进程最多有 2202^{20} 页。相应的,一个进程的页表中,最多会有 2202^{20} = 1M = 1,048,576 个页表项,所以一个页表最大需要 2202^{20} * 4B = 2222^{22} B,共需要 2222^{22}/2122^{12} = 2102^{10}个页框存储该页表。
根据页号查询页表的方法:K 号页对应的页表项存放位置 = 页表始址 + K * 4 要在所有的页表项都连续存放的基础上才能用这种方法找到页表项

第一个问题:需要专门给进程分配 2102^{10} = 1024 个连续的页框来存放它的页表【与自身离散存储的优点相违背】

第二个问题:根据局部性原理可知,很多时候,进程在一段时间内只需要访问某几个页面就可以正常运行了。因此没有必要让整个页表都常驻内存


  • 如何解决单级页表的问题?

问题一:页表必须连续存放,因此当页表很大时,需要占用很多个连续的页框。

思考:我们是如何解决进程在内存中必须连续存储的问题的?

将进程地址空间分页,并为其建立一张页表,记录各页面的存放位置
同样的思路也可用于解决“页表必须连续存放”的问题,把必须连续存放的页表再分页

可将长长的页表进行分组,使每个内存块刚好可以放入一个分组 (比如上个例子中,页面大小4KB,
每个页表项 4B,每个页面可存放 1K 个页表项,因此每1K个连续的页表项为一组,每组刚好占一个内存块,再将各组离散地放到各个内存块中)

另外,要为离散分配的页表再建立一张页表,称为页目录表(408常用),或称外层页表,或称顶层页表

看本小节下面内容 《两级页表的原理、地址结构

问题二:没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定的页面。

可以在需要访问页面时才把页面调入内存(虚拟存储技术)。可以在页表项中增加一个标志位,用于表示该页面是否已经调入内存

【实现】可以在页表增加标志位,如下图:

若想访问的页面不在内存中,则产生缺页中断(内中断/异常), 然后将目标页面从外存调入内存


  • 两级页表的原理、地址结构

32位逻辑地址空间,页表项大小为4B,页面大小为 4KB,则页内地址占12位

单级页表结构的逻辑地址结构:

进程最多有 2202^{20} 个页面, 用 20 位二进制刚好可以 表示 0~ 2202^{20} -1 个页号。 每个页面可存放 4KB/4B = 1K = 2102^{10} = 1024 个页表项

套娃,两级页表结构如下:

:将逻辑地址 (0000000000,0000000001,111111111111) 转换为物理地址(用十进制表示)。

如上图:

① 按照地址结构将逻辑地址拆分成三部分

② 从PCB 中读出页目录表始址,再根据一级页号查页目录表,找到下一级页表在内存中的存放位置

③ 根据二级页号查二级页表,找到最终想访问的内存块号

④ 结合页内偏移量得到物理地址

最终要访问的内存块号为4 该内存块的起始地址为 4*4096 = 16384 页内偏移量为 4095
最终的物理地址为 16384 + 4095= 20479


  • 需要注意的几个细节
  1. 若分为两级页表后,页表依然很长,则可以采用更多级页表,各级页表的大小不能超过一个页面

例:某系统按字节编址,采用 40 位逻辑地址,页面大小为 4KB,页表项大小为 4B,假设采用纯页式存储,则要采用()级页表,页内偏移量为()位?

页面大小 = 4KB =2122^{12}B,按字节编址,因此页内偏移量为12位
页号 = 40 - 12 = 28 位
问题:这里取顶,应该是取到整个字节,还是更要取到2的整数倍,比如23位的话,是取到3B呢,还是4B呢?看看考研什么要求吧】
页面大小 = 2122^{12}B,页表项大小 = 4B ,则每个页面可存放 2122^{12} / 4 = 2102^{10} 个页表项
因此各级页表最多包含 2102^{10} 个页表项,需要 10 位二进制位才能映射到 210 个页表项,因此每一级的页表对应页号应为10位。总共28位的页号至少要分为三级

如果只分为两级页表,则一级页号占 18 位, 也就是说页目录表中最多可能有 2182^{18}个页表项, 显然,一个页面是放不下这么多页表项的。

  1. 两级页表的访存次数分析(假设没有快表机构)
    第一次访存:访问内存中的页目录表
    第二次访存:访问内存中的二级页表
    第三次访存:访问目标内存单元
    规律:N级页表访问一个逻辑地址需要N+1次访存

3.1.10 基本分段存储管理方式

与“分页”最大的区别就是——离散分配时所分配地址空间的基本单位不同

进程的地址空间:按照程序自身的逻辑关系划分为若干个段,每个段都有一个段名(在低级语言 中,程序员使用段名来编程),每段从0开始编址

内存分配规则:以段为单位进行分配,每个段在内存中占据连续空间,但各段之间可以不相邻

由于是按逻辑功能模块划分,用户编程更方便,程序的可读性更高

1
2
LOAD 1, [D] | <A>; // 将分段D中A单元内的值读入寄存器1 
STORE 1, [X] | <B>; // 将寄存器1的内容存入X 分段的B单元中

段号的位数决定了每个进程最多可以分几个段
段内地址位数决定了每个段的 最大长度 是多少

在上述例子中,若系统是按字节寻址的,则
段号占16位,因此在该系统中,每个进程最多有 2162^{16} = 64K 个段
段内地址占 16位,因此每个段的最大长度是 2162^{16} = 64KB。

写程序时使用的 段名 [D]、[X] 会被编译程序翻译成对应段号
<A>单元、<B>单元会被编译程序翻译成段内地址


  • 段表

问题:程序分多个段,各段离散地装入内存,为了保证程序能正常运行,就必须能从物理内存中找到各个逻辑段的存放位置。为此,需为每个进程建立一张段映射表,简称“段表”。

  1. 每个段对应一个段表项,其中记录了该段在内存中的起始位置(又称 “基址”)和段的长度。
  2. 各个段表项的长度是相同的。例如:某系统按字节寻址,采用分段存储管理,逻辑地址结构为(段号16位, 段内地址16位),因此用16位 即可表示最大段长。物理内存大小为4GB(可用32位表示整个物理内存地址空间)。因此,可以让每个段表项占 16(段长)+32(基址长度) = 48位,即6B。由于段表项长度相同,因此段号可以是隐含的,不占存储空间。若段表存放的起始地址为 M,则 K号段对应的段表项存放的地址为 M + K*6

  • 地址变换
1
LOAD 1, [D] | <A>; // 将分段D中A单元内的值读入寄存器1 

经过编译程序编译后,形成等价的机器指令: “取出段号为2,段内地址为 1024 的内存单元中的内容,放到寄存 器1中”

CPU执行指令时需要将逻辑地址变换为物理地址:
机器指令中的逻辑地址用二进制表示: 00000000000000100000000100000000


  • 分段、分页管理的对比

信息的物理单位。分页的主要目的是为了实现离散分配,提高内存利用率。分页仅仅是系统管理上的需要,完全是系统行为,对用户是不可见的

信息的逻辑单位。分段的主要目的是更好地满足用户需求。一个段通常包含着一组属于一个逻辑模块的信息。分段对用户是可见的,用户编程时需要显式地给出段名。

页的大小固定且由系统决定。段的长度却不固定,决定于用户编写的程序。

分页的用户进程地址空间是一维的,程序员只需给出一个记忆符即可表示一个地址。

分段的用户进程地址空间是二维的,程序员在标识一个地址时,既要给出段名【我的理解是:相当于汇编语言,你自己设置了数据段,分页只知道地址就行了,分段就自己分的段 + 偏移地址】,也要给出段内地址。

分段比分页更容易实现信息的共享和保护
不能被修改的代码称为纯代码可重入代码(不属于临界资源),这样的代码是可以共享的【比如,有一个代码段只是简单的输出 “Hello World!”】。可修改的代码是不能共享的(比如,有一个代码段中有很多变量,各进程并发地同时访问可能造成数据不一致)
【分析:】

分段可以如下实现:设置标志位

访问一个逻辑地址需要几次访存?
分页(单级页表):第一次访存——查内存中的页表,第二次访存——访问目标内存单元。总共两次访存
分段:第一次访存——查内存中的段表,第二次访存——访问目标内存单元。总共两次访存
与分页系统类似,分段系统中也可以引入快表机构,将近期访问过的段表项放到快表中,这样可以 少一次访问,加快地址变换速度。


3.1.11 段页式管理方式

  • 分页、分段的优缺点分析
优点 缺点
分页管理 内存空间利用率高,不会产生外部碎片,只会有少量的页内碎片 不方便按照逻辑模块实现信息的共享和保护
分段管理 很方便按照逻辑模块实现信息的共享和保护 如果段长过大,为其分配很大的连续空间会很不方便。另外,段式管理会产生外部碎片

分段管理中产生的外部碎片也 可以用“紧凑”【上面介绍了,Ctrl + F吧!】来解决,只是需要付出较大的时间代价


  • 分段+分页=段页式管理

将进程按逻辑模块分段,再将各段分页(如每个页面4KB)
再将内存空间分为大小相同的内存块/页框/页帧/物理块
进程前将各页面分别装入各内存块中


  • 段页式管理的逻辑地址结构

分段系统的逻辑地址结构由段号和段内地址(段内偏移量)组成。如:

段页式系统的逻辑地址结构由段号、页号、页内地址(页内偏移量)组成。如:

段号的位数决定了每个进程最多可以分几个段
页号位数决定了每个段最大有多少页
页内偏移量决定了页面大小、内存块大小是多少

在上述例子中,若系统是按字节寻址的,则
段号占16位,因此在该系统中,每个进程最多有 2162^{16} = 64K 个段
页号占4位,因此每个段最多有 242^{4} = 16页
页内偏移量占12位,因此每个页面\每个内存块大小为 2122^{12} = 4096 = 4KB

“分段”对用户是可见的,程 序员编程时需要显式地给出段号、段内地址。而将各段“分页”对用户是不可见的。系统 会根据段内地址自动划分页号和页内偏移量。 因此段页式管理的地址结构是二维的


  • 段表、页表

每个段对应一个段表项,每个段表项由段号、页表长度页表存放块号(页表起始 地址) 组成。每个段表项长度相等,段号是隐含的

每个页面对应一个页表项,每个页表项由页号、页面存放的内存块号组成。每个页表项长度相等,页号是隐含的。

访问一个逻辑地址所需访存次数:

第一次:查段表、第二次:查页表、第三次:访问目标单元

可引入快表机构,以段号页号为关键字查询快表,即可直接找到最终的目标页面存放位置。引入快表后仅需一次访存


3.2.1 虚拟内存的基本概念

在传统存储管理方式的基础上引入了交换技术、覆盖技术,使得内存利用率有所提 升,并且能从逻辑上扩充内存容量。

  • 传统存储管理方式的特征、缺点

很多暂时用不到的数据也会长期占用内存, 导致内存利用率不高

传统存储管理

  • 连续分配
    • 单一连续分配
    • 固定分区分配
    • 动态分区分配
  • 非连续分配
    • 基本分页存储管理
    • 基本分段存储管理
    • 基本段页式存储管理

一次性作业必须一次性全部装入内存后才能开始运行。这会造成两个问题:
① 作业很大时,不能全部装入内存,导致大作业无法运行
② 当大量作业要求运行时,由于内存无法容纳所有作业,因此只有少量作业能运行,导致多道程序并发度下降
【可用虚拟存储技术解决问题】

驻留性:一旦作业被装入内存,就会一直驻留在内存中,直至作业运行结束。事实上,在一个时间段内,只需要访问作业的一小部分数据即可正常运行,这就导致了内存中会驻留大量的、暂时用不到的数据,浪费了宝贵的内存资源。


  • 局部性原理

时间局部性:如果执行了程序中的某条指令,那么不久后这条指令很有可能再次执行;如果某个数据被访问过,不久之后该数据很可能再次被访问。(因为程序中存在大量的循环)

空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也很有可能被访问。(因为很多数据在内存中都是连续存放的,并且程序的指令也是顺序地在内存中存放的)


  • 虚拟内存的定义和特征

高速缓存技术:使用频繁的数据放到更高速的存储器中

基于局部性原理,在程序装入时,可以将程序中很快会用到的部分装入内存,暂时用不到的部分留在外存, 就可以让程序开始执行。

在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。

若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。 在操作系统的管理下,在用户看来似乎有一个比实际内存大得多的内存,这就是虚拟内存

【操作系统虚拟性的一个体现,实 际的物理内存大小没有变,只是在逻辑上进行了扩充。】

易混知识点
虚拟内存的最大容量是由计算机的地址结构(CPU寻址范围)确定的
虚拟内存的实际容量 = min(内存和外存容量之和,CPU寻址范围)
如:某计算机地址结构为32位,按字节编址,内存大小为512MB,外存大小为2GB。 则虚拟内存的最大容量2322^{32} B = 4GB
虚拟内存的实际容量 = min(4GB, 512MB+2GB) = 2GB+512MB

虚拟内存有一下三个主要特征:

  1. 多次性:无需在作业运行时一次性全部装入内存,而是允许被分成多次调入内存。
  2. 对换性:在作业运行时无需一直常驻内存,而是允许在作业运行过程中,将作业换入、换出。
  3. 虚拟性:从逻辑上扩充了内存的容量,使用户看到的内存容量,远大于实际的容量。

  • 如何实现虚拟内存技术

虚拟内存技术,允许一个作业分多次调入内存。如果采用连续分配方式,会不方便实现。因此, 虚拟内存的实现需要建立在离散分配的内存管理方式基础上。

传统非连续分配存储管理

  • 基本分页存储管理
  • 基本分段存储管理
  • 基本段页式存储管理

虚拟内存的实现

  • 请求分页存储管理
  • 请求分段存储管理
  • 请求段页式存储管理

主要区别: 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。 【操作系统要提供请求调页(或请求调段)功能】

若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。【操作系统要提供页面置换(或段置换)的功能】


3.2.2 请求分页管理方式

  • 页表机制

与基本分页管理相比,请求分页管理中,为了实现“请求调页”,操作系统需要知道每个页面是否已经调入内存;如果还没调入,那么也需要知道该页面在外存中存放的位置。

当内存空间不够时,要实现“页面置换”,操作系统需要通过某些指标来决定到底换出哪个页面;有的页面没有被修改过,就不用再浪费时间写回外存。有的页面修改过,就需要将外存中的旧数据覆盖,因此,操作系统也需要记录各个页面是否被修改的信息。

请求分页存储管理的页表

请求页表项增加了四个字段:

  1. 状态位:是否已调入内存
  2. 访问字段:可记录最近被访问过几次,或记录上次访问的时间,供置换算法选择换出页面时参考
  3. 修改位:页面调入内存后是否被修改过
  4. 外存地址:页面在外存中的存放位 置

  • 缺页中断机构

假设此时要访问逻辑地址 =(页号,页内偏移量)=(0, 1024)

  • 在请求分页系统中,每当要访问的页面不在内存时,便产生一个缺页中断,然后由操作系统的缺页中断处理程序处理中断

  • 此时缺页的进程阻塞,放入阻塞队列,调页完成后再将其唤醒,放回就绪队列。

  • 如果内存中有空闲块,则为进程分配一个空闲块,将所缺页面装入该块,并修改页表中相应的页表项。

  • 如果内存中没有空闲块,则由页面置换算法选择一个页面淘汰,若该页面在内存期间被修改过,则要将其写回外存。未修改过的页面不用写回外存。

缺页中断是因为当前执行的指令想要访问的目标页面未调入内存而产生的,因此属于内中断

一条指令在执行期间,可能产生多次缺页中断。(如:copy A to B,即将逻辑地址A中的数据复制到逻辑地址B,而A、B属于不同的页面,则有可能产生两次中断)


  • 地址变换机构

请求分页存储管理基本分页存储管理的主要区别:

  • 在程序执行过程中,当所访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,然后继续执行程序。 【操作系统要提供请求调页(或请求调段)功能】
  • 若内存空间不够,由操作系统负责将内存中暂时用不到的信息换出到外存。【操作系统要提供页面置换(或段置换)的功能】

新增步骤1:请求调页(查到页表项时进行判断)

新增步骤2:页面置换(需要调入页面,但没有空闲内存块时进行)

新增步骤3:需要修改请求页表中新增的表项

快表中有的页面一定是在内存中的。若某个页面被换出外存, 则快表中的相应表项也要删除, 否则可能访问错误的页面。

书上:

补充细节:
① 只有“写指令”才需要修改“修改位”。并且,一般来说只需修改快表中的数据,只有要将快表项删除时才需要写回内存中的慢表。这样可以减少访存次数。

② 和普通的中断处理一样,缺页中断处理依然需要保留CPU现场。

③ 需要用某种“页面置换算法” 来决定一个换出页面(下节内容)

④ 换入/换出页面都需要启动慢速的I/O操作,可见,如果换入/ 换出太频繁,会有很大的开销。

⑤ 页面调入内存后,需要修改慢表,同时也需要将表项复制到快 表中。【在具有快表机构的请求分页系统中,访问一个逻辑地址 时,若发生缺页,则地址变换步骤是: 查快表(未命中)——查慢表(发现未调入内存)——调页(调入的页面对应的表项会直接加入快表,当然慢表也有)——查快表(命中)——访问目标内存单元】


3.2.3 页面置换算法

页面的换入、换出需要磁盘 I/O,会有较大的开销,因此好的页面置换算法应该追求更少的缺页率

  • 最佳置换算法(OPT)

最佳置换算法(OPT,Optimal):每次选择淘汰的页面将是以后永不使用,或者在最长时间内不再被访问的页面,这样可以保证最低的缺页率。

例:假设系统为某进程分配了三个内存块,并考虑到有一下页面号引用串(会依次访问这些页面):
7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1

整个过程缺页中断发生了9次,页面置换发生了6次。
注意:缺页时未必发生页面置换。若还有可用的空闲内存块, 就不用进行页面置换。
缺页率 = 9/20 = 45%

最佳置换算法可以保证最低的缺页率,但实际上,只有在进程执行的过程中才能知道接下来会访问到的是哪个页面。操作系统无法提前预判页面访问序列。因此,最佳置换算法是无法实现的


  • 先进先出置换算法(FIFO)

先进先出置换算法(FIFO):每次选择淘汰的页面最早进入内存的页面

实现方法:把调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时选择队头页面即可。
队列的最大长度取决于系统为进程分配了多少个内存块。

例:假设系统为某进程分配了三个内存块,并考虑到有以下页面号引用串:
3, 2, 1, 0, 3, 2, 4, 3, 2, 1, 0, 4

分配三个内存块时,缺页次 数:9次

分配四个内存块时, 缺页次数:10次

Belady 异常——当为进程分配的物理块数增大时,缺页次数不减反增的异常现象。

只有 FIFO 算法会产生 Belady 异常。另外,FIFO算法虽然实现简单,但是该算法与进程实际运行时的规律不适应,因为先进入的页面也有可能最经常被访问。因此,算法性能差


  • 最近最久未使用置换算法(LRU)

最近最久未使用置换算法(LRU,least recently used):每次淘汰的页面最近最久未使用的页面

实现方法:赋予每个页面对应的页表项中,用访问字段记录该页面自上次被访问以来所经历的时间t。 当需要淘汰一个页面时,选择现有页面中 t 值最大的,即最近最久未使用的页面。

例:假设系统为某进程分配了四个内存块,并考虑到有以下页面号引用串:
1, 8, 1, 7, 8, 2, 7, 2, 1, 8, 3, 8, 2, 1, 3, 1, 7, 1, 3, 7

在手动做题时,若需要淘汰页面,可以逆向检查此时在内存中的几个页面号。在逆向扫描过程中最后一个出现的页号就是要淘汰的页面

该算法的实现需要专门的硬件支持,虽然算法性能好, 但是实现困难,开销大


  • 时钟置换算法(CLOCK)

最佳置换算法性能最好,但无法实现;先进先出置换算法实现简单,但算法性能差;
最近最久未使用 置换算法性能好,是最接近OPT算法性能的,但是实现起来需要专门的硬件支持,算法开销大。

时钟置换算法是一种性能和开销较均衡的算法,又称CLOCK算法,或最近未用算法NRU,Not Recently Used)

  • 简单的CLOCK 算法实现方法

为每个页面设置一个访问位,再将内存中的页面都通过链接指针链接成一个循环队列。当某页被访问时,其访问位置为1。当需要淘汰一个页面时,只需检查页的访问位。 如果是0,就选择该页换出;如果是1,则将它置为0,暂不换出,继续检查下一个页面,若第一轮扫描中所有页面都是1,则将这些页面的访问位依次置为0后,再进行第二轮扫描(第二轮扫描中一定会有访问位为0的页面,因此简单的CLOCK 算法选择一个淘汰页面最多会经过两轮扫描

例:假设系统为某进程分配了五个内存块,并考虑到有以下页面号引用串:
1, 3, 4, 2, 5, 6, 3, 4, 7


  • 改进型的时钟置换算法

简单的时钟置换算法仅考虑到一个页面最近是否被访问过。事实上,如果被淘汰的页面没有被修改过, 就不需要执行I/O操作写回外存。只有被淘汰的页面被修改过时,才需要写回外存

因此,除了考虑一个页面最近有没有被访问过之外,操作系统还应考虑页面有没有被修改过。在其他条件都相同时,应优先淘汰没有修改过的页面,避免I/O操作。这就是改进型的时钟置换算法的思想。 修改位=0,表示页面没有被修改过;修改位=1,表示页面被修改过。

为方便讨论,用(访问位修改位)的形式表示各页面状态。如(1,1)表示一个页面近期被访问过, 且被修改过。

算法规则:将所有可能被置换的页面排成一个循环队列

  1. 第一轮:从当前位置开始扫描到第一个(0, 0)的帧用于替换。本轮扫描不修改任何标志位
    【第一优先级:最近没访问, 且没修改的页面】
  2. 第二轮:若第一轮扫描失败,则重新扫描,查找第一个(0, 1)的帧用于替换。本轮将所有扫描过的帧访问位设为0
    【第二优先级:最近没访问, 但修改过的页面】
  3. 第三轮:若第二轮扫描失败,则重新扫描,查找第一个(0, 0)的帧用于替换。本轮扫描不修改任何标志位
    【第三优先级:最近访问过, 但没修改的页面】
  4. 第四轮:若第三轮扫描失败,则重新扫描,查找第一个(0, 1)的帧用于替换。
    【第四优先级:最近访问过, 且修改过的页面】

由于第二轮已将所有帧的访问位设为0,因此经过第三轮、第四轮扫描一 定会有一个帧被选中,因此改进型CLOCK置换算法选择一个淘汰页面最多会进行四轮扫描

例子:【自己推】下图都是(起始状态,最终状态)

需要1轮扫描

需要2轮扫描

需要3轮扫描

需要4轮扫描


  • 知识点与考点
算法规则 优缺点
OPT 优先淘汰最长时间内不会被访问的页面 缺页率最小,性能最好;但无法实现
FIFO 优先淘汰最先进入内存的页面 实现简单;但性能很差,可能出现Belady异常
LRU 优先淘汰最近最久没访问的页面 性能很好;但需要硬件支持,算法开销大
CLOCK(NRU) 循环扫描各页面
第一轮淘汰访问位=0的,并将扫描过的页面访问位改为1。若第一轮没选中,则进行第二轮扫描。
实现简单,算法开销小;但未考虑页面是否被修改过
改进型CLOCK(改 进型NRU) 若用(访问位, 修改位)的形式表述,则
第一轮:淘汰(0, 0)
第二轮:淘汰(0, 1),并将扫描过的页面访问位都置为0
第三轮:淘汰(0, 0)
第四轮:淘汰(0, 1)
算法开销较小,性能也不错

3.2.4 页面分配策略

  • 页面分配、置换策略

驻留集:指请求分页存储管理中给进程分配的物理块的集合。
在采用了虚拟存储技术的系统中,驻留集大小一般小于进程的总大小。

若驻留集太小,会导致缺页频繁,系统要花大量的时间来处理缺页,实际用于进程推进的时间很少; 驻留集太大,又会导致多道程序并发度下降,资源利用率降低。所以应该选择一个合适的驻留集大小。
【考虑一个极端情况,若某进程共有100个页面,则该进程的驻留集大小为100时进程可以全部放入内存,运行期间不可能再发生缺页。若驻留集大小为1,则进程运行期间必定会极频繁地缺页】

固定分配:操作系统为每个进程分配一组固定数目的物理块,在进程运行期间不再改变。即,驻留集大小不变

可变分配:先为每个进程分配一定数目的物理块,在进程运行期间,可根据情况做适当的增加或减少。即,驻留集大小可变

局部置换:发生缺页时只能选进程自己的物理块进行置换。

全局置换:可以将操作系统保留的空闲物理块分配给缺页进程,也可以将别的进程持有的物理块置换到外存,再分配给缺页进程。

全局置换意味着一个进程拥有的物理块数量必然会改变,因此不可能是固定分配

局部置换 全局置换
固定分配 -
可变分配

固定分配局部置换:系统为每个进程分配一定数量的物理块,在整个运行期间都不改变。若进程在运行中发生缺页,则只能从该进程在内存中的页面中选出一页换出,然后再调入需要的页面。这种策略的缺点是:很难在刚开始就确定应为每个进程分配多少个物理块才算合理。(采用这种策略的系统可 以根据进程大小、优先级、或是根据程序员给出的参数来确定为一个进程分配的内存块数)

可变分配全局置换:刚开始会为每个进程分配一定数量的物理块。操作系统会保持一个空闲物理块队列。当某进程发生缺页时,从空闲物理块中取出一块分配给该进程;若已无空闲物理块,则可选择一个未锁定的页面换出外存,再将该物理块分配给缺页的进程。采用这种策略时,只要某进程发生缺页, 都将获得新的物理块,仅当空闲物理块用完时,系统才选择一个未锁定的页面调出。被选择调出的页可能是系统中任何一个进程中的页,因此这个被选中的进程拥有的物理块会减少,缺页率会增加
【系统会锁定一些页面,这些页面中的内容不能置换出外存(如:重要的内核数据可以设置为“锁定”)】

可变分配局部置换:刚开始会为每个进程分配一定数量的物理块。当某进程发生缺页时,只允许从该进程自己的物理块中选出一个进行换出外存。如果进程在运行中频繁地缺页,系统会为该进程多分配几个物理块,直至该进程缺页率趋势适当程度;反之,如果进程在运行中缺页率特别低,则可适当减少分配给该进程的物理块。

可变分配全局置换:只要缺页就给分配新物理块
可变分配局部置换:要根据发生缺页的频率来动态地增加或减少进程的物理块


  • 何时调入页面

预调页策略:根据局部性原理【主要指空间局部性】,一次调入若干个相邻的页面可能比一次调入一个页面更高效。但如果提前调入的页面中大多数都没被访问过,则又是低效的。因此可以预测不久之后可能访问到的页面,将它们预先调入内存,但目前预测成功率只有50%左右。故这种策略主要用于进程的首次调入【运行前调入】, 由程序员指出应该先调入哪些部分。

请求调页策略:进程在运行期间发现缺页时才将所缺页面调入内存。由这种策略调入的页面一定会被访问到,但由于每次只能调入一页,而每次调页都要磁盘I/O操作,因此I/O开销较大。


  • 从何处调入页面

1、系统拥有足够的对换区空间:页面的调入、调出都是在内存与对换区之间进行,这样可以保证页面的调入、调出速度很快。在进程运行前,需将进程相关的数据从文件区复制到对换区。

2、系统缺少足够的对换区空间:凡是不会被修改 的数据都直接从文件区调入,由于这些页面不 会被修改,因此换出时不必写回磁盘,下次需 要时再从文件区调入即可。对于可能被修改的 部分,换出时需写回磁盘对换区,下次需要时 再从对换区调入。

3、UNIX 方式:运行之前进程有关的数据全部放在文件区,故未使用过的页面,都可从文件区调入。若被使用过的页面需要换出,则写回对换区,下次需要时从对换区调入。


  • 抖动(颠簸)现象

刚刚换出的页面马上又要换入内存,刚刚换入的页面马上又要换出外存,这种频繁的页面调度行为称为抖动,或颠簸。产生抖动的主要原因是进程频繁访问的页面数目高于可用的物理块数(分配给进程的物理块不够

为进程分配的物理块太少,会使进 程发生抖动现象。为进程分配的物理块太多,又会降低系统整体的并发度,降低某些资源的利用率

为了研究为应该为每个进程分配多少个物理块,Denning 提出了进程“工作集”的概念


  • 工作集

驻留集:指请求分页存储管理中给进程分配的内存块的集合。
工作集:指在某段时间间隔里,进程实际访问页面的集合。

操作系统会根据“窗口尺寸”来算出工作集。例:
某进程的页面访问序列如下,窗口尺寸为 4,各时刻的工作集为?

工作集大小可能小于窗口尺寸,实际应用中,操作系统可以统计进程的工作集大小,根据工作集大小给进程分配若干内存块。如:窗口尺寸为5,经过一段时间的监测发现某进程的工作集最大为3,那么说明该进程有很好的局部性,可以给这个进程分配3个以上的内存块即可满足进程的运行需要。

一般来说,驻留集大小不能小于工作集大小,否则进程运行过程中将频繁缺页

拓展:基于局部性原理可知,进程在一段时间内访问的页面与不久之后会访问的页面是有相关性的。因此,可以根据进程近期访问的页面集合(工作集)来设计一种页面置换算法——选择一个不在工作集中的页面进行淘汰。


3.2.5 内存映射文件

  • 传统的文件访问方式

open 系统调用——打开文件
seek 系统调用――将读写指针移到某个位置
read 系统调用――从读写指针所指位置读入若干数据(从磁盘读入内存)
write 系统调用――将内存中的指定数据,写回磁盘(根据读写指针确定要写回什么位置)


  • 内存映射文件(Memory-Mapped Files)

open系统调用――打开文件
mmap系统调用一一将文件映射到进程的虚拟地址空间

  • 以访问内存的方式访问文件数据
  • 文件数据的读入、写出由操作系统自动完成
  • 进程关闭文件时,操作系统自动将文件被修改的数据写回磁盘

多个进程可以映射同一个文件,实现共享

在物理内存中,一个文件对应同一份数据,当一个进程修改文件数据时,另一个进程可以立马“看到”


  • 知识回顾

内存映射文件:

  • 特性
    • 进程可使用系统调用,请求操作系统将文件映射到进程的虚拟地址空间
    • 以访问内存的方式读写文件
    • 进程关闭文件时,操作系统负责将文件数据写回磁盘,并解除内存映射
    • 多个进程可以映射同一个文件,方便共享
  • 优点
    • 程序员编程更简单,已建立映射的文件,只需按访问内存的方式读写即可
    • 文件数据的读入/写出完全由操作系统负责,I/O效率可以由操作系统负责优化【eg:预读入、缓写出等】

第四章 文件管理

4.1.1 初识文件管理

  • 前情回顾

操作系统的功能和目标——作为系统资源的管理者

  • 提供的功能:
    • 处理机管理
    • 存储器管理
    • 文件管理
    • 设备管理
  • 目标:
    • 安全、高效

文件的定义:文件——就是一组有意义的信息/数据集合

计算机中存放了各种各样的文件,一个文件有哪些属性?

文件内部的数据应该怎样组织起来?

文件之间又应该又应该怎么组织起来?

从下往上看,OS应提供哪些功能,才能方便用户、应用程序使用文件?
从上往下看,文件数据应该怎么存放在外存(磁盘)上?


  • 文件的属性

  • 文件名:由创建文件的用户决定文件名,主要是为了方便用户找到文件,同一目录下不允许有重名文件

  • 标识符:一个系统内的各文件标识 符唯一,对用户来说毫无可读性, 因此标识符只是操作系统用于区分各个文件的一种内部名称。

  • 类型:指明文件的类型

  • 位置:文件存放的路径(让用户使用)、在外存中的地址(操作系统使用,对用户不可见)

  • 大小:指明文件大小

  • 创建时间上次修改时间文件所有者信息……

  • 保护信息:对文件进行保护的访问控制信息


  • 文件内部的数据应该怎样组织起来?

无结构文件(如文本文件)——由一些二进制或字符流组成,又称“流式文件

有结构文件(如数据库表)——由一组相似的记录组成,又称“记录式文件

记录是一组相关数据项的集合
数据项是文件系统中最基本的数据单位

总结:

有结构文件中,各个记录间应该如何组织的问题——应该顺序存放? 还是用索引表来表示记录间的顺序?——这是“文件的逻辑结构” 重点要探讨的问题


  • 文件之间应该怎样组织起来?

所谓的“目录” 其实就是我们熟悉的“文件夹

用户可以自己创建一层一 层的目录,各层目录中存放相应的文件。系统中的各个文件就通过一层一层的目录合理有序的组织起来了

目录其实也是一种特殊的有结构文件(由记录组成),如何实现文件目录是之后会重点探讨的问题


  • 操作系统应该向上提供哪些功能?

可以“创建文件”, (点击新建后,图形 化交互进程在背后调 用了“create 系统调用”)

可以“读文件”,将文件数据读入内存,才能让CPU处理(双击后,“记事本”应用程序通过操作系统 提供的“读文件”功能,即 read 系统调用,将文件数据从外存读入内存,并显示在屏幕上)

可以“写文件”,将更改过的文件数据写回外存(我们在“记事本”应用程序中编辑文件内容,点击“保存”后, “记事本”应用程序通过操作系统提供 的“写文件”功能,即 write 系统调用, 将文件数据从内存写回外存)

可以“删除文件”(点了“删除”之后,图形化交互进程通过操作系统提供的“删除文件”功能,即 delete 系统调用, 将文件数据从外存中删除)

向上提供的几个最基本的功能

  • 创建文件( create 系统调用)
  • 读文件( read 系统调用)
  • 写文件( write 系统调用)
  • 删除文件( delete系统调用)
  • 打开文件( open系统调用)
    读/写文件之前,需要“打开文件”
  • 关闭文件( close系统调用)
    读/写文件结束之后, 需要“关闭文件”

可用几个基本操作完成更复杂的操作,比如:“复制文件”: 先创建一个新的空文件,再把源文件读入内存,再将内存中的数据写到新文件中

操作系统在背后做的处理会在以后进行探讨


  • 从上往下看,文件应如何存放在外存?

与内存一样,外存也是由一个个存储单元组成的,每个存储单元可以存储一定量的数据(如 1B)。每个存储单元对应一个物理地址

操作系统以“块”为单位为文件分配存储空间,因此即使一个文件大小只有10B,但它依然需要占用 1KB 的磁盘块。外存中的数据读入内存时同样以块为单位

类似于内存分为一个个“内存块”,外存会分为一个个“块/磁盘块/物理块”。每个磁盘块的大小是相等的,每块一般包含2的整数幂个地址(如本例中,一块包含 2102^{10} 个地址,即 1KB)。同样类似的是,文件的逻辑地址也可以分为(逻辑块号,块内地址),操作系统同样需要将逻辑地址转换为外存的物理地址(物理块号,块内地址)的形式。块内地址的位数取决于磁盘块的大小

  • 文件数据放在连续的几个磁盘块中:

  • 文件数据放在离散的几个磁盘块中。 此时,应该如何记录各个磁盘块之间的先后顺序呢?
    操作系统又应该怎么管理空闲磁盘块?

操作系统又应该怎么管理空闲磁盘块?


  • 其他需要由操作系统实现的文件管理功能

文件共享:使多个用户可以共享使用一个文件

文件保护:如何保证不同的用户对文件有不同的操作权限

之后会结合 Windows操作系统的实际应用进行探讨


4.1.2 文件的逻辑结构

  • 无结构文件
  • 有结构文件
    • 顺序文件
    • 索引文件
    • 索引顺序文件

所谓的“逻辑结构”,就是指在用户看来, 文件内部的数据应该是如何组织起来的。而 “物理结构”指的是在操作系统看来,文件的数据是如何存放在外存中的。

类似于数据结构的“逻辑结构”和“物理结构”。
如“线性表”就是一种逻辑结构,在用户角度看来,线性表就是一组有先后关系的元素序列,如:a, b, c, d, e ……
“线性表”这种逻辑结构可以用不同的物理结构实现,如:顺序表/链表。顺序表的各个元素在逻辑上相邻,在物理上也相邻;而链表的各个元素在物理上可以是不相邻的。因此,顺序表可以实现“随机访问”,而“链表”无法实现随机访问。
可见,算法的具体实现与逻辑结构、物理结构都有关(文件也一样,文件操作的具体实现与文件的逻辑结构、物理结构都有关)

  • 无结构文件

无结构文件:文件内部的数据就是一系列二进制流或字符流组成。又称“流式文件”。如: Windows 操作系统中的 .txt 文件。

文件内部的数据其实就是一系列字符流,没有明显的结构特性。因此也不用探讨无结构文件的“逻辑结构”问题。


  • 有结构文件

有结构文件:由一组相似的记录组成,又称“记录式文件”。每条记录又若干个数据项组成。如: 数据库表文件。一般来说,每条记录有一个数据项可作为关键字(作为识别不同记录的ID【数据库的Primary Key】)

这是一张数据库表,记录了各个学生的信息:

每个学生对应一条记录,每条记录由若干个数据项组成
在本例中, “学号”即可 作为各个记录的关键字

根据各条记录的长度(占用的存储空间)是否相等,又可分为定长记录可变长记录两种

定长记录

这个有结构文件由定长记录组成,每条记录的长度都相同(共 128 B)。各数据项都处在记录中相同的位置,具有相同的顺序和长度 (前32B一定是学号,之后32B一定是姓名……)

可变长记录

这个有结构文件由可变长记录组成,由于各个学生的特长存在很大区别,因此“特长” 这个数据项的长度不确定,这就导致了各条记录的长度也不确定。当然,没有特长的学生甚至可以去掉“特长”数据项。


  • 有结构文件的逻辑结构

根据有结构文件中的各条记录在逻辑上如何组织,可以分为三类

  • 有结构文件
    • 顺序文件
    • 索引文件
    • 索引顺序文件

  • 顺序文件

顺序文件:文件中的记录一个接一个地顺序排列(逻辑上),记录可以是定长的或可变长的。各个记录在物理上可以顺序存储链式存储

顺序存储——逻辑上相邻的记录物理上也相邻(类似于顺序表)

链式存储——逻辑上相邻的记录物理上不一定相邻 (类似于链表)

顺序文件:

  • 串结构
    记录之间的顺序与关键字无关
    【通常按照记录存入的时间决定记录的顺序】
  • 顺序结构
    记录之间的顺序按关键字顺序排列

假设:已经知道了文件的起始地址(也就是第一个记录存放的位置)

思考1:能否快速找到第 i 个记录对应的地址?(即能否实现随机存取)

思考2:能否快速找到某个关键字对应的记录存放的位置?

顺序文件

  • 链式存储
    无论是定长/可变长记录,都无法实现随机存取,每次只能从第一个记录开始依次往后查找
  • 顺序存储
    • 可变长记录
      无法实现随机存取。每次只能从第一个记录开始依次往后查找
    • 定长记录
      • 可实现随机存取。记录长度为L,则第 i个记录存放的相对位置是 i*L
      • 若采用串结构,无法快速找到某关键字对应的记录
      • 若采用顺序结构,可以快速找到某关键字对应的记录(如折半查找【二分查找】)

结论:定长记录的顺序文件,若物理上采用顺序存储,则可实现随机存取;若能再保证记录的顺序结构,则可实现快速检索 (即根据关键字快速找到对应记录)

注:一般来说,考试题目中所说的“顺序文件”指的是物理上顺序存储的顺序文件【所以不用考虑链式存储】。之后的讲解中提到的顺序文件也默认如此。

可见,顺序文件的缺点增加/删除一个记录比较困难(如果是串结构则相对简单)


  • 索引文件

对于可变长记录文件,要找到第 i 个记录,必须先顺序第查找前 i-1 个记录, 但是很多应用场景中又必须使用可变长记录。如何解决这个问题?

建立一张索引表【连续存储】以加快文件检索速度。每条记录对应一个索引项。
文件中的这些记录在物理上可以离散地存放。

索引表本身是定长记录的顺序文件。因此可以快速找到第 i 个记录对应的索引项。

可将关键字作为索引号内容,若按关键字顺序排列,则还可以支持按照关键字折半查找。

每当要增加/删除一个记录时,需要对索引表进行修改。由于索引文件有很快的检索速度,因此主要用于对信息处理的及时性要求比较高的场合

另外,可以用不同的数据项建立多个索引表。如: 学生信息表中,可用关键字“学号”建立一张索引表。也可用“姓名”建立一张索引表。这样就可以根据“姓名”快速地检索文件了。 (Eg:SQL 就支持根据某个数据项建立索引的功能)


  • 索引顺序文件

思考索引文件的缺点:每个记录对应一个索引表项,因此索引表可能会很大。比如:文件的每个记录平均只占 8B,而每个索引表项占32个字节,那么索引表都要比文件内容本身大4倍,这样对存储空间的利用率就太低了。

索引顺序文件是索引文件和顺序文件思想的结合。索引顺序文件中,同样会为文件建立一张索引表,但不同的是:并不是每个记录对应一个索引表项,而是一组记录对应一个索引表项。

在本例中,学生记录按照学生姓名的开头字母进行分组。每个分组就是一个顺序文件, 分组内的记录不需要按关键字排序

索引顺序文件的索引项也不需要按关键字顺序排列,这样可以极大地方便新表项的插入。


  • 索引顺序文件(检索效率分析)

用这种策略确实可以让索引 表“瘦身”,但是是否会出 现不定长记录的顺序文件检索速度慢的问题呢?

若一个顺序文件有10000个记录,则根据关键字检索文件,只能从头开始顺序查找(这里指的并不是定长记 录、顺序结构的顺序文件),平均须查找 5000 个记录。 若采用索引顺序文件结构,可把 10000 个记录分为 10000\sqrt{10000} = 100 组,每组 100 个记录。则需要先顺序查找索引表找到分组(共100个分组,因此索引表长度为 100,平均需要查 50 次),找到分组后,再在分组中顺序查找记录(每个分组100 个记录,因此平均需要查 50 次)。可见,采用索引顺序文件结构后,平均查找次数减少为 50+50 = 100 次

同理,若文件共有 10610^6个记录,则可分为 1000 个分组,每个分组 1000 个记录。根据关键字检索一个记录平均需要查找 500+500 = 1000 次。这个查找次数依然很多,如何解决呢?


  • 多级索引顺序文件

为了进一步提高检索效率,可以为顺序文件建立多级索引表。例如,对于一个含 10610^6个记录的文件,可先为该文件建立一张低级索引表,每 100 个记录为一组,故低级索引表中共有 10000 个表项(即10000个定长 记录),再把这 10000 个定长记录分组,每组100个,为其建立顶级索引表,故顶级索引表中共有 100 个表 项。

此时,检索一个记 录平均需要查找 50+50+50 = 150 次

Tips: 要为 N 个记录的文件建立 K 级索引,则最优的分组是每组 N1/(K+1)N^{1/(K+1)}个记录。

检索一个记录的平均查找次数是 N1/(K+1)2×(K+1)\frac{N^{1/(K+1)}}{2}\times (K+1)

如:本例中,建立2级索引,则最优分组为每组1000001/3=100100000^{1/3} = 100 个记录, 平均查找次数是 (100/2) * 3 = 150 次


4.1.3 文件目录

文件目录:就是我们很熟悉 的 Windows 操作系统的“文件夹”

这种目录结构对 于用户来说有什么好处

  • 文件之间的组织结构清晰,易于查找
  • 编程时也可以很方便的用文件路径找到一个文件。如: FILE *fp; fp=fopen(“F:\data\myfile.dat”); 用户可以轻松实现“按名存取”

从操作系统的角度来看,这些目录结构应该是如何实现的?

  • 文件控制块FCB

当我们双击“照片”后,操作系统会在这个目录表中找到关键字“照片”对应的目录项(也就是记录),然后从外存中将“照片”目录的信息读入内存,于是,“照片”目录中的内容就可以显示出来了。

FCB 的有序集合称为“文件目录”,一个FCB就是一个文件目录项。 FCB 中包含了文件的基本信息文件名物理地址、逻辑结构、物理结构等),存取控制信息(是否可读/可写、禁止访问的用户名单等),使用信息(如文件的建立时间、修改时间等)。 最重要最基本的还是文件名文件存放的物理地址
【FCB 实现了文件名和文件之间的映射。使用户(用户程序)可以实现“按名存取”】

需要对目录进行哪些操作

  1. 搜索:当用户要使用一个文件时,系统要根据文件名搜索目录,找到该文件对应的目录项
  2. 创建文件:创建一个新文件时,需要在其所属的目录中增加一个目录项
  3. 删除文件:当删除一个文件时,需要在目录中删除相应的目录项
  4. 显示目录:用户可以请求显示目录的内容,如显示该目录中的所有文件及相应属性
  5. 修改目录:某些文件属性保存在目录中,因此这些属性变化时需要修改相应的目录项(如:文件重命名)

  • 目录结构——单级目录结构

早期操作系统并不支持多级目录,整个系统中只建立一张目录表,每个文件占一个目录项。

单级目录实现了“按名存取”,但是不允许文件重名

在创建一个文件时,需要先检查目录表中有没有重名文件,确定不重名后才能允许建立文件,并将新文件对应的目录项插入目录表中。

显然,单级目录结构不适用于多用户操作系统【容易重复】。


  • 目录结构——两级目录结构

早期的多用户操作系统,采用两级目录结构。分为主文件目录(MFD,Master File Directory)和用户文件目录(UFD,User Flie Directory)。

主文件目录记录用户名及相应用户文件目录的存放位置
用户文件目录由该用户的文件FCB组成

允许不同用户的文件重名。文件名虽然相 同,但是对应的其实是不同的文件

两级目录结构允许不同用户的文件重名,也可以在目录上实现实现访问限制(检查此时登录的用户名是否匹配)。但是两级目录结构依然缺乏灵活性,用户不能对自己的文件进行分类


  • 目录结构——多级目录结构
    又称树形目录结构

用户(或用户进程)要访问某个文件时要用文件路径名标识文件,文件路径名是个字符串。各级目录之间 用“/”隔开。从根目录出发的路径称为绝对路径。例如:自拍.jpg 的绝对路径是 “/照片/2015-08/自拍.jpg”

系统根据绝对路径一层一层地找到下一级目录。刚开始从外存读入根目录的目录表;找到“照片”目录的 存放位置后,从外存读入对应的目录表;再找到“2015-08”目录的存放位置,再从外存读入对应目录表; 最后才找到文件“自拍.jpg”的存放位置。整个过程需要3次读磁盘I/O操作

很多时候,用户会连续访问同一目录内的多个文件(比如:接连查看“2015-08”目录内的多个照片文件), 显然,每次都从根目录开始查找,是很低效的。因此可以设置一个“当前目录”。
例如,此时已经打开了“照片”的目录文件,也就是说,这张目录表已调入内存,那么可以把它设置为 “当前目录”。当用户想要访问某个文件时,可以使用从当前目录出发的“相对路径” 。
在 Linux 中,“.”表示当前目录,因此如果“照片”是当前目录,则”自拍.jpg”的相对路径为: “./2015-08/自拍.jpg”。从当前路径出发,只需要查询内存中的“照片”目录表,即可知道”2015-08”目录表的存放位置,从外存调入该目录,即可知道“自拍.jpg”存放的位置了。

可见,引入“当前目录”和“相对路径”后,磁盘I/O的次数减少了。这就提升了访问文件的效率。

树形目录结构可以很方便地对文件进行分类,层次结构清晰,也能够更有效地进行文件的管理和保护。但是,树形结构不便于实现文件的共享。为此,提出了“无环图目录结构”。


  • 目录结构——无环图目录结构

在树形目录结构的基础上,增加一些指向同一节点的有向边,使 整个目录成为一个有向无环图。 可以更方便地实现多个用户间的文件共享。

可以用不同的文件名指向同一个文件,甚至可以指向同一个目录(共享同一目录下的所有内容)。

需要为每个共享结点设置一个共享计数器,用于记录此时有多少个地方在共享该结点。用户提出删除结点的请求时,只是删除该用户的FCB、并使共享计数器减1,并不会直接删除共享结点。
只有共享计数器减为0时,才删除结点

注意:共享文件不同于复制文件。在共享文件中,由于各用户指向的是同一个文件,因此只要其中一个用户修改了文件数据,那么所有用户都可以看到文件数据的变化


  • 索引结点(FCB的改进)

其实在查找各级目录的过程中只需要用到“文件名”这个信 息,只有文件名匹配时,才需要读出文件的其他信息。因此可以考虑让目录表“瘦身”来提升效率。

思考有何好处? 假设一个FCB是64B,磁盘块的大小为1KB,则每个盘块中只能存放 16个FCB。若一个文件目录中共有640个目录项,则共需要占用 640/16 = 40 个盘块。因此按照某文件名检索该目录,平均需要查询320 个目录项,平均需要启动磁盘20次(每次磁盘I/O读入一块)。

使用索引结点机制,文件名占14B,索引结点指针站2B,则每个盘块可存放64个目录项,那么按文件名检索目录平均只需要读入 320/64 = 5 个磁盘块。显然,这将大大提升文件检索速度

当找到文件名对应的目录项时,才需要将索引结点调入内存,索引结点中记录了文件的各种信息,包括文件在外存中的存放位置,根据“存放位置”即可找到文件。

放在外存中的索引结点称为“磁盘索引结点”,当索引结点放入内存后称为“内存索引结点”。 相比之下内存索引结点中需要增加一些信息,比如:文件是否被修改、此时有几个进程正在访问该文件等。


4.1.4 文件的物理结构(上)

要探讨的问题:

“文件的物理结构/文件分配方式” 即:文件数据应该怎样存放在外存中?

“文件存储空间管理”

  • 文件块、磁盘块

类似于内存分页,磁盘中的存储单元也会被分为一个个“块/磁盘块/物理块”。很多操作系统中,磁盘块的大小与内存块、页面的大小相同

内存与磁盘之间的数据交换(即读/写操作、磁盘I/O)都是以“块”为单位进行的。即每次读入一块,或每次写出一块

在内存管理中,进程的逻辑地址空间被分为一个一个页面
同样的,在外存管理中,为了方便对文件数据的管理,文件的逻辑地址空间也被分为了一个一个的文件“块” 。 于是文件的逻辑地址也可以表示为(逻辑块号块内地址)的形式。

操作系统为文件分配存储空间都是以块为单位的
用户通过逻辑地址来操作自己的文件,操作系统要负责实现从逻辑地址到物理地址的映射


  • 文件分配方式——连续分配

连续分配方式要求每个文件在磁盘上占有一组连续的块

用户通过逻辑地址来操作自己的文件,操作系统如何实现从逻辑地址到物理地址的映射?
(逻辑块号,块内地址)→ (物理块号,块内地址)。只需转换块号就行,块内地址保持不变

用户给出要访问的逻辑块号,操作系统找到该文件对应的目录项(FCB)…
物理块号 = 起始块号 + 逻辑块号
当然,还需要检查用户提供的逻辑块号是否合法(逻辑块号 ≥ 长度 就不合法)

【可以直接算出逻辑块号对应的物理块号,因此连续分配支持顺序访问和直接访问(即随机访问)】

如下图,读取某个磁盘块时,需要移动磁头。访问的两个磁盘块相隔越远,移动磁头所需时间就越长。

结论连续分配的文件在顺序读/写时速度最快

eg: 如下图:若此时文件A要拓展,需要再增加一个磁盘块(总共 需要连续的4个磁盘块)。 由于采用连续结构,因此文件A占用的磁盘块必须是连续的。 因此只能将文件A全部“迁移”到绿色区域。

结论:物理上采用连续配的文件不方便拓展

结论:物理上采用连续分配存储空间利用率低会产生难以利用的磁盘碎片
可以用紧凑【之前介绍了】来处理碎片,但是需要耗费很大的时间代价。


  • 连续分配(总结)

连续分配方式要求每个文件在磁盘上占有一组连续的块

优点:支持顺序访问和直接访问(即随机访问);连续分配的文件在顺序访问时速度最快

缺点:不方便文件拓展;存储空间利用率低,会产生磁盘碎片


  • 文件分配方式——链接分配

链接分配采取离散分配的方式,可以为文件分配离散的磁盘块。分为隐式链接显式链接两种。


  • 链接分配——隐式链接

除了文件的最后一个磁盘块之外,每 个磁盘块中都会保存指向下一个盘块的指针,这些指针对用户是透明的

目录中记录了文件存放的起始块号和结束块号。当然, 也可以增加一个字段来表示文件的长度

如何实现文件的逻辑块号到物理块号的转变?

  • 用户给出要访问的逻辑块号 i,操作系统找到该文件对应的目录项(FCB)…
  • 从目录项中找到起始块号(即逻辑上的0号块),将0号逻辑块读入内存,由此知道1号逻辑块存放的物理块号,于是读入1号逻辑块,再找到2号逻辑块的存放位置……以此类推。
  • 因此,读入i号逻辑块,总共需要 i+1 【0\toi】次磁盘 I/O。

结论:采用链式分配(隐式链接) 方式的文件,只支持顺序访问,不支持随机访问,查找效率低。另外,指向下一个盘块的指针也需要耗费少量的存储空间。

是否方便拓展文件?

若此时要拓展文件,则可以随便找一个空闲磁盘块,挂到文件的磁盘块链尾,并修改文件的FCB

结论:采用隐式链接的链接分配方式很方便文件拓展。 另外,所有的空闲磁盘块都可以被利用,不会有碎片问题, 外存利用率高

隐式链接——除文件的最后一个盘块之外,每个盘块中都存有指向下一个盘块的指针。文件目录包括文件第一块的指针和最后一块的指针。
优点:很方便文件拓展,不会有碎片问题,外存利用率高。
缺点:只支持顺序访问,不支持随机访问,查找效率低,指向下一个盘块的指针也需要耗费少量的存储空间。


  • 链接分配——显式链接

把用于链接文件各物理块的指针显式地存放在一张表中。即文件分配表FAT,File Allocation Table)

目录中只需记录文件的起始块号

假设某个新创建的文件“aaa”依次存放在磁盘块 2 →5 →0 →1
假设某个新创建的文件“bbb”依次存放在磁盘块 4 →23 →3
【结果如上图】

注意:一个磁盘仅设置一张FAT。 开机时,将FAT读入内存,并常驻内存。 FAT 的各个表项在物理上连续存储,且每一个表项长度相同,因此“物理块号”字段可以是隐含的

如何实现文件的逻辑块号到物理块号的转变?

用户给出要访问的逻辑块号 i,操作系统找到该文件对应的目录项(FCB)…

从目录项中找到起始块号,若i>0,则查询内存中的文件分配表FAT, 往后找到 i 号逻辑块对应的物理块号。逻辑块号转换成物理块号的过程不需要读磁盘操作

结论:采用链式分配(显式链接) 方式的文件,支持顺序访问,也支持随机访问(想访问 i 号逻辑块时,并不需要依次访问之前的 0 ~ i-1 号逻辑块),由于块号转换的过程不需要访问磁盘,因此相比于隐式链接来说,访问速度快很多。

显然,显式链接也不会产生外部碎片也可以很方便地对文件进行拓展


  • 链接分配(总结)

链接分配采取离散分配的方式,可以为文件分配离散的磁盘块。分为隐式链接显式链接两种。

隐式链接——除文件的最后一个盘块之外,每个盘块中都存有指向下一个盘块的指针。文件目录包括文件第一块的指针和最后一块的指针。
优点:很方便文件拓展,不会有碎片问题,外存利用率高。
缺点:只支持顺序访问,不支持随机访问,查找效率低,指向下一个盘块的指针也需要耗费少量的存储空间。

【考试题目中遇到未指明隐式/显式的“链接分配”,默认指的是隐式链接的链接分配】

显式链接——把用于链接文件各物理块的指针显式地存放在一张表中,即 文件分配表FAT,File Allocation Table)。一个磁盘只会建立一张文件分配表。开机时文件分配表放入内存,并常驻内存
优点:很方便文件拓展,不会有碎片问题,外存利用率高,并且支持随机访问。相比于隐式链接来说,地址转换时不需要访问磁盘,因此文件的访问效率更高。
缺点:文件分配表的需要占用一定的存储空间。


4.1.4 文件的物理结构(下)

即:文件数据应该怎样存放在外存中?

  • 文件分配方式——索引分配

索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表,索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表——建立逻辑页面到物理页之间的映射关系)。索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块

假设某个新创建的文件“aaa”的数据依次存放在磁盘块 2→5→13→9 。 7号磁盘块作为“aaa”的索引块, 索引块中保存了索引表的内容。

注:在显式链接的链式分配方式中,文件分配表FAT是一个磁盘对应一张。而索引分配方式中,索引表是一个文件对应一张。

可以用固定的长度表示物理块号(如: 假设磁盘总容量为1TB=2402^{40}B,磁盘块大小为1KB,则共有2302^{30}个磁盘块,则可用 4B 表示磁盘块号),因此,索引表中的“逻辑块号”可以是隐含的。


如何实现文件的逻辑块号到物理块号的转变?

用户给出要访问的逻辑块号 i,操作系统找到该文件对应的目录项(FCB)…

从目录项中可知索引表存放位置,将索引表从外存读入内存,并查找索引表即可只 i 号逻辑块在外存中的存放位置。

可见,索引分配方式可以支持随机访问。文件拓展也很容易实现(只需要给文件分配一个空闲块,并增加一个索引表项即可)但是索引表需要占用一定的存储空间


若每个磁盘块1KB,一个索引表项4B,则一个磁盘块只能存放 256 个索引项。
如果一个文件的大小超过了256 块,那么一个磁盘块是装不下文件的整张索引表的,如何解决这个问题?
①链接方案; ②多层索引; ③混合索引.

  1. 链接方案:如果索引表太大,一个索引块装不下,那么可以将多个索引块链接起来存放。

假设磁盘块大小为1KB,一个索引表项占4B,则一个磁盘块只能存放256 个索引项。

若一个文件大小为 256*256KB = 65,536 KB = 64MB

该文件共有 256*256 个块,也就对应 256*256个索引项,也就需要 256 个索引块来存储,这些索引块用链接方案连起来。

若想要访问文件的最后一个逻辑块, 就必须找到最后一个索引块(第256 个索引块),而各个索引块之间是用 指针链接起来的,因此必须先顺序地读入前 255 个索引块。

这显然是很低效的。如何解决呢?

  1. 多层索引

假设磁盘块大小为1KB,一个索引表项占4B,则一个磁盘块只能存放256个索引项。

若某文件采用两层索引,则该文件的最大长度可以到 256*256*1KB = 65,536 KB = 64MB【常考计算】

可根据逻辑块号算出应该查找索引表中的哪个表项。 如:要访问 1026 号逻辑块,则
1026/256 = 4,1026%256 = 2
因此可以先将一级索引表调入内存,查询 4 号表项, 将其对应的二级索引表调入内存,再查询二级索引表 的2号表项即可知道 1026 号逻辑块存放的磁盘块号了。 访问目标数据块,需要3次磁盘I/O

若采用三层索引,则文件的最大长度为 256*256*256*1KB = 16GB 类似的,访问目标数据块,需要4次磁盘I/O

规律】采用K层索引结构,且顶级索引表未调入内存,则访问一个数据块只需要K+1次读磁盘操作

  1. 混合索引

混合索引:多种索引分配方式的结合。例如,一个文件的顶级索引表中,既包含直接地址索引(直接指向数据块),又包含一级间接索引(指向单层索引表)、还包含两级间接索引(指向两层索引表) 。

这种结构的索引支持的最大文件长度为8 + 256 + 256 * 256 = 65800KB

若顶级索引表还没读入内存
访问0-7号逻辑块:两次读磁盘
访问8-263:三次读磁盘
访问264~65799:四次读磁盘

对于小文件,只需较少的读磁盘次数就可以访问目标数据块(一般计算机中小文件更多)


  • 索引分配(总结)

索引分配允许文件离散地分配在各个磁盘块中,系统会为每个文件建立一张索引表,索引表中记录了文件的各个逻辑块对应的物理块(索引表的功能类似于内存管理中的页表——建立逻辑页面到物理页之间的映射关系) 。索引表存放的磁盘块称为索引块。文件数据存放的磁盘块称为数据块。

若文件太大,索引表项太多,可以采取以下三种方法解决:
链接方案:如果索引表太大,一个索引块装不下,那么可以将多个索引块链接起来存放。缺点:若文件很大,索引表很长,就需要将很多个索引块链接起来。想要找到 i 号索引块,必须先依次读入 0~i-1 号索引块,这就导致磁盘I/O次数过多,查找效率低下。

多层索引:建立多层索引(原理类似于多级页表)。使第一层索引块指向第二层的索引块。还可根据文件大小的要求再建立第三层、第四层索引块。采用 K 层索引结构,且顶级索引表未调入内存,则访问 一个数据块只需要 K + 1 次读磁盘操作。缺点:即使是小文件,访问一个数据块依然需要K+1次读磁盘。

混合索引:多种索引分配方式的结合。例如,一个文件的顶级索引表中,既包含直接地址索引(直接指向数据块),又包含一级间接索引(指向单层索引表)、还包含两级间接索引(指向两层索引表) 。 优点:对于小文件来说,访问一个数据块所需的读磁盘次数更少。

超级超级超级重要考点
①要会根据多层索引、混合索引的结构计算出文件的最大长度(Key各级索引表最大不能超过一个块);
②要能自己分析访问某个数据块所需要的读磁盘次数(Key:FCB中会存有指向顶级索引块的指针,因此可以根据FCB读入顶级索引块。每次读入下一级的索引块都需要一次读磁盘操作。另外,要注意题目条件——顶级索引块是否已调入内存


  • 知识点回顾与重要考点
How? 目录项内容 优点 缺点
顺序分配 为文件分配的必须是连续的磁盘块 起始块号、文件长度 顺序存取速度快,支持随机访问 会产生碎片,不利于文件拓展
链接分配
隐式链接
除文件的最后一个盘块之外,每个盘块中都存有指向下一个盘块的指针 起始块号、结束块号 可解决碎片问题,外存利用率高,文件拓展实现方便 支持随机访问,易于实现文件的拓展
链接分配
显式链接
建立一张文件分配表(FAT),显式记录盘块的先后关系(开机后FAT常驻内存) 起始块号 除了拥有隐式链接的优点之外,还可通过查询内存中的FAT实现随机访问 FAT需要占用一定的存储空间
索引分配 为文件数据块建立索引表。若文件太大,可采用链接方案、多层索引、混合索引 链接方案记录的是第一个索引块的块号,多层/混合索引记录的是顶级索引块的块号 支持随机访问,易于实现文件的拓展 索引表需占用一定的存储空间。访问数据块前需要先读入索引块。若采用链接方案,查找索引块时可能需要很多次读磁盘操作。

4.1.5 逻辑结构Vs物理结构

我觉得有基础不用看了,挺简单的,明确两个点就好了:

  • 逻辑结构是用户看的,用户可以自己设计怎么存储,但是真正怎么分配内存的时候还是得看操作系统
  • 物理结构是给操作系统看的

还有一个懵逼点是要明确顺序文件是什么,简单来说,顺序文件就是有结构的,文件内容有一定顺序的文件

4.1_5_逻辑结构VS物理结构_哔哩哔哩_bilibili


4.1.6 文件存储空间管理

学习时要注意从三个方面进行理解:

  1. 用什么方式记录,组织空闲快?
  2. 如何分配磁盘块
  3. 如何回收磁盘块
  • 存储空间的划分与初始化

eg: 安装 Windows 操作系统的时候,一个必经步骤是–为磁盘分区 (C:盘、D:、E: 盘等)
【存储空间的划分: 将物理磁盘划分为一个个文件卷(逻辑卷、逻辑盘)】

存储空间的初始化:将各个文件卷划分为目录区、文件区

目录区主要存放文件目录信息、用于磁盘存储空间管理的信息
文件区用于存放文件数据


  • 存储空间管理——空闲表法

适合”连续分配方式“

如何分配磁盘块:与内存管理中的动态分区分配很类似,为一个文件分配连续的存储空间。同样可采用首次适应、最佳适应、最坏适应等算法来决定要为文件分配哪个区间。

如何回收磁盘块:与内存管理中的动态分区分配很类似,当回收某个存储区时需要有四种情况——
① 回收区的前后都没有相邻空闲区;
② 回收区的前后都是空闲区;
③ 回收区前面是空闲区;
④ 回收区后面是空闲区。
总之,回收时需要注意表项的合并问题。


  • 空闲链表法
    • 空闲盘块链
      以盘块为单位组成一条空闲链
    • 空闲盘区链
      以盘区为单位组成一条空闲链

  • 空闲盘块链

操作系统保存着链头链尾指针
如何分配:若某文件申请 K个盘块,则从链头开始依次摘下K个盘块分配,并修改空闲链的链头指针。
如何回收:回收的盘块依次挂到链尾,并修改空闲链的链尾指针。

【适用于离散分配的物理结构。为文件分配多个盘块时可能要重复多次操作】

  • 空闲盘区链

操作系统保存着链头链尾指针
如何分配:若某文件申请 K 个盘块,则可以采用首次适应、最佳适应等算法,从链头开始检索按照算法规则找到一个大小符合要求的空闲盘区分配给文件。若没有合适的连续空闲块,也可以将不同盘区的盘块同时分配给一个文件,注意分配后可能要修改相应的链指针、盘区大小等数据
如何回收:若回收区和某个空闲盘区相邻,则需要将回收区合并到空闲盘区中。若回收区没有和任何空闲区相邻,将回收区作为单独的一个空闲盘区挂到链尾。

【离散分配、连续分配都适用。为一个文件分配多个盘块时效率更高】


  • 存储空管理——位示图法

位示图:每个二进制位对应一个盘块。在本例中,“0”代表盘块空闲,“1”代表盘块已分配。位示图一般用连续的“字”来表示,如本例中个字的字长是16位,字中的每一位对应一个盘块。因此可以用 (字号,位号) 对应一个盘块号。当然有的题目中也描述为(行号,列号)

重要重要重要:要能自已推出盘块号与(字号位号)相转换的公式。注意题目条件:盘块号、字号、位号到底是从0开始还是从1开始

如本例中盘块号、字号、位号从0开始,若n表示字长,则…
(字号,位号)=(i,j) 的二进制位对应的 盘块号 b = ni +j
b号盘块对应的字号i= b/n,位号j= b%n

如何分配:若文件需要K个块,① 顺序扫描位示图,找到K个相邻或不相邻的“0”,② 根据字号、位号算出对应的盘块号,将相应盘块分配给文件,③ 将相应位设置为“1”
如何回收: ① 根据回收的盘块号计算出对应的字号、位号; ② 将相应进制位设为“0”


  • 存储空间管理——成组链接法
    【不方便用文字描述也很难作为考题】

空闲表法、空闲链表法不适用于大型文件系统,因为空闲表或空闲链表可能过大。UNIX系统中采用了成组链接法对磁盘空闲块进行管理。

文件卷的目录区中专门用一个磁盘块作为“超级块”,当系统启动时需要将超级块读入内存。并且要保证内存与外存中的“超级块”数据一致

注意:成组链接法的每个分组是有上限的,本例中是100

什么是一组,直观地理解就是图中一列就是一组

如何分配?
Eg:需要1个空闲块
① 检查第一个分组的块数是否足够。1<100,因此是足够的。
② 分配第一个分组中的1个空闲块【最后一个,上图中的201】,
③ 并修改相应数据【超级块:100→99】

Eg:需要100个空闲块
检查第一个分组的块数是否足够。100=100,是足够的。
分配第一个分组中的100个空闲块。但是由于300号块内存放了再下一组的信息,因此300号块的数据需要复制到超级块中【相当于300号成了新的超级块】

如何回收?
Eg: 假设每个分组最多为100个空闲块,此时第一个分组已有100个块,还要再回收一块
需要将超级块中的数据复制到新回收的块中,并修改超级块的内容,让新回收的块成为第一个分组


4.1.7 文件的基本操作

向上提供的几个最基本的功能

  • 创建文件( create 系统调用)
  • 读文件( read 系统调用)
  • 写文件( write 系统调用)
  • 删除文件( delete系统调用)
  • 打开文件( open系统调用)
    读/写文件之前,需要“打开文件”
  • 关闭文件( close系统调用)
    读/写文件结束之后, 需要“关闭文件”
  • 创建文件

可以“创建文件”, (点击新建后,图形化交互进程在背后调用了“create 系统调用”)

进行 Create 系统调用时,需要提供的几个主要参数:

  1. 所需的外存空间大小(如:一个盘块,即1KB)
  2. 文件存放路径(“D:/Demo”)
  3. 文件名(这个地方默认为“新建文本文档.txt”)

操作系统在处理 Create 系统调用时,主要做了两件事:

  1. 在外存中找到文件所需的空间(结合上小节学习的空闲链表法、位示图、成组链接法等管理策略,找到空闲空间)
  2. 根据文件存放路径的信息找到该目录对应的目录文件(此处就是 D:/Demo 目录),在目录中创建该文件对应的目录项。目录项中包含了文件名、文件在外存中的存放位置等信息。

  • 删除文件

可以“删除文件”(点了“删除”之后,图形化交互进程通过操作系统提供的“删除文件”功能,即 delete 系统调用,将文件数据从外存中删除)

进行 Delete 系统调用时,需要提供的几个主要参数:

  1. 文件存放路径(“D:/Demo”)
  2. 文件名(“test.txt”)

操作系统在处理 Delete 系统调用时,主要做了几件事:

  1. 根据文件存放路径找到相应的目录文件,从目录中找到文件名对应的目录项
  2. 根据该目录项记录的文件在外存的存放位置、文件大小等信息,回收文件占用的磁盘块。 (回收磁盘块时,根据空闲表法、空闲链表法、 位图法等管理策略的不同,需要做不同的处理)
  3. 从目录表中删除文件对应的目录项

  • 打开文件

在很多操作系统中,在对文件进行操作之前,要求用户先使用 open 系统调用 “打开文件”,需要提供的几个主要参数:

  1. 文件存放路径(“D:/Demo”)
  2. 文件名(“test.txt”)
  3. 要对文件的操作类型(如:r 只读; rw 读写等)

操作系统在处理 open 系统调用时,主要做了几件事:

  1. 根据文件存放路径找到相应的目录文件,从目录中找到文件名对应的的目录项,并检查该用户是否有指定的操作权限。
  2. 将目录项复制到内存中的“打开文件表”中。并将对应表目的编号返回给用户。之后用户使用打开文件表的编号来指明要操作的文件

读/写指针:记录了该进程对文件的读/写操作进行到的位置
访问权限:如果打开文件时声明的是 “只读”,则该进程不能对文件进行写操作
打开计数器:记录此时有多少个进程打开了此文件

系统的打开文件表可以方便实现某些文件管理的功能。例如:在Windows系统中,我们尝试删除某个txt文件,如果此时该文件已被某个“记事本” 进程打开,则系统会提示我们“暂时无法删除该文件”。其实系统在背后做的事就是先检查了系统打开文件表,确认此时是否有进程正在使用该文件。


  • 关闭文件

进程使用完文件后,要“关闭文件” 操作系统在处理 Close 系统调用时,主要做了几件事:

  1. 将进程的打开文件表相应表项删除
  2. 回收分配给该文件的内存空间等资源
  3. 系统打开文件表的打开计数器count 减1,若 count = 0,则删除对应表项。

  • 读文件

可以“读文件”,将文件数据读入内存,才能让CPU处理(双击后,“记事本”应用程序通过操作系统提供的“读文件”功能,即 read 系统调用,将文件数据从外存读入内存,并显示在屏幕上)

读/写文件之前,需要“打开文件”:

进程使用 read系统调用完成写操作。需要指明是哪个文件(在支持“打开文件”操作的系统中,只需要提供文件在打开文件表中的索引号【看”打开文件“的图】即可),还需要指明要读入多少数据(如:读入1KB)、指明读入 的数据要放在内存中的什么位置。 操作系统在处理 read 系统调用时,会从读指针指向的外存中,将用户指定大小的数据读入用户指定的内存区域中。


  • 写文件

可以“写文件”,将更改过的文件数据写回外存(我们在“记事本”应用程序中编辑文件内容,点击“保存”后,“记事本”应用程序通过操作系统提供的“写文件”功能,即 write 系统调用, 将文件数据从内存写回外存)

进程使用 write 系统调用完成写操作,需要指明是哪个文件(在支持“打开文件”操作的系统中,只需要提供文件在打开文件表中的索引号即可),还需要指明要写出多少数据(如:写出1KB)、写回外存的数据放在内存中的什么位置操作系统在处理 write 系统调用时,会从用户指定的内存区域中,将指定大小的数据写回写指针指向的 外存。


  • 知识点与重要考点

打开文件时并不会把文件数据直接读内存。“索引号”也称“文件描述符
【只是把文件目录项复制到内存中(知识点回顾:一个FCB就是一个文件目录项)】

“读/写文件”用“文件描述符”即可指明文件, 不再需要用到“文件名”

读/写文件的时候才把文件从外存读入内存/从内存写回外存


4.1.8 文件共享

操作系统为用户提供文件共享功能,可以让多个用户共享地使用同一个文件

注意:多个用户共享同一个文件,意味着系统中只有“一份”文件数据。并且只要某个用户修改了该文件的数据,其他用户也可以看到文件数据的变化。
如果是多个用户都“复制”了同一个文件,那么系统中会有“好几份”文件数据。其中一个用户修改了自己的那份文件数据,对其他用户的文件数据并没有影响。

  • 基于索引结点的共享方式(硬链接)

知识回顾:索引结点,是一种文件目录瘦身策略。由于检索文件时只需用到文件名,因此可以将除了文件名之外的其他信息放到索引结点中。这样目录项就只需要包含文件名、索引结点指针。

索引结点中设置一个链接计数变量 count,用于表示链接到本索引结点上的用户目录项数。

若 count = 2,说明此时有两个用户目录项链接到该索引结点上,或者说是有两个用户在共享此文件。 若某个用户决定“删除”该文件,则只是要把用户目录中与该文件对应的目录项删除,且索引结点的 count值减 1。

若 count>0,说明还有别的用户要使用该文件,暂时不能把文件数据删除,否则会导致指针悬空。

当 count = 0 时系统负 责删除文件。


  • 基于符号链的共享方式(软链接)

【Link 类型的文件,记录了文件1的存放路径 “C:/User1/aaa”。 类似于 Windows 操作系统的“快捷方式”】

当 User3 访问“ccc”时,操作系统判断文件“ccc”属于 Link 类型文件,于是会根据其中记录的路径层层查找目录,最终找到 User1 的目录表中的“aaa”表项,于是就找到了文件1的索引结点。

Link 类型的文件名可以不同

例如QQ快捷方式:双击快捷方式图标打开时,操作系统判断这个文件是Link类型的 “快捷方式”文件,于是会根据其中记录的“路径 信息”检索目录,最终找到“QQScLauncher.exe”

但是如果QQScLauncher.exe已经删除,不存在了,QQ快捷方式打开时会提示错误。

由于用软链接的方式访问共享文件时要查询多级目录,因此用软链接访问会有多次磁盘I/O


4.1.10 文件保护

  • 口令保护

为文件设置一个“口令”(如:abc112233),用户请求访问该文件时必须提供“口令”。

【口令一般存放在文件对应的 FCB 或索引结点中。用户访问文件前需要先输入“口令”,操作系统会将用户提供的口令与FCB中存储的口令进行对比, 如果正确,则允许该用户访问文件】

优点:保存口令的空间开销不多,验证口令的时间开销也很小。

缺点:正确的“口令”存放在系统内部【被入侵】,不够安全。


  • 加密保护

使用某个“密码”对文件进行加密,在访问文件时需要提供正确的“密码”才能对文件进行正确的解密。

Eg:一个最简单的加密算法——异或加密
假设用于加密/解密的“密码”为“01001”

优点:保密性强,不需要在系统中存储“密码”

缺点:编码/译码,或者说加密/解密要花费一定时间。


  • 访问控制

在每个文件的FCB(或索引结点)中增加一个访问控制列表(Access-Control List, ACL),该表中记录了各个用户可以对该文件执行哪些操作。

有的计算机可能会有很多个用户, 因此访问控制列表可能会很大,可以用精简的访问列表解决这个问题

精简的访问列表:以“组”为单位,标记各“组”用户可以对文件执行哪些操作。 如:分为 系统管理员、文件主、文件主的伙伴、其他用户 几个分组。
当某用户想要访问文件时,系统会检查该用户所属的分组是否有相应的访问权限。【系统需要管理分 组的信息】

若想要让某个用户能够读取文件,只需要把该用户放入 “文件主的伙伴” 这个分组即可

如果对某个目录进行了访问权限的控制,那也要对目录下的所有文件进行相同的访问权限控制


  • Windows 的访问控制

4.1_9_文件保护_哔哩哔哩_bilibili 08:38~11:55


4.3.1 文件系统的层次结构

说明:408大纲不要求考察本节内容

本视频中介绍的“文件系统层次结构”,主要参考了国内教材;23版王道书介绍的“文件系统层次结构”,主要参考了国外教材。因此,二者看起来会有一些差异

不同的学者对文件系统的层次结构划分方法不同,本视频与王道书介绍的两种分层方法都是正确的,大家简要了解即可,不用追求“标准答案

  • 文件系统的层次结构

用户接口:文件系统需要向上层的用户提供一些简单易用的功能接口。这层就是用于处理用户发出的系统调用请求(Read、Write、Open、 Close 等系统调用)

文件目录系统:用户是通过文件路径来访问文件的,因此这一层需要根据用户给出的文件路径找到相应的FCB或索引结点。所有和目录、目录项相关的管理工作都在本层完成,如:管理活跃的文件目录表、管理打开文件表等。

存取控制模块:为了保证文件数据的安全,还需要验证用户是否有访问权限。这一层主要完成了文件保护相关功能。

逻辑文件系统与文件信息缓冲区:用户指明想要访问文件记录号,这一层需要将记录号转换为对应的逻辑地址

物理文件系统:这一层需要把上一层提供的文件逻辑地址转换为实际的物理地址

辅助分配模块:负责文件存储空间的管理,即负责分配和回收存储空间

设备管理模块:直接与硬件交互,负责和硬件直接相关的一些管理工作。如:分配设备、分配设备缓冲区、磁盘

调度、启动设备、释放设备等


【用一个例子来辅助记忆文件系统的层次结构】
假设某用户请求删除文件 “D:/工作目录/学生信息.xlsx” 的最后100条记录。

  1. 用户需要通过操作系统提供的接口发出上述请求
    ——用户接口
  2. 由于用户提供的是文件的存放路径,因此需要操作系统一层一层地查找目录,找到对应的目录项
    ——文件目录系统
  3. 不同的用户对文件有不同的操作权限,因此为了保证安全,需要检查用户是否有访问权限
    ——存取控制模块(存取控制验证层)
  4. 验证了用户的访问权限之后,需要把用户提供的“记录号”转变为对应的逻辑地址
    ——逻辑文件系统与文件信息缓冲区
  5. 知道了目标记录对应的逻辑地址后,还需要转换成实际的物理地址
    ——物理文件系统
  6. 要删除这条记录,必定要对磁盘设备发出请求
    ——设备管理程序模块
  7. 删除这些记录后,会有一些盘块空闲,因此要将这些空闲盘块回收
    ——辅助分配模块

4.3.2 文件系统的全局结构(布局)

  • 原始磁盘


  • 物理格式化后

·

物理格式化,即低级格式化–划分扇区,检测坏扇区,并用备用扇区替换坏扇区

【坏扇区的存在对于操作系统来说也是透明的;当操作系统要访问一个坏扇区时,(磁盘驱动器格式化后,知道这是个坏的)磁盘驱动器会用备用扇区来替代扇区】


  • 逻辑格式化后
    又叫 高级格式化

逻辑格式化后,磁盘分区 (分卷 Volume)完成各分区的文件系统初始化

注:逻辑格式化后,灰色部分就有实际数据了,白色部分还没有数据
注:“主引导记录MBR、引导块”的作用,可结合第一章“操作系统引导”小节来学习

【说明:i节点——索引节点】


  • 文件系统在内存中的结构

注:近期访问过的目录文件会缓存在内存中,不用每次都从磁盘读入,这样可以加快目录检索速度

案例:


4.3.3 虚拟文件系统

  • 普通文件系统

有好几个不同的接口,对程序员不友好


  • 虚拟文件系统

虚拟文件系统的特点:

  1. 向上层用户进程提供统一标准的系统调用接口,屏蔽底层具体文件系统的实现差异
  2. VFS要求下层的文件系统必须实现某些规定的函数功能,如:open/read/write。一个新的文件系统想要在某操作系统上被使用,就必须满足该操作系统VFS的要求

存在的问题:不同的文件系统,表示文件数据结构各不相同打开文件后,其在内存中的表示就不同

解决:打开文件就在主存中创建vnode结点

  1. 每打开一个文件,VFS就在主存(内存)中新建一个 vnode,用统一的数据结构表示文件,无论该文件存储在哪个文件系统。

注意: vnode 只存在于主存中,而 inode(i结点(索引结点)) 既会被调入主存,也会在外存中存储

打开文件后,创建vnode,并将文件信息复制到vnode中,vnode的功能指针指向具体文件系统的函数功能。


  • 文件系统挂载(mounting)

文件系统挂载(mounting),即文件系统安装/装载——如何将文件系统挂载到操作系统中?

文件系统挂载要做的事:

  1. 在VFS中注册新挂载的文件系统内存中的挂载表 (mount table) 包含每个文件系统的相关信息,包括文件系统类型、容量大小等。
  2. 新挂载的文件系统,要向VFS提供一个函数地址列表
  3. 将新文件系统加到挂载点 (mount point),也就是将新文件系统挂载在某个父目录下


第五章 输入/输出(I/O)管理

5.1.1 I/O设备的概念和分类

前情回顾

操作系统的功能和目标——作为系统资源的管理者

  • 提供的功能:
    • 处理机管理
    • 存储器管理
    • 文件管理
    • 设备管理
  • 目标:
    • 安全、高效
  • 什么是I/O设备

“I/O” 就是 “输入/输出”(Input/Output)

I/O 设备就是可以将数据输入到计算机,或者可以接收计算机输出数据的外部设备,属于计算机中的硬件部件。

鼠标、键盘——典型的输入型设备:

显示器——输出型设备:

移动硬盘——即可输入、又可输出的设备:

UNIX系统将外部设备抽象为一种特殊的文件,用户可以使用与文件操作相同的方式对外部设备进行操作。
Write操作:向外部设备写出数据
Read操作:从外部设备读入数据


  • I/O设备的分类——按使用特性
    • 人机交互类外部设备:数据传输速度慢
      鼠标、键盘、打印机等——用于人机交互
    • 存储设备:数据传输速度快
      移动硬盘、光盘等——用于数据存储
    • 网络通信设备:数据传输速度介于上述二者之间
      调制解调器等——用于网络通信

  • I/O设备的分类——按传输速率分类
    • 低速设备
      鼠标、键盘等——传输速率为每秒几个到几百字节
    • 中速设备
      如激光打印机等——传输速率为每秒数千至上万个字节
    • 高速设备
      如磁盘等—— 传输速率为每秒数千字节至千兆字节的设备

  • I/O设备的分类——按信息交换的单位分类
    • 块设备:传输速率较高,可寻址,即对它可随机地读/写任一块
      如磁盘等——数 据传输的基本单位是“块”
    • 字符设备:传输速率较慢,不可寻址,在输入/输出时常采用中断驱动方式【一种I/O控制方式】
      鼠标、键盘 等——数据传输的基本单位 是字符。

5.1.2 I/O控制器

I/O设备由机械部件和电子部件组成

  • I/O设备的机械部件

I/O设备的机械部件主要用来执行具体I/O操作。
如我们看得见摸得着的鼠标/键盘的按钮;显示器的LED屏;移动硬盘的磁臂、磁盘盘面。

I/O设备的电子部件通常是一块插入主板扩充槽的印刷电路板。


  • I/O设备的电子部件(I/O控制器)

CPU无法直接控制I/O设备的机械部件,因此I/O设备还要有一个电子部件作为CPU和I/O设备机械部件之间的“中介”,用于实现CPU对设备的控制。

这个电子部件就是I/O控制器,又称设备控制器。CPU可控制I/O控制器,又由I/O控制器来控制设备的机械部件。

I/O控制器的功能

  1. 接受和识别CPU发出的命令
    如CPU发来的 read/write 命令,I/O 控制器中会有相应的控制寄存器来存放命令和参数
  2. 向CPU报告设备状态
    I/O控制器中会有相应的状态寄存器,用于记录I/O设备的当前状态。如:1表示空闲,0表示忙碌
  3. 数据交换
    I/O控制器中会设置相应的数据寄存器。输出时,数据寄存器用于暂存CPU发来的数据,之后再由控制器传送设备。输入时,数据寄存器用于暂存设备发来的数据,之后CPU从数据寄存器中取走数据。
  4. 地址识别
    类似于内存的地址,为了区分设备控制器中的各个寄存器,也需要给各个寄存器设置一个特定的“地址”。I/O控制器通过CPU提供的“地址”来判断CPU要读/写的是哪个寄存器。

  • I/O控制器的组成

值得注意的小细节:
① 一个I/O控制器可能会对应多个设备;
② 数据寄存器、控制寄存器、状态寄存器可能有多个(如:每个控制/状态寄存器对应一个具体的设备),且这些寄存器都要有相应的地址,才能方便CPU操作。有的计算机会让这些寄存器占用内存地址的一部分,称为内存映像I/O;另一些计算机则采用I/O专用地址,即寄存器独立编址


  • 内存映像I/O v.s. 寄存器独立编址


5.1.3 I/O控制方式

I/O控制方式,即:用什么样的方式来控制 I/O设备的数据读/写

需要注意的问题:

  1. 完成一次读/写操作的流程;
  2. CPU干预的频率;
  3. 数据传送的单位;
  4. 数据的流向;
  5. 主要缺点和主要优点。
  • 程序直接控制方式

key word:轮询
完成一次读/写操作的流程(以读操作为例)

  1. 完成一次读/写操作的流程(见下图,Key word:轮询王道书上I/O模块这里写的是I/O控制器

【问题】:将CPU寄存器中的内容写到内存中?请看以下例子

1
2
3
int a;	// a存放在内存中
scanf("%d", &a); // 输入的数据最终要放到内存中(a变量存放在内存中)
printf("%d", a); // 同理,输出的数据也存放在内存中,需要从内存取出
  1. CPU干预的频率
    很频繁,I/O操作开始之前、完成之后需要CPU介入,并且在等待I/O完成的过程中CPU需要不断地轮询检查

  2. 数据传送的单位
    每次读/写一个字

  3. 数据的流向
    读操作(数据输入):I/O设备→CPU【指的是CPU 的寄存器】→内存
    写操作(数据输出):内存→CPU→I/O设备
    每个字的读/写都需要CPU的帮助

  4. 主要缺点和主要优点
    优点:实现简单。在读/写指令之后,加上实现循环检查的一系列指令即可(因此才称为“程序直接控制方式”)
    缺点CPU和I/O设备只能串行工作,CPU需要一直轮询检查, 长期处于“忙等”状态 ,CPU利用率低。


  • 中断驱动方式

引入中断机制。由于I/O设备速度很慢,因此在CPU发出读/写命令后,可将等待I/O的进程阻塞,先切换到别的进程执行。当I/O 完成后,控制器会向CPU发出一个中断信号,CPU检测到中断信号后,会保存当前进程的运行环境信息,转去执行中断处理程序处理该中断。处理中断的过程中,CPU从I/O控制器读一个字的数据传送到CPU寄存器,再写入主存。接着,CPU恢复等待I/O的进程(或其他进程)的运行环境,然后继续执行

下图中 王道书上I/O模块这里写的是I/O控制器

注意:
① CPU会在每个指令周期的末尾检查中断;
② 中断处理过程中需要保存、恢复进程的运行环境,这个过程是需要一定时间开销的。可见,如果中断发生的频率太高,也会降低系统性能。

  1. 完成一次读/写操作的流程(见右图,Key word:中断
  2. CPU干预的频率
    每次I/O操作开始之前、完成之后需要CPU介入。
    等待I/O完成的过程中CPU可以切换到别的进程执行
  3. 数据传送的单位
    每次读/写一个字
  4. 数据的流向
    读操作(数据输入):I/O设备→CPU【指的是CPU 的寄存器】→内存
    写操作(数据输出):内存→CPU→I/O设备
  5. 主要缺点和主要优点
    优点:与“程序直接控制方式”相比,在“中断驱动方式”中,I/O控制器会通过中断信号主动报告I/O已完成,CPU不再需要不停地轮询。CPU和I/O设备可并行工作,CPU利用率得到明显提升。
    缺点:每个字在I/O设备与内存之间的传输,都需要经过CPU。而频繁的中断处理会消耗较多的CPU时间

  • DMA方式

与“中断驱动方式”相比,DMA方式( Direct Memory Access,直接存储器存取。主要用于块设备的I/O控制)有这样几个改进:
数据的传送单位是“块” 。不再是一个字、一个字的传送;
② 数据的流向是从设备直接放入内存,或者从内存直接到设备。不再需要CPU作为“快递小哥”。
③ 仅在传送一个或多个数据块的开始和结束时,才需要CPU干预。

下图中 王道书上I/O模块这里写的是I/O控制器,DMA模块写的是DMA控制器

【CPU指明此次要进行的操作(如:读操作),并说明要读入多少数据、数据要存放在内存的什么位置、数据在外部设备上的地址(如:在磁盘上的地址)】
【控制器会根据CPU提出的要求完成数据的读/写工作,整块数据的传输完成后,才向CPU发出中断信号】

DR (Data Register,数据寄存器):暂存从设备到内存,或从内存到设备的数据。
MAR (Memory Address Register,内存地址寄存器):在输入时,MAR 表示数据应放到内存中的什么 位置;输出时 MAR 表示要输出的数据放在内存中的什么位置。
DC (Data Counter,数据计数器):表示剩余要读/写的字节数。
CR(Command Register,命令/状态寄存器):用于存放CPU发来的I/O命令,或设备的状态信息。

【由图,DMA和内存之间可以直接进行读写,不再经过CPU】
【DMA其实也是一个字一个字读的,先读到DR里面,再写入内存】

  1. 完成一次读/写操作的流程)
  2. CPU干预的频率
    仅在传送一个或多个数据块的开始和结束时,才需要CPU干预。
  3. 数据传送的单位
    每次读/写一个或多个块(注意:每次读写的只能是连续的多个块, 且这些块读入内存后在内存中也必须是连续的) 【离散的块也是需要CPU多次干预的】
  4. 数据的流向(不再需要经过CPU
    读操作(数据输入):I/O设备→内存
    写操作(数据输出):内存→I/O设备
  5. 主要缺点和主要优点
    优点:数据传输以“块”为单位,CPU介入频率进一步降低。数据的传输不再需要先经过CPU再写入内存,数据传输效率进一步增加。CPU和I/O设备的并行性得到提升。
    缺点:CPU每发出一条I/O指令,只能读/写一个或多个连续的数据块。
    如果要读/写多个离散存储的数据块,或者要将数据分别写到不同的内存区域时,CPU要分别发出多条I/O指令,进行多次中断处理才能完成。

  • 通道控制方式

通道:一种硬件,可以理解为是 “弱鸡版的CPU”。通道可以识别并执行一系列通道指令
【与CPU相比,通道可以执行的指令很单一,并且通道程序是放在主机内存中的,也就是说通道与CPU共享内存】

  1. 完成一次读/写操作的流程(见下图)

  1. CPU干预的频率
    极低,通道会根据CPU的指示执行相应的通道程序,只有完成一组数据块的读/写后才需要发出中断信号,请求CPU干预。
  2. 数据传送的单位
    每次读/写一组数据块
  3. 数据的流向(在通道的控制下进行
    读操作(数据输入):I/O设备→内存
    写操作(数据输出):内存→I/O设备
  4. 主要缺点和主要优点
    缺点:实现复杂,需要专门的通道硬件支持
    优点:CPU、通道、I/O设备可并行工作,资源利用率很高

  • 知识点回顾与重要考点

难点理解:
通道=弱鸡版CPU
通道程序=任务清单

完成一次读/写的过程 CPU干预频率 每次I/O的数据传输单位 数据流向
程序直接控制方式 CPU发出I/O命令后需要不断轮询 极高 I/O设备→CPU→内存
内存→CPU→I/O设备
中断驱动方式 CPU发出I/O命令后可以做其他事,本次I/O完成后设备控制器发出中断信号 I/O设备→CPU→内存
内存→CPU→I/O设备
DMA方式 CPU发出I/O命令后可以做其他事,本次I/O完成后DMA控制器发出中断信号 I/O设备→内存
内存→I/O设备
通道控制方式 CPU发出I/O命令后可以做其他事。通道会执行通道程序以完成I/O,完成后通道向CPU发出中断信号 一组块 I/O设备→内存
内存→I/O设备

优缺点:每一个阶段的优点都是解决了上一阶段的最大缺点。 总体来说,整个发展过程就是要尽量减少CPU对I/O过程的干预,把CPU从繁杂的I/O控制事务中解脱出来,以便更多地去完成数据处理任务。


5.1.4 I/O软件层次结构

每一层会利用其下层提供的服务,实现某些功能,并屏蔽实现的具体细节,向高层提供服务(“封装思想”)

  • 用户层软件

【有的题目中也会称设备独立性软件为系统调用处理层】

Windows 操作系统向外提供的一系列系统调用,但是由于系统调用的格式严格,使用麻烦,因此在用户层上封装了一系列更方便的库函数接口供用户使用(Windows API)


  • 设备独立性软件

设备独立性软件,又称设备无关性软件。与设备的硬件特性无关的功能几乎都在这一层实现。

主要实现的功能:
① 向上层提供统一的调用接口(如 read/write 系统调用);

② 设备的保护;
原理类似与文件保护。设备被看做是一种特殊的文件,不同用户对各个文件的访问权限是不一样的,同理,对设备的访问权限也不一样。

③ 差错处理
设备独立性软件需要对一些设备的错误进行处理【差错类型太多了,基本没法考】

④ 设备的分配与回收

⑤ 数据缓冲区管理
可以通过缓冲技术屏蔽设备之间数据交换单位大小和传输速度的差异

⑥ 建立逻辑设备名到物理设备名的映射关系;根据设备类型选择调用相应的驱动程序
用户或用户层软件发出I/O操作相关系统调用的系统调用时,需要指明此次要操作的I/O设备的逻辑设备名(eg:去学校打印店打印时,需要选择打印机1/打印机2/打印机3 ,其实这些都是逻辑设备名

设备独立性软件需要通过“逻辑设备表(LUT,Logical Unit Table)”来确定逻辑设备对应的物理设备,并找到该设备对应的设备驱动程序

操作系统系统可以采用两种方式管理逻辑设备表(LUT):
第一种方式整个系统只设置一张LUT,这就意味着所有用户不能使用相同的逻辑设备名,因此这种方式只适用于单用户操作系统。
第二种方式为每个用户设置一张LUT,各个用户使用的逻辑设备名可以重复,适用于多用户操作系统。系统会在用户登录时为其建立一个用户管理进程,而LUT就存放在用户管理进程的PCB中。


  • 思考:为何不同的设备需要不同的设备驱动程序?

各式各样的设备,外形不同,其内部的电子部件(I/O控制器)也有可能不同

eg:
佳能打印机的厂家规定状态寄存器为 0 代表空闲,1代表忙碌。有两个数据寄存器
惠普打印机的厂家规定状态寄存器为 1代表空闲,0代表忙碌。有一个数据寄存器

不同设备的内部硬件特性也不同,这些特性只有厂家才知道,因此厂家须提供与设备相对应的驱动程序,CPU执行驱动程序的指令序列,来完成设置设备寄存器,检查设备状态等工作


  • 设备驱动程序

注:驱动程序一般会以一个独立进程的方式存在。如下图


  • 中断处理程序

当I/O任务完成时,I/O控制器会发送一个中断信号,系统会根据中断信号类型找到相应的中断处理程序并执行。中断处理程序的处理流程如下:


  • 知识点回顾与重要考点
层次 作用
用户层软件 实现与用户交互的接口,向上提供方便易用的库函数
设备独立性软件 ① 向上层提供统一的调用接口(如 read/write 系统调用);
② 设备的保护;
③ 差错处理;
④ 设备的分配与回收;
⑤ 数据缓冲区管理;
⑥ 建立逻辑设备名到物理设备名的映射关系;根据设备类型选择调用相应的驱动程序…
设备驱动程序 设置设备寄存器、检查设备状态
中断处理程序 进行中断处理
硬件 执行I/O操作,有机械部件、电子部件组成(参考“I/O控制器”小节的视频)

理解并记住I/O软件各个层次之间的顺序,要能够推理判断某个处理应该是在哪个层次完成的(最常考的是设备独立性软件、设备驱动程序这两层。只需理解一个特点即可:直接涉及到硬件具体细节、且与中断无关的操作肯定是在设备驱动程序层完成的;没有涉及硬件的、对各种设备都需要进行的管理工作都是在设备独立性软件层完成的


5.1.5 输入/输出应用程序接口和设备驱动程序接口

  • 输入/输出应用程序接口

显然,用户层的应用程序无法用一个统一的系统调用接口来完成所有类型设备的I/O

  • 字符设备接口
    get/put 系统调向字符设备读/写一个字符;
    基本特征:传送速率低,不可寻址,并且在输入输出时通常采用中断驱动方式。

  • 块设备接口
    read/write 系统调用:向块设备的读写指针位置读/写多个字符;
    seek系统调用:修改读写指针位置
    基本特征:传送速率高,可寻址。

  • 网络设备接口,又称“网络套接字(socket)接口
    socket = (主机IP地址,端口号)
    socket 系统调用:创建一个网络套接字,需指明网络协议 (TCP? UDP?)
    bind:将套接字绑定到某个本地“端口
    connect:将套接字连接到远程地址
    read/write:从套接字读/写数据


  • 阻塞/非阻塞 I/O

阻塞I/O:应用程序发出I/O系统调用,进程需转为阻塞态等待
eg:字符设备接口——从键盘读一个字符 get【scanf()】

非阻塞I/O:应用程序发出I/O系统调用,系统调用可迅速返回,进程无需阻寒等待
eg:块设备接口——往磁盘写数据 write


  • 设备驱动程序接口

若各公司开发的设备驱动程序接口不统一,则操作系统很难调用设备驱动程序


  • 统一标准的设备驱动程序接口

操作系统规定好设备驱动程序的接口标准,各厂商必须按要求开发设备驱动程序

不同的操作系统,对设备驱动程序接口的标准各不相同

设备厂商必须根据操作系统的接口要求,开发相应的设备驱动程序,设备才能被使用。eg:


5.2.1 I/O核心子系统

因此I/O核心子系统要实现的功能其实就是中间三层要实现的功能 (参考上节)

考研中,我们需要重点理解和掌握的功能是:I/O调度、设备保护、假脱机技术 (SPOOLing技术)、设备分配与回收、缓冲区管理(即缓冲与高速缓存)这些功能要在哪个层次实现?

  • 这些功能要在哪个层次实现?

注:假脱机技术(SPOOLing 技术)需要请求“磁盘设备”的设备独立性软件的服务,因此一般来说假脱机技术是在用户层软件实现的。但是408大纲又将假脱机技术归为“I/O核心子系统” 的功能,因此考试时还是以大纲为准


  • I/O调度

I/O调度:用某种算法确定一个好的顺序来处理各个I/O请求。

如:磁盘调度(先来先服务算法、最短寻道优先算法、SCAN算法、C-SCAN算法、LOOK算法、 C-LOOK算法)。当多个磁盘I/O请求到来时,用某种调度算法确定满足I/O请求的顺序。

同理,打印机等设备也可以用先来先服务算法、优先级算法、短作业优先等算法来确定I/O调度顺序。


  • 设备保护

操作系统需要实现文件保护功能,不同的用户对各个文件有不同的访问权限(如:只读、读和 写等)。

在UNIX系统中,设备被看做是一种特殊的文件,每个设备也会有对应的FCB。当用户请求访问某个设备时,系统根据FCB中记录的信息来判断该用户是否有相应的访问权限,以此实现“设备保护”的功能。(参考“文件保护”小节)


5.2.2 假脱机技术

  • 什么是脱机技术

手工操作阶段:主机直接从I/O设备获得数据,由于设备速度慢,主机速度很快。人机速度矛盾明显,主机要浪费很多时间来等待设备

批处理阶段引入了脱机输入/输出技术(用磁带完成):

在外围控制机的控制下, 慢速输入设备的数据先被输入到更快速的磁带上。 之后主机可以从快速的磁带上读入数据,从而缓解了速度矛盾

Tips:为什么称为“脱机”——脱离主机的控制进行的输入/输出操作。

引入脱机技术后,缓解了CPU与慢速I/O设备的速度矛盾。另一方面,即使CPU在忙碌,也可以提前将数据输入到磁带;即使慢速的输出设备1正在忙碌,也可以提前将数据输出到磁带。


  • 假脱机技术——输入井和输出井

假脱机技术”,又称“SPOOLing 技术”是用软件的方式模拟脱机技术。 SPOOLing 系统的组成如下:

在磁盘上开辟出两个 存储区域——“输入井”和“输出井

“输入井”模拟脱机输入时的磁带,用于收容I/O设备输入的数据

“输出井”模拟脱机输出时的磁带,用于收容用户进程输出的数据

“输入进程”模拟脱机输入时的外围控制机

“输出进程”模拟脱机输出时的外围控制机

注意,输入缓冲区和输出缓冲区是在内存中的缓冲区

在输入进程的控制下,“输入缓冲区”用于暂存从输入设备输入的数据,之后再转存到输入井中

在输出进程的控制下,“输出缓冲区”用于暂存从输出井送来的数据,之后再传送到输出设备上


  • 共享打印机原理分析

独占式设备——只允许各个进程串行使用的设备。一段时间内只能满足一个进程的请求。
共享设备——允许多个进程“同时”使用的设备(宏观上同时使用,微观上可能是交替使用)。可以同时满足多个进程的使用请求。

打印机是种“独占式设备”,但是可以用 SPOOLing 技术改造成“共享设备”

独占式设备的例子:若进程1 正在使用打印机,则进程2 请求使用打印机时必然阻塞等待

共享打印机原理分析如下

当多个用户进程提出输出打印的请求时,系统会答应它们的请求,但是并不是真正把打印机分配给他们, 而是由假脱机管理进程为每个进程做两件事:

(1)在磁盘输出井中为进程申请一个空闲缓冲区(也就是说,这个缓冲区是在磁盘上的),并将要打印的数据送入其中;

(2)为用户进程申请一张空白的打印请求表,并将用户的打印请求填入表中(其实就是用来说明用户的打印数据存放位置等信息的),再将该表挂到假脱机文件队列上。

当打印机空闲时,输出进程会从文件队列的队头取出一张打印请求表,并根据表中的要求将要打印的数 据从输出井传送到输出缓冲区,再输出到打印机进行打印。用这种方式可依次处理完全部的打印任务

虽然系统中只有一个台打印机,但每个进程提出打印请求时,系统都会为在输出井中为其分配一个存储区(相当于分配了一个逻辑设备),使每个用户进程都觉得自己在独占一台打印机,从而实现对打印机的共享。

SPOOLing 技术可以把一台物理设备虚拟成逻辑上的多台设备,可将独占式设备改造成共享设备


5.2.3 设备的分配与回收

  • 设备分配时应考虑的因素
    • 设备的固有属性
    • 设备的分配算法
    • 设备分配中的安全性

设备的固有属性可分为三种:独占设备、共享设备、虚拟设备。
独占设备——一个时段只能分配给一个进程(如打印机)
共享设备——可同时分配给多个进程使用(如磁盘),各进程往往是宏观上同时共享使用设备,而微观上交替使用。
虚拟设备——采用 SPOOLing 技术将独占设备改造成虚拟的共享设备,可同时分配给多个进程使用(如采用 SPOOLing 技术实现的共享打印机)

设备的分配算法
先来先服务
优先级高者优先
短任务优先
……

进程运行的安全性上考虑,设备分配有两种方式:

  • 安全分配方式:为进程分配一个设备后就将进程阻塞,本次I/O完成后才将进程唤醒。

    一个时段内每个进程只能使用一个设备
    优点:破坏了“请求和保持”条件,不会死锁
    缺点:对于一个进程来说,CPU和I/O设备只能串行工作

  • 不安全分配方式:进程发出I/O请求后,系统为其分配I/O设备,进程可继续执行,之后还可以发出新的I/O请求。只有某个I/O请求得不到满足时才将进程阻塞。(eg:考虑进程请求打印机打印输出的例子)

    一个进程可以同时使用多个设备
    优点:进程的计算任务和I/O任务可以并行处理,使进程迅速推进
    缺点:有可能发生死锁(死锁避免、死锁的检测和解除)【银行家算法……】


  • 静态分配和动态分配

静态分配:进程运行前为其分配全部所需资源,运行结束后归还资源
【破坏了“请求和保持”条件,不会发生死锁】

动态分配:进程运行过程中动态申请设备资源


  • 设备分配管理中的数据结构

“设备、控制器、通道”之间的关系:

一个通道可控制多个设备控制器,每个设备控制器可控制多个设备。

设备控制表(DCT) :系统为每个设备配置一张DCT,用于记录设备情况

注:“进程管理”章节中曾经提到过“系统会根据阻塞原因不同,将进程PCB挂到不同的阻塞队列中”

控制器控制表(COCT) :每个设备控制器都会对应一张COCT。操作系统根据COCT的信息对控制器进行操作和管理。

通道控制表(CHCT) :每个通道都会对应一张CHCT。操作系统根据CHCT的信息对通道进行操作和管理。

系统设备表(SDT) :记录了系统中全部设备的情况,每个设备对应一个表目。


  • 设备分配的步骤

① 根据进程请求的物理设备名查找SDT(注:物理设备名是进程请求分配设备时提供的参数)

② 根据SDT找到DCT,若设备忙碌则将进程PCB挂到设备等待队列中,不忙碌则将设备分配给进程。

③ 根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程。

④ 根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程

注:只有设备、控制器、通道三者都分配成功时,这次设备分配才算成功,之后便可启动I/O设备进行数据传送

【一个通道控制多个控制器, 一个控制器控制多个设备】


  • 设备分配步骤的改进

之前的缺点:
① 用户编程时必须使用“物理设备名”,底层细节对用户不透明,不方便编程
② 若换了一个物理设备,则程序无法运行
③ 若进程请求的物理设备正在忙碌,则即使系统中还有同类型的设备,进程也必须阻塞等待

改进方法:建立逻辑设备名与物理设备名的映射机制,用户编程时只需提供逻辑设备名

改进过程:
① 根据进程请求的逻辑设备名查找SDT(注:用户编程时提供的逻辑设备名其实就是“设备类型”)
② 查找SDT,找到用户进程指定类型的、并且空闲的设备,将其分配给该进程。操作系统在逻辑设备表(LUT)中新增一个表项

剩下的一样:
③ 根据DCT找到COCT,若控制器忙碌则将进程PCB挂到控制器等待队列中,不忙碌则将控制器分配给进程。
④ 根据COCT找到CHCT,若通道忙碌则将进程PCB挂到通道等待队列中,不忙碌则将通道分配给进程。

逻辑设备表(LUT)建立了逻辑设备名与物理设备名之间的映射关系

某用户进程第一次使用设备时使用逻辑设备名向操作系统发出请求,操作系统根据用户进程指定的设备类型(逻辑设备名)查找系统设备表,找到一个空闲设备分配给进程,并在 LUT中增加相应表项。

如果之后用户进程再次通过相同的逻辑设备名请求使用设备,则操作系统通过LUT表即可知道用户进程实际要使用的是哪个物理设备了,并且也能知道该设备的驱动程序入口地址

逻辑设备表的设置问题:
整个系统只有一张LUT:各用户所用的逻辑设备名不允许重复,适用于单用户操作系统
每个用户一张LUT:不同用户的逻辑设备名可重复,适用于多用户操作系统


5.2.4 缓冲区管理

  • 什么是缓冲区?有什么作用?

缓冲区是一个存储区域,可以由专门的硬件寄存器组成,也可利用内存作为缓冲区。

使用硬件作为缓冲区的成本较高,容量也较小,一般仅用在对速度要求非常高的场合(如存储器管理中所用的联想寄存器【快表】,由于对页表的访问频率极高,因此使用速度很快的联想寄存器来存放页表项的副本)

一般情况下,更多的是利用内存作为缓冲区,“设备独立性软件”的缓冲区管理就是要组织管理 好这些缓冲区

缓冲区的作用

  • 缓和CPU和I/O设备之间速度不匹配的矛盾

CPU可以把要输出的数据快速地放入缓冲区,之后就可以做别的事
慢速的I/O设备可以慢慢从缓冲区取走数据
数据输入时类似

  • 减少对CPU的中断频率,放宽对CPU中断响应时间的限制

如果是字符型设备,则每输出完一个字符就要向CPU发送一次中断信号

  • 解决数据粒度不匹配的问题

如:输出进程每次可以生成一块数据,但I/O设备每次只能输出一个字符

  • 提高CPU与I/O设备之间的并行性

  • 单缓冲

假设某用户进程请求某种块设备读入若干块的数据。若采用单缓冲的策略,操作系统会在主存中为其分配一个缓冲区(若题目中没有特别说明,一个缓冲区的大小就是一个块)。

注意:当缓冲区数据非空时,不能往缓冲区冲入数据,只能从缓冲区把数据传出;当缓冲区为空时,可以往缓冲区冲入数据,但必须把缓冲区充满以后,才能从缓冲区把数据传出

【用户进程的内存空间中,会分出一片工作区来接受输入/输出数据(一般也默认工作区大小与缓冲区相同)】

常考题型:计算每处理一块数据平均需要多久?
技巧:假定一个初始状态,分析下次到达相同状态需要多少时间,这就是处理一块数据平均所需时间。

在“单缓冲”题型中,可以假设初始状态为工作区满,缓冲区空。
[分析1]:【如上图,T M C表示的都是时间】
初始状态:工作区满,缓冲区空
假设T>C【因此CPU处理完数据后暂时不能将下一块数据传送到工作区,必须等待缓冲区中冲满数据】

所以处理一块数据的平均用时 = T+M

[分析2]:
初始状态:工作区满,缓冲区空
假设T<C【因此缓冲区中冲满数据后暂时不能继续冲入下一块数据, 必须等待CPU处理结束后将数据从缓冲区传送到工作区】

所以处理一块数据的平均用时 = C+M

结论:采用单缓冲策略,处理一块数据平均耗时 Max(C, T) + M


  • 双缓冲

假设某用户进程请求某种块设备读入若干块的数据。若采用双缓冲的策略,操作系统会在主存中为其分配两个缓冲区(若题目中没有特别说明,一个缓冲区的大小就是一个块)

双缓冲题目中,假设初始状态为:工作区空,其中一个缓冲区满,另一个缓冲区空

[分析1]:
假设T>C+M
【状态分析(缓冲区1,缓冲区2)0:(满,空)T:(空,满)2T:(满,空)】

处理一块数据的平均用时 = T

[分析2]:
假设T<C+M【没办法周期的回到初始状态了,之前的方法不适用】
假设2T<2M+C,则I/O设备将缓冲区1冲满时,缓冲区2的数据尚未取空,因此I/O设备暂时不能冲入数据,结果如下图

注:M(1) 表示“将缓冲区1中的数据传送到工作区”; M(2) 表示“将缓冲区2中的数据传送到工作区”

总之,T<C+M 意味着设备输入数据块的速度要比处理机处理数据块的速度更快。每处理一个数据块平均耗时C+M

结论:采用双缓冲策略,处理一个数据块的平均耗时为 Max(T, C+M)


  • 使用单/双缓冲在通信时的区别

两台机器之间通信时,可以配置缓冲区用于数据的发送和接受。

显然,若两个相互通信的机器只设置单缓冲区,在任一时刻只能实现数据的单向传输。如下图:

若两个相互通信的机器设置双缓冲区,则同一时刻可以实现双向的数据传输。

注:管道通信中的“管道”其实就是缓冲区。要实现数据的双向传输,必须设置两个管道


  • 循环缓冲区

将多个大小相等的缓冲区链接成一个循环队列

注:以下图示中,橙色表示已充满数据的缓冲区,绿色表示空缓冲区


  • 缓冲池

缓冲池由系统中共用的缓冲区组成。这些缓冲区按使用状况可以分为:空缓冲队列、装满输入数据的缓冲队列(输入队列)、装满输出数据的缓冲队列(输出队列)。

另外,根据一个缓冲区在实际运算中扮演的功能不同,又设置了四种工作缓冲区:用于收容输入数据的工作缓冲区(hin)、用于提取输入数据的工作缓冲区(sin)、用于收容输出数据的工作缓冲区(hout)、用于提取输出数据的工作缓冲区(sout)

原理分析:

① 输入进程请求输入数据

从空缓冲队列中取出一块作为收容输入数据的工作缓冲区(hin)。冲满数据后将缓冲区挂到输入队列队尾

② 计算进程想要取得一块输入数据

从输入队列中取得一块冲满输入数据的缓冲区作 “提取输入数据的工作缓冲区(sin)”。缓冲区读空后挂到空缓冲区队列

③ 计算进程想要将准备好的数据冲入缓冲区

从空缓冲队列中取出一块作为“收容输出数据的工作缓冲区(hout)”。数据冲满后将缓冲区挂到输出队列队尾

④ 输出进程请求输出数据

从输出队列中取得一块冲满输出数据的缓冲区作为 “提取输出数据的工作缓冲区(sout)”。缓冲区读空后挂到空缓冲区队列


5.3.1 磁盘结构

  • 磁盘、磁道、扇区

磁盘的表面由一些磁性物质组成,可以用这些磁性物质来记录二进制数据

如下图:
磁盘的盘面被划分成一个个磁道这样的一个“圈”就是一个磁道
一个磁道又被划分成一个个扇区,每个扇区就是一个“磁盘块”。各个扇区存放的数据量相同 (如1KB)
最内侧磁道上的扇区面积最小,因此数据密度最大


  • 如何在磁盘中读/写数据

需要把“磁头”移动到想要读/写的扇区所在的磁道,磁盘会转起来,让目标扇区从磁头下面划过,才能完成对扇区的读/写操作。


  • 盘面、柱面

一个盘片可能会有两个盘面【正反面】

每个盘面对应一个磁头

所有的磁头都是连在同一个磁臂上的,因此所有磁头只能“共进退“

所有盘面中相对位置相同的磁道组成柱面【下图的黄色磁道】


  • 磁盘的物理地址

可用 (柱面号,盘面号,扇区号) 来定位任意一个“磁盘块”。在“文件的物理结构”小节中,我们经常提到文件数据存放在外存中的几号块,这个块号就可以转换成(柱面号,盘面号,扇区号)的地址形式

可根据该地址读取一个“块”

  1. 根据“柱面号”移动磁臂,让磁头指向指定柱面;
  2. 激活指定盘面对应的磁头;
  3. 磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。

  • 磁盘的分类

磁头可以移动的称为活动头磁盘。磁臂可以来回伸缩来带动磁头定位磁道

磁头不可移动的称为固定头磁盘。这种磁盘中每个磁道有一个磁头【下图中别的盘面中的磁头只是没画出来

盘片可以更换的称为可换盘磁盘
盘片不可更换的称为固定盘磁盘


5.3.2 磁盘的调度算法

  • 一次磁盘读/写需要的时间

寻找时间 (寻道时间) TST_S: 在读/写数据前,将磁头移动到指定磁道所花的时间。

  1. 启动磁头臂是需要时间的。假设耗时为s;
  2. 移动磁头也是需要时间的。假设磁头匀速移动,每跨越一个磁道耗时为 m,总共需要跨越n条磁道。则:
    寻道时间 TST_S= s + m*n

延迟时间 TRT_R。: 通过旋转磁盘,使磁头定位到目标扇区所需要的时间。设磁盘转速为r (单位:转/秒,或转/分),则:
平均所需的延迟时间 TRT_R = (1/2)*(1/r) = 1/2r【1/r 就是转一圈需要的时间。找到目标扇区平均需要转半圈,因此再乘以 1/2】

传输时间 TtT_t: 从磁盘读出或向磁盘写入数据所经历的时间,假设磁盘转速为r,此次读/写的字节数为b,每个磁道上的之节数为 N。则:
传输时间TtT_t= (1/r) * (b/N) = b/(rN)
【每个磁道要可存 N 字节的数据,因此 b 字节的数据需要 b/N 个磁道才能存储。而读/写一个磁道所需的时间刚好又是转一圈所需要的时间 1/r】

总的平均存取时间 TaT_a = TsT_s + 1/2r + b/(rN)

延迟时间和传输时间与磁盘转速相关,且为线性相关。而转速是硬件的固有属性,因此操作系统也无法优化延迟时间和传输时间

但是操作系统的磁盘调度算法会直接影响寻道时间


  • 先来先服务算法(FCFS)

根据进程请求访问磁盘的先后顺序进行调度。

假设磁头的初始位置是100号磁道,有多个进程先后陆续地请求访问 55、58、39、18、90、160、150、38、184 号磁道

按照 FCFS 的规则,按照请求到达的顺序,磁头需要依次移动到55、58、39、18、90、160、150、38、184 号磁道

磁头总共移动了45+3+19+21+72+70+10+112+146 = 498 个磁道

响应一个请求平均需要移动 498/9 = 55.3 个磁道(平均寻找长度)

优点:公平;如果请求访问的磁道比较集中的话,算法性能还算过的去。

缺点:如果有大量进程竞争使用磁盘,请求访问的磁道很分散,则FCFS在性能上很差,寻道时间长。【相当于随机访问了】


  • 最短寻找时间优先 (SSTF)

SSTF 算法会优先处理的磁道是与当前磁头最近的磁道。可以保证每次的寻道时间最短,但是并不能保证总的寻道时间最短。 (其实就是贪心算法的思想,只是选择眼前最优,但是总体未必最优)

假设磁头的初始位置是100号磁道,有多个进程先后陆续地请求访问 55、58、39、18、90、160、150、38、184 号磁道

磁头总共移动了 (100-18)+(184-18) = 248 个磁道

响应一个请求平均需要移动 248/9 = 27.5个磁道(平均寻找长度)

优点:性能较好,平均寻道时间短

缺点:可能产生“饥饿”现象
Eg:本例中,如果在处理18号磁道的访问请求时又来了个38号磁道的访问请求,处理38号的访问请求时又来了一个18号磁道的访问请求。如果有源源不断的 18号、38号磁道的访问请求到来的话,150、160、184 号磁道的访问请求就永远得不到满足,从而产生“饥饿”现象。
【产生饥饿的原因在于:磁头在一个小区域内来回来去地移动】


  • 扫描算法(SCAN)

SSTF 算法会产生饥饿的原因在于:磁头有可能在一个小区域内来回来去地移动。为了防止这个问题,可以规定,只有磁头移动到最外侧磁道的时候才能往内移动,移动到最内侧磁道的时候才能往外移动。这就是扫描算法 (SCAN) 的思想。由于磁头移动的方式很像电梯,因此也叫电梯算法

假设某磁盘的磁道为 0~200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问 55、58、39、18、90、160、150、38、184号磁道

磁头总共移动了(200-100) +(200-18) = 282 个磁道

响应一个请求平均需要移动 282/9 =31.3 个磁道(平均寻找长度)

优点:性能较好,平均寻道时间较短,不会产生饥饿现象

缺点:
① 只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了184号磁道的访问请求之后就不需要再往右移动磁头了。
② SCAN算法对于各个位置磁道的响应频率不平均(如:假设此时磁头正在往右移动,且刚处理过90号磁道,那么下次处理90号磁道的请求就需要等磁头移动很长一段距离;而响应了184号磁道的请求之后,很快又可以再次响应 184 号磁道的请求了)


  • LOOK 调度算法

扫描算法(SCAN) 中,只有到达最边上的磁道时才能改变磁头移动方向,事实上,处理了184号磁道的访问请求之后就不需要再往右移动磁头了。

LOOK 调度算法就是为了解决这个问题,如果在磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向。 (边移动边观察,因此叫 LOOK)

假设某磁盘的磁道为 0~200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问 55、58、39、18、90、160、150、38、184号磁道

磁头总共移动了 (184-100)+(184-18)= 250个磁道

响应一个请求平均需要移动 250/9 = 27.5个磁道(平均寻找长度)

优点:比起 SCAN 算法来,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时间进一步缩短


  • 循环扫描算法(C-SCAN)

SCAN算法对于各个位置磁道的响应频率不平均,而 C-SAN 算法就是为了解决这个问题。规定只有磁头朝某个特定方向移动时才处理磁道访问请求,而返回时直接快速移动至起始端而不处理任何请求

假设某磁盘的磁道为 0~200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问 55、58、39、18、90、160、150、38、184 号磁道

磁头总共移动了(200-100)+(200-) +(90-0)= 390个磁道

响应一个请求平均需要移动 390/9 = 43.3 个磁道(平均寻找长度)

优点:比起SCAN 来,对于各个位置磁道的响应频率很平均。


  • C-LOOK 调度算法

C-SCAN 算法的主要缺点是只有到达最边上的磁道时才能改变磁头移动方向,并且磁头返回时不一定需要返回到最边缘的磁道上。C-LOOK 算法就是为了解决这个问题。如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。

假设某磁盘的磁道为 0~200号,磁头的初始位置是100号磁道,且此时磁头正在往磁道号增大的方向移动,有多个进程先后陆续地请求访问 55、58、39、18、90、160、150、38、184 号磁道

磁头总共移动了 (184-100) +(184-18) +(90-18)= 322 个磁道

响应一个请求平均需要移动 322/9 =35.8个磁道(平均寻找长度)

优点:比起 C-SCAN 算法来,不需要每次都移动到最外侧或最内侧才改变磁头方向,使寻道时问进一步缩短


  • 知识点回顾与重要考点

磁盘调度算法影响的指标:移动磁头的时间

若题目中无特别说明,则
SCAN 就是 LOOK,
C-SCAN 就是C-LOOK


5.3.3 减少磁盘延迟时间的方法

假设要连续读取橙色区域的 2、3、4扇区:磁头读取一块的内容(也就是一个扇区的内容)后,需要一小段时间处理,而盘片又在不停地旋转

因此,如果2、3号扇区相邻着排列,则读完2号扇区后无法连续不断地读入3号扇区,必须等盘片继续旋转,3号扇区再次划过磁头,才能完成扇区读入

结论磁头读入一个扇区数据后需要一小段时间处理,如果逻辑上相邻的扇区在物理上也相邻,则读入几个连续的逻辑扇区,可能需要很长的“延迟时间“

  • 减少延迟时间的方法:交替编号

若采用交替编号的策略,即让逻辑上相邻的扇区在物理上有一定的间隔,可以使读取连续的逻辑扇区所需要的延迟时间更小。


  • 磁盘地址结构设计

思考:为什么磁盘的物理地址是(柱面号,盘面号,扇区号),而不是 (盘面号,柱面号,扇区号)

假设某磁盘有8个柱面/磁道(假设最内侧柱面/磁道号为0),4个盘面,8个扇区。则可用3个二进制位表示柱面,2个二进制位表示盘面,3个二进制位表示扇区。

  • 若物理地址结构是(盘面号,柱面号,扇区号) ,且需要连续读取物理地址 (00,000,000) ~ (00,001,111)的扇区:

    (00,000,000) ~ (00,000,111) 转两圈可读完

    之后再读取物理地址相邻的区域,即(00,001,000) ~ (00,001,111),需要启动磁头臂将磁头移动到下一个磁道

  • 若物理地址结构是(柱面号,盘面号,扇区号) ,且需要连续读取物理地址 (000,00,000) ~ (000,01,111)的扇区:

    (000,00,000) ~ (000,00,111) 由盘面0的磁头读入数据

    之后再读取物理地址相邻的区域,即(000,01,000) ~ (000,01,111),由于柱面号/磁道号相同只是盘面号不同,因此不需要移动磁头臂。只需要激活相邻盘面的磁头即可

所以:答:读取地址连续的磁盘块时,采用(柱面号,盘面号,扇区号) 的地址结构可以减少磁头移动消耗的时间【可能考选择题】

【可以这么好,柱面号是移动磁头,移动磁头花费大;而把柱面号放在最外面,那么柱面号变化的频率最小,自然也就减少了花费】


  • 减少延迟时间的方法:错位命名

注意,所有盘面都是一起连轴转的

方案一:若相邻的盘面相对位置相同处扇区编号相同

读取完磁盘块 (000, 00, 111) 之后,需要短暂的时间处理,而盘面又在不停地转动,因此当(000, 01, 000)第一次划过磁头下方时读取数据,只能再等该扇区再次划过磁头

方案二错位命名

由于采用错位命名法,因此读取完磁盘块(000, 00,111) 之后,还有一段时间处理,当 (000, 01,000)第一次划过1号盘面的磁头下方时,就可以直接读取数据,减少了延迟时间


5.3.4 磁盘的管理

  • 磁盘初始化

Step 1:进行低级格式化(物理格式化) 【出厂之前】,将磁盘的各个磁道划分为扇区。一个扇区通常可分为 头、数据区域(如512B大小)、尾三个部分组成。管理扇区所需要的各种数据结构一般存放在头、尾两个部分,包括扇区校验码(如奇偶校验、CRC循环冗余校验码等,校验码用于校验扇区中的数据是否发生错误)

Step 2:将磁盘分区,每个分区由若干柱面组成(即分为我们熟悉的 C盘、D盘、E盘)

Step 3:进行逻辑格式化,创建文件系统。包括创建文件系统的根目录、初始化存储空间管理所用的数据结构(如位示图、空闲分区表)


  • 引导块

计算机开机时需要进行一系列初始化的工作,这些初始化工作是通过执行初始化程序 (自举程序) 完成的

初始化程序可以放在ROM (只读存储器) 中。ROM中的数据在出厂时就写入了,并且以后不能再修改

注:ROM一般是出厂时就集成在主板上的

初始化程序程序(自举程序) 放在ROM中存在什么问题?
万一需要更新自举程序,将会很不方便,因为ROM中的数据无法更改。如何解决呢?

解决:ROM中只存放很小的“自举装入程序”;
完整的自举程序放在磁盘的启动块(即引导块/启动分区)上,启动块位于磁盘的固定位置;
开机时计算机先运行“自举装入程序”,通过执行该程序就可找到引导块,并将完整的“自举程序”读入内存,完成初始化。

拥有启动分区的磁盘称为启动磁盘或系统磁盘(C:盘)


  • 坏块的管理

坏了、无法正常使用的扇区就是“坏块”。这属于硬件故障,操作系统是无法修复的。应该将坏块标记出来,以免错误地使用到它

对于简单的磁盘,可以在逻辑格式化时(建立文件系统时)对整个磁盘进行坏块检查,标明哪些扇区是坏扇区,比如:在 FAT 表上标明。 (在这种方式中,坏块对操作系统不透明)

对于复杂的磁盘,磁盘控制器(磁盘设备内部的一个硬件部件)会维护一个坏块链表。
在磁盘出厂前进行低级格式化(物理格式化)时就将坏块链进行初始化。
会保留一些“备用扇区“,用于替换坏块。这种方案称为扇区备用。且这种处理方式中,坏块对操作系统透明


5.3.5 固态硬盘SSD

计组,操作系统考研大纲新考点

操作系统:
固态硬盘;读写性能特性,磨损均衡

计算机组成原理:固态硬盘(SSD)


  • 机械硬盘 vs 固态硬盘


  • 固态硬盘的结构

每个闪存芯片由若干块组成,比如块大小:16KB~512KB

每个块再拆解为一个一个页,比如页大小,512B~4KB

系统读写以 页 为单位,磁盘中则是块(扇区),这里作区分


  • 理想情况下,固态硬盘的寿命

某固态硬盘采用磨损均衡技术,大小为2402^{40}B=1TB,闪存块的擦写寿命只有2102^{10}=1K次。某男子平均每天会对该固态硬盘写2372^{37}B=128GB数据。在最理想的情况下,这个固态硬盘可以用多久?

SSD采用磨损均衡技术,最理想情况下,SSD中每个块被擦除的次数都是完全均衡的。

1TB/128GB =8
因此,平均每8天,每个闪存块需要擦除一次。每个闪存块可以被擦除1K次,因此,经过8K天,约23年后,该固态硬盘被男子玩坏

所以没那么容易坏