文章目录
- 第1章
- 第2章
- 2.1.1 进程的定义、组成、组织方式、特征
- 2.1.2 进程的状态与转换
- 2.1.3 进程控制
- 2.1.4 进程通信
- 2.1.5 - 2.1.7 线程概念、实现和多线程模型以及转换
- 2.2.1 处理机调度的概念、层次
- 2.2.2 - 2.2.3 进程调度的时机、切换与过程调度方式
- 2.2.4 调度算法评价指标
- 2.2.5 FCFS、SJF、HRRN调度算法
- 2.2.6 - 2.2.7 RR、优先级调度算法、多级队列调度算法
- 2.3.1 进程同步、进程互斥
- 2.3.2 进程互斥的软件实现方法
- 2.3.3 进程互斥的硬件实现方法
- 2.3.4互斥锁
- 2.3.5 信号量机制
- 2.3.6 用信号量实现进程互斥、同步、前驱关系
- 2.3.6 生产者-消费者问题
- 2.3.7 多生产者-多消费者模型
- 2.3.8 吸烟者问题
- 2.3.9 读者-写者问题
- 2.3.10 哲学家进餐问题
- 2.3.11 管程
- 2.4.1 死锁的概念
- 2.4.2 死锁的处理策略——预防死锁
- 2.4.3 死锁的处理策略——避免死锁
第1章
1.1.1 操作系统的概念、功能和目标
作为用户和计算机硬件之间的接口。
提供的功能
- 命令接口
- 联机命令接口
- 脱机命令接口
- 程序接口
- GUI(图形用户界面)
- Windows
- iOS
- Android
目标
- 方便用户使用
1.1.2 操作系统的特征
并发与并行
- 并发:多个事件交替发生(宏观上同时发生、微观上交替进行)
- 并行:多个事件同时发生
共享
- 两种资源共享方式
- 互斥共享方式:一个时间段内只允许
一个进程
访问该资源 - 同时共享方式:一个时间段内允许
多个进程
“同时”访问
- 互斥共享方式:一个时间段内只允许
虚拟
概念:把一个物理上的实体变为若干个逻辑上的对应物。
- 空分复用计数
- 时分复用计数
异步
概念:在多道程序环境下,允许多个程序并发执行,但由于资源有限,进程的执行不是一贯到底的,而是走走停停的,以不可预知的速度向前推进。只有系统具有并发性,才有可能导致异步性。
1.1.3 操作系统的发展与分类
OS的发展与分类
- 手工操作阶段
- 纸带机(用户独占全机、人机速度矛盾)
- 批处理阶段——单道
- 单道批处理系统(外围机——磁带)
- 多道批处理系统(操作系统开始出现)
- 分时操作系统
- 轮流处理作业
- 不能处理紧急任务
- 实时操作系统
- 优先处理紧急任务
- 硬实时系统:必须在严格的时间内完成处理
- 软实时系统:可以偶尔犯错
- 网络操作系统
- 分布式操作系统
- 个人计算机操作系统
1.1.4 操作系统的运行机制与体系结构
OS的运行机制和体系结构
运行机制
- 两种指令
- 特权指令
- 非特权指令
- 两种处理器状态
- 核心态(root)
- 用户态
- 两种程序
- 内核程序(运行在核心态)
- 应用程序
操作系统内核
- 时钟管理(实现计时功能)
- 中断处理
- 原语(程序运行具有原子性,不可中断)
- 对系统资源进行管理的功能
- 进程管理
- 存储器管理
- 设备管理
操作系统的体系结构
- 大内核(将操作系统的主要功能模块都作为系统内核,运行在核心态)
- 微内核(只把最基本的功能保留在内核)
1.1.5 中断和异常
中断机制的诞生:操作系统介入,开展管理工作。“用户态—>核心态”是通过中断实现的,并且中断是唯一途径。
中断的概念和作用
中断的分类
- 内中断(异常)
- 陷阱(trap)
- 故障(fault)
- 中止(abort)
- 外中断(CPU外部)
外中断的处理过程
1.1.6 系统调用
概念:应用程序通过系统调用请求操作系统的服务。保证系统的稳定性和安全性。
系统调用和库函数的区别:
- 系统调用是操作系统向上层提供的接口。
- 有的库函数是对系统调用的进一步封装。
- 当今编写的应用程序大多是通过高级语言提供的库函数间接地进行系统调用。
第2章
2.1.1 进程的定义、组成、组织方式、特征
定义:
组成:PCB(进程存在唯一的标志),程序段,数据段。
组织方式:
- 链接方式,指针指向不同的队列;
- 索引方式,索引表。
特征:
- 动态性
- 并发性
- 独立性
- 异步性
- 结构性
2.1.2 进程的状态与转换
状态:
-
运行态:占有CPU,并在CPU上运行。单核只能一个进程运行,双核则可以运行两个。(CPU: √,其它资源: √)
-
就绪态:已经具备运行条件,但是没有空闲的CPU,暂时不能运行。(CPU: X,其它资源: √)
-
阻塞态:等待某个事件的发生,暂时不能运行。(CPU: X,其它资源: X)
-
创建态:创建进程控制块(PCB),程序段,数据段。
-
终止态:回收内存,程序段,数据段,撤销PCB。
在进程的大部分生命周期,都处于三种基本状态。
进程状态间的转换
2.1.3 进程控制
基本概念:
什么是进程控制?
答:实现各种进程状态转换。
如何实现进程控制?
答:用“原语”实现。
原语做的事情:
- 更新PCB中的信息
- 将PCB插入合适的队列
- 分配/回收资源
原语
实现过程中会用到关中断
和开中断
。
进程控制相关的原语:
1. 进程的创建:
- 创建原语:申请空白PCB、为新进程分配所需资源、初始化PCB、将PCB插入就绪队列
- 引起进程创建的事件:用户登录、作业调度、提供服务、应用请求
2. 进程的终止:
- 撤销原语
- 引起进程中止的事件:正常结束、异常结束、外界干预
3. 进程的阻塞:
- 阻塞原语:运行态->阻塞态
- 引起进程阻塞的事件:需要等待系统分配某种资源、需要等待相互合作的其他进程完成工作
4. 进程的唤醒:
- 唤醒原语:阻塞态->就绪态
- 引起进程唤醒的事件:等待的事件发生
5. 进程的切换
- 切换原语
- 引起进程切换的事件:当前进程事件片到、有更高优先级的进程到达、当前进程主动阻塞、当前进程终止
2.1.4 进程通信
1. 共享存储
-
分配共享空间:确保进程间的数据共享,并通过互斥(
通信进程
负责)操作确保进程访问的同步性(例如,使用P、V操作)。- 基于数据结构的共享:预先固定分配内存空间(低级方法)。
- 基于存储区的共享:动态划分存储区域(高级方法)。
2. 消息传递
-
消息组成:由消息头和消息体组成。
- 直接通信方式:消息发送者直接将消息发送给接收者。
- 间接通信方式:消息通过中间实体(如信箱)发送。
3. 管道通信(pipe)
管道是一个特殊的文件,称为管道(pipe)文件。
- 一个管道仅支持半双工通信,即某一时段内,数据仅能在一个方向上流动,不可双向同时通信,要实现双向同时通信需要两个管道。
- 需要互斥(
操作系统
负责)操作: - 管道
满
时,不能写入
数据,此时写进程
会阻塞,只要不满就可写入; - 管道为
空
时,不能读入
数据,此时读进程
会阻塞,只要不空就可读取。
因为数据一旦被读出就会在管道中消失,所以,如果有多个读进程
,可能导致错乱。有两种解决方案:
- 多个写进程,一个读进程。
- 多个写进程,多个读进程,但是系统会让各个读进程依次从管道中读取数据(
Linux
实现)。
2.1.5 - 2.1.7 线程概念、实现和多线程模型以及转换
什么是线程,为什么要引入线程?
线程是一个基本的CPU执行单元,也是程序执行流的最小单位。引入线程主要是为了提高系统的并发度。在引入线程之前,进程才是最小单位。
引入线程机制后有什么变化?
- 资源分配与调度:进程是
资源分配
的基本单位,而线程是调度
的基本单位。 - 并发性:线程可以在进程内部
并发
执行,进一步提升了系统的并发度。 - 系统开销:线程间的切换比进程间的切换开销要小,因为线程间
共享许多资源
。
线程的重要属性
- 线程是处理机调度的基本单位。
- 在多CPU计算机中,各个线程可以占用不同的CPU。
- 每个线程都有一个线程ID和线程控制块(TCB)。
- 线程有就绪、阻塞、运行三种基本状态。
- 线程几乎不拥有系统资源,主要共享其所属进程的资源。
- 同一进程的线程之间可以共享进程的资源,如内存地址空间。
- 由于共享内存地址空间,同一进程中的线程通信可以无需系统干预。
- 同一进程中的线程切换不会引起进程切换。
- 不同进程中的线程切换会引起进程切换。
- 同一进程内的线程切换开销小,而进程切换开销较大。
线程的实现方式
- 用户级线程(ULT):
- 由应用程序管理的线程。
- 用户级线程是从用户的视角可以看到的线程。
- 优点:开销低。
- 缺点:对于一个进程,一旦一个用户级线程被阻塞,所有的相关线程都无法运行。
- 内核级线程(KLT):
- 由操作系统管理的线程。
- 内核级线程是操作系统内核可以管理和调度的线程。
- 多个ULT可以映射到多个KLT上(n≥m)。
- 内核级线程是实际处理机分配的单位。
- 优点:即使有一个内核级线程被阻塞,也不会导致其它内核级线程无法运行。
- 缺点:开销大。
多线程模型
- 多对一模型:
- 多个用户级线程(ULT)映射到一个内核级线程(KLT)。
- 优点:上下文切换开销小,实现简单。
- 缺点:一个线程阻塞会导致所有线程阻塞,多核CPU利用率低。
- 一对一模型:
- 一个用户级线程映射到一个内核级线程。
- 优点:线程间不会互相影响,利用多核CPU并发能力强。
- 缺点:每个线程都需要一个内核线程,资源消耗大,上下文切换开销较高。
- 多对多模型:
- 多个用户级线程映射到多个内核级线程(n≥m)。
- 结合了上述两个模型的优点,尝试平衡开销和并发能力。
2.2.1 处理机调度的概念、层次
基本概念
由于通常进程数量大于处理机数量,需要按照一定的算法选择进程,并将处理机分配给它运行,这样可以实现进程的并发执行。
三个层次
高级调度(作业调度)
- 负责辅助外存与内存之间的调度。
- 当作业调入时,会建立相应的进程控制块(PCB),作业调出时才撤销PCB。
- 调入可以由操作系统决定,调出则通常在作业运行结束时进行。
中级调度(内存调度)
- 主要将暂时不用的进程放到外存中(PCB保留在内存),以此提高内存利用率和系统吞吐量。
- 进程进入挂起状态,并形成挂起队列。
低级调度(进程调度)
- 最基本的调度层次,频繁进行,通常每几十毫秒进行一次。
- 使用特定的算法为进程分配处理机资源。
三层调度的联系与对比
- 各层调度之间是相互配合、层层筛选的关系。
- 高级调度决定哪些作业会被加载到内存。
- 中级调度决定哪些进程会进入挂起状态,以便其他进程能更有效地使用内存资源。
- 低级调度负责实际的处理机分配,是最频繁执行的调度活动。
进程的“挂起态”
- 在七状态模型中对进程的状态进行了扩展。
- 除了基本的五状态,还包括了就绪挂起和阻塞挂起。
- 这允许系统将进程暂停运行,而不是完全结束其生命周期。
2.2_2 进程调度的时机、切换与过程调度方式
1、时机
什么时候需要进程调度?
主动放弃(进程正常终止、运行过程中发生异常而终止、进程主动请求阻塞)
被动放弃(分给进程的时间片用完、有更紧急的事需要处理、有更高优先级的进程进入就绪队列)
什么时候不能进行进程调度?
在处理中断的过程中
在操作系统内核程序临界区中
临界资源:一个时段段内各进程互斥地访问临界资源
临界区:访问临界资源的那段代码
内核程序临界区会访问就绪队列,导致其上锁
在原子操作过程中(原语)
2、切换与过程
“狭义的调度”与“进程切换”的区别
狭义:选择一个进程
广义:狭义+进程切换
进程切换的过程需要做什么?
对原来运行进程各种数据的保存
对新的进程各种数据的恢复
3、方式
非剥夺调度方式(非抢占式)
只允许进程主动放弃处理机
剥夺调度方式(抢占式)
进程被动放弃,可以优先处理紧急任务,适合分时操作系统、实时操作系统
2.2.2 - 2.2.3 进程调度的时机、切换与过程调度方式
时机
什么时候需要进程调度?
主动放弃
(进程正常终止
、发生异常终止、进程请求阻塞
)被动放弃
(时间片用完、有更紧急的事物需要处理、有更高优先级进程进入就绪队列)
什么时候不需要进程调度?
- 在处理
中断
的过程中 - 在操作系统
内核程序临界区
中临界资源
:一个时段内各进程互斥地访问临界资源临界区
:访问临界资源的代码- 内核程序临界区会访问就绪队列,导致其上锁。
- 原子操作过程中(
原语
)
切换与过程
狭义的调度
与进程切换
的区别。
狭义
:选择
一个进程广义
:狭义 + 进程切换
进程切换的过程需要做什么?
- 对
原来
运行进程各种数据的保存
- 对
新
进程各种数据的恢复
方式
非剥夺调度方式(非抢占式)
- 只允许进程
主动放弃
处理机
剥夺调度方式(抢占式)
- 进程
被动放弃
,可以优先处理紧急任务,适合分时操作系统和实时操作系统。
闲逛进程(idle)
特性:
- 优先级最低
- 可以是0地址指令,占一个完整指令周期。
- 能耗低
2.2.4 调度算法评价指标
CPU利用率
C P U 利用率 = C P U 忙碌的时间 / 总时间 CPU利用率=CPU忙碌的时间/总时间 CPU利用率=CPU忙碌的时间/总时间
系统吞吐量
系统吞吐量 = 总共完成了多少道作业 / 总共花了多少时间 系统吞吐量=总共完成了多少道作业/总共花了多少时间 系统吞吐量=总共完成了多少道作业/总共花了多少时间
周转时间
- 周转时间(从
提交
作业到完成
作业的时间)、平均周转时间(各个周转时间之和/
作业数) - 带权周转时间(作业
周转
时间/
作业实际
运行时间)、平均带权周转时间(各作业带权周转时间/
作业数)
等待时间
进程或作业等待处理机状态时间的和
- 进程等待时间:进程被创建后等待被服务的时间之和
- 作业等待时间:作业建立后的等待时间 + 作业在外存后备队列中等待的时间
响应时间
从用户提交请求到首次产生响应
所使用的时间。
2.2.5 FCFS、SJF、HRRN调度算法
先来先服务(FCFS, F)
先到达先进行服务
- 作业->后备队列;
- 进程->就绪队列
特性:
- 非抢占式
- 算法简单
- 对长作业有利、对短作业不利(短作业的
带权周转时间
很长)、不会饥饿。
短作业优先
服务时间最短
的作业优先服务- 如果服务时间相同,则先到达的被服务
非抢占式(SJF, Shortest Job First):选最短需要时间的作业进入运行态。注:如果是用于进程调度的算法,应该叫SPF(Shortest Process First)。(题目说短作业优先的话,默认是非抢占式的)
抢占式(SRTN,最短剩余时间优先):有新作业进入就绪队列,或者有作业完成了,选择队列中的最小需要时间的作业,如有需要则让新进程抢占处理机。这个算法平均等待时间
和平均周转时间
最少。
特性:
- 对
短作业
有利,对长作业
不利。 - 产生
饥饿
现象,甚至饿死(一直得不到服务)。
高响应比优先(HRRN)
综合考虑了作业/进程的等待时间
和要求服务的时间
即可作业调度,也可进程调度。
响应比 = 等待时间 + 要求服务时间 要求服务时间 响应比 = \frac{等待时间 + 要求服务时间}{要求服务时间} 响应比=要求服务时间等待时间+要求服务时间
特性:
非抢占式
- 计算所有就绪进程的
响应比
,响应比最高
的进程上处理机 - 不会
饥饿
2.2.6 - 2.2.7 RR、优先级调度算法、多级队列调度算法
这三种算法适合交互式系统
。
时间片轮转(Round-Robin)
轮流让各个进程执行一个时间片
,若未能在时间片内执行完,则剥夺处理机,将进程放在就绪队列
队尾重新排队。
常用于分时操作系统
,注重响应时间。
特性:
-
抢占式
-
由时钟装置发出时钟
中断
来通知CPU时间片已到 -
不会
饥饿
-
时间片
太大
,使得每个进程都可以在一个进程内完成,则时间片轮转算法退化为先来先服务
算法。 -
时间片
太小
,导致进程切换过于频繁,导致系统花费大量时间在切换进程上。
优先级调度算法
每个作业/进程有各自的优先级,调度时选择优先级最高的作业/进程。
就绪队列不一定只有一个。
根据优先级是否可动态改变,分为两种
静态优先级
:创建进程时确定,之后一直不变。动态优先级
:有初始优先级,但是根据情况动态调整。
特性:
- 有
抢占式
,也有非抢占式
的版本。 - 可以根据灵活调整各个进程被服务的机会。
- 如果不断有高优先级进程到来,就会导致
饥饿
。
多级反馈队列算法
设置多级队列,各级队列优先级
从高到低
,时间片从小到大
。
新进程
到达时,先进入1级队列
,按FCFS原则
排队等待被分配时间片。若用完时间片进程还未结束
,则进程进入下一级
队列队尾。如果此时已经在最下级队列,则重新放回最下级队列队尾。
只有第k级队列
为空,第k + 1级队列
才会被分配时间片。
特性:
抢占式
- 不必估计进程的运行时间(可以防止用户对运行时间造假)
- 拥有
RR
的优点(很快得到相应),也拥有SPF
的优点(短进程只用较少时间就可完成),平衡优秀。 - 可灵活调整对各类进程的偏好程度
- 会导致
饥饿
。
2.3.1 进程同步、进程互斥
进程同步
进程具有异步性
。
操作系统使用了进程同步
来满足了程序所要求的先后顺序(比如先写数据,后读数据)。
同步亦称为直接制约关系
,指为了完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调
它们的工作次序
而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
进程互斥
我们把互斥共享方式
中,也就是一个时间段只允许一个进程使用的资源成为临界资源
。很多物理设备都属于临界资源。
对临界资源的访问,必须互斥地进行。
互斥也称为间接制约关系
。进程互斥
指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。
临界区
代表正在访问临界资源
的代码。如果进入临界区,则进入区
应该设置正在访问临界资源的标志,在退出区
中,解除访问临界资源的标志。
为了实现进程互斥,需要遵循以下规则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待。当已有进程进入临界区时,其它试图进入临界区的进程必须等待。
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不饥饿)。
- 让权等待。当进程不能进入临界区时,应立即释放处理机,防止忙等待。
2.3.2 进程互斥的软件实现方法
单标志法
算法思想:
两个进程在访问完临界区
后,会把临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能由另一个进程赋予
。
该算法可以实现同一个时刻最多只允许一个进程访问临界区
。
违背空闲让进
原则。
双标志先检查法
算法思想:
设置一个布尔型数组flag[],数组中各个元素用来标记各个进程想进入临界区的意愿
,flag[0] = true代表进程0有意愿。先检查后上锁
。
主要问题:
违反了忙则等待
原则。
双标志后检查法
设置一个布尔数组flag[]来标记自己是否想要进入临界区的意愿,不过是先上锁后检查
。
可能两个同时上锁,都进不去,导致饥饿
。
Peterson算法
结合双标志法
、单标志法
的思想。主动让对方使用处理机。
谁最后进行了“主动谦让”
,则谁失去行动的优先权。
遵循空闲让进
、忙则等待
、有限等待
三个原则。
未遵循让权等待
原则。
2.3.3 进程互斥的硬件实现方法
中断屏蔽方法
利用“开/关中断指令”实现(与原语的实现思想相同)。
优点:
简单高效
缺点:
不适用于多处理机,只适用于操作系统内核。
TestAndSet指令
简称TS
指令。也有地方成为TestAndSetLock
指令,或者TSL
指令。
TSL指令用硬件实现,执行的过程不允许被中断,只能一气呵成。
TSL指令把上锁
和检查
操作用硬件的方式变成了原子操作。
优点:
实现简单,无需像软件实现那样严格检查是否有逻辑漏洞;适用于多处理机环境。
缺点:
不满足让权等待
原则,导致忙等待。
Swap指令
也有地方叫做Exchange
指令,或者简称XCHG
指令。
Swap指令是用硬件实现的,不允许中断。
逻辑与TSL并无太大区别。都是先记录下此时临界区是否已经被上锁,再上锁标记lock
为true
,最后检查old
,如果old
为false
则说明之前没有别的进程对临界区上锁,则可挑出循环,进入临界区。
优点:
实现简单,无需像软件实现那样严格检查逻辑漏洞,适合多处理机环境。
缺点:
不满足让权等待
原则。
2.3.4互斥锁
解决临界区最简单的工具就是互斥锁(mutex lock),一个进程在进入临界区时应该获得锁;在退出临界区时释放锁。当一个进程获取不可用的锁时,会被阻塞,直到锁被释放。
互斥锁的主要缺点:
忙等待,违反让权等待
。
优点:
等待期间不用切换进程上下文,多核处理器系统重,若上锁时间短,则等待代价很低。
缺点:
不太适用于单处理机系统,忙等的过程不可能解锁。
2.3.5 信号量机制
用户进程可以通过使用操作系统提供的一对原语
来对信号量
进行操作,很方便的实现了进程互斥、进程同步。
信号量
其实就是一个变量,可以用一个信号量来表示系统中某种资源的数量
。
一对原语
:
wait(S)
原语和signal(S)
原语,可以把原语理解为我们自己写的函数,函数名分别为wait何signal,括号里的信号量S
其实就是函数调用时传入的一个参数。
wait、signal原语
称为P、V操作
(来自荷兰语proberen和verhogen)。因此,做题的时候常把wait(S)
和signal(S)
两个操作分别写为P(S)
和V(S)
。
整形信号量
用一个整数型变量
作为信号量,表示系统中的某种资源的数量
。
C语言描述:
// 假设S表示当前系统中可用的打印机资源
int S = 1;
// wait原语
void wait(int S) {
while(S <= 0);
S -= 1;
}
// signal原语
void signal (int S) {
S += 1;
}
因为原语的操作一气呵成,所以避免了并发、异步导致的问题。
不满足让权等待
原则,可能陷入忙等待。
对信号量操作只有三种:
- 初始化
- P操作
- V操作
记录型信号量(非常牛逼)
因为整型信号量存在忙等待问题,因此人们使用了记录型数据结构表示信号量。
C语言代码描述:
typedef struct {
int value; // 剩余资源
Struct process *L; // 等待队列
} semaphore;
void wait(semaphore S) {
S.value--;
if (S.value < 0) {
block(S.L);
}
}
void signal(semaphore S) {
s.value++;
if (S.value <= 0) {
wakeup(S.L);
}
}
2.3.6 用信号量实现进程互斥、同步、前驱关系
1.实现进程互斥
设置互斥信号量
mutex,初值为1
对不同的临界资源需要设置不同的互斥信号量
PV必须成对出现
- 临界区之前:对信号量执行
P
操作 - 临界区之后:对信号量执行
V
操作
2.实现进程同步
保证一前一后的操作顺序
设置同步信号量
S,初始为0
在“前操作”之后对相应的同步信号量执行V(S)
在“后操作”之前对相应的同步信号量执行P(S)
3.实现进程的前驱关系
1、要为每一对前驱关系各设置一个同步信号量S
2、在“前操作”之后对相应的同步变量执行V
操作
3、在“后操作”之前对相应的同步变量执行P
操作
2.3.6 生产者-消费者问题
- 只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待
- 只有缓冲区不空时,消费者才能从中取出产品,否则必须等待
缓冲区是临界资源,各个进程互斥访问
实现互斥
的P
操作要放在实现同步
的P
操作之后
,不然会发生死锁
(生产者和消费者同时被阻塞,可能是一个在等待某种资源,另一个在等待互斥的锁被释放)。
V
操作不会导致进程发生阻塞
的状态,所以可以交换
使用操作不要放在临界区,不然并发度会降低
2.3.7 多生产者-多消费者模型
在生产-消费者问题中,如果缓冲区大小为1
,那么有可能不需要
设置互斥信号量就可以实现互斥访问缓冲区,但是如果有多个缓冲区,那么就必须设置互斥信号量以防止多个生产者造成的数据覆盖
等的问题。
解决多生产者-多消费者问题
的关键,在于理清复杂的同步关系,分析同步问题是,应该从“事件”的角度来考虑,分析事件的先后顺序
以确定同步关系。
2.3.8 吸烟者问题
解决“可以让生产多个产品的单生产者”问题提供一个思路;
若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个V操作应该放在各自对应的“事件”发生之后的位置
2.3.9 读者-写者问题
-
允许多个读者同时对文件执行读操作
-
只允许一个写者往文件中写信息
-
任一写者在完成写操作之前不允许其他读者或写者工作
-
写者执行写操作前,应让已有的读者和写者全部退出
利用mutex
这个互斥信号量,实现互斥访问count
,防止无法一气呵成地执行count
的检查
和赋值
导致的问题。
由于w
的存在,如果有写者
在等待,新的读者
就会被阻塞,这样可以解决写者
的饥饿
甚至饿死的问题。
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);
}
}
2.3.10 哲学家进餐问题
五个人,必须拿左右的筷子才能吃饭。
如果没有互斥锁,将会导致所有哲学家都只拿着一根筷子,因为死锁
全部饿死。
关键是要避免死锁发生。
解决方案:
-
可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐,这样可以保证至少有一个哲学家是可以拿到左右两只筷子的。
-
要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有其中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一只后再等待另一只的情况。
-
仅当一个哲学家左右两只筷子都可用时才允许他抓起筷子。
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]);
思考...
}
}
2.3.11 管程
1. 为什么要引入管程
PV操作容易出错、麻烦。
2. 管程的定义和基本特征
定义(组成部分):
-
局部于管程的
共享数据结构
说明 -
对该数据结构进程操作的
一组过程
-
对局部于管程的共享数据设置
初始值
的语句 -
管程有一个
名字
基本特征:
-
局部于
管程数据结构
只能被局部于管程的过程
所访问 -
一个进程只有通过调用管程内的
过程
才能进入管程访问共享数据
-
每次仅允许一个进程
在管程内执行某个内部过程
心得:相当于C++的class
,管程是数据放在private
中,函数放在public
中
拓展1:用管程解决生产者消费者问题
monitor producerconsumer
condition full,empty;
int count = 0;
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 = 生产一个产品;
producerconsumer.insert(item);
}
}
consumer(){
while(1){
item = producerconsumer.remove();
消费产品 item;
}
}
拓展2:Java中类似于管程的机制
java中用synchronized
来描述一个函数,这个函数同一时间
只能被一个线程
调用
static class monitor {
private Item buffer[] = new Item[N];
private int count = 0;
public synchronized void insert (Item item) {
...
}
}
2.4.1 死锁的概念
1. 什么是死锁
各进程互相等待对方手里的资源,导致各进程都阻塞
,无法向前推进的现象。
2.进程死锁、饥饿、死循环的区别
-
死锁
:-
定义:各进程互相等待对方手里的资源,导致各进程都
阻塞
,无法向前推进的现象。 -
区别:至少两个或两个的进程同时发生死锁
-
-
饥饿
:-
定义:由于长期得不到想要的资源,卡在
阻塞态
(得不到I/O设备),或者卡在就绪态
(得不到处理机),无法向前推进的现象。 -
区别:可能只有一个进程发生饥饿
-
-
死循环
:-
定义:某进程执行过程中一直跳不出某个循环的现象。
-
区别:死循环是程序员的问题
-
3. 死锁产生的必要条件
下面任一条件
不成立,死锁就不会发生。
- 互斥条件:
多个进程争夺
资源发生死锁 - 不剥夺条件:进程获得的资源
不能由其它进程强行抢夺
,只能主动释放。 - 请求和保持条件:某个进程
持有了至少一个资源
,还在请求资源
,但是又对自己的资源保持不释放
。 - 循环等待条件:存在资源的
循环等待链
注意:发生死锁时,一定
有循环等待;发生循环等待时,不一定
发生死锁。
4. 什么时候会发生死锁
- 对系统资源的
竞争
- 进程推进
顺序
非法 信号量
的使用不当也会造成死锁
5. 死锁的处理策略
预防
死锁。 破坏四个必要条件之一。避免
死锁。 防止系统进入不安全状态(银行家算法)。- 死锁的
检测
和解除
。 允许死锁发生,由系统检测并解除。
2.4.2 死锁的处理策略——预防死锁
1. 不允许死锁发生
-
静态策略
:预防死锁 -
破坏
互斥条件
(有些不能破坏)- 把互斥的资源改造为共享资源
-
破坏
不剥夺条件
(复杂,造成之前工作失效,降低系统开销,会全部放弃、导致饥饿)- 方案1:当请求得不到满足的时候,立即释放手里的资源
- 方案2:由系统介入,强行帮助剥夺
-
破坏
请求和保持条件
(资源利用率极低,可能会导致某些进程饥饿)- 采用
静态分配方法
,一次性全部申请,如果申请不到,不要允许
- 采用
-
破坏
循环等待条件
(不方便增加新的设备,实际使用与递增顺序不一致,会导致资源的浪费,必须按规定次序申请资源)- 顺序资源分配法:对资源编号,进程按编号递增顺序请求资源
-
动态检测
:避免死锁
2、允许死锁发生
死锁的检测和解除
2.4.3 死锁的处理策略——避免死锁
动态检测:避免死锁
什么是安全序列
:
- 进行后面的某些情况,不会使系统发生
死锁
什么是系统的不安全状态
,与死锁
有何联系:
- 如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定时在不安全状态)
如何避免系统进入不安全状态——银行家算法
初始分配完成后,优先全部分配给最少的,并且拿回资源
步骤:
-
检查此次申请是否超过了之前声明的最大需求数
-
检查此时系统剩余的可用资源是否还能满足这次请求
-
试探着分配,更改各数据结构
-
用安全性算法检查此次所分配是否会导致系统进入不安全状态