1.概述
1.1 临界资源
1.1.1 概述
一次仅允许一个进程使用的资源称为临界资源。
许多物理设备属于临界资源,许多变量、数据等也可以被多个进程共享使用,因此也属于临界资源。
1.1.2 临界资源的访问过程
进入区
。在进入区需要检查是否能够进入临界区(Critical Section),若能,则设置正在访问临界区的标志,以阻止其他进程同时进入该临界区。临界区/临界段
。进程中访问临界资源的那段代码。退出区
。清除正在访问临界区的标志。剩余区
。代码中的其余部分。
do{
entry section; // 进入区
critical section; // 临界区
exit section; // 退出区
remainer section; // 剩余区
}while(true)
临界资源与共享资源的区别在于,临界资源在一个时间段内只允许一个进程访问,而共享资源则可以允许多个(并发使用)。
临界资源有:公用队列、打印机、共享变量、共享缓冲区…
非临界资源有:非共享数据、私用数据、磁盘存储介质、可重入的程序代码…
银行家算法是死锁避免的算法,不能用来解决临界区问题。
1.2 同步/直接制约关系
1.2.1 概述
是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。
要对并发进程进行同步的原因是:并发进程是异步的。
1.3 互斥\间接制约关系
1.3.1 概述
当一个进程进入临界区使用临界资源时,另一个进程必须等待其占用完并退出临界区后,才被允许进入临界区使用临界资源。
1.3.2 临界区的同步准则
空闲让进
。临界区空闲时,可允许一个请求进入临界区使用临界资源。忙则等待
。当已经有一个进程处在临界区时,其他试图进入临界区的进程必须等待。有限等待
。对于请求进入临界区的进程,应保证能在有限时间内进入临界区。让权等待
。当进程不能进入临界区时,应立即释放处理机,避免进程处于忙等待状态。(非必需实现,如Peterson算法)
2.实现临界区互斥的基本方法
2.1 软件实现方法
在进入区中设置并检查一些标志来表明是否有进程在临界区中,若已有进程在临界区中,则进入区通过循环检查进行等待。进程离开临界区后则在退出区修改标志。
2.1.1 单标志法
2.1.1.1 概述
该算法只设置一个公用整型变量turn,用于指示被允许进入临界区的进程编号,即若 turn = 0
,则P0进程允许进入。该算法可确保每次只允许一个进程进入临界区。
2.1.1.2 缺点
若P0顺利进入临界区并顺利离开,此时临界区是空闲的,而P1此时并没有想再次进入临界区的打算,因而P0进程将无法再次进入临界区。
2.1.2 双标志法先检查
2.1.2.1 概述
该算法规定在每个进程访问临界区资源之前,先检查临界区资源是否正在被访问,若正在被访问,则进程需等待;否则则进程才进入自己的临界区。
2.1.2.2 优点
无需进程之间交替进入,单个进程可连续进出临界区。
2.1.2.3 缺点
可能会出现多个进程同时进入临界区的情况。(检查对方的flag与切换自己的flag之间有一段时间差)
2.1.3 双标志法后检查
2.1.3.1 概述
该算法会先将进程自己的flag置为TRUE
,然后再检测对方的flag,若对方flag为TRUE
,则进程等待,否则进入临界区。
2.1.3.2 缺点
当两个进程同时企图进入临界区时,双方均会预先置自己的flag为TRUE,然后双方同时检测对方的状态,发现都是TRUE,从而双方互相谦让,结果谁也进不去,从而导致“饥饿”现象。
2.1.4 Peterson’s Algorithm
2.1.4.1 概述
在双标志的基础上增设一个允许进入标志turn(其实是对flag的进一步说明,表明虽然我的flag为TRUE,但是我turn表明你是可以被允许进入的,turn其实就相当于双方的谦让动作),从而进程在进入林窃取之前,需要检测另一个进程的进程状态标志flag和允许进入标志turn。
2.1.4.2 评估
该算法是算法一和算法二的结合,同时借助标志turn能有效解决进程“饥饿”问题。
2.2 硬件实现方法/低级方法/元方法
2.2.1 中断屏蔽方法
2.2.1.1 概述
由于CPU只会在发生中断时引起进程的切换,因此关中断能保证当前运行的进程能够让临界区的代码执行完,执行完之后再开启中断。
2.2.1.2 评估
这种方法将限制了处理机交替执行程序的能力,因此执行效率会明显降低。同时将关中断交给用户也不是一个很明智的做法,若一个进程关中断后不再开中断,则系统将因此而终止。
2.2.2 硬件指令方法
下面的指令均由硬件逻辑直接实现,不会被中断。
2.2.2.1 TestAndSet
指令
该指令是原子操作,其功能是读出指定标志后并将该标志置为TRUE。
利用该指令实现互斥访问的过程:
进入临界区之前,利用该指令检查标志lock,若临界区空闲,则该值为false,则允许进入,同时该指令也将置lock值为true,表示不再允许其他进程进入。离开临界区时,则将该值置为false。
2.2.2.2 Swap
指令
该指令功能是负责交换两个字(字节)的内容。
利用该指令实现互斥访问的过程:
首先需要为每一个临界资源设置一个共享布尔变量lock,初值为false,然后每个进程中再设置一个局部变量key,用于与lock交换信息,然后在进入临界区前,不断与lock进行内容交换,然后检查key的状态,直至检查到的key为false,然后才进入临界区。
硬件方法的 优缺点
优点:
- 适用于任意数目的进程,而不管是单处理机还是多处理机。
- 简单,容易检验其正确性。
- 可以支持进程内的多个临界区,只需为每个临界区设置一个布尔变量即可。
缺点:
- 进程等待进入临界区需要耗费处理机时间,不能让权等待。
- 从等待进程中随机选择一个进程进入临界区,有的进程可能一直都选不上,因而产生“饥饿”现象。
硬件方法(TAS、Swap)不能实现让权等待原则。
Peterson方法实现了有限等待原则,但未实现让权等待原则。
(记录型)信号量实现了让权等待原则。
3.互斥锁(Mutex Lock)
3.1 概述
一个进程在进入临界区时获得锁,在退出临界区时释放锁。
函数acquire()
获得锁,函数release()
释放锁。
每个互斥锁都会有一个布尔变量available
,表明锁是否可用。若锁可用,则调用acquire()
成功,且锁不再可用;若锁不可用,则调用acquire()
阻塞,直至锁可用。
acquire()
与release()
的执行必须是原子操作,因此常用硬件机制来实现。
3.2 评估
当有一个进程在临界区中时,任何其他试图进入临界区的进程必须连续循环调用acquire()
。当多个进程共享一个CPU时,这就浪费了CPU周期。因此互斥锁常用于多处理机系统,一个线程可以在一个处理器上等待而不影响其他线程的运行。
4.信号量
4.1 概述
信号量机制是一种较强的机制,可以用来解决互斥与同步问题,它只能被两个标准的的原语访问,分别是wait(S)、signal(S),分别记为“P操作”、“V操作”。
P、V操作是一种低级的进程通信原语,不是系统调用命令。
4.2 分类
4.2.1 整型信号量
整型信号量被定义为一个用于表示资源数目的整型量S。
该wait
操作中,若信号量S ≤ 0
,则就会不断地测试S,直至S > 0
。
该机制并未遵循让权等待原则,而是使进程处于忙等待状态。
4.2.2 记录型信号量
记录型变量是在整型信号量的基础上再增设一个等待进程链表L,从而构成一个记录型的数据结构。
对于wait(S)
操作:
将进行S.value--
操作,若S.value < 0
,则说明此类资源已耗尽,此时进程将调用block
原语,进行自我阻塞,放弃处理机,并移入该类资源的等待队列中(S.L
)。
该机制遵循了让权等待原则。
对于signal(S)
操作:
表示进程释放一个资源,是系统中该类资源的可供分配个数+1
,即S.value++
。
若S.value ≤ 0
,则说明还有进程在S.L
中等待该类资源,此时还需要调用wakeup
原语来将S.L
中的第一个进程唤醒。
4.3 利用信号量实现同步
下面是P2进程的y语句需要用到P1进程x语句的执行结果:
若P2先执行,直行到P(S)处时,由于此时S = 0,因此P2将被阻塞,并移入阻塞队列中去,而P1执行完x语句之后,然后执行V(S),去唤醒处在阻塞队列中的P2,于是P2将从阻塞队列移入就绪队列中,待处理机得到时,继续运行。
用P、V操作实现进程同步时,信号量的初值由用户而定。
4.4 利用信号量实现互斥
下面的图已经很清晰的说明了,这里不做过多赘述:
注:P(S)
操作会执行S--
,当S = 0
时,P(S)
操作执行会阻塞;V(S)
操作会执行S++
。
通过信号量机制实现互斥时,互斥信号量的初值应为
1
。
4.5 利用信号量实现前驱关系
实现算法如下(也很简洁明了):
若某个行为需要用到某种资源,则在这个行为面前P这种资源一下(
资源数目--
);若某个行为会提供某种资源,则在这个行为后面V这种资源一下(资源数目++
)。
5.管程(Monitor)
5.1 概述
将系统中的共享资源抽象为共享数据结构,同时将对共享资源的访问抽象为对共享数据结构的申请与释放等操作,进而将这些操作定义为一组过程。
管程 = 代表共享资源的数据结构 + 对这些共享数据结构的一组操作过程 = 资源管理程序
。
管程的特性保证了进程互斥,无需开发人员自己实现互斥,从而降低死锁发生的可能性。
5.2 结构
- 管程的名称。
- 对管程内部的共享数据结构的说明。
- 对该管程内部的共享数据结构进行操作的一组过程(函数)。
- 对该管程内部的共享数据结构设置初始值的语句。
各个进程只能串行执行管程内的过程。
管程是被进程调用的,管程是语法范围,无法创建和撤销。
管程每次只允许一个进程进入管程。
管程是进程的同步工具,解决信号量机制大量同步操作分散的问题。
管程中的
signal
操作与信号量机制中的V
操作不同,管程中的signal
是针对某个条件变量的,若不存在因该条件变量而阻塞的进程,则signal
将不会产生影响,而V操作一定会改变信号量的值(S++
)。
管程可以实现进程间的同步和互斥。
管程是由编程语言支持的进程同步机制。
管程中定义的变量只能被管程内的过程访问。
5.3 条件变量(Condition)
5.3.1 概述
一个进程被阻塞的原因可能会有很多种,因此我们可以定义多个条件变量。
每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程。
5.3.2 操作
x.wait
当x对应的条件不满足时,正在调用管程的进程将调用该函数把自己移入x条件对应的等待队列中,并释放管程,此时其他进程可使用该管程。
x.signal
当x对应的条件发生变化时,则调用该函数,唤醒一个因x条件而阻塞的进程。
5.3.3 评估
条件变量&信号量
相同点:条件变量的wait与signal操作与信号量的PV操作类似,都可以唤醒和阻塞进程。
不同点:条件变量是没有值的,仅实现排队等待功能;信号量是有值的,其值代表着剩余资源数目。
6.经典同步问题
6.1 生产者-消费者问题
过程与前面讲到的管道通信一致,这里不再赘述。(或者是管道通信的本质就是它)
对应算法如下:
P(empty)
与P(mutex)
这两个操作不可颠倒,若颠倒可能会引起死锁问题(生产者 -> 生产者 -> 消费者);V(mutex)
与V(empty)
谁先谁后执行无所谓。
6.2 读者-写者问题
- 允许多个读者对文件同时执行读操作,只允许一个写者往文件执行写操作。
- 任意一个写者在完成写操作之前不允许其他读者或者写者执行操作。
- 写者执行写操作前,应让已有的读者和写者全部退出。
6.3 哲学家进餐问题
一张圆桌上坐着5名哲学家,每两名哲学家之间的桌上摆一根筷子,两根筷子中间是一碗米饭。哲学家们只有思考和进餐两个动作。哲学家在思考时不会影响其他人,哲学家饥饿时才试图拿起左右两根筷子(一根一根拿起)。若筷子已在他人手上,则需要等待。饥饿的哲学家只有在同时拿到两根筷子之后才可以进餐,进餐完毕后,需要放下两根筷子继续思考。
算法实现:
6.4 吸烟者问题
一个系统内有一个供应者进程&三个抽烟者进程。每个抽烟者不停地卷烟并抽掉它,但卷烟需要烟草、纸和胶水这三种原材料。第一抽烟者有烟草,第二个有纸,第三个有胶水。供应者进程无限地提供三种材料,供应者每次会将两种材料放到桌子上,拥有剩下那种材料的抽烟者将卷一根烟并抽掉它,然后给供应者进程发送一个抽完一支烟的信号,接着供应者将另外两种材料放到桌上,如此重复。
算法实现: