2.1 进程
进程的概念
程序:静态的,存放在磁盘里的可程序文件,一系列指令集合,被动实体
进程:动态的,一个程序的执行过程,是系统进行资源分配和调度的一个独立单位,活动实体(当一个可执行文件装入内存时,一个程序才能成为进程)
当进程被创建时,OS为进程创建一个唯一的、不重复的标识PID
PCB进程控制块:OS除了要记录PID,还要记录UID、分配的资源、运行情况等等,而这些信息都被保存在PCB中。
PCB是进程存在的唯一标志。进程被创建/结束,PCB被创建/回收。
进程映像:
- PCB
进程描述信息、进程控制和管理信息、资源分配清单、处理机相关信息 - 程序段
程序的代码 - 数据段
运行过程中产生的各种数据
PCB是给OS用的,程序段和数据段是给进程自己用的。
一个进程实体(进程映像)是由PCB、程序段、数据段组成的。
进程实体反映了一个进程在某一时刻的状态。
进程是动态的,而进程实体是静态的。
进程的特征:
- 动态性
最基本特征,进程是程序的一次执行过程,是动态的产生、变化和消亡的 - 并发性
内存中有多个进程实体,各进程可并发执行 - 独立性
进程是能独立运行、独立获得资源、独立接受调度的基本单位 - 异步性
各进程按各自独立的、不可预知的速度向前推进,会导致并发程序执行结果的不确定性,OS要提供进程同步机制来解决一部问题 - 结构性
每个进程都会配置一个PCB,结构上看,进程由PCB、程序段、数据段组成
进程状态及转换:
五个状态:
新new、就绪ready、运行running、等待/阻塞waiting/blocked、终止terminated
状态间转换条件:一次只有一个进程可在处理器上运行,但是多个进程可处于就绪或等待状态
进程状态转换——利用原语实现。
进程调度:
调度队列:
- 作业队列job queue:系统中所有的进程
- 就绪队列ready queue:驻留在内存中,就绪的等待运行的进程
- 设备队列device queue:等待IO设备的进程
链接方式(常用):
索引方式:
上下文切换:
将CPU 切换 到另一个进程需要保存当前进程的状态并恢复另一个进程的状态,这一任务称为上下文切换。
CPU 在进程间的切换由中断或系统调用引起,需要保存状态至原进程的 PCB,然后再从新进程的 PCB 中获取状态。
上下文切换时间与硬件支持密切相关
进程创建——系统调用fork():
Fork()的两个要点:
- ①内核为子进程做一个父进程的上下文拷贝。
拷贝包括:- (1)复制父进程的 PCB 作为子进程的 PCB
- (2)在新的地址空间中复制父进程的一个拷贝
- ②父进程与子进程在不同的地址空间上运行。
关于①的理解:子进程与父进程共享子进程创建前的所有资源。
关于②的理解:子进程在创建后和父进程是竞争关系,两个进程依据进程调度的规则分别执行。
总结:先继承,后分离
Fork()的返回值:
Pid=fork();
- 正确执行:父进程返回子进程号;子进程返回 0。
- 出现错误:返回-1。
进程结束:
正常情况下,
子进程结束后会有父进程回收子进程的资源。
但是有些时候在子进程结束前,父进程已经结束或是没有 wait()语句等待子进程结束,这时子进程就不可能被父进程处理了,需要操作系统出面解决问题。不同的系统有不同的处理方式:
- 有的系统不允许子进程在没有父进程的情况下继续执行
- 而 Linux 和 UNIX 会交由 1 号进程(init)作为父进程回收这类子进程
阻塞和唤醒:
成对使用
进程切换:
进程间通信IPC:
Inter Process communication:指两个进程之间产生数据交互。
进程协作的优点:
- 信息共享
- 提高运算速度
- 模块化
- 方便
进程间通信的基本模式:
- 消息传递
通过OS提供的发送&接受消息两个原语进行数据交换;- 直接通信方式——指明消息的接受者/传送者
- 间接通信方式——以信箱作为中间实体进行消息传递
- 共享内存
各个进程对共享区域的访问是互斥的 - 管道通信
单向——半双工通信;管道是一个特殊的共享文件,在内存中开辟一个大小固定的内存缓冲区;写入读出的顺序可以看成一个队列
两种方法比较:
消息传递适用于交换较少量的数据,易于实现计算机间的通信;但是由于通过系统调用实现,需要内核介入,所以速度慢
共享内存速度块,因为仅在建立共享内存区域时需要系统调用
2.2 线程
概念:
线程是 CPU 使用的基本单元,它由线程 ID,程序计数器,寄存器集合和栈组成。
它与属于同一进程的其他线程共享代码段,数据段和其他操作系统资源。
线程控制块TCB,类似于PCB
基本特征:
(1) 可拥有资源的独立单位
(2) 可独立调度和分派的基本单位
OS 中引入进程是为了使多个程序并发执行,改善资源利用率,提高系统的吞吐量;
OS 中引入线程是为了减少程序并发执行时所付出的时空开销,使 OS 具有更好的并发性。
线程–是进程的一个实体,是被系统独立调度和分派的基本单位,自己基本不拥有资源,只拥有一点在运行中必不可少的资源(程序计数器、寄存器、栈)。
多线程编程的优点:
(1) 响应度高:对一个交互程序采用多线程,即使其部分阻塞或执行较冗长的操作,该程序仍能继续执行,从而增加了对用户的响应程度
(2) 资源共享:线程默认共享它们所属进程的内存和资源
(3) 经济:创建和切换线程比较经济
(4) 多处理器体系结构的利用
进程与线程的区别与关系:
传统的 OS 中,调度和分派的基本单位是进程,拥有资源的基本单位也是进程;
引入线程的 OS 中,调度和分派的基本单位是线程,拥有资源的基本单位是进程;
同一进程中,线程的切换不会引起进程切换;不同进程中的线程之间的切换要引起进程的切换
同一进程中的多个线程具有相同的地址空间,致使他们之间的同步和通信的实现,也变得比较容易。
用户线程与内核线程:
用户级线程仅存在于用户空间中。对于这种线程的创建、 撤消、切换、线程之间的同步与通信等功能,都无须利用系统调用来实现,而是通过用户级线程库来实现。用户线程受内核支持,而无需内核管理。
内核线程由操作系统直接支持和管理。
内核级线程才是处理机分配的单位!
两者的映射模型:
当用户级线程执行时,要将线程映射到核心线程才能执行
- 多对一模型
将许多用户线程映射到同一个内核线程。 - 一对一模型
每个用户线程映射到一个内核线程。允许多个线程并行运行在多处理器系统上。 - 多对多模型
复用了许多用户线程到同样数量或更小数量的内核线程上。开发人员可以创建任意多的用户线程,并且相应内核线程能在多处理器系统上并发执行。 - 二级模型
多对多模型的基础上也允许将一个用户线程绑定到某个内核线程上
四种映射的比较:
- 多对一模型将许多用户级线程映射到一个内核线程。线程管理是由线程库在用户空间进行的,因而效率比较高。但是如果一个线程执行阻塞了系统调用,那么整个进程会阻塞。因为任意时刻只有一个线程能访问内核,多个线程不能并行运行在多处理器上——可创建任意多用户线程,不并发,不并行
- 一对一模型将每个用户线程映射到一个内核线程。它提供了比多对一模型更好的并发性,也允许多个线程能并行地运行在多处理器系统上——限制线程数量,并发,并行
- 多对多模型多路复用了许多用户线程到同样数量或更小数量的内核线程上。开发人员可以创建任意多的用户线程,并且相应内核线程能在多处理器系统上并发执行。而且,当一个线程执行阻塞系统调用时,内核能调度另一个线程来执行。——可创建任意多用户线程,并发,并行
- 二级模型在多对多模型的基础上也允许将一个用户线程绑定到某个内核线程上——可创建任意多用户线程,并发,并行
2.3 调度
三种调度程序:
- 长期调度程序(作业调度程序)
从缓冲池中选择进程,并装入内存以准备执行 - 短期调度程序(CPU调度程序)
从准备执行的进程中选择进程,并为之分配 CPU - 中期调度程序swapper
将进程从内存(或从 CPU 竞争)中移出,从而降低多道程序设计的程度;之后,进程能被重新调入内存,并从中断处继续执行。挂起状态、挂起队列
关于挂起:
挂起:外存;阻塞:内存
三种程序的区别:
长期调度程序又称为作业调度程序,从缓冲池中选择进程,并装入内存以准备执行。长期调度程序执行得并不频繁,控制多道程序设计的程度,还能平衡 IO 与 CPU的利用率。
短期调度程序又称为 CPU /进程调度程序,从准备执行的进程中选择进程,并为之分配CPU。短期调度程序要频繁地为 CPU 选择新进程,所以必须要快,它影响着 CPU 的利用率和与系统吞吐量。
中期调度程序的核心思想是能将进程从内存(或从 CPU 竞争)中移出,从而降低多道程序设计的程度。进程可换出,并在后来可被换入,一种交换(swapping)的方案。
CPU 成功调度依赖于进程执行的如下特性:进程执行由 CPU 执行和 I/O 等待周期组成,进程在这两个状态之间切换。最终,最后的 CPU 区间通过系统请求终止执行。
CPU 约束程序(CPU-bound process):I/O 请求并不频繁,花费更多的时间在计算上。
I/O 约束程序(I/O-bound process):相比于计算,花费更多的时间在 I/O 上。
进程调度时机可以发生在如下几种情况:
(1)当一个进程从运行状态切换到等待状态
(2)当一个进程从运行状态切换到就绪状态
(3)当一个进程从等待状态切换到就绪状态
(4)当一个进程结束
不能进行进程调度与切换:
- 处理中断的过程中
- 进程在内核程序临界区
- 在原子操作过程中
分派程序dispatcher:
分派程序是 CPU 调度的一个部分,它负责将 CPU 的控制交给由 CPU 调度程序选择的进程。其功能包括:
(1)切换上下文
(2)切换到用户模式
(3)跳转到用户程序的合适位置,以重新启动程序
调度的准则:
-
CPU 使用率:需要使 CPU 尽可能的忙碌。
-
吞吐量(throughput):它指一个时间单位内所完成的进程的数量。
为了使吞吐量最大化,系统要平衡 CPU 约束程序和 I/O 约束程序,不能让 CPU 一直忙碌,或者一直空闲 -
周转时间:从进程提交到进程完成的时间段称为周转时间。
周转时间 = 等待时间 + 执行时间 = 结束时间 - 进入就绪队列时间。
带权周转时间=周转时间/实际运行时间 -
等待时间:等待时间为在就绪队列中等待所花费的时间的总和。
-
响应时间:从提交请求到产生第一响应的时间。
调度算法:
先到先服务调度算法FCFS:
缺点:护航效应——当执行一个大进程时,所有的进程都要等待大进程释放CPU,导致平均等待时间较长。对长作业有利,对短作业不利。
不会导致饥饿。
最短作业优先调度算法SJF:
- 从就绪队列中调度最短 CPU 区间的的进程执行
- 如果有多个满足条件的进程则按照 FCFS 的策略调度
- 非抢占
SJF调度算法的平均等待时间是最小的,系统吞吐量增大,对短作业有利,对长作业不利。
缺点:可能造成长进程饥饿;很难知道下一个CPU区间的长度,因此很难实现。
最短剩余时间优先SRTF:
该调度方法是抢占的SJF调度算法,新进来的进程 pi 的 CPU 区间时间如果比当前正在执行的进程 pj 的剩余时间小,则发生抢占。
最高响应比优先:
综合考虑等待时间和CPU区间。
在每次调度时先计算各个进程响应比,选择最高的为其服务,非抢占!
优点:
等待时间相同,CPU区间短的优先(SJF)
CPU区间相同,等待时间长的优先(FCFS)
避免了长作业饥饿的问题!
优先级调度:
每个进程都和一个优先级对应,高优先级的进程比低优先级的进程更早得到 CPU的调度。具有相同优先级的进程,按照 FCFS 调度。
SJF 调度可以看成一种简单的优先级调度,它的优先级同 CPU 区间时间成反比。
优先级调度也会产生饥饿现象。
饥饿现象——就绪,但一直等待CPU调度。
解决方法:aging,逐渐提高在系统中已经等待很长时间的进程的优先级。
轮转法调度RR:
抢占的机制是定义一个较小的时间单元称为时间片,当分配给一个进程的时间片结束时,切换进程(抢占)。
如果进程在时间片内运行结束,那么多余的时间片回收,然后切换进程。
RR算法的性能很大程度上取决于时间片的大小:
时间片太大——接近于FCFS,增大进程响应时间;
时间片太小——进程切换频繁,系统花大量时间处理切换;
RR 的调度算法在 FCFS 基础上改进,一定程度上避免了护航效应,但是仍然存在平均等待时间较长的缺点。
多级队列调度和多级反馈队列调度:
多级队列调度算法将就绪队列分成多个独立队列,每个队列都有自己的调度算法,每个队列与更低队列相比都有绝对的优先级。
多级反馈队列调度允许进程在队列之间移动,在较低优先级队列中等待时间过长的进程会被转移到更高优先级队列,抢占式。这种形式的老化可以阻止饥饿。
可能产生饥饿的调度算法?
SJF、SRTF、优先级、多级队列、多级反馈队列
2.4 进程同步、互斥
一些概念:
(1) 原子操作 atomic operation:一个完整的没有中断的操作,要么全做要么全不做,是一种不可分操作。
这些原子操作在核心态 下运行,常驻内存
(2) 原语 primitive:一段完成一定功能的执行期间不能被中断的程序段
(3) 临界资源 critical resource:系统中可以供给多个进程使用但是同一段时间内却只允许一个进程访问的资源。当一个进程正在访问该资源时,其它欲访问的进程必须等待知道该进程访问完并释放该资源后。
(4) 临界区 critical section:程序中访问临界资源的那段代码,要将对临界资源的互斥访问转化为对临界区的互斥访问,即没有两个进程同时在临界区内执行。
(5) 竞争条件 race condition:多个进程并发访问和操作统一数据且执行结果和访问发生的特定顺序有关
(6)进程同步:同步亦指直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调他们的工作次序而产生的制约关系。
(7)进程互斥:指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前进程访问结束并释放资源后,另一个进程才能去访问
do{
entry section;//检查能否进入,若能,则需要上锁
critical section;//访问临界资源的代码
exit section;//解锁
remainder section;
}
临界区问题原则:
- 互斥 mutual exclusion:忙则等待。进程不同时在临界区内执行
- 空闲让进progress:有空让进。当无进程在临界区执行时,若有进程进入应允许
- 有限等待 bounded waiting:进程进入临界区的要求必须在有限时间内得到满足
- 让权等待 no busy waiting:等待的时候可以选择释放 CPU 执行权(非必须)
解决方案:
最简单的工具就是互斥锁,互斥锁的缺点是忙等,因此,互斥锁常用于多处理器系统。
需要连续循环忙等的互斥锁,都可称为自旋锁spin lock。如TSL指令、swap指令、单标志法。
只使用令牌turn——单标志法:
Process i
do{
while(turn != i);//令牌在对方手里,则等待
critical section//否则进入临界区
turn = j;//出临界区将令牌交给对方
reminder section//执行剩余区代码
}while(1)
Process j
do{
while(turn != j);
critical section
turn = i;
reminder section
}while(1)
可以实现互斥。
当获得令牌的那一方不执行程序的时候,没有令牌的一方即使在临界区空闲的时候也会等待,不符合 progress 规则——空闲让进
只使用登记簿flag[]——双标志法
Process i
do{
flag[i] := true;//先表明自己想要访问临界区
while(flag[j]);//如果对方也想访问,谦让
critical section//否则进入临界区
flag[i] = false;//出临界区flag改为false
reminder section//执行剩余区
}while(1)
Process j
do{
flag[j] := true
while(flag[i]);
critical section
flag[j] = false;
reminder section
}while(1)
正常情况是可以的,但是当两者几乎同时到达的时候,可能两个进程测试另一个进程已经登记,就会互相谦让,都会陷入无限循环之中,不符合 progress,且会无限等待。
使用令牌和登记簿——Peterson算法:
Process i
do{
flag[i] := true;//先表明自己想要访问临界区
turn = j; // 但把令牌先交给对方,谦让
while(flag[j] and turn == j);//如果此时对方也想访问,那就让对方访问,谦让
critical section//否则自己进入临界区
flag[i]=false;//出临界区flag改为false
reminder section
}while(1)
Process j
do{
flag[j] := true;
turn = i;
while(flag[i] and turn == i);
critical section
flag[j]=false;
reminder section
}while(1)
能够实现空闲让进、忙则等待、有限等待三个原则,但不能实现让权等待。
2、硬件同步synchronization hardware
计算机提供特殊硬件指令来解决临界区问题。
(1)中断屏蔽
利用开/关中断指令实现:
关中断
临界区
开中断
特权指令,只适用于OS内核进程,不适用于用户进程
(2)TestAndSet指令
硬件实现,执行过程不允许被中断。
bool TestAndSet(bool *lock){
bool old = *lock; //记录原来lock的值
lock = true; //无论之前是否加锁,lock都设为true
return old; //返回之前的锁状态
}
while(TestAndSet(&lock)); // 函数返回的是之前的锁状态!若为true则等待;否则跳过while并上锁
critical section;
lock = false; //执行完解锁
remainder section;
不满足让权等待
(3)swap指令
硬件实现,执行过程不允许被中断。
Swap(bool *a,bool *b){
bool temp = *a;
*a = *b;
*b = temp;
}
bool old = true;
while(old == true){
swap(&lock,&old);
}
critical section;
lock = false;
remainder section;
不满足让权等待
3、信号量semaphore
信号量其实就是一个变量,可以是整数型变量,也可以是记录型变量,表示系统中某种资源的数量。
一对原语:wait(S)和signal(S),常简称为P(S)、V(S)操作
对于整型信号量:
操作只有三种:初始化、P操作、V操作;
不满足让权等待,存在忙等。
wait(S){
while(S<=0); //忙等,依旧不满足让权等待
S--;
}
signal(S){
S++;
}
Process:
...
wait(S);
critical section;
signal(S);
...
对于记录型信号量:
value 为正数表示可用资源数;value 为负数表示等待进程数
可以看到,wait 就是对 value-1 ,同时在没有可用资源的时候将当前进程加入等待队列;
而 signal 则是 value+1 ,同时将正在等待的一个进程唤醒。
typedef struct {
int value;
struct process *L;
}semaphore;
wait(S){
S.value--;
if(S.value<0){ //说明剩余资源不够,进程进入阻塞态,并挂至S的等待队列L
block(S.L);
}
}
signal(S){
S.value++;
if(S.value >= 0){ //说明S的等待队列里有被阻塞的进程,则唤醒一个进程,从阻塞态变为运行态
wakeup(S.L);
}
}
没有剩余资源,使用block原语进行自我阻塞,主动放弃处理及,因此不会出现忙等,实现了“让权等待”
使用信号量实现进程互斥:
- 设置互斥信号量mutex,初值为1
- 进入区P(mutex)
- 退出区V(mutex)
- P、V成对出现
- 对不同的临界资源需要设置不同的互斥信号量
使用信号量实现进程同步:
目的——让并发进程按要求有序进行。
前V后P
使用信号量实现前驱关系:
4、管程
管程是一种特殊的软件模块,基本特征:
局部于管程的数据只能被局部于管程的过程所访问;
一个进程只有通过调用管程内的过程(就是入口、函数)才能进入管程访问共享数据;
每次仅允许一个进程在管程内执行某个内部过程;
利用管程解决生产者消费者问题:
2.5 经典问题
1、生产者消费者问题
缓冲池中有 n 个缓冲项,每个缓冲项能存一个数据,有多个生产者向缓冲池中送数,同时有多个消费者从缓冲池中取数。
缓冲区没满----->生产者生存
缓冲区没空----->消费者消费
临界资源-------->互斥访问
semaphore empty,full,mutex;
empty= n; //空闲缓冲区数量
full=0; //产品的数量,非空缓冲区数量
mutex=1; //互斥信号量
生产者
while(ture){
produce a product
P(empty);
P(mutex);
put it into buffer
V(mutex);
V(full);
}
消费者
while(true){
P(full);
P(mutex);
remove an item from buffer
V(mutex);
V(empty);
consume the product
}
同一信号量 P、V 必须成对出现,互斥信号量必须成对出现在一个程序中,资源信号量出现在不同程序中;
多个P操作顺序是:同步在前、互斥在后,否则会导致死锁问题。
也就是说P操作的顺序不可换,V操作无影响。
2、读者写者问题
一个数据库被多个并发进程共享,有的进程只需要读数据库;有的进程需要写数据库。
- 允许多个读者同时读
- 只允许一个写者写
- 写者在写时,不允许其他读者读和写者写
- 写者在前,必须保证其他读者和写者全部退出
对于读者,有两个比较重要:第一个进入的和最后一个离开的。第一个进入的应该拒绝写者但不能拒绝其他读者;最后一个离开的应唤醒写者。因此需要一个计数器记录读者个数。计数器被读者共享,需要互斥锁。
semaphore rw = 1; //实现读写互斥
int count = 0; //记录正在读的读者数量
semaphore mutex = 1; //对变量count互斥访问
writer:
while(1){
P(rw); //写前加锁
write to the buffer;
V(rw); //写后解锁
}
reader:
while(1){
P(mutex); //读进程需要互斥访问count
if(count == 0) //第一个进程负责对rw加锁
P(rw);
count++; //在读进程数+1
V(mutex); //解开对count的锁
read from the buffer;
P(mutex); //对count加锁
count--; //读完count--
if(count == 0) //最后一个读进程负责解开rw锁
V(rw);
V(mutex); //解锁count
}
可能会导致写者饥饿。
解决办法:再添加一个互斥信号量以实现读写公平(先来先服务):
semaphore rw = 1; //实现读写互斥
int count = 0; //记录正在读的读者数量
semaphore mutex = 1; //对变量count互斥访问
semaphore w =1;
writer:
while(1){
P(w);
P(rw); //写前加锁
write to the buffer;
V(rw); //写后解锁
V(w);
}
reader:
while(1){
P(w);
P(mutex); //读进程需要互斥访问count
if(count == 0) //第一个进程负责对rw加锁
P(rw);
count++; //在读进程数+1
V(mutex); //解开对count的锁
V(w);
read from the buffer;
P(mutex); //对count加锁
count--; //读完count--
if(count == 0) //最后一个读进程负责解开rw锁
V(rw);
V(mutex); //解锁count
}
3、哲学家就餐问题
五个哲学家用五支筷子在一个圆桌上就餐,每个哲学家需要两只筷子吃饭。
最简单的方法是分别为五支筷子设置信号量初始化为 1,每个哲学家的进程都是申请两只筷子吃饭然后释放。
semaphore chopsticks[5];
while(true){
P(chopstick[i]);
P(chopstick[i+1]);
eat;
V(chopstick[i]);
V(chopstick[i+1]);
think;
}
这样会导致死锁,也就是会出现一人拿到一只筷子的情况
解决:
-
最多允许 4 个人坐在桌上,即设置一个信号量 seat=4
-
第 5 个哲学家先申请右边的筷子,其他人先申请左边的筷子
-
第奇数个哲学家先申请左边,第偶数个先申请右边
-
使用管程
4、睡眠理发师问题
某理发店有一个接待室和一个理发室组成。理发室中有一把理发椅,接待室中有 n 把椅子。
- 若没有顾客等待理发,则理发师睡眠等待。
- 当一个顾客到达理发店后,若发现座位已满,则选择离开;
- 若发现理发师忙而接待室中有空座位,顾客则坐在椅子上等待;
- 若发现理发师正在睡眠,则将理发师唤醒。
需要整型变量 waiting=0 记录等待的顾客数
互斥锁 barber=0 记录理发师是否可以服务
互斥锁 mutex=1 实现对 waiting 的互斥访问。
初始状态是理发师在睡觉,需要被唤醒
5、吸烟者问题
一支烟需要三种材料,系统中有三个吸烟者每人各有一种材料同时有一个进程随机供应两种材料。吸烟者等待适配的材料然后抽烟。
//由于缓冲区大小为1,所以可以不专门设置互斥信号量
semaphore offer1 = 0; //组合一的数量
semaphore offer2 = 0; //组合二
semaphore offer3 = 0; //组合三
semaphore finish = 0; //抽烟是否完成
int i = 0; //实现轮流抽烟
provider:
while(1){
if(i == 0)
put offer1;
V(offer1);
else if (i == 1)
put offer2;
V(offer2);
else
put offer3;
V(offer3);
i = (i+1)%3;
P(finish);
}
smoker1:
while(1){
P(offer1);
remove offer1 from buffer and smoke;
V(finish);
}
smoker2:
while(1){
P(offer2);
remove offer2 from buffer and smoke;
V(finish);
}
smoker3:
while(1){
P(offer3);
remove offer3 from buffer and smoke;
V(finish);
}
2.6 死锁
概念:
- 死锁:一组处于等待(阻塞)状态的进程,每一个进程持有其他进程所需要的资源,而又等待使用其他进程所拥有的资源,致使这组进程互相等待,均无法向前推进。
另一种定义:当一组进程中每个进程都在等待一个事件,而这一事件只能由这一组进程的另一个进程引起时,这组进程处于死锁状态。 - 饥饿:就绪进程长时间得不到调度,处于等待状态,而不是死锁中的互相等待。若信号量的等待队列按照 LIFO 或优先级管理,则可能导致饥饿。
- 死循环:某进程执行过程中一直跳不出某个循环的现象,有时是因为bug,有时是程序员故意设计的。
区别:
死锁产生的必要条件:
必须四个条件同时满足,才会引起死锁
- mutual exclusion 互斥:至少一个资源要求互斥地共享
- hold and wait 占有并等待:一个进程至少占有一个资源并等待另一资源,该资源为其它进程所占有
- no preemption 非抢占(不可剥夺):资源不能被抢占,只能进程完成任务后自动释放
- circular wait 循环等待:互相等待形成一个环
发生死锁时一定有循环等待,但产生循环等待时不一定有死锁。
资源分配图:
资源分配图如果没有环就一定没有死锁;
如果有环且环中每种资源只有一个实例也一定是死锁;
否则就要根据四个必要条件展开讨论。
处理死锁的办法:
- 使用协议预防或避免死锁
死锁预防 prevention 是一组方法,需要确定至少一个必要条件不成立
死锁避免 avoidance 要求操作系统事先得到有关进程申请使用资源的额外信息。
当进程申请资源时,若发现满足该资源的请求可能导致死锁发生,则拒绝该申请。 - 允许进入死锁状态,检测并加以恢复
- 忽视死锁问题:大多数系统使用,因为死锁发生并不频繁,预防、避免和恢复耗费太大。
死锁预防:
一定不会发生死锁,但降低了资源利用率和系统吞吐量。
-
互斥
非共享资源必须互斥,共享资源不要求互斥所以不会死锁。无法从互斥条件下手避免死锁。 -
占有并等待
(1) 拥有不等待,资源静态分配策略,要求一个进程在执行前获得所有资源
(2) 等待不拥有,进程在申请其他资源的时候必须释放已分配的资源。
缺点:资源利用率低,分配以后可能很久不被使用;产生饥饿,对于第一种协议,如果有进程需要多个常用资源,就可能会永久等待。 -
非抢占
如果一个进程占有一些资源并在申请一些无法立刻分配到的资源,那么它占有的这些资源就都可以被抢占。该进程将会在它重新获得原有的资源以及原本要申请的资源的时候重启。
用于状态可保存和恢复的资源如 CPU,memory 等,不适用于打印机等。 -
循环等待
对所有资源类型进行完全排序,要求每个进程按递增顺序申请资源。
如哲学家就餐问题,桌上的五根筷子分别编号,所有人只能按照从小到大的顺序申请筷子,所以永远不会出现每个人获得一只筷子的情况。
缺点:不方便增加设备;进程实际使用资源的顺序可能和编号递增的顺序不一致,导致资源浪费;
死锁避免:
安全状态 safe state:
如果系统能按照某个顺序为每个进程分配资源并能避免死锁,系统状态就是安全的。或者说,如果存在一个安全序列 safe sequence<P0,P1,P2,…,Pn>使得前面的进程能够得到足够的资源完成,同时它释放的资源又能满足后面的进程的话,就是安全的。
安全序列
<
P
0
,
P
1
,
P
2
>
<P_0,P_1,P_2>
<P0,P1,P2>
有安全序列,一定没有死锁;
不安全状态不一定会导致死锁,死锁状态一定是不安全状态;
单实例——RAG资源分配图:
虚边:将要请求或可能使用 claim edge
实边:请求边和分配边 request,assignment
规定:图中如果没有环,就是安全状态;如果有环,即使环中有虚边,也是不安全状态,在避免算法里面是不允许出现的;如果出现了实边环,就是死锁。
步骤:
- 请求资源,看资源是否可用,不可用等待,可用转下一步
- 假分配看是否会出现死锁,如果不会就进行分配,如果会有死锁那么请求资源
的进程进入等待状态
多实例——银行家算法:
安全性算法:
①向量 finish[n]存储进程是否已经完成,初始状态为 false;向量 work[m]存储当前每种资源的剩余可用量,初始值为资源总量。
②寻找是否存在 finish=false 且所有资源 need≤work 的进程,如果存在,让这个进程获得所需要的资源执行结束,然后释放资源,这个时候它的 finish=true,而总的 work 也要加上该进程原来占有的资源
③循环执行②直到没有符合条件的进程。这时候如果 finish 全部为 true,那么系统处于安全状态,我们就能获得一个安全序列
资源请求:
①确定 request≤need 否则出错;request≤available,否则等待;
②按照请求假分配,修改系统当前的 available、need,然后判断假分配以后系统状态是否安全。如果安全,该分配得到允许
安全序列<P1,P3,P4,P2,P0>
死锁检测:
检测并恢复方案会有额外开销,包括维护所需信息和执行检测算法的运行开销和死锁恢复所引起的损失。
单实例:等待图
多实例:类似银行家算法
这时如果 P2 又有了一个 C 的请求,活用银行家算法可以发现无论如何 finish 向量中都会有 false,所以就检测到了死锁;
死锁检测算法与死锁避免的算法的不同在于,银行家算法是假分配,而这里是分配以后进行检测,相对而言变得简单了一些;
其实 多实例也可以利用资源分配图来解决,只是资源与进程之间不只有一条边了。
死锁恢复:
进程终止
无论采用哪种方法,系统都会回收分配给终止进程的所有资源。
(1) 终止所有死进程:代价高昂,被终止的进程都要重新计算
(2) 一次只终止一个直到死锁消失:开销大,因为需要不停调用死锁检测
资源抢占
从进程中抢占资源给其他进程用。需要处理以下三个问题:
(1) 选择一个牺牲品:代价最小化
(2) 回滚:被抢占的进程需要回滚到安全状态
(3) 饥饿:必须保证不会发生饥饿,因为有可能被抢占的总是同一个进程。
——整理自王道计算机教育、吉鹏智库