- 定义:在计算机发展史上,“进程"是为了解决什么问题而被引入的?
- 组成:每个进程由哪些部分组成?
- 组织方式:系统中各个进程之间是如何被组织起来的?
- 特征:相比于程序,进程有哪些特征?
引入多道程序技术之后:为了方便操作系统管理,完成各程序并发执行,引入了进程、进程实体的概念。
内存中同时放入多道程序,各个程序的代码、运算数据存放的位置不同。操作系统要怎么才能找到各程序的存放位置呢?
系统为每个运行的程序配置一个数据结构,称为进程控制块(PCB),用来描述进程的各种信息(如程序代码存放位置)
PCB、程序段、数据段三部分构戚了进程实体(进程映像)。
一般情况下,我们把进程实体就简称为进程,例如,所谓创建进程,实质上是创建进程实体中的 PCB;而撤销进程,实质上是撤销进程实体中的 PCB。
注意:PCB 是进程存在的唯一标志!
从不同的角度,进程可以有不同的定义,比较传统典型的定义有:
- 进程是程序的一次执行过程。
- 进程是一个程序及其数据在处理机上顺序执行时所发生的活动。
- 进程是具有独立功能的程序在数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位.
强调“动态性”。
引入进程实体的概念后,可把进程定义为:
进程是进程实体的运行过程,是操作系统进行资源分配和调度的一个独立单位。
注意:
严格来说,进程实体和进程并不一样,进程实体是静态的,进程则是动态的。
不过,除非题目专门考察二者区别,否则可以认为进程实体就是进程。
因此我们也可以说“进程由程序段、数据段、PCB 三部分组成”。
- PCB:操作系统通过 PCB 来管理进程,因此 PCB 中应该包含操作系统对其进行管理所需的各种信息
- 程序段:程序代码即存放在此
- 数据段:程序运行时使用、产生的运算数据。如全局变量、局部变量、宏定义的常量就存放在数据段内
PCB 的组成:
- 进程描述信息
- 进程标识符 PID
- 用户标识符 UID
- 进程控制和管理信息
- 进程当前状态
- 进程优先级
- 资源分配清单
- 程序段指针
- 数据段指针
- 键盘
- 鼠标
- 处理机相关信息
- 各种寄存器值
当进程切换时需要把进程当前的运行情况记录下来保存在 PCB 中,如程序计数器的值表示了当前程序执行到哪一句。
PCB 的组成(另一种说法):
- 进程标识符
- 处理机状态
- 进程调度信息
- 进程控制信息
进程的组成:
- PCB:进程的管理者(操作系统)所需的数据都在 PCB 中
- 进程描述信息
- 进程控制和管理信息
- 资源分配清单
- 处理机相关信息
- 程序段:存放要执行的代码
- 数据段:存放程序运行过程中处理的各种数据
在一个系统中,通常有数十、数百乃至数千个 PCB。为了能对他们加以有效的管理,应该用适当的方式把这些 PCB 组织起来。
注意:
进程的组成讨论的是一个进程内部由哪些部分构成的问题;
而进程的组织讨论的是多个进程之间的组织方式问题。
进程的组织方式:
- 链接方式
- 按照进程状态将 PCB 分为多个队列
- 操作系统持有指向各个队列的指针
- 索引方式
- 根据进程状态的不同,建立几张索引表
- 操作系统持有指向各个索引表的指针
- 动态性:进程是程序的一次执行过程,是动态地产生、变化和消亡的
- 并发性:内存中有多个进程实体,各进程可并发执行
- 独立性:进程是能独立运行、独立获得资源、独立接受调度的基本单位
- 异步性:各进程按各自独立的、不可预知的速度向前推进,操作系统要提供“进程同步机制"来解决异步问题
- 结构性:每个进程都会配置一个 PCB。结构上看,进程由程序段、数据段、PCB 组成
注意:
动态性是进程最基本的特征。
独立性是指进程是操作系统进行资源分配和调度的一个独立单位。
异步性有可能会导致并发程序执行结果的不确定性。
进程的状态:
- 运行状态
- 就绪状态
- 阻塞状态
- 创建状态
- 终止状态
进程状态间的转换:
- 就绪态 -> 运行态 : 获得处理机资源,进程被调度
- 运行态 -> 就绪态 :时间片到,或处理机被抢占
- 运行态 -> 阻塞态 :系统调用(进程主动行为)
- 阻塞态 -> 就绪态 :申请的资源被分配,或等待的事件发生(进程被动行为)
进程是程序的一次执行。在这个执行过程中,有时进程正在被 CPU 处理,有时又需要等待 CPU 服务,可见,进程的状态是会有各种变化。为了方便对各个进程的管理,操作系统需要将进程合理地划分为几种状态。
进程的三种基本状态:
- 运行态(Running):占有 CPU,并在 CPU 上运行
- 单核处理机环境下,每时刻最多只有一个进程处于运行态
- 双核环境下可以同时有两个进程处于运行态
- 就绪态(Ready):已经具备运行条件,但由于没有空闲 CPU,而暂时不能运行
- 进程已经拥有了除处理机之外所有需要的资源,一旦获得处理机,即可立即进入运行态开始运行
- 即:万事俱备,只欠 CPU
- 阻塞态(Waiting / Blocked,又称:等待态):因等待某一事件而暂时不能运行
- 如:等待操作系统分配打印机、等待读磁盘操作的结果
- CPU 是计算机中最昂贵的部件,为了提高 CPU 的利用率,需要先将其他进程需要的资源分配到位,才能得到 CPU 的服务
进程的另外两种状态:
- 创建态(New,又称:新建态):进程正在被创建,操作系统为进程分配资源、初始化 PCB
- 终止态(Terminated,又称:结束态):进程正在从系统中撒销,操作系统会回收进程拥有的资源、撤销 PCB
基本概念:
- 什么是进程控制
- 如何实现进程控制:用“原语”实现
进程控制相关的原语:
- 进程的创建
- 进程的终止
- 进程的阻塞
- 进程的唤醒
- 进程的切换
进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。
简化理解:反正进程控制就是要实现进程状态转换。
用原语实现进程控制。原语的特点是执行期间不允许中断,只能一气呵成。
这种不可被中断的操作即原子操作。
原语采用“关中断指令”和“开中断指令”实现。
显然,关/开中断指令的权限非常大,必然是只允许在核心态下执行的特权指令。
学习技巧:进程控制会导致进程状态的转换。无论哪个原语,要做的无非三类事情:
- 更新 PCB 中的信息(如修改进程状态标志、将运行环境保存到 PCB、从 PCB 恢复运行环境)
- 所有的进程控制原语一定都会修改进程状态标志
- 剥夺当前运行进程的 CPU 使用权必然需要保存其运行环境
- 某进程开始运行前必然要恢复期运行环境
- 将 PCB 插入合适的队列
- 分配 / 回收资源
创建原语:
- 申请空白 PCB
- 为新进程分配所需资源
- 初始化 PCB
- 将 PCB 插入就绪队列
引起进程创建的事件:
- 用户登录:分时系统中,用户登录成功,系统会建立为其建立一个新的进程决
- 作业调度:多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程
- 提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求
- 应用请求:由用户进程主动请求创建一个子进程
撤销原语:
- 从 PCB 集合中找到终止进程的 PCB
- 若进程正在运行,立即剥夺 CPU,将 CPU 分配给其他进程
- 终止其所有子进程
- 将该进程拥有的所有资源归还给父进程或操作系统
- 删除 PCB
引起进程终止的事件:
- 正常结束
- 异常结束
- 外界干预
阻塞原语:
- 找到要阻塞的进程对应的 PCB
- 保护进程运行现场,将 PCB 状态信息设置为“阻塞态",暂时停止进程运行
- 将 PCB 插入相应事件的等待队列
引起进程阻塞的事件:
- 需要等待系统分配某种资源
- 需要等待相互合作的其他进程完成工作
唤醒原语:
- 在事件等待队列中找到 PCB
- 将 PCB 从等待队列移除,设置进程为就绪态
- 将 PCB 插入就绪队列,等待被调度
引起进程唤醒的事件:
- 等待事件的发生
注意:阻塞原语、唤醒原语必须成对使用。因何事阻塞,就应由何事唤醒。
切换原语:
- 将运行环境信息存入 PCB
- PCB 移入相应队列
- 选择另一个进程执行,并更新其 PCB
- 根据 PCB 恢复新进程所需的运行环境
引起进程切换的事件:
- 当前进程时间片到
- 有更高优先级的进程到达
- 当前进程主动阻塞
- 当前进程终止
- 共享存储
- 基于数据结构的共享
- 基于存储区的共享
- 消息传递
- 直接通信方式
- 间接通信方式
- 管道通信
顾名思义,进程通信就是指进程之间的信息交换进程是分配系统资源的单位(包括内存地址空间),因此各进程拥有的内存地址空间相互独立。
为了保证安全,一个进程不能直接访问另个进程的地址空间。
但是进程之间的信息交换又是必须实现的。
为了保证进程间的安全通信,操作系统提供了一些方法。
两个进程对共享空间的访问必须是互斥的(互斥访问通过操作系统提供的工具实现)。
操作系统只负责提供共享空间和同步互斥工具(如 P、V 操作)。
基于数据结构的共享:比如共享空间里只能放个长度为 10 的数组。这种共享方式速度慢、限制多,是一种低级通信方式。
基于存储区的共享:在内存中画出一块共享存储区,数据的形式、存放位置都由进程控制,而不是操作系统。相比之下,这种共享方式速度更快,是一种高级通信方式。
“管道”是指用于连接读写进程的一个共享文件,又名 pipe 文件。其实就是在内存中开辟个大小固定的缓冲区。
- 管道只能采用半双工通信,某一时间段内只能实现单向的传输。如果要实现双向同时通信,则需要设置两个管道。
- 各进程要互斥地访问管道。
- 数据以字符流的形式写入管道,当管道写满时,写进程的 write() 系统调用将被阻塞,等待读进程将数据取走。当读进程将数据全部取走后,管道变空,此时读进程的 read() 系统调用将被阻塞。
- 如果没写满,就不允许读。如果没读空,就不允许写。
- 数据一旦被读岀,就从管道中被抛拋弃,这就意味着读进程最多只能有一个,否则可能会有读错数据的情况。
进程间的数据交换以格式化的消息(Message)为单位。进程通过操作系统提供的“发送消息/接收消息”两个原语进行数据交换。
消息头包括:发送进程 ID、接受进程 ID、消息类型、消息长度等格式化的信息(计算机网络中发送的“报文”其实就是种格式化的消息)
- 直接通信方式:消息直接挂到接收进程的消息缓冲队列上
- 间接通信方式:消息要先发送到中间实体(信箱)中,因此也称“信箱通信方式”。Eg:计网中的电子邮件系统
- 什么是线程,为什么要引入线程?
- 引入线程机制后,有什么变化?
- 线程有哪些重要的属性?
- 线程的实现方式
- 用户级线程
- 内核级线程
- 多线程模型
- 多对一模型
- 一对一模型
- 多对多模型
有的进程可能需要“同时”做很多事,而传统的进程只能串行地执行一系列程序。为此,引入了“线程”,来增加并发度。
- 传统的进程是程序执行流的最小单位
- 引入线程后,线程成为了程序执行流的最小单位
可以把线程理解为“轻量级进程”。
线程是一个基本的 CPU 执行单元,也是程序执行流的最小单位。
引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务(如 QQ 视频、文字聊天、传文件)。
引入线程后,进程只作为除 CPU 之外的系统资源的分配单元(如打印机、内存地址空间等都是分配给进程的)。
- 资源分配、调度
- 传统进程机制中,进程是资源分配、调度的基本单位
- 引入线程后,进程是资源分配的基本单位,线程是调度的基本单位
- 并发性
- 传统进程机制中,只能进程间并发
- 引入线程后,各线程间也能并发,提升了并发度
- 系统开销
- 传统的进程间并发,需要切换进程的运行环境,系统开销很大
- 线程间并发,如果是同一进程内的线程切换,则不需要切换进程环境,系统开销小
- 引入线程后,并发所带来的系统开销减小
- 线程是处理机调度的单位
- 多 CPU 计算机中,各个线程可占用不同的 CPU
- 每个线程都有一个线程 ID、线程控制块(TCB)
- 线程也有就绪、阻塞、运行三种基本状态
- 线程几乎不拥有系统资源
- 同一进程的不同线程间共享进程的资源
- 由于共享内存地址空间,同一进程中的线程间通信甚至无需系统
- 干预同一进程中的线程切换,不会引起进程切换
- 不同进程中的线程切换,会引起进程切换
- 切换同进程内的线程,系统开销很小
- 切换进程,系统开销较大
用户级线程(User-Level Thread,ULT)由应用程序通过线程库实现。
所有的线程管理工作都由应用程序负责(包括线程切换)。
用户级线程中,线程切换可以在用户态下即可完成,无需操作系统干预。
在用户看来,是有多个线程。但是在操作系统内核看来,并意识不到线程的存在。(用户级线程对用户不透明,对操作系统透明)
可以这样理解,“用户级线程”就是“从用户视角看能看到的线程”。
内核级线程的管理工作由操作系统内核完成。
线程调度、切换等工作都由内核负责,因此内核级线程的切换必然需要在核心态下才能完成。
可以这样理解,“内核级线程”就是“从操作系统内核视角看能看到的线程”。
在同时支持用户级线程和内核级线程的系统中,可采用二者组合的方式:将 n 个用户级线程映射到 m 个内核级线程上(n>=m)。
操作系统只“看得见”内核级线程,因此只有内核级线程才是处理机分配的单位。
多对一模型:多个用户级线程映射到一个内核级线程。每个用户进程只对应一个内核级线程。
优点:
- 用户级线程的切换在用户空间即可完成,不需要切换到核心态
- 线程管理的系统开销小,效率高
缺点:
- 当一个用户级线程被阻塞后,整个进程都会被阻塞,并发度不高
- 多个线程不可在多核处理机上并行运行
一对一模型:一个用户及线程映射到一个内核级线程。每个用户进程有与用户级线程同数量的内核级线程。
优点:
- 当一个线程被阻塞后,别的线程还可以继续执行,并发能力强
- 多线程可在多核处理机上并行执行
缺点:
- 一个用户进程会占用多个内核级线程
- 线程切换由操作系统内核完成,需要切换到核心态
- 线程管理的成本高,开销大。
多对多模型:n 用户及线程映射到 m 个内核级线程(n>=m)。每个用户进程对应 m 个内核级线程。
克服了多对一模型并发度不高的缺点,又克服了一对一模型中一个用户进程占用太多内核级线程,开销太大的缺点。
- 基本概念
- 三个层次
- 高级调度(作业调度)
- 中级调度(内存调度)(引出“七状态”模型)
- 低级调度(进程调度)
- 三层调度的联系、对比
- 补充知识
- 进程的挂起态
- “七状态”模型
当有一堆任务要处理,但由于资源有限,这些事情没法冋时处理。这就需要确定某种规则来决定处理这些任务的顺序,这就是“调度”研究的问题。
在多道程序系统中,进程的数量往往是多于处理机的个数的,这样不可能同时并行地处理各个进程。
处理杋调度,就是从就绪队列中按照一定的算法选择一个进程并将处理机分配给它运行,以实现进程的并发执行。
由于内存空间有限,有时无法将用户提交的作业全部放入内存,因此就需要确定某种规则来决定将作业调入内存的顺序。
高级调度(作业调度)。按一定的原则从外存上处于后备队列的作业中挑选一个(或多个)作业,给他们分配内存等必要资源,并建立相应的进程(建立 PCB),以使它(们)获得竞争处理机的权利。
高级调度是辅存(外存)与内存之间的调度。每个作业只调入一次,调出一次。
作业调入时会建立相应的 PCB,作业调出时才撤销 PCB。
高级调度主要是指调入的问题,因为只有调入的时机需要操作系统来确定,但调出的时机必然是作业运行结束才调出。
引入了虚拟存储技术之后,可将暂时不能运行的进程调至外存等待。等它重新具备了运行条件且内存又稍有空闲时,再重新调入内存。
这么做的目的是为了提高内存利用率和系统吞吐量。
暂时调到外存等待的进程状态为挂起状态。值得注意的是,PCB 并不会一起调到外存,而是会常驻内存。
PCB 中会记录进程数据在外存中的存放位置,进程状态等信息,操作系统通过内存中的 PCB 来保持对各个进程的监控、管理。
被挂起的进程 PCB 会被放到的挂起队列中。
中级调度(内存调度),就是要决定将哪个处于挂起状态的进程重新调入内存。
一个进程可能会被多次调出、调入内存,因此中级调度发生的频率要比高级调度更高。
暂时调到外存等待的进程状态为挂起状态(挂起态,suspend)。
挂起态又可以进一步细分为就绪挂起、阻塞挂起两种状态
注意:
“挂起”和“阻塞”的区别,
两种状态都是暂时不能获得 CPU 的服务,
但挂起态是将进程映像调到外存去了,
而阻塞态下进程映像还在内存中。
有的操作系统会把就绪挂起、阻塞挂起分为两个挂起队列。
甚至会根据阻塞原因不同再把阻塞挂起进程进一步细分为多个队列。
低级调度(进程调度),其主要任务是按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。
进程调度是操作系统中最基本的一种调度,在一般的操作系统中都必须配置进程调度。
进程调度的频率很高,一般几十毫秒一次。
- 高级调度(作业调度)
- 按照某种规则,从后备队列中选择合适的作业将其调入内存,并为其创建进程
-
$外存 \rightarrow 内存$ (面向作业) - 频率最低
$无 \rightarrow 创建态 \rightarrow 就绪态$
- 中级调度(内存调度)
- 按照某种规则,从挂起队列中选择合适的进程将其数据调回内存
-
$外存 \rightarrow 内存$ (面向进程) - 频率中等
$挂起态 \rightarrow 就绪态(阻塞挂起 \rightarrow 阻塞态)$
- 低级调度(进程调度)
- 按照某种规则,从就绪队列中选择一个进程为其分配处理机
$内存 \rightarrow CPU$ - 频率最高
$就绪态 \rightarrow 运行态$
- 时机
- 什么时候需要进程调度(引出进程调度的方式)
- 什么时候不能进行进程调度
- 切换与过程
- “狭义的调度”与“切换"的区别
- 进程切换的过程需要做什么?
- 方式
- 非剥夺调度方式(非抢占式)
- 剥夺调度方式(抢占式)
进程调度(低级调度),就是按照某种算法从就绪队列中选择一个进程为其分配处理机。
需要进行进程调度与切换的情况:
- 当前运行的进程主动放弃处理机
- 进程正常终止
- 运行过程中发生异常而终止
- 进程主动请求阻塞(如等待 I/O)
- 当前运行的进程被动放弃处理机
- 分给进程的时间片用完
- 有更紧急的事需要处理(如 I/O 中断)
- 有更高优先级的进程进入就绪队列
不能进行进程调度与切换的情况:
- 在处理中断的过程中。中断处理过程复杂,与硬件密切相关,很难做到在中断处理过程中进行进程切换
- 进程在操作系统内核程序临界区中。
- 在原子操作过程中(原语)。原子操作不可中断,要一气呵成(如之前讲过的修改 PCB 中进程状态标志,并把 PCB 放到相应队列)
进程在操作系统内核程序临界区中不能进行调度与切换。(正确)
(2012 年联考真题)进程处于临界区时不能进行处理机调度。(错误)
- 临界资源:一个时间段内只允许一个进程使用的资源。各进程需要互斥地访问临界资源。
- 临界区:访问临界资源的那段代码。
内核程序临界区一般是用来访问某种内核数据结构的,比如进程的就绪队列(由各就绪进程的 PCB 组成)
如果某个进程还没退出内核程序临界区(还没解锁)就进行进程调度,但是进程调度相关的程序也需要访问就绪队列,但此时就绪队列被锁住了,因此又无法顺利进行进程调度。
内核程序临界区访问的临界资源如果不尽快释放的话,极有可能影响到操作系统内核的其他管理工作。因此在访问内核程序临界区期间不能进行调度与切换。
在打印机打印完成之前,进程一直处于临界区内,临界资源不会解锁。但打印机又是慢速设备,此时如果一直不允许进程调度的话就会导致 CPU 一直空闲。
普通临界区访问的临界资源不会直接影响操作系统内核的管理工作。因此在访问普通临界区时可以进行调度与切换。
- 非剥夺调度方式,又称非抢占方式
- 只允许进程主动放弃处理机。在运行过程中即便有更紧迫的任务到达,当前进程依然会继续使用处理机,直到该进程终止或主动要求进入阻塞态
- 实现简单,系统开销小但是无法及时处理紧急任务
- 适合于早期的批处理系统
- 剥夺调度方式,又称抢占方式
- 当一个进程正在处理机上执行时,如果有一个更重要或更紧迫的进程需要使用处理机,则立即暂停正在执行的进程,将处理机分配给更重要紧迫的那个进程
- 可以优先处理更紧急的进程,也可实现让各进程按时间片轮流执行的功能(通过时钟中断)
- 适合于分时操作系统、实时操作系统
“狭义的进程调度”与“进程切换”的区别:
狭义的进程调度指的是从就绪队列中选中一个要运行的进程。(这个进程可以是刚刚被暂停执行的进程,也可能是另一个进程,后一种情况就需要进程切换)
进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程。
广义的进程调度包含了选择一个进程和进程切换两个步骤。
进程切换的过程主要完成了:
- 对原来运行进程各种数据的保存
- 对新的进程各种数据的恢复(如:程序计数器、程序状态字、各种数据寄存器等处理杋现场信息,这些信息一般保存在进程控制块)
注意:进程切换是有代价的,因此如果过于频繁的进行进程调度、切换,必然会使整个系统的效率降低,使系统大部分时间都花在了进程切换上,而真正用于执行进程的时间减少。
- CPU 利用率
- 系统吞吐量
- 周转时间
- 周转时间、平均周转时间
- 带权周转时间、平均带权周转时间
- 等待时间
- 响应时间
对于计算机用户来说,会希望自己的提交的请求(比如通过键盘输入了一个调试命令)尽早地开始被系统服务、回应。
响应时间,指从用户提交请求到首次产生响应所用的时间。
- 先来先服务(FCFS)
- 短作业优先(SJF)
- 高响应比优先(HRRN)
Tips:各种调度算法的学习思路:
- 算法思想
- 算法规则
- 这种调度算法是用于作业调度还是进程调度?
- 抢占式?非抢占式?
- 优点和缺点
- 是否会导致饥饿:某个进程/作业长期得不到服务
优点:
- 公平、算法实现简单
缺点:
- 排在长作业(进程)后面的短作业需要等待很长时间,带权周转时间很大,对短作业来说用户体验不好。即 FCFS 算法对长作业有利,对短作业不利
优点:
- “最短的”平均等待时间、平均周转时间
缺点:
- 不公平。对短作业有利,对长作业不利。可能产生饥饿现象
- 作业/进程的运行时间是由用户提供的,并不一定真实,不一定能做到真正的短作业优先
注意几个小细节:
- 如果题目中未特别说明,所提到的“短作业/进程优先算法”默认是非抢占式的
- 很多书上都会说“SJ 调度算法的平均等待时间、平均周转时间最少”
- 严格来说,这个表述是错误的,不严谨的。之前的例子表明,最短剩余时间优先算法得到的平均等待时间、平均周转时间还要更少
- 应该加上一个条件“在所有进程同时可运行时,采用 SJF 调度算法的平均等待时间、平均周转时间最少
- 或者说“在所有进程都几乎同时到达时,采用 SJF 调度算法的平均等待时间、平均周转时间最少”
- 如果不加上述前提条件,则应该说“抢占式的短作业/进程优先调度算法(最短剩余时间优先,SRNT 算法)的平均等待时间、平均周转时间最少
- 虽然严格来说,SJF 的平均等待时间、平均周转时间并不一定最少,但相比于其他算法(如 FCFS), s 依然可以获得较少的平均等待时间、平均周转时间
- 如果选择题中遇到“SF 算法的平均等待时间、平均周转时间最少”的选项,那最好判断其他选项是不是有很明显的错误,如果没有更合适的选项,那也应该选择该选项
- 综合考虑了等待时间和运行时间(要求服务时间)
- 等待时间相同时,要求服务时间短的优先(SJF 的优点)
- 要求服务时间相同时,等待时间长的优先(FCFS 的优点)
- 对于长作业来说,随着等待时间越来越久,其响应比也会越来越大,从而避免了长作业饥饿的问题
注:这几种算法主要关心对用户的公平性、平均周转时间、平均等待时间等评价系统整体性能的指标,但是不关心“响应时间”,也并不区分任务的紧急程度,因此对于用户来说,交互性很糟糕。
因此这三种算法一般适合用于早期的批处理系统,当然,FCFS 算法也常结合其他的算法使用,在现在也扮演着很重要的角色。
而适合用于交互式系统的调度算法将在下个小节介绍。
- 时间片轮转调度算法(RR,Round-Robin)
- 优先级调度算法
- 多级反馈队列
Tips:各种调度算法的学习思路:
- 算法思想,为了解决什么问题
- 算法规则
- 这种调度算法是用于作业调度还是进程调度?
- 抢占式?非抢占式?
- 优点和缺点
- 是否会导致饥饿
优点:
- 公平
- 响应快,适用于分时操作系统
缺点:
- 由于高频率的进程切换,因此有一定开销
- 不区分任务的紧急程度
如果时间片太大,使得每个进程都可以在一个时间片内就完成,则时间片轮转调度算法退化为先来先服务调度算法,并且会增大进程响应时间。因此时间片不能太大。
另一方面,进程调度、切换是有时间代价的(保存、恢复运行环境),因此如果时间片太小,会导致进程切换过于频繁,系统会花大量的时间来处理进程切换,从而导致实际用于进程执行的时间比例减少。可见时间片也不能太小。
一般来说,设计时间片时要让切换进程的开销占比不超过 1%。
优点:
- 用优先级区分紧急程度、重要程度,适用于实时操作系统
- 可灵活地调整对各种作业/进程的偏好程度。
缺点:
- 若源源不断地有高优先级进程到来,则可能导致饥饿
根据优先级是否可以动态改变,可将优先级分为静态优先级和动态优先级两种。
- 静态优先级:创建进程时确定,之后一直不变
- 动态优先级:创建进程时有一个初始值,之后会根据情况动态地调整优先级
进程优先级设置通常:
- 系统进程优先级高于用户进程
- 前台进程优先级高于后台进程
- 操作系统更偏好 I/O 型进程(或称 I/O 繁忙型进程)
补充:与 I/O 型进程相对的是计算型进程(或称 CPU 繁忙型进程)。
- 对各类型进程相对公平(FCFS 的优点)
- 每个新到达的进程都可以很快就得到响应(RR 的优点)
- 短进程只用较少的时间就可完成(SPF 的优点)
- 不必实现估计进程的运行时间(避免用户作假)
- 可灵活地调整对各类进程的偏好程度,比如 CPU 密集型进程、I/O 密集型进程
拓展:可以将因 I/O 而阻塞的进程重新放回原队列,这样 I/O 型进程就可以保持较高优先级。
同步步亦称直接制约关系。
它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
对临界资源的访问,必须互斥地进行。
互斥,亦称间接制约关系。
进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界赍源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后,另一个进程才能去访问临界资源。
do {
entry section; // 进入区
critical section; // 临界区
exit section; // 退出区
remainder section; // 剩余区
} while(true);
- 进入区:负责检查是否可进入临界区,若可进入,则应设置正在访问临界资源的标志(可理解为“上锁”),以阻止其他进程同时进入临界区
- 临界区:访问临界资源的那段代码
- 退出区:负责解除正在访问临界资源的标志(可理解为“解锁”)
- 剩余区:做其他处理
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区
- 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
- 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待
- 单标志法
- 双标志先检查
- 双标志后检查
- Peterson 算法
学习提示:
- 理解各个算法的思想、原理
- 结合上小节学习的“实现互斥的四个逻辑部分”,重点理解各算法在进入区、退出区都做了什么
- 分析各算法存在的缺陷(结合“实现互斥要遵循的四个原则”进行分析)
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。
- 中断屏蔽方法
- TestAndSet 指令(TS 指令/TSL 指令)
- swap 指令(XCHG 指令)
学习提示:
- 理解各方法的原理
- 了解各方法的优缺点
进程互斥的四种软件实现方式:
- 单标志法
- 双标志先检査
- 双标志后检査
- Peterson 算法
进程互斥的三种硬件实现方式:
- 中断屏蔽方法
- TS/TSL 指令
- Swap/XCHG 指令
复习回顾+思考:之前学习的这些进程互斥的解决方案分别存在哪些问题?
- 在双标志先检查法中,进入区的“检査”、“上锁”操作无法一气呵成,从而导致了两个进程有可能同时进入临界区的问题
- 所有的解决方案都无法实现“让权等待”
1965 年,荷兰学者 Dijkstra 提出了一种卓有成效的实现进程互斥、同步的方法:信号量机制。
- 整型信号量
- 记录型信号量
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为 1 的信号量。
原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语是由关中断/开中断指令实现的。软件解决方案的主要问题是由“进入区的各种操作无法一气呵成”,因此如果能把进入区、退出区的操作都用“原语”实现,使这些操作能“一气呵成”就能避免问题。
对原语:wait(S) 原语和 signal(S) 原语,可以把原语理解为我们自己写的函数,函数名分别为 wait 和 signal,括号里的信号量 S 其实就是函数调用时传入的一个参数。
wait、signal 原语常简称为 P、V 操作(来自荷兰语 proberen 和 verhogen)。因此,做题的时候常把 Wat(S)、signal(S)两个操作分别写为 P(S)、V(S)。
用一个整数型的变量作为信号量,用来表示系统中某种资源的数量。
与普通整数变量的区别对信号量的操作只有三种:
-
初始化
-
P 操作
-
V 操作
-
“检查”和“上锁”一气呵成,避免了并发、异步导致的问题
-
存在的问题:不满足“让权等待”原则,会发生“忙等”
整型信号量的缺陷是存在“忙等”问题,因此人们又提岀了“记录型信号量”,即用记录型数据结构表示的信号量。
// 记录型信号量的定义
typedef struct {
int value;
struct process *L;
} semaphore;
如果剩余资源数不够,使用 block 原语使进程从运行态进入阻塞态,并把挂到信号量 S 的等待队列(即阻塞队列)中。
// 某进程需要使用资源时,通过 wait 原语申请
void wait(semaphore S) {
S.value--;
if (S.value < 0) {
block(S.L);
}
}
释放资源后,若还有别的进程在等待这种资源,则使用 wakeup 原语唤醒等待队列中的个进程,该进程从阻塞态变为就绪态。
// 使用完资源后,通过 signal 原语申请释放
void signal(semaphore S) {
s.value++;
if (S.value <= 0) {
wakeup(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 原语唤醒等待队列中的第一个进程(被唤醒进程从阻塞态 -> 就绪态)。
// 信号量机制实现进程互斥
semaphore mutex=1;
P1() {
// ...
P(mutex)
// 临界区代码
V(mutex)
// ...
}
P2() {
// ...
P(mutex)
// 临界区代码
V(mutex)
// ...
}
- 分析并发进程的关键活动,划定临界区
- 设置互斥信号量
mutex
,初值为1
- 在临界区之前执行
P(mutex)
- 在临界区之后执行
V(mutex)
注意:
对不同的临界资源需要设置不同的互斥信号量。
P、V 操作必须成对出现。
- 分析什么地方需要实现“同步关系”,明确“一前一后”的逻辑
- 设置同步信号量
S
,初始为0
- 在“前操作”之后执行
V(S)
- 在“后操作”之前执行
P(S)
- 要为每一对前驱关系各设置一个同步变量
- 在“前操作”之后对相应的同步变量执行 V 操作
- 在“后操作”之前对相应的同步变量执行 P 操作
- 生产者、消费者共享一个初始为空、大小为 n 的缓冲区
- 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待
- 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待
- 缓冲区是临界资源,各进程必须互斥地访问
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
- 整理思路。根据各进程的操作流程确定 P、V 操作的大致顺序。
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。
- 互斥信号量初值一般为 1
- 同步信号量的初始值要看对应资源的初始值是多少
// 互斥信号量,实现对缓冲区的互斥访问
semaphore mutex = 1;
// 同步信号量,表示空闲缓冲区的数量
semaphore empty = n;
// 同步信号量,表示产品的数量,也即非空缓冲区的数量
semaphore full = 0;
producer() {
while(1) {
// 生产一个产品
P(empty);
P(mutex);
// 把产品放入缓冲区
V(mutex);
V(full);
}
}
consumer() {
while(1) {
P(full);
P(mutex);
// 缓冲区取出一个产品
V(mutex);
V(empty);
// 使用产品
}
}
- 实现互斥的 P 操作一定要在实现同步的 P 操作之后
- V 操作不会导致进程阻塞,因此两个 V 操作可以相互交换
互斥关系:
- 对缓冲区(盘子)的访问要互斥地进行(
mutex=1
)
同步关系:
- 父亲将苹果放入盘子后,女儿才能取苹果(
apple=0
) - 母亲将橘子放入盘子后,儿子才能取橘子(
orange=0
) - 只有盘子为空时,父亲或母亲才能放入水果(
plate=1
)
// 实现互斥访问盘子(缓冲区)
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);
// 吃掉橘子
}
}
// 盘子中有几个苹果
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);
// 吃掉橘子
}
}
结论:即使不设置专门的互斥变量 mutex,也不会出现多个进程同时访可盘子的现象。
原因在于:本题中的缓冲区大小为 1,在任何时刻,apple、 orange、 plate 三个同步信号量中最多只有一个是 1。
因此在任何时刻,最多只有一个进程的 P 操作不会被阻塞,并顺利地进入临界区。
如果盘子的大小为 2 的话...
// 盘子中有几个苹果
semaphore apple = 0;
// 盘子中有几个橘子
semaphore orange = 0;
// 盘子中还可以放多少个水果,或还有几个盘子可以用来放水果
semaphore plate = 2;
会出现了两个进程同时访问缓冲区的情况,有可能导致两个进程写入缓冲区的数据相互覆盖的情况。
因此,如果缓冲区大小大于 1,就必须专门设置一个互斥信号量 mutex 来保证互斥访问缓冲区。
总结:在生产者-消费者问题中,如果缓冲区大小为 1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析。
建议:在考试中如果来不及仔细分析,可以加上互斥信号量,保证各进程一定会互斥地访问缓冲区。
但需要注意的是,实现互斥的 P 操作一定要在实现同步的 P 操作之后,否则可能引起“死锁”。
PV 操作题目的解题思路:
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系
- 整理思路。根据各进程的操作流程确定 P、V 操作的大致顺序
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值
- 互斥信号量初值一般为 1
- 同步信号量的初始值要看对应资源的初始值是多少
正确的分析方法应该从“事件”的角度来考虑,我们可以把上述四对“进程行为的前后关系”抽象为对“事件的前后关系”。
- “盘子变空事件”既可由儿子引发,也可由女儿引发
- “放水果事件”既可能是父亲执行,也可能是母亲执行
这样的话,就可以用一个同步信号量解决问题了
![抽烟者问题描述]](images/smoker-question1.jpg)
互斥关系:
- 桌子可以抽象为容量为 1 的缓冲区,要互斥访问(
mutex=1
)
应该将在桌子上的两件物品看成一个组合
- 组合一:纸 + 胶水
- 组合二:烟草 + 胶水
- 组合三:烟草 + 纸
同步关系:
- 桌上有组合一 -> 第一个抽烟者取走东西(
offer1=0
) - 桌上有组合二 -> 第二个抽烟者取走东西(
offer2=0
) - 桌上有组合三 -> 第三个抽烟者取走东西(
offer3=0
) - 发出完成信号 -> 供应者将下一个组合放到桌上(
finish=0
)
// 桌子上组合一的数量
semaphore offer1 = 0;
// 桌子上组合二的数量
semaphore offer2 = 0;
// 桌子上组合三的数量
semaphore offer3 = 0;
// 抽烟是否完成
semaphore finish = 0;
// 用于实现三个抽烟者轮流抽烟
int i = 0;
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);
// 卷烟,抽掉
}
}
缓冲区大小为 1,同一时刻,四个同步信号量中至多有一个的值为 1。
因此,不需要一个专门的互斥信号量。
吸烟者问题可以为我们解决“可以生产多个产品的单生产者”问题提供一个思路。
值得吸取的精华是:“轮流让各个吸烟者吸烟”必然需要“轮流的在桌上放上组合一、二、三”,注意体会我们是如何用一个整型变量 i 实现这个“轮流”过程的。
如果题目改为“每次随机地让一个吸烟者吸烟”,我们有应该如何用代码写出这个逻辑呢?
若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个 V 操作应该放在各自对应的“事件”发生之后的位置。
- 允许多个读者可以同时对文件执行读操作
- 只允许一个写者往文件中写信息
- 任一写者在完成写操作之前不允许其他读者或写者工作
- 写者执行写操作前,应让已有的读者和写者全部退出
两类进程:
- 写进程
- 读进程
互斥关系:
- 写进程-写进程
- 写进程-读进程
读进程-读进程不互斥。
// 用于实现对文件的互斥访问,表示当前是否有进程在访问共享文件
semaphore rw = 1;
// 记录当前有几个读进程在访问文件
int count = 0;
// 用于保证对count变量的互斥访问
semaphore mutex = 1;
writer() {
while(1) {
P(rw);
// 写文件
V(rw);
}
}
reader() {
while(1) {
P(mutex);
if(count==0)
P(rw);
count++;
V(mutex);
// 读文件
P(mutex);
count--;
if(count==0)
v(rw);
V(mutex);
}
}
可以设置另一个互斥信号量来保证各读进程对 count 的访问是互斥的。
潜在的问题:只要有读进程还在读,写进程就要一直阻塞等待,可能“饿死”。因此,这种算法中,读进程是优先的。
// 用于实现对文件的互斥访问,表示当前是否有进程在访问共享文件
semaphore rw = 1;
// 记录当前有几个读进程在访问文件
int count = 0;
// 用于保证对count变量的互斥访问
semaphore mutex = 1;
// 用于实现“写优先”
semaphore w = 1;
writer() {
while(1) {
P(w);
P(rw);
// 写文件
V(rw);
V(w);
}
}
reader() {
while(1) {
P(w);
P(mutex);
if(count==0)
P(rw);
count++;
V(mutex);
V(w);
// 读文件
P(mutex);
count--;
if(count==0)
v(rw);
V(mutex);
}
}
结论:
- 在这种算法中,连续进入的多个读者可以同时读文件
- 写者和其他进程不能同时访问文件
- 写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则
有的书上把这种算法称为“读写公平法”。
读者写者问题为我们解决复杂的互斥问题提供了一个参考思路。
其核心思想在于设置了一个计数器 count 用来记录当前正在访问共享文件的读进程数。我们可以用 count 的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。
另外,对 count 变量的检査和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
最后,还要认真体会我们是如何解决“写进程饥饿”问题的。
绝大多数的考研 PV 操作大题都可以用之前介绍的几种生产者-消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。
philosopher[5] = {0, 1, 2, 3, 4};
chopstick[5] = {1, 1, 1, 1, 1};
对于哲学家
- 左边的筷子编号为
$i$ - 右边的筷子编号为
$(i+1) % 5$
P1() {
while(1) {
P(philosopher[i]);
P(philosopher[(i+1)%5]);
// 吃饭
V(philosopher[i]);
V(philosopher[(i+1)%5]);
// 思考
}
}
- 思路一:最多只允许 4 个哲学家同时进餐
- 思路二:奇数哲学家先拿左边,偶数哲学家先拿右边
- 思路三:仅当左右两边都可用时才允许抓起筷子
philosopher[5] = {0, 1, 2, 3, 4};
chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1;
P1() {
while(1) {
P(mutex);
P(philosopher[i]);
P(philosopher[(i+1)%5]);
V(mutex);
// 吃饭
V(philosopher[i]);
V(philosopher[(i+1)%5]);
// 思考
}
}
哲学家进餐问题的关键在于解决进程死锁。
这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁”问题的隐患。
如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。
可以参考哲学家就餐问题解决死锁的三种思路。
- 为什么要引入管程
- 管程的定义和基本特征
- 拓展 1:用管程解决生产者消费者问题
- 拓展 2:Java 中类似于管程的机制
管程是一种特殊的软件模块,有这些部分组成:
- 局部于管程的共享数据结构说明
- 对该数据结构进行操作的一组过程
- 对局部于管程的共享数据设置初始值的语句
- 管程有一个名字
Tips:“过程”其实就是“函数”。
- 局部于管程的数据只能被局部于管程的过程所访问
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据
- 每次仅允许一个进程在管程内执行某个内部过程
monitor ProducerConsumer
// 条件变量用来实现同步
condition full, empty;
// 缓冲区中的产品数
int count=0;
// 把产品 item 放入缓冲区
void insert(Item item) {
if(count == N)
wait(full);
count++;
insert_item(item);
if(count == 1)
signal(empty);
}
// 从缓冲区取出一个产品
Item remove() {
if(count == 0)
wait(empty);
count--;
if(count == N-1)
signal(full);
return remove_item();
}
end monitor;
// 生产者进程
producer() {
while(1) {
item = Item();
ProducerConsumer.insert(item);
}
}
// 消费者进程
Consumer() {
while(1) {
item = ProducerConsumer.remove();
// 消费 item
}
}
引入管程的目的无非就是要更方便地实现进程互斥和同步。
- 需要在管程中定义共享数据(如生产者消费者问题的缓冲区)
- 需要在管程中定义用于访问这些共享数据的“入口”一一其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品)
- 只有通过这些特定的“入口”才能访问共享数据
- 管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证个时间段内最多只会有一个进程在访问缓冲区。注意:这种互压特性是由编译器负责实现的,程序员不用关心)
- 可在管程中设置条件变量及等待/唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒
程序员可以用某种特殊的语法定乂一个管程(比如:monitor ProducerConsumer…… end monitor;之后其他程序员就可以使用这个管程提供的特定“入口”很方便地使用实现进程同步/互斥了。
synchronized
关键字
static class monitor {
private Item buffer[] = new Item[];
private int count = 0;
public synchronized void insert(Item item) {
// ...
}
}
- 什么是死锁
- 进程死锁、饥饿、死循环的区别
- 死锁产生的必要条件
- 什么时候会发生死锁
- 死锁的处理策略
在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,寻致各进程都阻塞,都无法向前推进的现象,就是“死锁”发生死锁后若无外力干涉,这些进程都将无法向前推进。
共同点:都是进程无法顺利向前推进的现象(故意设计的死循环除外)
- 死锁
- 各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进的现象。
- 死锁一定是“循环等待对方手里的资源”导致的,因此如果有死锁现象,那至少有两个或两个以上的进程同时发生死锁。
- 发生死锁的进程一定处于阻塞态。
- 饥饿
- 由于长期得不到想要的资源,某进程无法向前推进的现象。
- 比如:在短进程优先(SPF)算法中,若有源源不断的短进程到来,则长进程将一直得不到处理机,从而发生长进程“饥饿”。
- 可能只有一个进程发生饥饿。
- 发生饥饿的进程既可能是阻塞态(如长期得不到需要的 I/O 设备),也可能是就绪态(长期得不到处理机)
- 死循环
- 某进程执行过程中一直跳不出某个循环的现象。
- 有时是因为程序逻辑 bug 导致的,有时是程序员故意设计的。
- 可能只有一个进程发生死循环。
- 死循环的进程可以上处理机运行(可以是运行态),只不过无法像期待的那样顺利推进。
区别和联系:
- 死锁和饥饿问题是由于操作系统分配资源的策略不合理导致的
- 死循环是由代码逻辑的错误导致的
- 死锁和饥饿是管理者(操作系统)的问题
- 死循环是被管理者的问题
产生死锁必须同时满足一下四个条件,只要其中任一条件不成立,死锁就不会发生。
- 互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。
- 不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
注意:
发生死锁时一定有循环等待,但是发生循环等待时未必死锁。
循环等待是死锁的必要不充分条件。
如果同类资源数大于 1,则即使有循环等待,也未必发生死锁。
但如果系统中每类资源都只有个,那循环等待就是死锁的充分必要条件了。
- 对系统资源的竞争。各进程对不可剥夺的资源(如打印机)的竞争可能引起死锁,对可剥夺的资源(CPU)的竞争是不会引起死锁的。
- 进程推进顺序非法。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程 P1、P2 分别申请并占有了资源 R1、R2,之后进程 P1 又紧接着申请资源 R2,而进程 P2 又申请资源 R1 两者会因为申请的资源被对方占有而阻塞,从而发生死锁。
- 信号量的使用不当也会造成死锁。如生产者-消费者问题中,如果实现互斥的 P 操作在实现同步的 P 操作之前,就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源)
总之,对不可剥夺资源的不合理分配,可能导致死锁。
- 不允许死锁发生
- 静态策略:预防死锁。破坏死锁产生的四个必要条件中的一个或几个。
- 动态策略:避免死锁。用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)
- 允许死锁发生
- 死锁的检测和解除。允许死锁的发生,不过操作系统会负责检测出死锁的发生,然后采取某种措施解除死锁。
- 破坏互斥条件
- 破坏不剥夺条件
- 破坏请求和保持条件
- 破坏循环等待条件
互斥条件:只有对必须互斥使用的资源的争抢才会导致死锁。
如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如:SPOOLing 技术。
操作系统可以采用 SPOOLing 技术把独占设备在逻辑上改造成共享设备。比如,用 SPOOLing 技术将打印机改造为共享设备...
该策略的缺点:并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时候都无法破坏互斥条件。
不剥夺条件:进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放破坏不剥夺条件:
- 方案一:当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条
- 方案二:当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程使用)
该策略的缺点:
- 实现起来比较复杂。
- 释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如 CPU。
- 反复地申请和释放资源会增加系统开销,降低系统吞吐量。
- 若采用方案一,意味着只要暂时得不到某个资源,之前获得的那些资源就都需要放弃,以后再重新申请。如果一直发生这样的情况,就会导致进程饥饿。
请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放。
可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源。
该策略实现起来简单,但也有明显的缺点:
有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。
循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。
可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递増的顺序请求资源,同类资源(即编号相同的资源)一次申请完。
原理分析:一个进程只有已占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已持有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。
该策略的缺点:
- 不方便增加新的设备,因为可能需要重新分配所有的编号
- 进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费
- 必须按规定次序申请资源,用户编程麻烦
- 什么是安全序列
- 什么是系统的不安全状态,与死锁有何联系
- 如何避免系统进入不安全状态——银行家算法
所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。
如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。
如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)
因此可以在资源分配之前预先判断这次分配是否会导致系统进入不安全状态,以此决定是否答应资源分配请求。这也是“银行家算法”的核心思想。
系统处于安全状态,不可能发生死锁。
系统处于不安全状态,有可能发生死锁。
银行家算法的流程。
银行家算法步骤:
- 检査此次申请是否超过了之前声明的最大需求数
- 检査此时系统剩余的可用资源是否还能满足这次请求
- 试探着分配,更改各数据结构
- 用安全性算法检查此次分配是否会导致系统进入不安全状态
安全性算法步骤:
- 检査当前的剩余可用资源是否能满足某个进程的最大需求
- 如果可以,就把该进程加入安全序列,并把该进程持有的资源全部回收
- 不断重复上述过程,看最终是否能让所有进程都加入安全序列
系统处于不安全状态未必死锁,但死锁时一定处于不安全状态。系统处于安全状态一定不会死锁。
- 死锁的检测
- 死锁的解除
为了能对系统是否已发生了死锁进行检测,必须:
- 用某种数据结构来保存资源的请求和分配信息
- 提供一种算法,利用上述信息来检测系统是否已进入死锁状态
如果按上述过程分析,最终能消除所有边,就称这个图是可完全简化的。此时一定没有发生死锁(相当于能找到一个安全序列)。
如果最终不能消除所有边,那么此时就是发生了死锁。
最终还连着边的那些进程就是处于死锁状态的进程。
死锁定理:如果某时刻系统的资源分配图是不可完全简化的,那么此时系统死锁。
一旦检测出死锁的发生,就应该立即解除死锁。
补充:
并不是系统中所有的进程都是死锁状态。
用死锁检测算法化简资源分配图后,还连着边的那些进程就是死锁进程。
解除死锁的主要方法有:
- 资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿
- 撤销进程法(或称终止进程法)。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。
- 进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。
进程处理策略:
- 进程优先级
- 已执行多长时间
- 还要多久能完成
- 进程已经使用了多少资源
- 进程是交互式的还是批处理式的