1 进程与线程
1.1 进程的概念和特征
引入进程:更好的使多道程序并发执行,提供资源利用率和系统吞吐量
进程控制块PCB:使参与并发执行的每个程序(含数据)都能独立运行,为之配置一个专门的数据结构
系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程
进程实体/进程映像:程序段,相关数据段和PCB组成,是静态的,但是进程是动态的
创建进程:实质是创建进程实体中的PCB
撤销进程:实质是撤销进程的PCB
PCB是进程存在的唯一标志
进程的定义:(1)进程是程序的一次执行过程
(2)进程是一个程序及其数据在处理机上顺序执行时所发生的活动
(3)进程是具有独立功能的程序在一个数据集合上运行的过程,是系统进行资源分噢诶和调度的一个独立单位
特征:(1)动态性
(2)并发性
(3)独立性
(4)异步性
1.2 进程的状态和转换
进程的状态:(1)运行态
(2)就绪态
(3)阻塞态
(4)创建态
(5)终止态
进程状态的转换:
1.3 进程的组成
进程控制块PCB
PCB中保存了进程的现行状态及优先级;处理机状态信息;程序和数据的内存始址;与其他进程合作是也需要访问PCB;进程暂停时也需要保存信息到PCB中
进程的整个生命周期系统总是通过PCB对进程进行控制,只有通过PCB才能感知进程的存在
为了方便管理,需要把进程的PCB用适当的方法组织起来
组织方式:
(1)链接方式:同一状态的PCB链接成一个队列
(2)索引方式:把同一状态的进程组织在一个索引表中,索引表的表项指向相应的PCB,不同状态对应不同的索引表
程序段
程序代码段,程序可被多个进程共享,即多个进程运行同一程序
数据段
是进程对应的程序加工处理的原始数据或程序执行时产生的中间或最终结果
1.4 进程控制
一般把进程控制用的程序段称为原语
进程的创建
一个进程创建另一个进程,创建者称为父进程,被创建的是子进程
子进程可以继承父进程拥有的资源,子进程撤销时,应归还资源;撤销父进程也会同时撤销所有子进程
创建原语:
(1)为新进程分配一个唯一的进程标识号,并申请一个空白PCB
(2)为进程分配运行所需的资源
(3)初始化PCB
(4)进程就绪队列能接纳新进程,就插入就绪队列
进程的终止
引起终止的事件:正常结束;异常结束;外界干预
终止原语:
(1)根据进程的标识符检索出PCB,读出进程的状态
(2)若处于运行状态,立即终止,将处理机资源分配给其他进程
(3)若还有子孙进程,将所有子孙进程终止
(4)将进程的全部资源归还给父进程或操作系统
(5)将PCB在队列中删除
进程的阻塞和唤醒
进程的主动行为,阻塞原语:
(1)找到标识号对应的PCB
(2)若该进程为运行态,保护现场,转为阻塞态停止运行
(3)把PCB插入相应的等待队列,将处理机资源调度给其他就绪进程
当资源就绪时,由相关进程调用唤醒原语:
(1)在等待队列中找到进程对应的PCB
(2)移出等待队列并置为就绪态
(3)把PCB插入就绪队列,等待调度程序调度
阻塞和唤醒必须成对使用,否则阻塞进程永远阻塞
1.5 进程的通信
进程之间的信息交换,PV操作是低级通信方式
高级通信方法:
共享存储
通信的进程之间有一块可直接访问的共享空间,通过对这片空间的读写操作实现信息交换
读写操作需要使用同步互斥工具(P操作,V操作)
(1)低级方式:基于数据结构的共享
(2)高级方式:基于存储区的共享
进程空间一般是独立的,进程运行期间一般不能访问其他进程的空间,想让两个进程共享空间,必须经过特殊的系统调用
消息传递
进程间的数据交换以格式化的消息(Message)为单位;进程通过系统提高的发送消息和接收消息两个原语进程数据交换
(1)直接通信方式:把消息直接发给接收进程,挂在接收进程的消息缓冲队列上,接收进程从信息缓冲队列中取得消息
(2)间接通信方式:把信息发送到某个中间实体,接收进程从中间实体(信箱)获得消息,
管道通信
两个进程按生产者-消费者方式进行通信;数据在管道中是先进先出;管道机制必须提供的协调能力:互斥,同步和确定对方的存在
管道也是一种文件,和一般的文件又不同,管道可以克服使用文件进程通信的两个问题
(1)限制管道的大小
(2)读进程也可能工作得比写进程块
父进程创建管道,子进程可以继承并与父进程通信,普通管道是单向的,双向通信需要定义两个管道
读数据是一次性的,读完就释放空间以便写入新数据
1.6 线程和多线程模型
引入线程:减小程序在并发执行时所付出的时空开销,提高操作系统的并发性能
线程——轻量级进程——一个基本的CPU执行单元,程序执行流的最小单元
线程是进程中的一个实体,是系统独立调度和分派的基本单位,不拥有系统资源,但共享进程拥有的资源
引入线程后进程只作为除CPU外的系统资源的分配单元,线程则作为CPU的分配单元
线程与进程的比较:
(1)调度:传统进程的切换开销大,线程作为独立调度的基本单位,开销小,同一进程内的线程切换不引起进程的切换
(2)并发性:一个进程内的线程可并发,不同进程的线程也可以并发执行
(3)拥有资源:进程是系统中拥有资源的基本单位,线程不拥有系统资源,但共享进程的
(4)独立性:每个进程独立的地址空间和资源,不允许其他进程访问
(5)系统开销:进程的PCB和其他系统资源在创建和撤销时开销大;线程共享进程的资源,并且线程之间的同步与通信非常容易实现
(6)支持多处理机系统:单线程进程只能运行在一个处理机上;多线程进程可以将进程中的多个线程分配到多个处理机上执行
线程的属性:
进程在执行,实际指进程内的某个线程在执行
(1)线程是个轻型实体,不拥有系统资源,有一个唯一的标识符和线程控制块TCB
(2)不同的线程可以执行相同的程序,即同一程序被不同用户调用,操作系统把它创建成不同线程
(3)同一进程中的线程共享进程的资源
(4)是处理机的独立调度单位,多个线程可以并发执行
(5)仍会经历就绪态,阻塞态,运行态的变化
线程控制块包括:线程标识符;一组寄存器;线程运行状态;优先级;线程专有存储区;堆栈指针
线程的实现方式
(1)用户级线程ULT
线程的管理等所有工作由应用程序在用户空间中完成,内核意识不到线程的存在;调度仍以进程为单位
优点:线程切换不需要切到内核空间,节省开销;调度算法可以进程专用;实现与操作系统无关,对线程管理的代码是用户程序的一部分
缺点:系统调用的阻塞问题,一个线程执行系统调用,所有线程都被阻塞;不能发挥多处理机的优势
(2)内核级线程KLT
线程管理所有工作在内核空间实现,内核根据线程控制块感知线程的存在
优点:发挥多处理机的优势;如果进程中的一个线程被阻塞,内核可以调度进程中的其他线程占用处理机,或其他进程的线程;内核支持线程具有很小的数据结构和堆栈,切换块,开销小;内核本身的多线程技术,提高系统的执行速度和效率
缺点:同一进程的线程切换需要从用户态切换到核心态,开销大
(3)组合方式
内核支持多个内核级线程的建立调度和管理,同时允许用户程序建立调度和管理用户级线程
一些内核级线程对应多个用户级线程,这是用户级线程通过时分多路复用内核级线程实现的
线程库:为程序员提供创建和管理线程的API,实现方法:
(1)在用户空间中提供一个没有内核支持的库
(2)实现由操作系统之间支持的内核级的一个库
多线程模型
(1)多对一模型:多个用户级线程映射到一个内核级线程
优点:线程管理在用户空间,效率高
缺点:一个线程访问内核阻塞,整个进程阻塞;只能有一个线程能访问内核,多个线程不能在多个处理机上运行
(2)一对一模型
优点:一个线程阻塞,另个线程上,并发能力强
缺点:一个用户线程一个内核线程,开销大
(3)多对多模型
内核线程大于等于用户线程数量
克服了前两者的缺点,同时拥有其优点
2 处理机调度
2.1 概念
概念
进程的数量多于处理机的个数,因此发生争用,处理机调度是对处理机进行分配,即从就绪队列中按照一定的算法选择一个并将处理机分配给它运行
层次
(1)作业调度(高级调度)
从外存上处于后背队列的作业中挑选一个或多个进入内存,并建立相应的进程,每个作业只调入一次,调出一次
(2)内存调度(中级调度)
将哪些暂时不能运行的进程调至外存等待,此时进程状态称为挂起态,满足资源或能运行时再调入内存
(3)进程调度(低级调度)
从就绪队列选择一个进程并分配处理机,最基本的调度
2.2 调度的目标
(1)CPU利用率
利用率= c p u 有效工作时间 C P U 有效工作时间 + 空闲时间 \dfrac{cpu有效工作时间}{CPU有效工作时间+空闲时间} CPU有效工作时间+空闲时间cpu有效工作时间
(2)系统吞吐量
单位时间内CPU完成作业的数量
(3)周转时间
从作业提交到完成所经历的时间
周转时间=作业完成时间 - 作业提交时间
平均周转时间= 周转时间 1 + 周转时间 2 + . . . . 周转时间 n n \dfrac{周转时间1+周转时间2+....周转时间n}{n} n周转时间1+周转时间2+....周转时间n
带权周转时间= 周转时间 作业实际运行时间 \dfrac{周转时间}{作业实际运行时间} 作业实际运行时间周转时间
平均带权周转时间= 带权 1 + 带权 2 + . . . . 带权 n n \dfrac{带权1+带权2+....带权n}{n} n带权1+带权2+....带权n
(4)等待时间
进程处于等处理机的时间之和,越长用户满意度越低;衡量一个调度算法的优劣,一般只需看等待时间
(5)响应时间
从用户提交请求到系统首次产生响应所用的时间
交互式系统一般采用响应时间作为衡量调度算法的准则
2.3 调度的实现
调度程序(调度器)
调度的时机,切换与过程
调度程序是操作系统内核程序
不能进行进程的调度与切换的情况:
(1)在处理中断过程中
(2)进程在操作系统内核临界区
(3)其他需要完成屏蔽中断的原子操作过程
应该进行进程调度与切换的情况:
(1)发生引起调度条件且当前进程无法继续运行下去时,可以马上进行调度与切换
(2)中断处理结束或自陷处理结束后,返回被中断进程的用户态程序执行线程前,若置上请求调度标志,即可马上进行进程调度与切换
进程调度方式
是指某个进程正在执行时,若有某个更为重要或紧迫的进程需要处理,此时应如何分配
(1)非抢占式调度方式,即非剥夺方式
实现简单,开销小,适用于大多数批处理系统;但不能用于分时系统和大多数实时系统
(2)抢占式调度方式,即剥夺方式
对提高系统吞吐率和响应效率有益,但必须遵循优先权,短进程优先和时间片原则等
闲逛进程
如果系统中没有就绪进程,就调度闲逛进程,优先级最低,且不需要CPU之外的资源,不会被阻塞
两种线程的调度
(1)用户级线程调度:内核不知道线程的存在,内核无变化,仍选一个进程运行
(2)内核级线程调度:内核选个特定的线程运行而不考虑属于哪个进程
2.4 典型的调度算法
2.4.1 先来先服务算法FCFS
使用范围:作业调度,进程调度
属于不可剥夺算法
算法简单,效率低;对长作业有利,对短作业不利;有利于CPU繁忙型作业,不利于I/O繁忙型作业
举例:假设四个作业,提交时间分别是8,8.4,8.8,9,运行时间依次2,1,0.5,0.2
2.4.2 短作业优先调度算法SJF
从后背队列/就绪队列选一个(若干个)运行时间最短的作业,调入内存/分配处理机
缺点:(1)对长作业不利,出现饥饿现象
(2)完全未考虑作业的紧迫程度
(3)作业的长短是根据用户所提供的估计执行时间而定,不一定真的做到短作业优先
平均等待时间,平均周转时间最少
2.4.3 优先级调度算法
使用范围:作业调度,进程调度
(1)非抢占式优先级调度算法:就算有更高优先级的到来,也必须等正在运行的进程执行完毕或阻塞才能执行
(2)抢占式优先级调度算法
优先级是否可变:(1)静态优先级:进程创建时确定并不变
(2)动态优先级:运行时动态调整
优先级设置原则:(1)系统进程>用户进程
(2)交互型进程>非交互型进程(前台>后台)
(3)I/O型进程>计算型进程
2.4.4 高响应比优先调度算法
主要用于作业调度,同时考虑了等待时间和运行时间
响应比RP= 等待时间 + 运行时间 运行时间 \dfrac{等待时间+运行时间}{运行时间} 运行时间等待时间+运行时间
特点:(1)等待时间相同,运行时间越短,响应比越高,有利于短作业,类似SJF
(2)运行时间相同,等待时间越长,响应比越高,类似于FCFS
(3)对于长作业,响应比随等待时间增加而提高,克服饥饿现象
2.4.5 时间片轮转调度算法
适用于分时系统按FCFS排成一个就绪队列,然后依次按给定的时间片执行
若时间片过长,退化成FCFS;过短导致系统开销大
2.4.6 多级队列调度算法
系统中设置多个就绪队列,将不同类型或性质的进程固定分配到不同的就绪队列,每个队列可以实施不同的调度算法,系统针对不同用户进程的需求,提供多种调度策略
同一队列的进程可以设置不同优先级,不同队列也可以设置不同优先级
2.4.7 多级反馈队列调度算法
是时间片轮转和优先级调度的综合与发展,动态调整进程优先级和时间片大小
实现思想:(1)设置多个就绪队列,且优先级不同,第一级队列优先级最高,依次递减
(2)各个队列的进程运行时间片的大小各不相同,优先级越高时间片越小
(3)每个队列都是FCFS算法,新进程进入内存,首先放在第一级队列的末尾,时间到还没执行完就放入下一个队列的末尾
(4)按队列优先级调度,仅当第一级队列为空,第二级才运行
优势:(1)终端型作业用户:短作业优先
(2)短批处理作业用户:周转时间短
(3)长批处理作业用户:按队列依次执行,不会长期得不到处理,克服饥饿
2.5 进程切换
上下文切换:
挂起一个进程,保存CPU上下文,包括程序计数器和其他寄存器
更新PCB
把进程的PCB移入相应的队列,如就绪,在某事件阻塞等队列
选择另一个进程执行,并更新其PCB
跳转到新进程PCB中的程序计数器所指向的位置执行
恢复处理机上下文
调度是一种决策行为,而切换是实际的执行行为
3 同步与互斥
3.1 基本概念
临界资源:
一次仅允许一个进程使用的资源,访问临界资源必须互斥的访问,访问临界资源的那段代码称为临界区
把临界资源的访问过程分成四个部分:
(1)进入区:检测可否进入临界区,若能,设置正在访问标志,阻止其他进程访问
(2)临界区:访问临界资源的代码
(3)退出区:清除正在访问的标志
(4)剩余区:代码中的其余部分
do{
entry section; \\进入
critical section; \\临界
exit section; \\退出
remainder section; \\剩余
}while(true)
同步:
直接制约关系,指完成某种任务而建立的两个或多个进程,这些进程协调的工作次序等待,传递信息所产生的制约关系,源于他们的合作
互斥:
间接制约关系,一个进程访问临界资源,另一个进程必须等待
同步机制必须遵循的准则:
(1)空闲让进
(2)忙则等待
(3)有限等待:应保证在有限时间内进入临界区
(4)让权等待:进程不能进入临界区时,应立即释放处理器,防止进程忙等待
3.2 实现临界区互斥的基本方法
3.2.1 软件实现方法
在进入区设置并检查一些标志来标明是否有进程在临界区中
(1)单标志法
设置一个公用整型变量turn,turn=0,则允许进入临界区;但是两个进程必须交替进入,我退出了,你不进去,我就进不了,违背了空闲让进原则
P0进程:
while(turn != 0); \\进入
critical section; \\临界
turn =1; \\退出
remainder section; \\剩余
P1进程:
while(turn != 1);
critical section;
turn =0;
remainder section;
(2)双标志法
进程访问前检查是否可以访问,设置一个bool型数组
Pi进程:
while(flag[j]); \\进入
flag[i]=true; \\进入
critical section; \\临界
flag[i]=false; \\退出
remainder section; \\剩余
Pj进程:
while(flag[i]);
flag[j]=true;
critical section;
flag[j]=false;
remainder section;
优点:不同交替进入,可连续使用
缺点:可能同时进入,违背了忙则等待
(3)双标志后检查法
算法2先检查后改变标志,导致可能同时进入;算法3先改变自己的标志,再检测对方的状态,若对方为true,则等待
Pi进程:
flag[i]=true; \\进入
while(flag[j]); \\进入
critical section; \\临界
flag[i]=false; \\退出
remainder section; \\剩余
Pj进程:
flag[j]=true;
while(flag[i]);
critical section;
flag[j]=false;
remainder section;
若同时进入,同时置为true,发现对方也是true,两个都进不了,发生饥饿现象
(4)Peterson`s Algorithm算法
为防止无限期等待,又设置turn,每个进程先设置自己的标志,再设置turn;再同时检测另个进程的状态标志和允许进入标志,以便保证两个进程同时要求进入临界区,只允许一个进程进入临界区
Pi进程:
flag[i]=true; turn=j; \\进入 turn解决饥饿现象,turn表示j已经在临界区内
while(flag[j]&& turn ==j); \\进入
critical section; \\临界
flag[i]=false; \\退出
remainder section; \\剩余
Pj进程:
flag[j]=true; turn =i;
while(flag[i]&& turn ==i);
critical section;
flag[j]=false;
remainder section;
3.2.2 硬件实现方法
计算机提供了特殊的硬件指令,允许对一个字中的内容进程检测和修正,或对两个字的内容进行交换,通过硬件支持实现临界段问题的方法称为低级方法/元方法
(1)中断屏蔽方法
典型模式:
...
关中断;
临界区;
开中断;
...
限制了处理交替执行程序的能力,执行效率明显降低
(2)硬件指令方法
TestAndSet指令:这条指令是原子操作,即执行该代码时不允许被中断,功能是读出指定标志后把该标志设置为真
boolean TestAndSet(boolean * lock)
{
boolean old;
old=*lock;
*lock=true;
return old;
}
为每个临界资源设置一个bool变量lock,true表示正在被占用,初值为FALSE
进程进入临界区之前,利用TestAndSet检查lock
while TestAndSet(&lock);
进程的临界区代码段;
lock=false;
进程的其他代码;
swap(boolean *a ,boolean *b)
{
boolean temp;
temp = *a;
*a = *b;
*b = temp;
}
\\TestAndSet与swap指令的描述仅是功能实现,并非软件实现的定义,他们由硬件直接实现,不会被中断
\\swap指令简单实现互斥,lock初值为false,重复交换和检测过程,直到进程退出
key = true;
while(key!=false)
swap(&lock,&key);
进程的临界区代码段;
lock = false;
进程的其他代码;
硬件方法的优点:使用任意数目的进程,单处理机和多处理机都可;简单,容易验证正确性;支持进程内有多个临界区
硬件方法的缺点:进程等待临界区耗费处理机时间,不能实现让权等待;从等待进程中随机选择一个进入临界区,有的进程可能一直选不上,产生饥饿现象
3.3 互斥锁
解决临界区最简单的工具,进入临界区时获得锁acquire(),退出时释放锁release();必须是原子操作,通常采用硬件机制实现
每个互斥锁一个bool变量available,表示锁是否可用
acquire()
{
while(!available); \\忙等待
available = false; \\获得锁
}
release()
{
available = true; \\释放锁
}
缺点:忙等待,一个进程在临界区,任何其他进程就会连续循环调用acquire();多个进程共享同一个CPU时,浪费了CPU周期;因此互斥锁通常用于多处理器系统,一个线程在一个处理器上等待,不影响其他线程的执行
举个例子:假设有两个线程A、B需要同时访问某个代码块(即临界区),它们都试图去获取该加锁标志。由于只有一个线程能够先获得该锁,如果A先获得了该锁,则B就会开始忙等待,一直重复尝试获取该锁,直到A释放该锁为止。
当多个进程共享同一个CPU时,如果有多个线程同时使用互斥锁去竞争同一个CPU资源时,则其中某些线程就可能会一边忙等待一边浪费其他进程已经被分配到的时间片。因此,在多处理器系统中使用互斥锁能更好地避免这种现象发生。当一个线程在一个处理器上等待时,其他线程可以继续在其他处理器上执行它们自己的任务而不被阻塞。
3.4 信号量
用来解决互斥与同步问题,只能被两个标准的原语wait(S)和signal(S)访问,也可记为“P操作”和“V操作”
整型信号量
被定义为一个用于表示资源数目的整型量S,则PV操作描述为
wait(S)
{
while(S<=0); \\不断测试
S=S-1;
}
signal(S)
{
S=S+1;
}
没有遵循让权等待的准则,使进程处于忙等的状态
记录型信号量
不存在忙等现象;需要一个代表资源数目的整型变量value,再增加一个进程链表L,用于链接所有等待该资源的进程
typedef struct \\记录型信号量
{
int value;
struct process *L;
}semaphore;
void wait(semaphore S) \\相当于申请资源
{
S.value--; \\表示进程请求一个资源
if(S.value <0) \\小于0表示资源已经分配完了,调用block自我阻塞,放弃处理机,并插入该类资源的等待队列
{
add this process to S.L;
block(S.L);
}
}
void signal(semaphore S) \\相当于释放资源
{
S.value++; \\释放资源,所有资源增加
if(S.value <=0) \\仍小于等于0表示等待队列里还有进程被阻塞,因此唤醒
{
remove a process P fron S.L;
wakeup(P);
}
}
遵循了让权等待准侧
利用信号量实现同步
设S为实现进程P1P2同步的公共信号量,初值为0
P2的语句y需要P1语句x的运行结果
semaphore S=0; \\初始化信号量
p1()
{
...
x;
V(S); \\告诉进程P2语句x已经完成
...
}
p2()
{
...
P(S); \\检查语句x是否运行完成
y; \\通过并运行y
...
}
P2先运行到P(S)时,S=0,P操作会把进程P2阻塞,并放入阻塞队列,V操作把P2从阻塞队列中放回就绪队列,P2得到处理机时,得以继续执行
利用信号量实现进程互斥
设S为实现进程P1P2互斥的信号量,初值为1(即可用资源为1),把临界区置于PV操作之间即可实现两个进程对临界资源的互斥访问
semaphore S=1; \\初始化信号量
P1()
{
...
P(S); \\准备访问临界资源,加锁
进程p1的临界区;
V(S); \\访问结束,解锁
...
}
P2()
{
...
P(S);
进程p2的临界区;
V(S);
...
}
S减为0时,再有进程进入临界区,执行P操作时会被阻塞,直到临界区的进程退出,就实现了互斥
互斥是不同进程对同一信号量进程PV操作实现的
在同步问题中,若某个行为要用到某种资源,则在这个行为前面P这种资源一下;若某个行为会提供某种资源,则在这个行为行为后面V这种资源一下
互斥问题中,PV操作要紧夹使用互斥资源的那个行为,中间不能有其他冗余代码
利用信号量实现前驱关系
3.5 管程
每个临界资源都要自备同步的PV操作,大量分散的同步操作给系统管理带来了麻烦,且容易导致系统死锁
管程的特性保证了进程互斥,无需程序员自己实现互斥,降低了死锁发生的可能性,同时提供了条件变量,让程序员灵活的实现进程同步
定义
系统中的各种硬件,软件资源都可用数据结构抽象的描述其资源特性;利用共享数据结构抽象的表示系统中的共享资源,把对该数据结构实施的操作定义为一组过程。过程对共享资源的申请,释放等操作,都通过这组过程来实现,这组过程还可以根据资源情况,或接收或阻塞进程的访问,确保每次仅有一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现互斥。
这个代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,称为管程
管程的组成:(1)管程的名称
(2)局部于管程内部的共享数据结构说明
(3)对该数据结构进行操作的一组过程(或函数)
(4)对局部于管程内部的共享数据设置初始值的语句
\\管程定义描述
monitor Demo \\定义一个叫demo 的管程
{
共享数据结构S; \\对应系统的某种资源
init_code()
{
S=5; \\初始资源数为5
}
take_away()
{
S--; \\可用资源数-1
...
}
give_back() \\可用资源数+1
{
S++;
...
}
}
管程很像一个类
(1)管程把对共享资源的操作封装起来
(2)每次仅允许一个进程进入管程,从而实现进程互斥
条件变量
一个进程进入管程被阻塞,直到阻塞的原因解除,将阻塞原因定义为条件变量
阻塞原因有多个,设置了多个条件变量,每个条件变量保存了一个等待队列,用于记录因这个原因阻塞的进程
对条件变量只能执行两个操作,即wait和signal
x.wait:x对应的条件不满足,正在调用管程的进程调用x.wait将自己插入x条件的等待队列,并释放管程
x.signal:x对应的条件发生的变化,调用x.signal,唤醒一个因x条件而阻塞的进程
monitor Demo
{
共享数据结构S;
condition x; \\定义条件变量x
init_code(){...}
take_away()
{
S--; \\可用资源数-1
if(S<=0) \\资源不够,在条件变量x上阻塞等待
x.wait();
...资源充足,分配资源,做一系列相应处理
}
give_back() \\可用资源数+1
{
归还资源,做一系列相应处理
S++;
if(有进程等待)
x.signal(); \\唤醒一个阻塞进程
...
}
}
条件变量与信号量的比较:
(1)条件变量的wait,signal操作类似信号量的PV操作,实现进程的阻塞唤醒
(2)但是条件变量是没有值的,仅实现排队等待功能;信号量有值,代表资源的数量
3.6 经典同步问题
3.6.1 生产者—消费者问题
问题描述:一组生产者进程和一组消费者进程共享一个初始为空,大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入缓冲区,否则必须等待;缓冲区不空,消费者才能从中取出消息,否则必须等待,由于缓冲区是临界资源,只允许一个生产者放入消息,或一个消费者从中取出消息
semaphore mutex =1; \\临界区互斥信号量
semaphore empty =n; \\空闲缓冲区
semaphore full =0; \\缓冲区初始化为空
producer()
{
while(1)
{
produce an item in nextp; \\生成数据
P(empty); \\获取空缓冲区单元
P(mutex); \\进入临界区
add nextp to buffer; \\将数据放入缓冲区
V(mutex); \\离开临界区,释放互斥信号量
V(full); \\满缓冲区数加1
}
}
consumer()
{
while(1)
{
P(full); \\获取满缓冲区单元
P(mutex); \\进入临界区
remove an item from buffer; \\从缓冲区取出数据
V(mutex); \\离开临界区,释放互斥信号量
V(empty); \\空缓冲区数加1
consume the item; \\消费数据
}
}
问题描述:桌子上有一个盘子,每次只能向其中放入一个水果;爸爸专门放苹果,妈妈专门放橘子,儿子专门吃橘子,女儿只吃苹果。只有盘子为空,爸爸或妈妈才能放一个水果;仅当有需要的水果时,儿子女儿才取出吃
semaphore plate =1, apple =0, orange =0;
dad()
{
while(1)
{
prepare an apple;
P(plate);
put the apple on the plate;
V(apple);
}
}
mom()
{
while(1)
{
prepare an orange;
P(plate);
put the orange on the plate;
V(orange);
}
}
son()
{
while(1)
{
P(orange);
take an orange from the plate;
V(plate);
eat the orange;
}
}
daughter()
{
while(1)
{
P(apple);
take an apple from the plate;
V(plate);
eat the apple;
}
}
3.6.2 读者—写者问题
问题描述:有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用;但若某个写进程和其他进程(读写)同时访问共享数据时可能导致数据不一致的错误
要求:(1)允许多个读者可以同时对文件执行读操作
(2)只允许一个写者往文件中写信息
(3)任意一个写者在完成写操作之前不允许其他读者或写者工作
(4)写者执行写操作前,应让已有的读者和写者全部退出
int count =0; \\记录当前读者数量
semaphore mutex =1; \\保护更新count变量时的互斥
semaphore rw =1; \\保证读者写者互斥访问文件
writer()
{
while(1)
{
P(rw); \\互斥访问共享文件
writing;
V(rw); \\释放共享文件
}
}
reader()
{
while(1)
{
P(mutex); \\互斥访问count
if(count==0) \\第一个读进程读共享文件时阻止写进程写
P(rw);
count++; \\读进程加1
V(mutex); \\释放count
reading; \\读取
P(mutex); \\读完再互斥访问count
count--; \\读进程减1
if(count==0) \\当最后一个读进程读完共享文件允许写进程写
V(rw);
V(mutex); \\释放count
}
}
上面算法读进程永远是优先的,只要有读的,写进程就可能无限期延后,导致写进程饿死
下面算法改进为写优先或读写公平
int count =0; \\记录当前读者数量
semaphore mutex =1; \\保护更新count变量时的互斥
semaphore rw=1; \\保证读者写者互斥访问文件
semaphore w=1; \\实现写优先
writer()
{
while(1)
{
P(w); \\无写进程时请求进入
P(rw); \\互斥访问共享文件
writing;
V(rw); \\释放共享文件
V(w); \\恢复对共享文件的访问
}
}
reader()
{
while(1)
{
P(w); \\无写进程时请求进入
P(mutex); \\互斥访问count
if(count==0)
P(rw);
count++;
V(mutex); \\释放count
V(w); \\恢复对共享文件的访问
reading;
P(mutex);
count--;
if(count==0)
V(rw); \\允许写进程写
V(mutex);
}
}
读者写者问题的一个关键特征即有一个互斥访问的计数器count,因此遇到一个不太好解决的同步互斥问题可以尝试利用count
3.6.3 哲学家进餐问题
问题描述:一张圆桌5个哲学家,每两名之间一根筷子,每人面前一碗米饭;哲学家只思考和进餐,思考时不影响其他人;哲学家饥饿时才试图拿起左右的筷子(一个一个的拿起);若筷子在别人手上,则需要等待;同时有两个筷子才能进餐,然后继续思考
定义互斥信号量数组chopstick[5]={1,1,1,1,1},用于对5个筷子的互斥访问;哲学家按顺序编号0~4,哲学家i左边筷子的编号为i,哲学家右边筷子的编号为(i+1)%5
semaphore chopstick[5]={1,1,1,1,1};
Pi() \\i号哲学家进程
{
do{
P(chopstick[i]); \\取左边筷子
P(chopsitck[(i+1)%5]); \\取右边筷子
eat;
V(chopstick[i]); \\放左筷子
V(chopsitck[(i+1)%5]); \\放右筷子
think;
}while(1);
}
该算法存在问题:如果5个哲学家同时拿起左边的筷子,就没有右边的筷子可以拿,导致死锁
防止死锁的发生,可采用:比如至多允许4名哲学家同时进餐;仅当一个哲学家的左右两个筷子都可用,才允许他抓起筷子;对哲学家顺序编号,要求奇数号哲学家先拿左筷子,然后拿右边的筷子,偶数号哲学家正好相反
假设仅当一个哲学家的左右两个筷子都可用,才允许他抓起筷子
semaphore chopstick[5]={1,1,1,1,1};
semophore mutex=1;
Pi()
{
do{
P(mutex); \\取筷子前获得互斥量
P(chopstick[i]); \\取左边筷子
P(chopsitck[(i+1)%5]); \\取右边筷子
V(mutex); \\释放取筷子的信号量
eat;
V(chopstick[i]); \\放左筷子
V(chopsitck[(i+1)%5]); \\放右筷子
think;
}while(1);
}
3.6.4 吸烟者问题
问题描述:假设一个系统三个抽烟者进程和一个供应者进程,抽烟者不停的卷烟抽烟,一只烟需要三种材料:烟草,纸和胶水;三个抽烟者中,第一个有烟草,第二个有纸,第三个有胶水;供应者进程无限的提供三种材料,每次放两种材料在桌子上,刚好能抽的抽烟者会卷烟并抽,并给供应者一个信号表示已经完成,此时供应者会放其他材料在桌上。
int num=0; \\存储随机数
semaphore offer1=0; \\定义信号量对应烟草和纸的组合的资源
semaphore offer2=0; \\烟草和胶水的组合的资源
semaphore offer3=0; \\对应纸和胶水的组合的资源
semaphore finish=0; \\定义信号量表示抽烟是否完成
process P1() \\供应者
{
while(1)
{
num++;
num=num%3; \\依次提供不同的材料组合
if(num==0)
V(offer1);
else if(num==1)
V(offer2);
else
V(offer3);
任意两种材料放在桌子上;
P(finish);
}
}
Process P2() \\拥有烟草的
{
while(1)
{
P(offer3);
抽烟;
V(finsih);
}
}
Process P3() \\拥有胶水的
{
while(1)
{
P(offer2);
抽烟;
V(finish);
}
}
Process P4() \\拥有胶水的
{
while(1)
{
P(offer1);
抽烟;
V(finish);
}
}
4 死锁
4.1 概念
定义:
指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程永远无法向前推进
死锁产生的原因:
(1)系统资源的竞争
(2)进程推进顺序非法:请求和释放资源的顺序不当;信号量使用不当
死锁产生的必要条件:
(1)互斥条件:某资源一段时间仅一个占有
(2)不剥夺条件:未使用完不能被其他进程强行夺走
(3)请求并保持条件:进程已有一个资源,又提出新的资源请求,该资源又被其他的占有,此时被阻塞,且持有的资源不释放
(4)循环等待条件:存在一种进程资源的循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求
循环等待和死锁的区别:循环等待有可能系统中的其他资源释放时,会打破循环;但是死锁的资源都是唯一,不能代替
死锁的处理策略:
(1)死锁预防:设置条件破坏死锁的4个必要条件
(2)避免死锁:资源的动态分配过程中防止系统进入不安全状态
(3)死锁的检测及解除:允许死锁发生并采取措施解除死锁
4.2 死锁预防
破坏四个必要条件
(1)破坏互斥条件
允许系统资源都可共享,就不会死锁,但不可能实现,有些资源根本不能同时访问
(2)破坏不剥夺条件
一个保持了一些不可剥夺的资源请求新的资源得不到满足时,必须释放所有持有的资源
实现复杂,且释放资源造成之前的工作失效,反复申请开销大,降低系统吞吐量
常用于状态易于保存和恢复的资源
(3)破坏请求并保持条件
采用预先静态分配方法,即进程在运行前一次申请完所有资源,不满足就不投入运行,一旦投入所有资源归它所有
实现简单,但系统资源被严重浪费,因为某些资源可能只用一会;而且导致饥饿现象
(4)破坏循环等待条件
可采用顺序资源分配法,给资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源一次申请完,即进程提出申请分配资源Ri,则该进程在以后的资源申请中就只能申请编号大于Ri的资源
但是限制了新设备的增加;会发生作用使用资源的顺序和系统规定顺序不同的情况,造成资源浪费;且给用户编程带来麻烦
4.3 死锁避免
4.3.1 系统安全状态
安全状态:系统能按某种进程推进顺序(P1P2….Pn)为每个进程Pi分配其所需的资源,直到满足每个进程对资源的最大需求,使每个进程都可顺序完成,此时称P1P2….Pn为安全序列,若无法找到一个安全序列,则系统处于不安全状态
并非所有不安全状态都是死锁,但是进入不安全状态后便可能进入死锁
4.3.2 银行家算法
思想:把系统视为银行家,资源相当于资金,请求分配资源相当于向银行家贷款。进程运行前先声明对各种资源的最大需求量,当进程在执行中继续申请资源时,先测试该进程已占有的资源数与本次申请的资源数之和是否超过该进程声明的最大需求量,若超过则拒绝分配资源,若未超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请分配资源,否则推迟分配
(1)数据结构描述
可利用资源向量available:含有m个元素的数组,其中每个元素代表一类可用的资源数目,available[j]=k,j类资源有k个
最大需求矩阵max:n*m矩阵,n个进程中每个进程对m类资源的最大需求;max[i,j]=k表示进程i需要j类资源的最大数量为k
分配矩阵allocation:n*m矩阵,定义系统中每类资源当前已分配给每个进程的资源数,allocation[i,j]=k表示进程i当前已分得j类资源的数目为k
需求矩阵need:n*m矩阵,表示每个进程接下来最多还需要多少资源,need[i,j]=k表示进程i还需要j类资源k个
need=max-allocation
(2)银行家算法描述
Ri表示进程Pi的请求向量,Ri[j]=k表示需要j类资源k个,发出请求后的检查步骤:
- 若Ri ≤ \leq ≤need[i,j],转下一步,否则出错
- 若Ri ≤ \leq ≤available[j],转下一步,否则表示资源不够,等待
- 尝试分配资源并修改数据结构的数值,不是真的分配了资源
available= available - R
allocation = allocation + R
need = need - R
- 系统执行安全性算法,检查这次分配系统是否还处于安全状态,若安全,才正式分配进程,完成分配;若不安全,将本次试探作废,恢复原来的资源分配状态,让进程Pi等待
(3)安全性算法
设置工作向量work,有m个元素,表示系统中剩余可用资源数目,执行安全性算法,work=available
- 初始安全序列为空
- 从need矩阵找出符合条件的行:该行对应的进程不在安全序列中,而且该行小于或等于work向量,找到后加入安全序列,找不到执行第四步
- 进程Pi进入安全序列后可顺利执行,直到完成,并释放分配给它的资源,因此应执行work=work+allocation,allocation[i]表示进程Pi代表的在allocation矩阵中对应的行,返回第二步
- 若安全序列中已有所有进程,则系统处于安全状态,否则系统处于不安全状态
4.3.3 安全性算法举例
(1)work=available,算出need矩阵
(2)在need中找到比work小的向量,把对应进程加入安全序列,并且在work加上对应进程的need,并更新need
再重复第二步,最后得到安全序列{P1,P3,P4,P2,P0}
4.3.4 银行家算法举例
(1)P1请求资源:R(1,0,2),进行检查并执行安全性算法
此时可找到安全序列,即系统是安全的,并立即分配P1所需的资源
(2)P4请求:R(3,3,0)
R>available(2,3,0),P4等待
(3)P0请求:R(0,2,0)
进行安全性检查,可用资源available(2,1,0)不能满足任何进程的需要,系统进入不安全状态,拒绝请求并让P0等待,并恢复available,need,allocation之前的值
4.4 死锁检测和解除
资源分配图
圆圈表示进程,框表示一类资源
进程到资源的有向边称为请求边
资源到进程的边称为分配边,表示该类资源已有一个资源分配给了该进程
死锁定理
简化资源分配图可检测系统状态S是否为死锁
(1)在资源分配图中找出既不阻塞又不孤点的进程PI,消去它所有的请求边和分配边,使之称为孤立的结点
即找出一条有向边与他相连,且该有向边对应资源的申请数量小于或等于系统中已有的空闲资源数量
(2)释放的资源可以唤醒某些阻塞的进程
S为死锁的条件时当且仅当S状态的资源分配图不可完全简化,该条件为死锁定理
死锁解除
(1)资源剥夺法:挂起死锁进程并抢占它的资源
(2)撤销进程法:轻质撤销部分甚至全部死锁进程并剥夺资源,可按进程优先级和撤销代价的高低进行
(3)进程回退法:让若干个进程回退到足以回避死锁的地步,进程回退时资源释放资源而非剥夺,要求保持历史信息,设置还原点