文章目录
一、互斥需遵守的原则
为禁止两个进程同时进入临界区,同步机制应遵循以下准则:
1)空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
2)忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
3)有限等待。对请求访问的进程,应保证能在有限时间内进入临界区,防止进程无限等待。
4)让权等待(原则上应该遵循,但非必须)。当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。
注意:
- 需要进行互斥的操作是对临界资源的访问,也就是说不同线程对同一个进程内部的、或不同进程对共享变量的访问才有可能需要进行互斥,不同进程的线程、代码段或变量不存在互斥访问的问题,不同线程内部的局部变量也不存在互斥访问的问题。同一进程的不同线程、或不同进程之间才需要对操作进行同步,同一线程或进程内部的操作顺序执行。
- 进程在操作系统内核程序临界区中不能进行调度和切换,但是在进入普通临界区和使用临界资源时不会直接影响操作系统内核的管理工作,因此在访问普通临界区时可以进行进程的调度与切换。
二、实现互斥的方法
(一)软件实现方法
在进入区设置并检查一些标志来标明是否有进程在临界区中,若已有进程在临界区,则在进入区通过循环检查进行等待,进程离开临界区后则在退出区修改标志。
1.单标志法(我想进!)
变量turn表示选择权在谁手上,turn=0表示P0可进入,turn=1表示P1可进入,进入后将turn设置为另一方值。
P0{
while(turn != 0); //进入区
临界区资源; //临界区
turn = 1; //退出区
}
P1{
while(turn != 1); //进入区
临界区资源; //临界区
turn = 0; //退出区
}
缺点:违反空闲让进、让权等待
2.双标志法先检查法(你想进不?我想进!)
布尔型数组 flag[2]用来标记各个进程是否想进临界区,flag[i]=true表示Pi想要进入临界区(i=0或1)。Pi进入临界区前,先检查对方是否想进入临界区,若想,则等待;否则,将flag[i]置为true后,再进入临界区;当Pi退出临界区时,将flag[i]置为false。
P0{
while(flag[1]); //进入区 你想进不?
flag[0] = true; //进入区 我想进!
临界区资源;//临界区
flag[0] = flase; //退出区
}
P1{
while(flag[0]); //进入区
flag[1] = true; //进入区
临界区资源;//临界区
flag[0] = flase; //退出区
}
缺点:违反忙则等待、让权等待
3.双标志法后检查法(我想进!你想进不?)
先检查法先检查对方的标志,再设置自己的标志,但这两个操作又无法一气呵成,于是使得两个进程同时进入临界区的问题。因此,想到先设置后检查法,以避免上述问题。后检查法先设置自己的标志,再检查对方的标志,若对方的标志为true,则等待;否则,进入临界区。
P0{
flag[0] = true; //进入区 我想进!
while(flag[1]); //进入区 你想进不?
临界区资源;//临界区
flag[0] = flase; //退出区
}
P1{
flag[1] = true; //进入区
while(flag[0]); //进入区
临界区资源;//临界区
flag[0] = flase; //退出区
}
缺点:违反空闲让进、有限等待、让权等待
4.皮特森算法(我想进!你先进吧~)
Peterson 算法结合了算法一和算法三的思想,利用flag[]解决互斥访问问题,而利用 turn解决“饥饿”问题。若双方都争着进入临界区,则可让进程将进入临界区的机会谦让给对方。也就是说,在每个进程进入临界区之前,先设置自己的flag标志,再设置允许进入turn标志;之后,再同时检测对方的flag和turn标志,以保证双方同时要求进入临界区时,只允许一个进程进入。
P0{
flag[0] = true; //进入区 我想进
turn = 1; //进入区 你先进吧
while(turn == 1 && flag[1] == true); //进入区 你先进吧
临界区资源; //临界区
flag[0] = false; //退出区
}
P1{
flag[1] = true; //进入区 我想进
turn = 0; //进入区 你先进吧
while(turn == 0 && flag[0] == true); //进入区 你先进吧
临界区资源; //临界区
flag[1] = false; //退出区
}
缺点:违反让权等待
总结:turn——选择权在谁手上,flag——是否想进入
(二)硬件实现方法
计算机提供了特殊的硬件指令,允许对一个字的内容进行检测和修正,或对两个字的内容进行交换等。
1.中断屏蔽方法
当一个进程正在执行它的临界区代码时,防止其他进程进入其临界区的最简单方法是关中断。因为CPU只在发生中断时引起进程切换,因此屏蔽中断能够保证当前运行的进程让临界区代码顺利地执行完,进而保证互斥的正确实现,然后执行开中断。
其典型模式为:
关中断;
临界区;
开中断;
缺点:
①限制了 CPU交替执行程序的能力。
②将关中断的权限交给用户不安全。
③不适用于多处理器系统,
2.硬件指令方法——TestAndSet 指令
TestAndSet 指令(简称 TS 指令)可以读出指定标志后将该标志设置为真,是原子操作。指令的功能描述如下:
boolean TestAndSet (boolean *lock){
boolean old;
old=*lock; //old 用来存放 lock 的旧值
*lock=true; //无论之前是否已加锁,都将lock 置为true
return old; //返回 lock 的旧值
}
lock有两种状态:true已加锁,false未加锁。进程在进入临界区之前,先用TS指令检查lock值:①若为 false,则表示没有进程在临界区,可以进入,并将 lock 置为 true,这意味着关闭了临界资源(加锁),使任何进程都不能进入临界区;②若为 true,则表示有进程在临界区中,进入循环等待,直到当前访问临界区的进程退出时解锁(将lock置为 false)。利用 TS指令实现互斥的过程描述如下:
while TestAndSet(&lock); //加锁并检查
临界区代码段;
lock=false; //解锁
相比于软件实现方法,TS 指令将“加锁”和“检查”操作用硬件的方式变成了一气呵成的原子操作。相比于关中断方法,由于“锁”是共享的,这种方法适用于多处理器系统。缺点是,暂时无法进入临界区的进程会占用CPU循环执行TS指令,因此不能实现让权等待。
3.硬件指令方法——Swap指令
用Swap指令管理临界区时,为每个临界资源设置一个共享布尔变量lock,初值为 false;在每个进程中再设置一个局部布尔变量key,初值为true,用于与 lock交换信息。从逻辑上看,Swap指令和TS指令实现互斥的方法并无太大区别,都先记录此时临界区是否已加锁(记录在变量 key中),再将锁标志lock置为true,最后检查key,若 key为false,则说明之前没有其他进程对临界区加锁,于是跳出循环,进入临界区。其处理过程描述如下:
boolean key=true;
while(key!=false)
Swap(&lock,&key);
临界区代码段;
lock=false;
缺点:
①等待进入临界区的进程会占用CPU 执行 while 循环,不能实现“让权等待”;
②从等待进程中随机选择一个进程进入临界区,有的进程可能一直选不上,从而导致“饥饿”现象。
(三)信号量机制
关于信号量的知识,请见我另一篇文章:【408精华知识】PV大题最全总结!
(四)管程
管程也是用来解决进程同步互斥的一种工具,它的操作比信号量更加简单,管程封装了同步操作,对进程隐藏了同步细节。管程之所以被说是一个类,是因为管程采用了封装的思想,把复杂的细节隐藏了,我们只需要调用管程提供的特定“入口”就能实现进程同步/互斥了。
背景:管程是在程序设计语宣中被引入的,是一种高级的同步机制;
特点:管程把对共享资源的操作封装起来;每次仅允许一个进程进入管程;
条件变量:
①x.wait:x条件不满足时,将自己插入x条件的等待队列,并释放管程,允许其他进程使用该管程(让其他人先用)
②x.signal:x条件满足时,唤醒一个因x条件而阻塞的进程
易错点:管程的wait()和signal()操作不会对条件变量进行检查,这个条件变量仅用于实现管程内部队列的排队功能(并不像信号量那样代表实际的资源数量)
(五)互斥锁
解决临界区最简单的工具就是互斥锁(mutex lock)。一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数acquire()
获得锁,而函数release()
释放锁。acquire()
或release()
的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。每个互斥锁有一个布尔变量available
,表示锁是否可用。如果锁是可用的,调用acquire()
会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。
代码如下:
acquire(){ //获取锁
while(!available); //忙等待
available = false; //获得锁
}
release(){ //释放锁
available = true; //释放锁
}
互斥锁的特点:
- 缺点:引发忙等,进程时间片用完才下处理机,违反“让权等待”,当有一个进程在临界区中,任何其他进程在进入临界区时必须连续循环调用
acquire()
。当多个进程共享同一CPU时,就浪费了CPU周期。 - 优点:等待期间不用切换进程上下文,多处理器系统中,若上锁的时间短,则等待代价很低,因此,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行,不太适用于单处理机系统,因为忙等的过程中不可能解锁。
- 需要连续循环忙等的互斥锁,都可称为自旋锁(spin lock),如 TSL指令、swap指令、单标志法。
写在后面
这个专栏主要是我在学习408真题的过程中总结的一些笔记,因为我学的也很一般,如果有错误和不足之处,还望大家在评论区指出。希望能给大家的学习带来一点帮助,共同进步!!!
参考资料
[1]王道408教材(2025版)
[2]王道课程资料