进程
进程是计算机中的程序的一次执行,是系统进行资源分配的基本单位,是操作系统的基础。为了使参与并发执行的程序(含数据)能独立运行,必须为其配备一个专门的数据结构进程控制块(PCB)。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。同时由程序段、相关数据段和PCB三部分构成了进程映像(进程实体)。
所谓创建进程,其实质为创建进程映像中的PCB;撤销进程,实质是撤销进程映像中的PCB。进程映像使静态的而进程使动态的。
特征:动态性、并发性、独立性、异步性、结构性。
进程控制块(PCB)
PCB是进程实体的一部分,是进程存在的唯一标志。
系统通过PCB来管理和控制进程。
PCB主要包括进程描述信息、进程控制和管理信息、资源分配清单和处理机相关信息。
- 进程描述信息:
(1)进程标识符(PID):标识各个进程每个进程都有一个唯一的标识符。
(2)用户标识符(UID):进程归属的用户,用户标识符主要为共享和保护服务。 - 进程控制和管理信息:
(1)进程当前状态:描述进程的状态信息,作为处理机分配和调度的单位。
(2)进程优先级:描述进程抢占处理机的优先级,优先级高的进程可优先获得处理机。
(3)代码程序入口地址
(4)程序的外存地址
(5)进入内存的时间
(6)处理机占有时间
(7)信号量使用 - 资源分配清单:用于说明内存地址空间和虚拟地址空间的状况,所打开的文件列表和所用于用于输入输出设备信息。
(1)代码段指针
(2)数据段指针
(3)堆栈段指针
(4)文件描述符
(5)鼠标
(6)键盘 - 处理机相关信息:
(1)通用寄存器的值
(2)地址寄存器的值
(3)控制寄存器的值
(4)标志寄存器的值
(5)状态字
进程的状态与转换
进程根据其生命周期的不同主要分为以下五个状态:
- 运行态:进程在处理机上运行。单机环境下,每个时刻最多只有一个进程处于运行态。
- 就绪态:进程获得了除处理机以外的所有资源,一旦获得处理机便可立即运行。就绪进程可能有多个,通常将其组成一个队列称为就绪队列。
- 阻塞态:进程正在等待某一事件而暂停运行。如等待某类资源或等待外部IO设备。
- 创建态:进程正在被创建,尚未转到就绪态。创建进程分为多步:①先申请一个空白的PCB,并填写一些控制和管理进程的信息。②系统为进程分配运行时所必要的资源。③将其转入就绪态。
- 结束态:进程从系统中消失,可能是进程正常结束或因其他原因中断推出运行。进程结束需要先设置进程为结束态,然后再进一步回收释放进程所持有的相关资源。
进程的状态转换模型通常由两种,三状态模型和五状态模型:
三状态模型
五状态模型
当然如果考虑进程的挂起,还有单挂起进程模型(引入阻塞挂起),此外还有双挂起进程模型(进入阻塞挂起和就绪挂起)。
进程间状态转换原因:
就绪态->运行态:进程被调度,即获得处理及资源。
运行态->就绪态:①处于运行态的进程时间片被用完。②再可剥夺操作系统中,有更高优先级的进程抢占。
运行态->阻塞态:进程请求某一资源的请求和分配或等待某一事件的发生。是一种主动行为。
阻塞态->就绪态:进程等待的事件或资源到来。是一种被动行为。
进程的控制过程
进程的创建: 允许一个进程创建另一个进程。被创建的为子进程,子进程拥有父进程的所有资源。当撤销父进程时需要一同撤销其所有子进程。
①为新进程分配一个唯一的进程标识符(PID),并申请一个空白的PCB(PCB可能申请失败,若申请失败则进程创建失败)。
②为进程分配必要的资源,如为新进程的程序和数据及用户空间分配内存空间(若分配失败则进程进入阻塞状态)。
③初始化PCB,主要初始化标志信息、处理机状态信息和处理机控制信息,以及设置进程优先级。
④将新进程插入就绪队列,等待被处理机调度。
进程的终止:
引起进程终止的集中情况:
①正常结束,表示进程的任务已完成并准备退出运行。
②异常结束,进程再运行时发生了某种异常事件,如:非法指令、运行超时、算术运算错。
③外界干预,进程应外界请求而终止,如:操作系统干预、父进程请求或父进程终止。
过程:
①根据被终止进程的标识符,检索PCB,从中读出进程状态。
②若进程处于执行状态,立即终止执行,并将处理机分配给其他进程。
③若该进程还有子孙进程,则应将所有子孙进程终止。
④将进程拥有的全部资源归还操作系统或其父进程。
⑤将PCB从所在队列中删除。
进程的阻塞与唤醒:
阻塞:
①找到要阻塞进程的标识符对应的PCB。
②若该进程为运行状态,则保护现场,将其转为阻塞态,停止运行。
③把该PCB插入就绪队列,等待调度程序调度。
唤醒:
①再该事件的等待队列中找到相应的PCB。
②将其从等待队列中移出,并设置状态为就绪态。
③把该PCB插入就绪队列,等待调度程序调度。
**进程切换:**进程切换是指处理机从一个进程转到另一个进程上运行,在这个过程中,进程的运行环境产生了实质性的变化。切换过程如下:
①保存处理机上下文,包括程序计数器和其他寄存器。
②更新PCB信息。
③把进程的PCB移入相应的队列,如就绪、再某事件阻塞队列。
④选择另一个进程执行,并更新其PCB。
⑤更新内存管理的数据结构。
⑥恢复处理机上下文。
进程切换与进程模式切换不同,模式切换时,处理及逻辑上可能还处于同一进程。
“调度”与“切换”的区别:调度是指决定资源分配给哪个进程的行为;切换是指实际分配的行为。
进程同步与互斥
临界资源: 一次只允许一个进程使用的资源,如打印机。对临界资源必须进行互斥访问。
临界区: 每个进程中,对临界资源进行访问的代码。对临界资源的访问过程主要分为四部分:
do{
entry section; //进入区:检查是否可进入临界区并设置访问标识,阻止其他进程进入
critical section; //临界区:访问临界资源的代码
exit section; //退出区:将正在访问的临界区标志清空
remainder section; //剩余区:代码的其他部分
}while(true)
进程同步:进程间直接制约关系,是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。例如:对某个缓冲区进行读写时输入进程和输出进程的关系。
进程互斥:进程间间接制约关系,当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一个进程才允许去访问此临界资源。
同步机制遵循的原则:对临界资源访问的原则
①空闲让进:临界区空闲时可允许一个请求进入临界区。
②忙则等待:当已有进程进入临界区时,其他试图进入的进程必须等待。
③有限等待:对请求访问的进程,应保证再有限的时间内进入临界区。
④让权等待:当进程不能进入临界区时应立即放弃处理器,防止进程忙等待。
实现临界区互斥的基本方法
软件实现方法
(1)单标志法:用一个公用整形变量turn,指示是否被允许进入临界区。该算法可保证每次只允许一个进程进入临界区,但两进程必须交替进入,若一个进程不在进入另一个进程页不能进入。(违背“空闲让进”)
P0进程: P1进程:
while(turn!=0); while(turn!=1);
critical section; critical section;
turn = 1; turn = 0;
remainder section; remainder section;
turn = 0 时P0进入临界区,为1时P1进入临界区。
(2)双标志先检查法:进入临界区之前先检查临界资源是否被访问,若被访问则等待,否则才进入临界区。
缺点:两个进程可能会同时进入临界区, (违背了“忙则等待”)
Pi进程: Pj进程:
while(flag[j]); while(flag[i]);
flag[i]=true; falg[j]=true;
critical section; critical section;
flag[i]=false; flag[j]=false;
remainder section; remainder section;
(3)双标志后检查法:进入临界区前先设置自己进入临界区的标志再检查对方是否进入临界区。
缺点:可能双方都无法进入临界区
Pi进程: Pj进程:
flag[i]=true; flag[j]=true;
while(flag[j]); while(falg[i]);
critical section; critical section;
flag[i]=false; flag[j]=false;
remainder section; remainder section;
(4)Peterson’s Algorithm:为防止两个进程为进入临界区而无限等待,又设置了变量turn,每个进程在先设置自己的标志后再设置turn标志。这时再检查另一个进程状态标志和不允许进入标志,以便保证两个进程同时要求进入临界区时,只允许一个进程进入。
Pi进程: Pj进程:
flag[i]=true;turn=j; flag[j]=true;turn=i;
while(flag[j]&&turn=j); while(flag[i]&&turn=i);
critical section; critical section;
flag[i]=false; flag[j]=false;
remainder section; remainder section;
硬件实现方法
(1)中断屏蔽法:当一个进程正在使用处理机执行他的临界代码时,防止其他进程进入其临界区进行访问的最简单的方法就是禁止一切中断发生,或称为屏蔽中断,关中断。
关中断;
临界区;
开中断;
(2)硬件指令法
①TestAndSet指令:每个临界资源设置一个共享布尔变量lock,表示资源的两种状态;true表示正被占用,初值为false。在进程访问临界资源前,利用TestAndSet指令检查和修改标志lock;若进程在临界区,则重复检查,知道进程退出。
TestAndSet指令的功能描述代码:
boolean TestAndSet(boolean *lock){
boolean old;
old = *lock;
*lock = true;
return old;
}
利用方法如下:
while TestAndSet(&lock);
进程的临界区代码段;
lock=false;
进程的其他代码段;
②swap指令
信号量
信号量机制时一种功能较强的机制,可以用来解决互斥与同步问题,他只能被两个标准的原语wait(S)和Signal(S)访问,也可记为”P操作“和”V操作“。
原语是指完成某种功能且不被分割、不被中断执行的操作序列,通常用硬件表示。
1、整型信号量:定义为一个用于表示资源数目的整形量S。
wait(S){
while(S<=0);
S=S-1;
}
signal(S){
S++;
}
wait操作中,只要S<=0就会不断的测试,该机制没有遵循让权等待原则。
2、记录型信号量:是不存在“忙等”现象的进程同步机制。出需要一个代表资源数目的整型变量value外,再增加一个进程链表L,用于连接所有等待该资源的进程。
typedef struct{
int value;
struct process *L;
}semaphore;
void wait(semaphore S){
S.value--;
if(S.value<0){
add this process to S.L;
block(S.L);
}
}
void signal(semaphore S){
S.value++;
if(S.value<=0){
remove a process P from S.L;
wakeup(P);
}
}
当进程所需要的资源不足时,进程调用block原语,进行自我阻塞,放弃处理机,遵循了让权等待的原则。
3、利用信号量实现同步:设S为P1,P2同步的公共信号量,P2进程的运行需要P1进程运行的结果才可以执行。
semaphore S=0; //初始化信号量
P1(){
...;
P1执行必要语句;
V(S); //告诉P2自己准备就绪
...;
}
P2(){
...;
P2执行语句;
P(S); //检查P1是否准备就绪
...;
}
4、利用信号量实现互斥:设S为实现进程P1和P2的信号量,每一次只允许一个进程进入临界区。
semaphore S = 1;
P1(){
...;
P(S); //准备访问资源加索
进程P1的临界区;
V(S); //访问结束,解锁
...;
}
P2(){
...;
P(S); //准备访问资源加锁
进程P2的临界区;
V(S); //访问结束,解锁
...;
}
5、利用信号量实现前驱关系
semaphore a1=a2=b1=b2=c=d=e=0;
S1(){
...;
V(a1);V(a2); //S1运行完成
}
S2(){
P(a1);
...;
V(b1);V(b2); //S2运行完成
}
S3(){
P(a2);
...;
V(c); //S3运行完成
}
S4(){
P(b1);
...;
V(d); //S4运行完成
}
S5(){
P(b2);
...;
V(e); //S5运行完成
}
S6(){
P(c);
PC(d);
P(e);
...;
}
管程
在信号量机制中,每个要访问临界资源的进程都必须自备同步的PV操作,大量分散的同步操作给系统管理带来了麻烦,且容易因为同步操作不当导致死锁发生。
管程的特性保证了进程互斥,无须程序员自己实现互斥。从而降低了死锁发生的可能性。
定义:管程定义了一个数据结构和能为并发进程所执行(在该结构上)的一组操作,这组操作能同步进程和改变管程中的数据。
管程的四个组成部分:
1:管程的名称
2:局部于管程内部的共享结构数据说明
3:对该数据结构进行操作的一组过程
4:对局部于管程内部的共享数据设置初始值的语句
定义描述举例:
monitor Demo{. //定义一个名为Demo的管程
//定义共享数据结构,对英语系统中的某些共享资源
共享数据结构S;
//对共享数据结构进行初始化的语句
init_code(){
S=5;
}
//过程一:申请一个资源
take_away(){
S—-;
. . .;
}
//归还一个资源
give_back(){
S++;
. . .;
}
}
经典同步问题
(1)生产者消费者问题
问题描述:一组生产者进程和一组消费者进程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时。生产者才能把消息放入缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。
分析:生产者和消费者对缓冲区的访问是互斥的关系。
生产者和消费者同时逻辑上又是同步的关系。
信号量设置:信号量mutex作为互斥信号量,用于控制互斥访问缓冲池。信号量full用于表示缓冲区满,初值为0;信号量empty表示缓冲区为空,初值为n。
进程代码描述:
semaphore mutex = 1; //临界区互斥信号量
semaphore empty = n; //空缓冲区
semaphore full = 0; //缓冲区初始为空
//生产者进程
producer(){
While(true){
Produce an item in next; //生产数据
P(empty); //获取空缓冲区单元
P(mutex); //进入临界区
add nextp to buffer;
V(mutex); //离开临界区
V(full); //满缓冲区数加1
}
}
//消费者进程
consumer(){
While(true){
P(full); //获取满缓冲区单元
P(mutex); //进入临界区
remove an item from buffer;
V(mutex); //离开临界区
V(empty); //空缓冲区数加1
}
}
(2)读者-写者问题
问题描述:有读者和写着两组并发进程,共享一个文件。要求:允许多个读者同时对文件读;只允许一个写者往里面写;写者进行写时应让已有的读者和写者全部退出。
分析:读写互斥;写写互斥;读读不互斥。
信号量:count用于记录读者数量。初始为0;mutex为用于保护count的互斥信号量;rw用于保证读写互斥。
进程代码描述:
int count = 0; //用于记录当前读者的数量
semaphore mutex=1; //用于保护更新count时的互斥
semaphore rw = 1; //用于保护读写互斥的访问文件
//写者进程
writer(){
P(rw); //互斥访问文件
writing;
V(rw); //释放共享文件
}
//读者进程
reader(){
While(true){
P(mutex); //互斥访问count变量
if(count==0) //第一个读进程访问
P(rw); //阻止写进程写
count++;
V(mutex); //释放互斥变量count
reading;
P(mutex); //互斥访问count变量
count—;
if(count==0) //若此时为最后一个问题
V(rw); //允许写进程写
V(mutext); //释放互斥变量count
}
}
(3)哲学家进餐问题
问题描述:一张圆桌上有5个哲学家,每两名哲学家之间摆一根筷子,两根筷子之间为一碗米饭。哲学家饿时试图拿起左右两根筷子。若筷子在他人手上则需要等待。
分析:5名哲学家对左右两支筷子是互斥访问的。
信号量设置:用信号量数组 chopstick[5]={1,1,1,1,1},用户对筷子的互斥访问。哲学家左边的筷子为i号,右边的为(I+1)%5号。
进程代码:
Semaphore chopsticks[5]={1, 1, 1, 1, 1};
Pi(){
do{
P(chopstick[i]); //取左边的筷子
P(chopstick[(i+1)%5]); //取右边的筷子
eating;
V(chopstick[i]); //放回左边的筷子
V(chopstick[(i+1)%5]); //放回右边的筷子
}while(true);
}
缺点:若哲学家总是同时拿起左边的筷子时最导致死锁。
改进:设置信号量mutex使哲学家在去筷子时首先检查左右两边是否有筷子
Semaphore chopstick[5]={1, 1, 1, 1, 1}
semaphore mutex = 1;
Pi(){
While(true){
P(mutex); //设置去筷子的信号量
P(chopstick[i]);
P(chopstick[(i+1)%5]);
V(mutex); //释放取筷子的信号量
eating;
V(chopstick[i]);
V(chopstickk[(i+1)%5]);
}
}
进程间通信
进程间的通信是指进程之间的信息交换,PV操作属于低级通信方式。而高级通信方式是指以较高的效率传输大量数据的通信方式。主要有以下三类:
(1)共享存储:再通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行写/读实现进程间的信息交换。其中基于数据结构的共享属于低级方式的共享;基于存储区的共享属于高级方式的贡献。
(2)消息传递:进程间的数据交换是以格式化的消息为单位的。若通信的进程间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接收消息两个原语进行数据交换。有如下两种方式:
- 直接通信方式:发送进程直接把消息发送给接收进程并挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。
- 间接通信方式:发送进程把消息发送非中间实体,接收进程从中间实体接收消息。中间实体一般称为信箱。
(3)管道通信:发送进程以字符流的形式将大量数据送入管道,接收进程从管道接受数据。
管道机制必须提供:互斥、同步和确定对方存在的能力。
管道是一个用于连接一个读进程和一个写进程以实现他们之间的通信的共享文件,又名pipe文件。
管道只能采用半双工通信,某一时刻只能单向传输。
线程与进程
线程
线程是一个“轻量级进程”,他是一个基本CPU执行单元,也是程序执行流的最小单元,由线程ID、程序计数器、寄存器集合和堆栈组成。是进程中的一个实体,是系统独立调度和分派的基本单元,本身不拥有系统资源,只具有一点再运行过程中必不可少的资源,但其共享进程所拥有的全部资源。
线程存在的目的:为了减少程序在并发执行时所付出的时空开销,提高操作系统的并发性。
线程主要属性:
- 线程是一个轻型实体,他不拥有系统资源,但每个线程都拥有一个唯一的标识符和一个线程控制块。
- 不同的线程可以执行相同的程序。
- 同一进程内的各线程共享该进程所拥有的全部资源。
- 线程时处理机的独立调度单位,多个线程可以并发执行。
- 一个线程被创建后便开始了自己的声明周期,直到终止。
线程与进程的比较
- 调度:在传统操作系统中,拥有资源和独立调度的基本单位都是进程。在引入线程的操作系统中,线程是独立调度的基本单位,而进程拥有系统资源的基本单位。同一进程中的线程切换不会引起进程切换,而不同进程间的线程切换会引起进程切换。
- 资源:进程是拥有系统资源的基本单位,而线程不拥有系统资源,但线程可以访问其隶属进程的系统资源。
- 并发性:不仅进程间可以并发,不同线程间也可以并发,提供了系统的并发性。
- 系统开销:线程切换只需要保存和设置少量寄存器的内容,开销小。进程切换的开销远大于线程切换。。
- 地址和空间和其他资源:进程的地址空间之间相互独立,同一进程的各个线程间共享进程的资源,某进程内的线程对其他进程不可见。
线程的实现方式
- 用户级线程:有关线程管理的所有工作都由应用程序完成内核感受不到线程的存在。
- 内核级线程:线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只有一个到内核级线程的编程接口。
多线程模型
-
多对一模型:将多个用户及线程映射到一个内核级线程,线程管理在用户空间完成。
- 优点:线程管理在用户空间完成,效率高。
- 缺点:一个线程在使用内核服务时被阻塞,整个进程都会被阻塞。
-
一对一模型:将一个用户及线程映射到一个内核级线程。
- 优点:当一个线程被阻塞后允许其他线程执行,并发能力强。
- 缺点:创建线程的开销比较大,会影响应用程序性能。
-
多对多模型:将n个用户级线程映射到m个内核级线程(n>m)。
- 特点:结合了一对一和多对一模型的优点,集二者之所长。
处理机调度
调度层次:
- 作业调度(高级调度):按一定原则从后备状态的作业中选择一个或多个作业,给它们分配内存、输入/输出设备等必要资源,并建立相应的进程。作业调度进使内存与辅存之间的调度。对于每个作业之调入调出一次。
- 中级调度(内存调度):其作用是提高内存利用率和系统吞吐量。将暂时不能允许的进程调至外存等待,同时称这时的进程状态为挂起态。
- 进程调度(低级调度):按照某种方法和策略从就绪队列中选取一个进程,将处理机分配给它。
三者联系:作业调度是从后备队列中选择一批作业进入内存,为他们建立进程,将其送入就绪队列;进程调度是从就绪队列中选取一个进程,并将其状态改为运行态,将CPU分配给它;中级调度是为了提高内存的利用率,系统将哪些暂时不用的进程挂起来,当内存宽松时再将其唤醒。
进程调度方式
进程调度方式,是指当某个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要处理,即更高优先权的进程进入就绪队列,该如何分配处理机。
- 非剥夺调度方式(非抢占式):当一个进程正在处理机上执行时,即使由某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到其结束,才将处理机分配给更为紧迫的进程。
- 剥夺调度方式(抢占式):当一个进程正在处理机上执行时,若此时有优先级更高的进程需要使用处理机时,则立即停止当前进程的执行,转去执行优先级更高的进程。
调度的基本准则
- CPU利用率:保持CPU尽量忙碌 = (CPU运行时间/进程运行时间)
- 系统吞吐量:表示单位时间内CPU完成的作业数。
- 周转时间:从作业提交到作业完成所经历的时间。
- 等待时间:指进程处于等处理机状态的时间之和,等待时间越长,用户满意度越低。
- 响应时间:用户提交请求到系统首次产生响应的时间。
典型的调度算法
适于作业调度:先来先服务算法、短作业优先调度算法、优先级调度算法、高响应比调度算法。
适于进程调度的算法:先来先服务算法、短作业优先调度算法、优先级调度算法、高响应比调度算法、时间片轮转调度算法、多级反馈队列调度算法。
(1)先来先服务调度算法(FCFS)
- 思想:算法每次从后备队列中选择最先进入该队列的一个或多个作业,将其调入内存,分配必要资源,创建进程放入就绪队列。(每次从就绪队列中选择最先进入的进程,为其分配处理机)
- FCFS为非抢占式算法。
- 特点:算法简单,但效率低;对长作业有利,对短作业不利;适合于CPU繁忙型作业,不利于IO繁忙型作业。
(2)短作业优先调度算法(SJF)
-
思想:从后备队列中选择一个或若干个估计运行时间最短的作业,将其调入内存。(每次从就绪队列中选择一个估计运行时间最短的进程,为其分配处理机)
-
SJF调度算法缺点:
- 对长作业不利。可能导致长作业长期不被调度造成”饥饿“。
- 算法完全没有烤炉作业的紧迫程度,不能保证紧迫性作业会被及时处理。
- 作业的长短主要由用户决定,不能保证真正的短作业调度。
-
SJF调度算法的平均等待时间、平均周转时间最少。
(3)优先级调度算法
-
思想:按优先级决定进程或作业的调度次序。
-
优先级调度算法根据其是否可以抢占可将其分为非剥夺式优先级调度算法和剥夺式优先级调度算法。同时根据进程创建后优先级是否可变,又可将进程优先级分为静态优先级和动态优先级。
-
优先级设置原则:
- 系统进程 > 用户进程
- 交互型进程 > 非交互型进程
- I/O型进程 > 计算型进程
(4)高响应比优先调度算法
-
思想:是对FCFS和SJF的综合考虑,同时考虑了作业的等待时间和估计运行时间。
响应比 = (等待时间+要求服务时间)/要求服务时间 -
特点
- 作业的等待时间相同时,要求服务时间越短,响应比越高,有利于短作业。
- 要求服务时间相同,等待时间越长,响应比越高,因此实现了先来先服务。
- 对于长作业,响应比可随其等待时间的延长得到提高,同时兼顾了长作业。
(5)时间片轮转调度算法
- 思想:适用与分时操作系统,按照先来先服务的原则,以一定的时间片轮流为进程提供服务。
- 时间片的长短决定因素:系统的响应时间、就绪队列中的进程数目、系统的处理能力。
(6)多级反馈队列调度算法
-
思想:
- 设置多个就绪队列,每个队列设置不同的优先级,第1级队列的优先级最高,第2级次之,其余队列逐次降低。
- 各级队列中进程执行时间片的大小各不相同,优先级高的队列中时间片越小。
- 新进程来临后,首先将其放在第1级队列,按照FCFS的原则进行服务,如果其能在该时间片内完成则撤离系统,否则放入第2级队列再次按FCFS的原则服务。
- 只有第1级队列为空时才能执行第2级队列,同理只有第1~i-1级队列为空时才能执行第i级队列。
-
优势:
- 终端型用户:短作业优先原则。
- 短批处理作业用户:周转时间较短。
- 长批处理作业:经过前面几个队列获得部分执行,不会长期得不到处理。
死锁
定义:是指多个进程因竞争资源而造成的一种僵局,若无外力作用,任何进程都将无法向前推进。
死锁产生原因:
① 系统资源的竞争:对不可剥夺资源的竞争。
②进程推进顺序非法
死锁产生的必要条件:
①互斥条件:进程要求对所分配的资源进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程申请该资源,则请求进程只能等待。
②不可剥夺条件:进程所获得的资源在未使用之前,不能被其他进程强行夺走,只能自己释放。
③请求并保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时进程阻塞,但不释放自己占有的资源。
④循环等待:存在一种进程资源的循环等待链,链中每个进程已获得的资源同时被链中下一个进程所请求。
死锁的处理策略:
①死锁预防:破坏死锁发生的四个条件。
②死锁避免:资源分配过程中,采用某种措施防止进入不安全状态。
③死锁检测与解除:无需采取任何限制性措施,允许系统进入死锁状态。通过系统的检测机构发现死锁并解除死锁。
死锁预防措施:
- 破坏互斥条件:允许系统资源都能共享使用。(不可行)
- 破坏不可剥夺:当一个已保持了某些不可剥夺资源的进程请求新的资源而得不到满足时,他必须释放已经保持的所有资源,待以后需要时再重新申请。(实现复杂,且可能导致前一阶段工作的失效,反复申请释放会增加系统开销,降低系统吞吐量)
- 破坏请求并保持:采用静态分配即在程序运行前一次申请完它所需要的全部资源,在其资源不满足前,不投入运行。(资源被严重浪费)
- 破坏循环等待:给资源编号,进程必须按顺序申请。
死锁避免
允许进程动态的申请资源,但系统在进行资源分配前,应先检查此次分配的安全性。若分配不会导致系统进入不安全状态,则允许分配;否则让进程等待。
安全状态: 指系统能按某种进程推进顺序为每个进程分配其所必须的资源,直到满足每个进程对资源的最大需求,是每个进程都可顺序完成。
银行家算法
①思想:允许进程动态地申请资源,系统在每次实施资源分配之前,先计算资源分配的安全性,若此次资源分配安全(即资源分配后,系统能按某种顺序来为每个进程分配其所需的资源,直至最大需求,使每个进程都可以顺利地完成),便将资源分配给进程,否则不分配资源,让进程等待。
②数据结构
-
可利用资源向量Available。这是一个含有m个元素的数组,其中的而每一个元素代表一类可利用资源数目,其初始值是系统中所配置的该类全部可用资源的数目,其数值随该类资源的分配和回收而动态的改变。如果Available[j]=K,则表示系统中现有Rj类资源K个。
-
最大需求矩阵Max。这是一个n*m的矩阵,它定义了系统中n个进程中的每一个进程对m类资源的最大需求。如果Max[i,j]=K;则表示进程i需要Rj类资源的最大数目为K。
-
分配矩阵Allocation。这也是一个n*m的矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。如果Allocation[i,j]=K,则表示进程i当前已分得Rj类资源的数目为K。
-
需求矩阵Need。这也是一个n*m的矩阵,用以表示每一个进程尚需的各类资源数。如果Need[i,j]=K,则表示进程i还需要Rj类资源K个,方能完成任务。
上述三个矩阵间存在下述关系:Need[i,j]=Max[i,j]-Allocation[i,j]
③设计思路
第一部分:银行家算法模块
1.如果Request<=Need,则转向2;否则,出错
2.如果Request<=Available,则转向3,否则等待
3.系统试探分配请求的资源给进程
4.系统执行安全性算法
第二部分:安全性算法模块
1.设置两个向量
工作向量:Work=Available(表示系统可提供给进程继续运行所需要的各类资源数目)
Finish:表示系统是否有足够资源分配给进程(True:有;False:没有).初始化为False
2.若Finish[i]=False&&Need<=Work,则执行3;否则执行4(i为资源类别)
3.进程P获得第i类资源,则顺利执行直至完成,并释放资源: Work=Work+Allocation; Finish[i]=true;转2
4.若所有进程的Finish[i]=true,则表示系统安全;否则,不安全!
④算法实现
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
public class os {
//可用资源矩阵
private int[] avaRes;
//分配矩阵
private int[][] allocate;
//最大需求矩阵
int[][] maxNeed;
//进程数量
private int processNum;
//资源种类
private int resourceNum;
/**
* 检查此时是否为安全状态
* */
public boolean checkStatus(int i,int j,int k){
//i 代表请求资源进程号 j 代表申请资源的种类 k代表申请的数量
List<Integer> securityList = new ArrayList<>();
int[] tempAva = new int[resourceNum];
//用于判断进程能否最终结束
boolean[] Finish = new boolean[processNum];
//上次检查的不安全序列
int lastNotSecurity = processNum;
for(int t=0;t<resourceNum;t++){
tempAva[t] = avaRes[t];
}
tempAva[j-1] = tempAva[j-1]-k;
allocate[i-1][j-1] = allocate[i-1][j-1]+k;
while(true) {
//每轮都将当前不安全状态置为0
int currentNotSecurity = 0;
int count=0;
for (int row = 0; row < processNum; row++) {
count=0;
if(Finish[row]){
continue;
}
//判断第row个进程是否安全
for(int col = 0;col<resourceNum;col++){
if(allocate[row][col]+tempAva[col] >= maxNeed[row][col]){
count++;
}else {
break;
}
}
//如果该进程安全可将其资源收回用于其他进程
if(count == resourceNum){
//将线程row设为安全
Finish[row] = true;
securityList.add(row);
for(int col = 0;col<resourceNum;col++){
//回收线程row拥有的资源为可用资源
tempAva[col] += allocate[row][col];
}
}else{
currentNotSecurity++;
}
}
if(currentNotSecurity == 0){
System.out.println("满足要求");
System.out.println("分配后的安全序列为:");
for (Integer p : securityList) {
System.out.print("进程"+(p+1)+" ");
}
System.out.println();
avaRes[j-1] -= k;
allocate[i-1][j-1] += k;
return true;
}
if(currentNotSecurity == lastNotSecurity){
System.out.println("拒绝分配");
allocate[i-1][j-1] = allocate[i-1][j-1]-k;
return false;
}else{
lastNotSecurity = currentNotSecurity;
}
}
}
/**
* 进行资源请求
* */
public void request(int i,int j,int k){
//i 代表请求资源进程号 j 代表申请资源的种类 k代表申请的数量
int temp = avaRes[j-1]-k;
int need = maxNeed[i-1][j-1]-allocate[i-1][j-1];
if(temp<0||k>need){
System.out.println("拒绝分配!");
return;
}
checkStatus(i,j,k);
}
/**
* 进行资源释放
* */
public void release(int i,int j,int k){
if(allocate[i-1][j-1]-k<0){
System.out.println("没有足够资源可以释放");
return;
}
allocate[i-1][j-1] -= k;
avaRes[j-1] += k;
System.out.println("资源释放成功");
}
/**
* 检查是否存在安全序列
* */
public boolean check(){
//i 代表请求资源进程号 j 代表申请资源的种类 k代表申请的数量
List<Integer> securityList = new ArrayList<>();
int[] tempAva = new int[resourceNum];
//用于判断进程能否最终结束
boolean[] Finish = new boolean[processNum];
//上次检查的不安全序列
int lastNotSecurity = processNum;
for(int t=0;t<resourceNum;t++){
tempAva[t] = avaRes[t];
}
while(true) {
//每轮都将当前不安全状态置为0
int currentNotSecurity = 0;
int count=0;
for (int row = 0; row < processNum; row++) {
count=0;
if(Finish[row]){
continue;
}
//判断第row个进程是否安全
for(int col = 0;col<resourceNum;col++){
if(allocate[row][col]+tempAva[col] >= maxNeed[row][col]){
count++;
}else
break;
}
//如果该进程安全可将其资源收回用于其他进程
if(count == resourceNum){
//将线程row设为安全
Finish[row] = true;
securityList.add(row);
for(int col = 0;col<resourceNum;col++){
//回收线程row拥有的资源为可用资源
tempAva[col] += allocate[row][col];
}
}else
currentNotSecurity++;
}
if(currentNotSecurity == 0){
System.out.println("存在安全序列安全序列为:");
for (Integer p : securityList) {
System.out.print("进程"+(p+1)+" ");
}
System.out.println();
return true;
}
if(currentNotSecurity == lastNotSecurity){
System.out.println("不存在安全序列");
return false;
}else{
lastNotSecurity = currentNotSecurity;
}
}
}
/**
* 进行资源初始化操作
* */
public void init(){
Scanner sc = new Scanner(System.in);
System.out.print("请输入进程数量:");
processNum = sc.nextInt();
System.out.print("请输入资源的种类:");
resourceNum = sc.nextInt();
//可用资源矩阵
avaRes = new int[resourceNum];
//分配矩阵
allocate = new int[processNum][resourceNum];
//最大需求矩阵
maxNeed = new int[processNum][resourceNum];
//进行矩阵信息的输入
System.out.println("请输入分配矩阵的信息:");
for(int i=0;i<processNum;i++){
for(int j=0;j<resourceNum;j++){
allocate[i][j]=sc.nextInt();
}
}
System.out.println("请输入最大需求矩阵的信息:");
for(int i=0;i<processNum;i++){
for(int j=0;j<resourceNum;j++){
maxNeed[i][j]=sc.nextInt();
}
}
System.out.println("请输入可用资源矩阵的信息:");
for(int i=0;i<resourceNum;i++){
avaRes[i] = sc.nextInt();
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
os o = new os();
o.init();
while(true){
String s ;
System.out.print(">");
s = in.nextLine();
int len = s.length();
int i=0,j=0,k=0;
if("check".equals(s)){
o.check();
continue;
}
if(s.contains("request")||s.contains("release")) {
String str = s.substring(8, len - 1);
String[] split = str.split(",");
i = Integer.parseInt(split[0]);
j = Integer.parseInt(split[1]);
k = Integer.parseInt(split[2]);
}
if(s.contains("request")){
//进行申请资源的请求
o.request(i,j,k);
}else if(s.contains("release")){
//进行释放资源请求
o.release(i,j,k);
}else{
System.out.println("输入格式错误!");
}
}
}
}
死锁解除
- 资源剥夺法:挂起某些死锁进程,并抢占他们的资源,将这些资源分配给其他死锁进程。
- 撤销进程法:强制撤销部分甚至全部进程并剥夺这些进程的资源。撤销的原则可以按进程优先级和撤销进程代价的高低进行。
- 进程回退法:让一个或多个进程回退到足以回避死锁的地步,进程回退时自愿放弃资源而非被剥夺。要求系统保持进程的历史信息,设置还原点。