操作系统中锁的实现

在多线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区代码的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。

锁机制的一个特点是它的同步原语都是原子操作。那么操作系统是如何保证这些同步原语的原子性呢?

操作系统之所以能构建锁之类的同步原语,是因为硬件已经为我们提供了一些原子操作,比如:中断禁止和启用(interrupt enable/disable),内存加载和存入(load/store)测试与设置(test and set)指令。禁止中断这个操作是一个硬件步骤,中间无法插入别的操作。同样,中断启用,测试与设置均为一个硬件步骤的指令。在这些硬件原子操作之上,我们便可以构建软件原子操作:锁,睡觉与叫醒,信号量等。

1.以中断启用和禁止来实现锁

要防止一段代码在执行过程中被别的进程插入,就要考虑在一个单处理器上,一个线程在执行途中被切换的途径。我们知道,要切换进程,必须要发生上下文切换,上下文切换只有两种可能:

①一个线程自愿放弃CPU而将控制权交给操作系统调度器(通过yield之类的操作系统调用来实现)

②一个线程被强制放弃CPU而失去控制权(通过中断来实现)

原语执行过程中,我们不会自动放弃CPU控制权,因此要防止进程切换,就要在原语执行过程中不能发生中断。所以采用禁止中断,且不自动调用让出CPU的系统调用,就可以防止进程切换,将一组操作变为原子操作。

lock之中断启用与禁止:

lock()

{

disable interrupt

while(value!=FREE)

{

enable interrupt //使其他线程可以抢占,从而改变value的值

disable interrunpt //只有在这两行语句之间,别的进程才拥有抢占时机

}

value=BUSY

enable interrupt

}

unlock之中断启用与禁止:

unlock()

{

disable interrupts

value=FREE

enable interrupts

}

2.以测试与设置指令来实现锁

原子操作:(设置操作)将1写入到指定内存单元,(读取操作)返回指定内存单元里原来的值,也即写入新值1之前的内容。

测试与设置指令:

test_and_set(x)

{

tmp=x

x=1

return (tmp)

}

test_and_set(x)的操作是将1写入到变量x里,并将写1之前x的值返回。

使用测试与设置指令实现lock:

(value初始值为0,代表锁是打开的。)

lock()

{

while(test_and_lock(value)==1) {} //每次执行完原子操作后都有可能会被抢占

}

如果锁是打开的,即value是0的话,则返回值是0,该指令将value设置为1,获得锁并退出循环。

如果锁是闭合的,即value是1的话,则返回值是1,循环继续。直至成功获得锁为止。

使用测试与设置指令实现unlock:

unlock()

{

value=0 //因为是赋值0,可以直接在总线上产生,不用中断包裹着也没有问题

}

3.以非繁忙等待,中断启用与禁止来实现锁

前面两种锁的实现方式都较为简单,但都有一个问题,就是存在繁忙等待。而繁忙等待浪费资源,我们想到对前两种方法进行改善。改善思路:不进行繁忙等待,在拿不到锁的时候去睡觉,等待别人的叫醒。

先看一种锁操作实现方式:

lock()

{

disable interrupt

if(value==free)

{

value=busy

}

else

{

添加到锁的等待队列

切换到下一个线程

}

enable interrupt

}

使用非繁忙等待中断禁止与启用来实现释放锁操作:

unlock()

{

disable interrupt

value=free

if(有线程在等待锁)

{

移到就绪队列

value=busy

}

enable interrupt

}

但是这种方式存在着问题:是因为切换到别的进程之后,该程序无法再执行,那么后面的中断启用指令就不能执行了。而我们是在中断处于禁止状态下切换到别的进程的,如果别的进程没有执行中断启用或者自动放弃CPU给另一个线程,系统将进入死锁状态。

解决办法是闭锁操作不启用中断,而是留给别的线程去启用中断。

也就是说,我们要求所有线程遵守下列约定:

①所有线程承诺在调用线程切换调用时将中断留在禁止状态。

②所有线程承诺在从切换返回时将中断重新启用。

因此,使用非繁忙等待中断禁止与启用来实现锁操作的正确方式如下:

这里注意,switch表示切换线程;中断的启用与禁止是在系统调用(lock和yield)里面实现的,即由操作系统实现。

4.以最少繁忙等待,测试与设置来实现锁

使用测试与设置来实现锁不可能完全避免繁忙等待,我们的目的就是尽可能降低等待的时间。

我们的中心思想就是:我们只用繁忙等待来执行闭锁的操作,如果不能这样做就放弃CPU。

使用一个额外的变量guard用来保证每次只有一个线程获得value并对其操作。

lock()

{

while(test_and_set(guard)){}

if(value==free)

{

value=busy

guard=0

}

else

{

添加到锁的等待队列

guard=0

切换线程

}

}

unlock()

{

while(test_and_set(guard)){}

value=free

if(有其他线程在等待锁)

{

移到就绪队列

value=busy

}

guard=0

}

我们了解了如何使用中断禁止,测试与设置两种硬件原语来实现软件的锁原语。这两种方式比较起来,显然测试与设置更加简单,也因此使用的更为普遍。此外,test and set还有一个优点,就是可以在多CPU环境下工作,而中断启用和禁止则不能。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值