1禁止中断、
2锁内存总线
3Test-and-Set指令
Swap指令
同步互斥
. 互斥与同步的概念
互斥和同步是两个紧密相关而又容易混淆的概念。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。
同步:任务间的直接制约关系,A要继续执行需要B完成某一个操作操作才能继续进行。
互斥:任务间的间接制约关系,A访问了资源B就不能去访问,必须等A访问完了才行。
原子操作:
锁分类:sleep-waiting、 busy-waiting。
数据的一致性。
所谓的锁,说白了就是内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。
看起来很简单,大家有没有想过,OS是怎样保证这个锁操作本身的原子性呢?举个例子,在多核环境中,两个核上的代码同时申请一个锁,两个核同时取出锁变量,同时判断说这个锁是空闲状态,然后有同时修改为上锁状态,同时返回成功。。。两个核同时获取到了锁,这种情况可能吗?废话,当然是不可能,可能的话,我们使用锁还有啥意义。但是,咦?等等,虽然我知道肯定不可能,但是你刚才说的貌似还有点道理,看来OS实现这个锁还不是看起来这么简单,还是有点道道的。
为了弄明白锁的实现原理,我们首先看看如果OS不采用任何其他手段,什么情况下会导致上锁失败?假如我们把加锁过程用如下伪码表示:
1、read lock;
2、判断lock状态;
3、如果已经加锁,失败返回;
4、把锁状态设置为上锁;
5、返回成功。
明白汇编的同学一看就明白上述每一步都能对应到一条汇编语句,所以我们可以认为每一步本身是原子的。
那么什么情况能够导致两个线程同时获取到锁呢?
1、中断:假设线程A执行完第一步,发生中断,中断返回后,OS调度线程B,线程B也来加锁并且加锁成功,这时OS调度线程A执行,线程从第二步开始执行,也加锁成功。
2、多核:当然了,想想上面举的例子,描述的就是两个核同时获取到锁的情况。
既然明白锁失败的原因,解决手段就很明确了:
先考虑单核场景:
1、既然只有中断才能把上锁过程打断,造成多线程操作失败。我先关中断不就得了,在加锁操作完成后再开中断。
2、上面这个手段太笨重了,能不能硬件做一种加锁的原子操作呢?能,大名鼎鼎的“test and set”指令就是做这个事情的。(怎么,test and set是干什么的?同学,看来你上课时不够专心啊,赶紧回头复习复习)
通过上面的手段,单核环境下,锁的实现问题得到了圆满的解决。那么多核环境呢?简单嘛,还是“test and set”不就得了,这是一条指令,原子的,不会有问题的。真的吗,单独一条指令能够保证该指令在单个核上执行过程中不会被中断打断,但是两个核同时执行这个指令呢?。。。我再想想,硬件执行时还是得从内存中读取lock,判断并设置状态到内存,貌似这个过程也不是那么原子嘛。对,多个核执行确实会存在这个问题。怎么办呢?首先我们得明白这个地方的关键点,关键点是两个核会并行操作内存而且从操作内存这个调度来看“test and set”不是原子的,需要先读内存然后再写内存,如果我们保证这个内存操作是原子的,就能保证锁的正确性了。确实,硬件提供了锁内存总线的机制,我们在锁内存总线的状态下执行test and set操作,就能保证同时只有一个核来test and set,从而避免了多核下发生的问题
断即可,在PSW中打开/关闭中断标志位,用不到自旋锁。反之,用了自旋锁机制,就不用再管开中断或者关中断。
自旋锁的设计初衷是忙则等待。
理解
目的:为了实现保护共享资源,任何时刻只能有一个保持者。即只有一个进程可以获得锁。
自旋锁机制下,如果自旋锁已经被别的执行单元保持,那么调用者就一直循环在那里看自旋锁的保持者是否释放了锁,即忙则等待。很多出题点喜欢在这个概念上混淆,故意出成让权等待。让全等待是出让CPU,这里没有体现这个思路。因为跟CPU无关。自旋是个很形象的动作,表示一直在原地运动,并未去睡眠。这是区别于互斥锁的。
互斥锁机制下,如果资源被占用,资源申请者就会进入睡眠状态。
原理的理解
执行单元想访问被自旋锁保护的共享资源,必须先获得锁。
访问完共享资源后,必须释放锁。
当准备去获取自旋锁时,发现锁没人在用,顺利获得锁,可以访问共享资源。否则,就一直在等。这就像在火车上用卫生间,如果空闲,就可以马上进去,且把门锁上,保护隐私。用完了把门打开走掉。如果里面有人,只好在外面等,如果着急,你肯定会一直等着,在那着急自旋。这就是自旋锁机制。如果你不等了,回座位休息去,那就是互斥锁机制。
自旋锁是比较低级的保护数据结构,贴近硬件层面实现。
可能存在的问题:
死锁
过多占用CPU资源
推导出自旋锁适合使用者保持锁时间较短的情况。
而信号量机制适合保持锁较长时间,因此没有资源可用时去睡眠。
实现
多处理机互斥算法:
do
{
…
while(Test_And_Set_Lock(&lock)); //特别注意分号,以及传递的参数是引用参数
//临界区
lock = False;
//剩余区
}while(True)
Test_And_Set_Lock函数是用硬件保持的原子运行,不被打断。
特别注意while(Test_And_Set_Lock(&lock));这句后面是个分号就是说会循环等待解锁。
理解到这个地步,就可以来思考一道真题了。
(2016.27)使用TSL指令实现进程互斥的伪代码如下:
do
{
…
while(Test_And_Set_Lock(&lock)); //特别注意分号,以及传递的参数是引用参数
//临界区
lock = False;
//剩余区
}while(True)
下列与该实现机制相关的叙述中,正确的是:
A. 退出临界区的进程负责唤醒阻塞态进程
B. 等待进入临界区的进程不会主动放弃CPU
C. 上述伪代码满足“让权等待”的同步准则
D. while(Test_And_Set_Lock(&lock))语句应在关中断状态下执行
分析:由上面的一大段分析可知,A项表述的是信号量机制这些进程较长时才有的状态,即进程无法访问临界资源时要去睡眠,在TSL里,都是短进程,都是精力充沛的,忙则等待的类型。因此,没人睡,自然也不存在唤醒。
C项让权等待与B项表达的东西是相反的。让权是什么意思?有必要特别说明。让权等待是一种策略,但是对象可以是CPU也可以是临界资源。这要看具体语境。这里,针对临界资源的访问,让权等待是指:当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。我们需要形成一种概念是,进程是程序的动态执行过程,忙则等待资源时,必然是在运行的进程,是有CPU在背后力挺的,睡眠就是说没有资源赶紧滚,别占着CPU!
想一想进程调度的三大基本形态:就绪态,运行态,阻塞态三态之间的互相转化。
因此,C项错,B项对,忙则等待就是要占着CPU,所幸的是进程不长,否则递归调用一定产生死锁。
D项是很细致的考察,前面专门提过,TSL是多处理器下的进程并发问题,只有在中断机制下采用PSW,关中断/开中断方式来进行进程的并发控制。