[概述]
操作系统中,银行家算法是避免死锁的一种重要算法。
本文针对《计算机操作系统(第四版)》(汤小丹)p123页的问题:**如果在银行家算法中把P0发出的请求向量改为Request0(0,1,0),系统是否能将资源分配给它,请读者考虑。**进行模拟。
本题有三种资源A,B,C.,5个进程P0~P4。
1.银行家算法中的数据结构
1)可利用资源Available,是一个含有3个元素的数组,分别表示A B C的可利用资源量。
2)最大需求矩阵Max,为一个53的矩阵。
3)分配矩阵Allocation,为一个53的矩阵。
4)需求矩阵Need,为一个5*3的矩阵。
其中Need[i,j] = Max[i,j]-Allocation[i,j]
5)结束数组finish,长度为5的bool型数组,用于标记P0~P4这五个进程是否顺序执行完成。
6)out_stack标记,这个标记为bool型,只用于安全性检验中。
有关这个问题的具体描述,请见课本相应位置。
下面,将通过代码模拟这道题的解题过程。
/*
该银行家算法模拟了第122页上的一个例子,从T0时刻模拟。
*/
#include
#include
using namespace std;
typedef struct Bank{
int Available[3] = {2,3,0}; //一共有多少资源
int Max[5][3] = {7,5,3,
3,2,2,
9,0,2,
2,2,2,
4,3,3};//需要多少资源
int Allocation[5][3] = {0,1,0,
3,0,2,
3,0,2,
2,1,1,
0,0,2}; //已分配的资源数
int Need[5][3]; //还需要多少资源
bool finish[5] = {false,false,false,false,false}; //完成标记
bool out_stack = false; //只用于安全性判断上。为true且在栈顶,出栈。
}Bank;
void Change_Need_Matrix(Bank* bank){
for(int i=0;i<5;i++){
for(int j=0;j<3;j++){
bank->Need[i][j] = bank->Max[i][j] - bank->Allocation[i][j];
}
}
}//根据Max和Allocation矩阵求得Need矩阵;Need = Max-Allocation;
Bank* Copy_Bank(Bank* Original_bank){
Bank* Copy = new Bank();
for(int i = 0;i<3;i++){
Copy->Available[i] = Original_bank->Available[i];
}
for(int i = 0;i<5;i++){
for(int j = 0;j<3;j++){
Copy->Max[i][j] = Original_bank->Max[i][j];
Copy->Allocation[i][j] = Original_bank->Allocation[i][j];
Copy->Need[i][j] = Original_bank->Need[i][j];
}
}
for(int i=0;i<5;i++){
Copy->finish[i] = Original_bank->finish[i];
}
Copy->out_stack = Original_bank->out_stack;
return Copy;
}//返回当前现场bank的拷贝。用于安全性检查中,不能在原题上做安全性检查,这样破坏了现场,所以做了一个拷贝,在这个拷贝上做安全性检查。
bool Judge_Simulation_End(Bank* bank){
for(int i=0;i<5;i++){
if(bank->finish[i] == false){
return false;
}
}
return true;
} //如果五个进程的finish均为true,就可以结束了。
bool Judge_Process_Finish(int Process_id,Bank* bank){
for(int i=0;i<3;i++){
if(bank->Need[Process_id][i] != 0 ){
return false;
}
}
return true;
} //判断一个进程是否运行完成,完成了把这个进程的finish置true。
bool Judge_Add_To_Stack(int* request,int* Available){
for(int i=0;i<3;i++){
if(request[i]>Available[i]){
return false;
}
}
return true;
} //判断要不要加到进程id堆栈和现场堆栈中。
bool Judge_Safe(int id,int* request,Bank* bank){
/*
这个函数模拟了一个请求,并判断这个请求是否具有安全性
id-->进程的id号,int型,0-4
request-->一个三维int型数组,分别记录进程id记录请求的A,B,C资源的量。
bank-->原题现场,拷贝的模板
*/
Bank* temporary_bank = Copy_Bank(bank); //做一个原现场bank的拷贝temporary_bank。
for(int i=0;i<3;i++){
temporary_bank->Need[id][i] -= request[i];
temporary_bank->Allocation[id][i] += request[i];
temporary_bank->Available[i] -= request[i];
} //在拷贝的现场temporary_bank上执行这个请求。
if(Judge_Process_Finish(id,temporary_bank) == true){
temporary_bank->finish[id] = true;
for(int i=0;i<3;i++){
temporary_bank->Available[i] += temporary_bank->Allocation[id][i];
}
} //如果执行完这个请求后,id进程结束,则将其Allocation返还给Available。
if(Judge_Simulation_End(temporary_bank)==true){
cout<<"资源分配完成!"<<endl;
exit(0); //资源分配完成,程序直接退出。
} //如果五个进程全都finish,则资源分配完成
//安全性检查的目的:要找出执行完当前请求后,后面能不能找到一个序列,使各个进程按照这个顺序分配资源执行,全都顺利结束。
//注意:只要找到一个能成功的分配资源序列,就说明其具有安全性,但其可能有很多分配顺序序列,不必全都找出。
//找出一个能成功的分配资源序列,需要用到两个栈,一个进程id堆栈,还有一个记录现场的堆栈。两个堆栈同步入栈和出栈。
stack <int>process_id_stack; //进程id栈。
stack <Bank*>scene; //记录现场的堆栈。这个现场,其实就指当前的temporary_bank。
for(int i=0;i<5;i++){
if((temporary_bank->finish[i] == false)&&(Judge_Add_To_Stack(temporary_bank->Need[i],temporary_bank->Available) == true)){
process_id_stack.push(i);
scene.push(temporary_bank);
}
}//Judge_Add_To_Stack函数根据每个进程的Need和此时刻的Available分别对每个进程判断是否能入栈。
while(!process_id_stack.empty()){
if(Judge_Simulation_End(temporary_bank) == true){
return true; //模拟过程中,进程全都可结束,其可被安全调度
}
else{
int top = process_id_stack.top(); //取栈顶元素,栈顶元素之所以能入栈,说明其此时可以分配给其全部Need资源。
Bank* get_bank_stack_top = Copy_Bank(scene.top());
scene.pop();
get_bank_stack_top->out_stack = true;
scene.push(get_bank_stack_top); //将当前现场栈栈顶的出栈变量修改为true。
for(int i=0;i<3;i++){
temporary_bank->Available[i] -= temporary_bank->Need[top][i];
temporary_bank->Allocation[top][i] += temporary_bank->Need[top][i];
temporary_bank->Need[top][i] = 0;
temporary_bank->Available[i] += temporary_bank->Allocation[top][i];
} //那就分配给它全部它需要的Need资源喽!
temporary_bank->finish[top] = true; //分配完后,Need减为0,这个进程按这种分配方式它已经结束了。
//但是到这里我们还没有判断这么做是不是满足安全性检查。
for(int i=0;i<5;i++){
if((temporary_bank->finish[i] == false)
&&(Judge_Add_To_Stack(temporary_bank->Need[i],temporary_bank->Available) == true)){
process_id_stack.push(i);
temporary_bank->out_stack = false;
scene.push(temporary_bank); //跟上面一样,能入栈的继续入栈。
}
}
}
if(Judge_Simulation_End(temporary_bank) == true){
return true; //模拟过程中,进程全都可结束,其可被安全调度
}
while(scene.top()->out_stack == true){
temporary_bank = scene.top();
scene.pop();
temporary_bank->finish[process_id_stack.top()] = false;
process_id_stack.pop();
} //结束一轮时若当前现场栈栈顶出栈标记为true,证明当前模拟过程为不安全的。栈顶元素出栈。
}
free(temporary_bank);
return false;
} //安全性检查函数
bool Judge_Request(int Process_id,int* request,Bank* bank){
if(bank->finish[Process_id] == true){
cout<<"进程"<<Process_id<<"已经完成了分配"<<endl;
return false;
} //1.已经完成资源分配的进程不能再进行资源分配。
int count = 3;
while(count){
if(bank->Need[Process_id][count-1]<request[count-1]){
cout<<"请求资源大于需要资源"<<endl;//2.判断请求是不是小于Need
return false;
}
if(bank->Available[count-1]<request[count-1]){
cout<<"请求资源大于已有资源"<<endl;//3.判断请求是不是小于Allocation
return false;
}
count--;
}
//4.最后进行安全性检查,如果通过安全性检查,就证明其具有安全性,否则不具有安全性。
return Judge_Safe(Process_id,request,bank);
} //判断请求是否能相应函数。
bool check_diedlock(Bank* bank){
int count = 0;
for(int i=0;i<5;i++){
if(Judge_Add_To_Stack(bank->Need[i],bank->Available) == false){
count++;
}
}
if(count == 5){
return true;
}
else{
return false;
}
} //检查死锁,如果死锁,操作系统都救不了了。所谓死锁,就是五个进程的Need均大于此时的Available。此时,无论怎么折腾,都是死锁。
void print_table(Bank* bank){
cout<<" Allocation Need Available"<<endl;
cout<<" A B C A B C A B C"<<endl;
for(int i=0;i<5;i++){
cout<<"P"<<i<<" "<<bank->Allocation[i][0]<<" "<<bank->Allocation[i][1]<<" "<<bank->Allocation[i][2];
cout<<" "<<bank->Need[i][0]<<" "<<bank->Need[i][1]<<" "<<bank->Need[i][2];
if(i == 0){
cout<<" "<<bank->Available[0]<<" "<<bank->Available[1]<<" "<<bank->Available[2];
}
cout<<endl;
}
} //打印书P123页的表格到终端。
int main(){
Bank* bank = new Bank();
Change_Need_Matrix(bank);
int id;
int request_a;
int request_b;
int request_c;
while(Judge_Simulation_End(bank) == false){ //只要进程资源分配没完成,就一直while。
cout<<"--------------------------------------------------------------------------------"<<endl;
if(check_diedlock(bank) == true){
cout<<"这个问题死锁,我也没办法了。"<<endl;
break;
} //先判断一下死锁,如果这个问题本身就是死锁的,也就不用再瞎折腾了。
/*
输入一个请求
*/
cout<<"请输入请求分配资源的进程:";
cin>>id;
cout<<"请输入请求分配的A类资源数:";
cin>>request_a;
cout<<"请输入请求分配的B类资源数:";
cin>>request_b;
cout<<"请输入请求分配的C类资源数:";
cin>>request_c;
int request[3] = {request_a,request_b,request_c};
if(Judge_Request(id,request,bank)==1){
cout<<"当前的请求安全,可以按照请求分配"<<endl;
/*
既然请求安全,就进行资源分配。
*/
bank->Need[id][0] -= request_a;
bank->Need[id][1] -= request_b;
bank->Need[id][2] -= request_c;
bank->Allocation[id][0] += request_a;
bank->Allocation[id][1] += request_b;
bank->Allocation[id][2] += request_c;
bank->Available[0] -= request_a;
bank->Available[1] -= request_b;
bank->Available[2] -= request_c;
if((bank->Need[id][0] == 0)&&(bank->Need[id][1] == 0)&&(bank->Need[id][2] == 0)){
bank->Available[0] += bank->Allocation[id][0];
bank->Available[1] += bank->Allocation[id][1];
bank->Available[2] += bank->Allocation[id][2];
bank->finish[id] = true;
} //执行完这个资源分配请求,如果有某个进程结束,它当前的finish置为true。
print_table(bank); //向用户展示P123页表格,方便下次输入请求。
}
else{
/*
请求不安全,啥也不做,接着等待用户输入下一个请求。
*/
cout<<"不安全"<<endl;
print_table(bank); //向用户展示P123页表格,方便下次输入请求。
}
}
return 0;
}
[安全性检验算法详解]
写这个代码唯一有收获的地方,就是怎么进行安全性检查。正如注释中所说,只要找到一个资源分配序列满足需求,就可以证明其具有安全性。
笔者采用两个堆栈实现,一个保存进程id号,另一个保存现场,这里的现场指的是temporary_bank。为什么要保存现场?因为在进行安全性测试的时候,其可能会陷入不安全。但此时还不能说明它是死锁的。比如:第一轮分配的时候,P1和P4的Need数组都对应小于此时的Allocation,究竟是选择走P1还是P4,我们不知道。这里有点像剪枝操作。由于P4比P1后入栈,我们先假设走P4这条路,如果走了P4这条路,下一步P0~P4的Need都小于Allocation,则说明P4这条路走不通。此时栈顶元素是P4,他就可以出栈了。出栈后,栈顶元素变成P1,测试P1,P1如果可以的话,就具有安全性。P1要是也不行,P1就也得出栈,此时栈就为空,所有的状态空间搜索完毕,也没有找到一个合适的资源分配序列。那就说明当前请求Request不具有安全性,需要用户重新输入一个具有安全性的Request。
再举个更详细的例子,比如:
当前决定为P1分配资源,就将1入process_id_stack入栈,将为P1分配资源前的bank现场入拷贝的现场栈。此时,现场栈栈顶元素的栈顶入栈标记out_stack为false。然后进入while(!process_id_stack.empty())循环,如果栈空,证明无法找出一个安全的资源分配序列,退出循环,如果栈非空,进入循环。这里栈非空,进入循环,然后temporary_bank为P1分配资源,更新Need,Allocation和Available。然后更新此时拷贝现场栈栈顶P1的出栈标记out_stack为true,然后调用Judge_Add_To_Stack函数判定此时能为哪个进程分配资源。假设此时能为P3,P4分配资源,则P3,P4入栈(注意:本文所说的入栈就是入两个栈,process_id_stack和拷贝的现场栈)。此时栈有底到顶为P1,P3,P4,P1现场栈出栈标记out_stack为true,P3现场栈出栈标记out_stack为false,P4现场栈出栈标记out_stack为false。由于栈顶元素P4为false,不执行栈顶出栈操作,进入下一个while(!process_id_stack.empty())循环,这里栈非空,继续循环,为栈顶进程P4分配资源,并将栈顶现场栈的出栈标记记为true。然后调用Judge_Add_To_Stack函数判定此时能为哪个进程分配资源。假设此时不能为任何进程分配资源,也就是说P1->P4这个分配顺序是不安全的。则没有任何入栈操作。由于栈顶元素P4为true,应该执行出栈操作,此时应该将P4出栈,栈顶指向P3,若P3经过上述类似P4的分析仍不存在一个安全序列,则P3的out_stack标记也会被置为true,在while(!process_id_stack.empty())循环的最后也会被出栈,若P3出栈,此时栈里只剩P1,由于P1现场栈出栈标记也被置为了true,所以P1也出栈。此时栈为空,证明无法找到一个安全的资源分配序列。即当前的资源分配请求是不安全的。这里需要注意的是:栈顶有多少true,就出栈多少。因为这些true对应的结点均为错误资源分配路径上的结点。只有找到false,才可继续判断,false代表沿着当前结点的这条资源分配路径还没有进行判断。
总的来说,这里:
1.每次模拟的时候,先看一下模拟是否结束,如果五个finish都为true,就结束了,就找到了安全序列,就跳出。如果没有,就先为当前栈顶进程分配资源,然后将拷贝现场栈出栈标记out_stack置true。
2.置true后,看当前状态能不能有新进程可以被分配资源,如果有,就将它们入栈,但入栈的时候它们的拷贝现场栈出栈标记仍为false。如果没有,就执行3。
3.栈顶为true,一直出栈。栈顶为false,则跳回到1执行。
下面这个图也是说明了上面这段文字。
[运行结果]
第一次分配
把P0发出的请求向量改为Request(0,1,0)。
第二次分配
P1请求资源分配
第三次分配
P3请求资源分配
第四次分配
P0请求资源分配
第五次分配
P2请求资源分配
第六次分配
P4请求资源分配
资源分配完成。
这里我们是手动的进行资源分配,也可以用一个算法实现自动的资源分配。