操作系统中关于解决进程互斥的方法有两种,分别是软件实现和硬件实现。在实现互斥时,需要关心的问题是:是否满足:忙则等待、让权等待、有限等待和空闲让进,这四个条件。
忙则等待:
意思是如果一个进程在使用临界资源,另一个进程也想访问该临界资源,必须等待在使用的进程使用完成。
让权等待:
如果一个进程发现另一个进程正在使用自己想要使用的临界资源,自己目前得不到该资源的访问权时,必须交出CPU资源进入阻塞状态。
有限等待:
一个进程等待访问临界资源,必须能保证在未来的时间里一定能拿到该临界资源的访问权,也就是该进程不会饥饿。
空闲让进:
当进程想访问临界资源时,如果该临界资源空闲,该进程就不用等待直接拿到该临界资源的访问权。
互斥资源访问软件实现:
1)单标志检查法
设置一个int型共享变量,每个进程访问临界资源之前必须检查该变量的值是否对应进程本身,若对应,则进入临界区,之后在退出时将该共享变量的值改为另一个进程所对应的值。
int turn = 0; //访问标志
int p1(){ //进程1
while(turn!=1); //若标志不与自己对应,则忙等待
//……临界资源代码段
turn = 0; //退出临界区时将标志指向另一个进程
}
int p0(){ //进程0
while(turn!=0); //若标志不与自己对应,则忙等待
//……临界资源代码段
turn = 1; //退出临界区时将标志指向另一个进程
}
该实现方法可以满足上述四种条件中的:忙则等待、有限等待,但是不满足空闲让进原则和让权等待原则。
2)双标志先检查法
为了满足空闲让进原则,该方法用一个bool数组来代替1)中的标志,形成双标志位,先检查对方的标志位是否为true,若为true则本进程忙等待,若为false则修改自己的flag位为true,访问临界资源。结束时将自身的flag位改为false。
bool flag[2];
int p1(){ //进程1
while(flag[0]!=false);//若对方正在使用,则忙等待
flag[1] = true;
//临界区代码段…
flag[1] = false;
}
int p0(){ //进程0
while(flag[1]!=false);//若对方正在使用,则忙等待
flag[0] = true;
//临界区代码段…
flag[0] = false;
}
该方法并不能满足忙则等待原则,会导致临界资源被同时访问:考虑并发场景,若p1检测到p0为false跳出循环之后发生CPU调度被剥夺CPU,此时p0得到CPU,发现p1也为false(p1还没来得及将自己的标志位修改就发生了进程调度),则p0也跳出了循环,则如果在临界区代码段发生进程调度,会使临界区资源被同时访问。
3)双标志后检查法
为了满足空闲让进原则,该方法用一个bool数组来代替1)中的标志,形成双标志位,每个进程想使用临界资源时将对应自己的bool数组的下标改为true,再循环检查对方是否为false,若自身为true,对方进程为false,则可以访问临界资源。最后访问完临界资源以后将自己的下标改为false,以便能让其他进程访问临界区。
bool flag[2];
int p1(){ //进程1
flag[1] = true;
while(flag[0]!=false);//若对方正在使用,则忙等待
//临界区代码段…
flag[1] = false;
}
int p0(){ //进程0
flag[0] = true;
while(flag[1]!=false);//若对方正在使用,则忙等待
//临界区代码段…
flag[0] = false;
}
该方法在并发运行条件中并不能保证有限等待,有可能两个进程发生死锁,若p1在修改完自己的flag值之后,CPU发生调度,p1被剥夺CPU,让p0进程开始运行,则p0也修改自己的值为true,则之后无论哪个进程运行都无法获得互斥资源。
接下来需要根据以上三种方法来考虑优化,1)满足了互斥访问的原因是两个进程对同一个变量turn进行检测,由于turn在同一时刻只有一个确定的值(宏观世界不考虑量子叠加态^_^),因此可以用来作为循环条件,双标志位的优点是可以根据标志位来标记自己想使用并检查,可以满足空闲让进。因此引出Peterson算法。
4)Peterson算法:
利用一个turn变量保证互斥访问,在此基础上,添加flag标志位标记自己是否想用来保证空闲让进,此算法的特点是两个进程之间相互礼让。
int turn;
bool flag[2];
int p1(){ //进程p1
flag[1] = true; //先标记自己想用
turn = 0; //礼让给进程0使用
while(turn==0 && flag[0]==false);
// 考虑双标志检查法失效的条件,flag位在退出循环后被修改导致同时访问临界区,
// 在Peterson算法中由于turn变量的存在,只要当中有一个进程跳出循环,
// turn的值就会一直保持到下次本进程礼让修改,临界区直到被访问完成turn的值也不会被改变,
// 另一个进程由于turn值已相对固定,所以无法跳出循环,而flag被修改也意味着正在访问临界区的进程已经完成访问。
//临界区代码......
flag[1] = false;
}
int p0(){ //进程p0
flag[0] = true; //先标记自己想用
turn = 1; //礼让给进程1使用
while(turn==1 && flag[1]==false);
//临界区代码......
flag[0] = false;
}
有了Peterson算法之后,可以保证空闲让进,忙则等待和有限等待,但是由于进程拿不到临界区访问权限时会一直处于while循环忙等待,因此该算法不能满足让权等待。
在某些条件下,这种不会导致进程阻塞的特点也能成为其优点,在未得到资源时一直处于忙等待的锁,又称为自旋锁。自旋锁比较适用于访问临界区的时间较短的情况,由于不会在未得到锁时发生阻塞,若进程阻塞CPU调度其他进程运行,会导致与本进程有关的Cache和TLB被替换,再该进程得到临界区资源被唤醒时,可能会发生多次Cache和TLB未命中的情况,导致进程开销变大。
互斥资源访问的硬件实现:
1)关中断指令
在单CPU并发操作系统中,通过关中断指令来屏蔽外部中断,由于CPU调度的时钟中断来自外部,因此可以屏蔽时钟中断来保证使用关中断指令开始访问临界资源时不会被打断,发生上下文切换。在访问完成之后再使用开中断指令来重新接收中断。该方法虽然在单CPU并发操作系统上有效,但是在多CPU环境下会失效,假如不同的CPU运行的进程访问临界资源,每个CPU只能保证自己当前的进程不会被打断,但是仍有可能有另一个CPU的进程也想同时访问该临界资源,这时就无法保证互斥访问了。除此之外,关中断指令和开中断指令属于特权指令,不能赋予用户进程来使用,只能由操作系统内核程序来使用,因此无法实际应用于互斥资源访问。
2)硬件实现TestAndSet方法(硬件将其实现为原子操作)
直接由硬件给出TestAndSet方法,由硬件保证该方法的原子性。TestAndSet方法的描述如下:
bool TestAndSet(bool* lock){
bool old = *lock;
*lock = true;
return old;
}
int p1(){ //p1进程
while(TestAndSet(&lock));
//临界区代码......
*lock = false;
}
3) 硬件实现Swap方法
跟2)同一个思路。
void Swap(bool *a,bool *b){
//a为key,b为lock,当临界区可以访问时,lock为false,要想访问临界区,需要条件key为false,key为
//false代表上次Swap时lock为false,且由于Swap操作已经得到访问权,其他进程再想访问临界区lock已经
//被当前进程替换为true,因此其他进程只能等待。
bool temp;
temp = *a;
*a = *b;
*b = temp;
}
int p1(){
bool key = true;
while(key!=false){
Swap(&key,&lock);
}
//临界区代码......
lock = false;
}