银行家算法解决死锁问题
一.概念引入
银行家算法( banker's algorithm )由 Dijkstra于1965提出,关键是将死锁的问题演示为一个银行家贷款的模型,由于能用于银行系统的现金贷款而出名。一个银行家向一群客户发放信用卡,每个客户有不同的信用额度。每个客户可以提出信用额度内的任意额度的请求,直到额度用完后再一次性还款。银行家承诺每个客户最终都能获得自己需要的额度。所谓“最终”,是说银行家可以先挂起某个额度请求较大的客户的请求,优先满足小额度的请求,等小额度的请求还款后,再处理挂起的请求。这样,资金能够永远流通。所以银行家算法其核心是:保证银行家系统的资源数至少不小于一个客户的所需要的资源数。
银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但银行家算法在系统在进行资源分配之前(并不是真的不分配,这样就没法做了,只不过是试探性分配,不满足的话再恢复),应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。为实现银行家算法,系统必须设置若干数据结构。要解释银行家算法,必须先解释操作系统安全状态和不安全状态。安全序列是指存在一个进程序列{P1,…,Pn}是安全的,不会死锁(至少两个线程占有某资源A,但是都不满足,剩余的资源A分配给谁仍然无法满足),安全状态如果存在一个由系统中所有进程构成的安全序列P1,…,Pn,则系统处于安全状态,安全状态一定是没有死锁发生;不安全状态不存在一个安全序列,不安全状态不一定导致死锁。
本算法在理论上是出色的,能非常有效地避免死锁,但从某种意义上说,它缺乏实用价值,因为很少有进程能够在运行前就知道其所需资源的最大值,且进程数也不是固定的,往往在不断地变化(如新用户登录或退出),况且原来可用的资源也可能突然间变成不可用(如打印机、磁带机可能被损坏)。
二.算法原理
银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。每分配一次资源就测试一次是否安全,不是资源全部就位后才测试,注意理解checkError函数的循环顺序。
我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。
为保证资金的安全,银行家规定:
当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客(试探性分配)
顾客可以分期贷款,但贷款的总数不能超过最大需求量(可能一次并不能满足所需要的全部资源)
当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款(不存在死锁)
当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金(运行后释放)
操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能存在安全状态,则按当前的申请量分配资源,否则也要推迟分配。
1、 设系统中有3种类型的资源(A,B,C)和5个进程P1、P2、P3、P4、P5,A资源的数量为17,B资源的数量为5,C资源的数量为20。在T0时刻系统状态见下表(T0时刻系统状态表)所示。系统采用银行家算法实施死锁避免策略。(12分)
T0时刻系统状态表
| 最大资源需求量 | 已分配资源数量 |
A B C | A B C | |
P1 | 5 5 9 | 2 1 2 |
P2 | 5 3 6 | 4 0 2 |
P3 | 4 0 11 | 4 0 5 |
P4 | 4 2 5 | 2 0 4 |
P5 | 4 2 4 | 3 1 4 |
T0时刻系统状态表
P2请求资源(0,3,4)(0,1,1)
(1) T0时刻是否为安全状态?若是,请给出安全序列。
(2) 在T0时刻若进程P2请求资源(0,3,4),是否能实施资源分配?为什么?
(3) 在(2)的基础上,若进程P4请求资源(2,0,1),是否能实施资源分配?为什么?
(4) 在(3)的基础上,若进程P1请求资源(0,2,0),是否能实施资源分配?为什么?
答:当前的系统状态描述为:
//资源最大需求数量
//已分配资源数量
//还需资源数量
//资源总量
//系统可用资源向量
(1)T0时刻是否为安全状态?若是,请给出安全序列。
在T0时刻,由于V(2,3,3)大于等于(C-A)中P5所在行的向量(1,1,0),因此V能满足P5的运行,在P5运行后,系统的状态为:
同样的,在P5运行后,V’(5,4,7)也大于等于C-A中P4所在的行(2,2,1),则能满足P4的运行。P4运行后,系统的状态为:
按照上述同样的方法,P4运行后,P3,P2,P1也能按顺序运行。(备注:考试时需要都写出来)。
因此,在T0时刻,存在安全序列:P5、P4、P3、P2、P1。
T0时刻是安全的。
-------------------------------另外一解法(书中解法)--------------------
执行序列选择:首先选择需求资源总数最少的优先。
| Work | need | Alloc | Work_alloc | Finish |
A B C | A B C | A B C | A B C | T | |
P5 | 2 3 3 | 1 1 0 | 3 1 4 | 5 4 7 | T |
P4 | 5 4 7 | 2 2 1 | 2 0 4 | 7 4 11 | T |
P3 | 7 4 11 | 0 0 6 | 4 0 5 | 11 4 16 | T |
P2 | 11 4 16 | 1 3 4 | 4 0 2 | 15 4 18 | T |
P1 | 15 4 18 | 3 4 7 | 2 1 2 | 17 5 20 | T |
Finish 都为TRUE
得到在T0时刻,安全序列:P5、P4、P3、P2、P1。
------------------------------另外一解法(书中解法)--------------------
(2)在T0时刻若进程P2请求资源(0,3,4),是否能实施资源分配?为什么?
P2申请资源(0,3,4),但在C-A中,P2所在行向量是(1,3,4)。对于资源R1,P2的申请超过它所预定的需求。因此,该申请不给予分配。
(3)在(2)的基础上,若进程P4请求资源(2,0,1),是否能实施资源分配?为什么?
A)P4申请(2,0,1)不超过C-A中P4所在行的向量(2,2,1)。
B)V(2,3,3)大于等于P4的申请(2,0,1)
C)对P4的申请(2,0,1)进行预分配,预分配后,系统的状态为:
可用资源V(0,3,2)大于等于C-A中P4所在的行(0,2,0),因此可以满足P4的运行。P4运行后,系统的状态为:
同样的方法(考试时需要列出),可计算出存在安全序列:P4,P5,P3,P2,P1。
因此,预分配后系统的状态是安全状态。
对于,P4请求资源(2,0,1),给予分配,分配后的系统新状态为:
-------------------------------另外一解法(书中解法)--------------------
A)P4申请(2,0,1)不超过C-A中P4所在行的向量(2,2,1)。
B)V(2,3,3)大于等于P4的申请(2,0,1)
C)对P4的申请(2,0,1)进行预分配,预分配后,系统的状态为:
| Max | alloc | Need | available |
A B C | A B C | A B C | A B C | |
P1 | 5 5 9 | 2 1 2 | 3 4 7 | 0 3 2(2 3 3) |
P2 | 5 3 6 | 4 0 2 | 1 3 4 |
|
P3 | 4 0 11 | 4 0 5 | 0 0 6 |
|
P4 | 4 2 5 | 4 0 5(2 0 4) | 0 2 0(2 2 1) |
|
P5 | 4 2 4 | 3 1 4 | 1 1 0 |
|
注意:()内的值为旧值
安全性算法:
| Work | need | Alloc | Work_alloc | Finish |
A B C | A B C | A B C | A B C |
| |
P4 | 0 3 2 | 0 2 0 | 4 0 5 | 4 3 7 | T |
P5 | 4 3 7 | 1 1 0 | 3 1 4 | 7 4 11 | T |
P3 | 7 4 11 | 0 0 6 | 4 0 5 | 11 4 16 | T |
P2 | 11 4 16 | 1 3 4 | 4 0 2 | 15 4 18 | T |
P1 | 15 4 18 | 3 4 7 | 2 1 2 | 17 5 20 | T |
Finish 都为TRUE
在P4申请资源(2,0,1),给予分配后,可以得到安全序列:P5、P4、P3、P2、P1。
P4申请资源(2,0,1)后,系统状态为:
| Max | alloc | Need | available |
A B C | A B C | A B C | A B C | |
P1 | 5 5 9 | 2 1 2 | 3 4 7 | 0 3 2 |
P2 | 5 3 6 | 4 0 2 | 1 3 4 |
|
P3 | 4 0 11 | 4 0 5 | 0 0 6 |
|
P4 | 4 2 5 | 4 0 5 | 0 2 0 |
|
P5 | 4 2 4 | 3 1 4 | 1 1 0 |
|
------------------------------另外一解法(书中解法)--------------------
(4)在(3)的基础上,若进程P1请求资源(0,2,0),是否能实施资源分配?为什么
进程P1请求资源(0,2,0)
A)P1申请(0,2,0)不超过C-A中P1所在行的向量(3,4,7)。
B)V(0,3,2)大于等于P1的申请(0,2,0)
C)对P1的申请(0,2,0)进行预分配,预分配后,系统的状态为:
V(0,2,1)不大于等于P1到P5任一进程在C-A中的向量,因此系统进行预分配后处于不安全状态。
对于P1申请资源(0,2,0),不给予分配。
------------------------------另外一解法(书中解法)--------------------
进程P1请求资源(0,2,0)
A)P1申请(0,2,0)不超过C-A中P1所在行的向量(3,4,7)。
B)V(0,3,2)大于等于P1的申请(0,2,0)
C)对P1的申请(0,2,0)进行预分配,预分配后,系统的状态为:
| Max | alloc | Need | available |
A B C | A B C | A B C | A B C | |
P1 | 5 5 9 | 2 3 2(2 1 2) | 3 2 7(3 4 7) | 0 1 2(0 3 2) |
P2 | 5 3 6 | 4 0 2 | 1 3 4 |
|
P3 | 4 0 11 | 4 0 5 | 0 0 6 |
|
P4 | 4 2 5 | 4 0 5 | 0 2 0 |
|
P5 | 4 2 4 | 3 1 4 | 1 1 0 |
|
注意:()内的值为旧值
有效资源不能满足所有进程的Need值,因此对P1的申请进行了分配后,系统处于不安全状态。
------------------------------另外一解法(书中解法)--------------------
三.算法的Java实现
1:import java.util.Arrays;
2:import javax.swing.JOptionPane;
3:
4:public class Banker {
5:
6: /*
7: * 资源向量必须全部设置成static,因为可能
8: * 同一个线程多次输入才满足条件
9: */
10: //每个线程需要的资源数
11: static int max[][] = { { 7, 5, 3 }, { 3, 2, 2 }, { 9, 0, 2 },
12: { 2, 2, 2 }, { 4, 3, 3 } };
13: //系统可用资源数
14: static int avaliable[] = {10,5,7};
15: //已经分配资源
16: static int allocation[][] = { { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 },
17: { 0, 0, 0 }, { 0, 0, 0 } };
18: //每个进程还需要的资源数,初试一个资源也没分配;实际上应该等于max-avaliable
19: static int need[][] = Arrays.copyOf(max,max.length);
20: //每次申请的资源数
21: static int request[] = { 0, 0, 0 };
22: //NUM个线程,N种资源
23: static final int NUM = 5, N = 3;
24: static Function function = new Function();
25:
26: public static void main(String[] args) {
27: JOptionPane jpane = new JOptionPane();
28:
29: //是否进行模拟标志,没有布尔,因为从JOpotionpane输入
30: int flag = 1;
31:
32: while(1==flag) {
33: /*
34: * 用与判断线程号是否合法
35: * 需要放在while内部,防止下次继续模拟时i还是上次输入的
36: */
37: int i = -1;
38: while(i<0||i>=NUM) {
39: String str = jpane.showInputDialog("输入申请资源的线程号(0到4):");
40: i = Integer.parseInt(str);
41: if(i<0||i>=NUM) {
42: JOptionPane.showMessageDialog(jpane, "输入的线程号不合法!!!");
43: }
44: }
45: //资源输入有效性标志
46: boolean tag = true;
47: for(int j=0; j<N; j++) {
48: String str = jpane.showInputDialog("输入线程"+i+"所申请的资源"+j+"数目:");
49: request[j] = Integer.parseInt(str);
50: //有效性检查
51: if(request[j]>need[i][j]) {
52: JOptionPane.showMessageDialog(jpane, "输入的资源数大于需要资源数!!!");
53: tag = false;
54: break;
55: }else {
56: if(request[j]>avaliable[j]) {
57: JOptionPane.showMessageDialog(jpane, "输入的资源数大于可用资源数!!!");
58: tag = false;
59: break;
60: }
61: }
62: }
63: //是否存在安全序列
64: boolean vis = true;
65: if(tag) {
66: function.allocateK(i);
67: vis = function.checkError(i);
68: if(false==vis) {
69: //上面调用了allocateK,所以不仅需要释放,还需要恢复
70: function.freeKAndRestore(i);
71: }else {
72: //测试是否全部资源到位
73: boolean f = function.checkRun(i);
74: if(true==f) {
75: JOptionPane.showMessageDialog(jpane
76: ,"进程"+i+"全部资源到位!!!"+"\n"+"即将释放所占用资源");
77: function.freeKNotRestore(i);
78: }
79: }
80: }else {
81: //实际上没必要清空,因为该数组是输入的,只为了展示一种良好习惯
82: Arrays.fill(request,0);
83: }
84: String str = JOptionPane.showInputDialog("是否继续模拟(1表示是,0退出)?");
85: flag = Integer.parseInt(str);
86: }
87: }
88: }
89:
90:class Function {
91: /*
92: *实际上完全是静态的,没必要新new一个Banker
93: */
94: Banker banker = new Banker();
95: //为线程k分配资源
96: public void allocateK(int k) {
97: for(int i=0; i<banker.N; i++) {
98: banker.avaliable[i] -= banker.request[i];
99: banker.need[k][i] -= banker.request[i];
100: banker.allocation[k][i] += banker.request[i];
101: }
102: }
103: public boolean checkError(int i) {
104: int work = 0;
105: //存储所有线程是否安全
106: boolean[] finish = new boolean[banker.NUM];
107: Arrays.fill(finish,false);
108: //存储一个安全序列
109: int temp[] = new int[banker.NUM];
110: Arrays.fill(temp,0);
111: //temp数组下标
112: int t = 0;
113:
114: //线程号参数是i
115: for(int j=0; j<banker.N; j++) {
116: work = banker.avaliable[j];
117: int k = i;
118:
119: while(k<banker.NUM) {
120: if(finish[k]==false&&work>=banker.need[k][j]) {
121: /*
122: * 注意不是max数组,因为此时线程k
123: * 所需资源不一定完全就位
124: * 加的是allocation,因为进行此项检查前先试探性地
125: * 分配给线程k资源了
126: */
127: //满足该线程,回收该项资源,看是否满足其它线程
128: work += banker.allocation[k][j];
129: finish[k] = true;
130: temp[t++] = k;
131: k = 0;
132:
133: }else {
134: k++;
135: }
136: }
137: //和while平级
138: for(int p=0; p<banker.NUM; p++) {
139: if(finish[p]==false) {
140: return false;
141: }
142: }
143: }
144: return true;
145: }
146: //释放线程k所占用资源并恢复
147: public void freeKAndRestore(int k) {
148: for(int i=0; i<banker.N; i++) {
149: banker.avaliable[i] += banker.request[i];
150: banker.need[k][i] += banker.request[i];
151: banker.allocation[k][i] -= banker.request[i];
152: }
153: }
154: //仅仅释放线程k所占用资源,仅在某线程全部得到资源运行后才调用
155: public void freeKNotRestore(int k) {
156: for(int i=0; i<banker.N; i++) {
157: banker.avaliable[i] = banker.avaliable[i] + banker.allocation[k][i];
158: }
159: }
160: //三种资源是否全部到位
161: public boolean checkRun(int k) {
162: int n = 0;
163: for(int i=0; i<banker.N; i++) {
164: if (banker.need[k][i] == 0)
165: n++;
166: }
167: if (n == 3)
168: return true;
169: else
170: return false;
171: }
172: }
用C语言实现银行家算法
定义了一个结构体:
typedefstruct {
int A;
int B;
int C;
}RESOURCE;
结构体里面的三个域分别表示三种资源的数量。
根据课本例题上的数据初始化三个矩阵和一个向量。
//最大需求矩阵
RESOURCEMax[PROCESSES_NUMBER] =
{
{7,5,3},
{3,2,2},
{9,0,2},
{2,2,2},
{4,3,3}
};
//已分配资源数矩阵
RESOURCEAllocation[PROCESSES_NUMBER] =
{
{0,1,0},
{2,0,0},
{3,0,2},
{2,1,1},
{0,0,2}
};
//需求矩阵
RESOURCENeed[PROCESSES_NUMBER] =
{
{7,4,3},
{1,2,2},
{6,0,0},
{0,1,1},
{4,3,1}
};
//可用资源向量
RESOURCEAvailable = {3,3,2};
为了能够输出安全状态时的安全序列,还可以添加一个记录安全序列的数组
intSafeSequence[PROCESSED_NUMBER]。
因为银行家算法使用的是试探分配的策略,如果进程请求分配的资源既不大于自己尚需的资源,又不大于系统现存的资源,那就可以先试探着将资源分配给该进程,然后测试分配后是不是有可能造成死锁,如果不会引起死锁(即安全状态)就可以完成分配,否则(即不安全状态)就将试探分配的资源回收回来让其等待。那么根据上面定义的数据就可以很容易的写出试探分配和回收资源的函数。
//试探分配
void ProbeAlloc(intprocess,RESOURCE *res)
{
Available.A -= res->A;
Available.B -= res->B;
Available.C -= res->C;
Allocation[process].A += res->A;
Allocation[process].B += res->B;
Allocation[process].C += res->C;
Need[process].A -= res->A;
Need[process].B -= res->B;
Need[process].C -= res->C;
}
//若试探分配后进入不安全状态,将分配回滚
void RollBack(intprocess,RESOURCE *res)
{
Available.A += res->A;
Available.B += res->B;
Available.C += res->C;
Allocation[process].A -= res->A;
Allocation[process].B -= res->B;
Allocation[process].C -= res->C;
Need[process].A += res->A;
Need[process].B += res->B;
Need[process].C += res->C;
}
接下来就是安全性检查函数了,在这个函数中还需要设置一个Work向量和一个Finish向量,函数实现主要就是通过一个for循环检查试探分配后系统的可用资源数是否能满足所有进程的需求,若能满足某一个进程的需求,则假设分配其所需资源使之完成运行,然后就可以将资源回收以分配给其它进程,如果依照这种方法所有的进程都可以成功执行,那么现在的状态就是安全状态,否则即为不安全状态,有可能引起死锁。
bool SafeCheck()
{
RESOURCE Work = Available;
bool Finish[PROCESSES_NUMBER] = {false,false,false,false,false};
int i;
int j = 0;
for (i = 0; i < PROCESSES_NUMBER; i++)
{
//该进程是否已执行完毕
if(Finish[i] == false)
{
//是否有足够的资源分配给该进程
if(Need[i].A <= Work.A && Need[i].B <= Work.B &&Need[i].C <= Work.C)
{
//有则使其执行完成,并将已分配给该进程的资源全部回收
Work.A += Allocation[i].A;
Work.B += Allocation[i].B;
Work.C += Allocation[i].C;
Finish[i] = true;
safeSeq[j++] = i; //顺便记录下来安全序列
i = -1; //需要从开始重新进行遍历
}
}
}
//如果所有进程的Finish向量都为true则处于安全状态,否则为不安全状态
for (i = 0; i < PROCESSES_NUMBER; i++)
{
if (Finish[i] == false)
{
return false;
}
}
return true;
}
有了以上三个函数就可以写出请求分配资源的函数了。
//资源分配请求
bool request(int process,RESOURCE *res)
{
//request向量需小于Need矩阵中对应的向量
if(res->A <= Need[process].A && res->B <=Need[process].B && res->C <= Need[process].C)
{
//request向量需小于Available向量
if(res->A <= Available.A && res->B <= Available.B&& res->C <= Available.C)
{
//试探分配
ProbeAlloc(process,res);
//如果安全检查成立,则请求成功,否则将分配回滚并返回失败
if(SafeCheck())
{
return true;
}
else
{
printf("安全性检查失败。原因:系统将进入不安全状态,有可能引起死锁。\n");
printf("正在回滚...\n");
RollBack(process,res);
}
}
else
{
printf("安全性检查失败。原因:请求向量大于可利用资源向量。\n");
}
}
else
{
printf("安全性检查失败。原因:请求向量大于需求向量。\n");
}
return false;
}
为了输出更直观的信息,也可以再加一个PrintTable函数将当前资源非配表显示出来
运行截图。