互斥量是一个可以处于两态之一的变量:加锁和解锁,这意味着一个二进制位就可以表示他,但在实现中,常常使用一个整型变量,0表示解锁,而其他所有的值表示加锁。互斥量使用两个过程。
1.当一个线程或者进程想要访问临界区时,它调用mutex_lock。如果该互斥量当前是解锁的,此调用成功,该线程进入临界区。另外,如果该互斥量已经加锁,调用线程将被阻塞,直到持有该锁的线程释放锁。如果多个线程阻塞在该互斥量上,将随机选择一个线程并允许它获得锁。
2.当线程要退出临界区时,调用mutex_unlock。
两过程代码如下
mutex_lock:
TSL REGISTER,MUTEX //将互斥信号量复制到寄存器,并且将互斥量置为1
CMP REGISTER,#0 //互斥量是否为0
JZE OK //互斥量为0,此锁是释放状态,该线程可以进入互斥区
CALL thread_yield //该锁被其他线程持有,调度另一个线程
JMP mutex_lock //稍后重试
ok:RET //返回调用线程,进入临界区
mutex_unlock:
MOVE MUTEX,#0 //释放锁
RET //返回
指令相当简单,介绍两点
1.TSL指令:称为测试并加锁(test and set lock),它将一个内存字lock读到寄存器中,然后在该内存地址上存一个非零值。执行TSL指令的cpu将锁住内存总线,以禁止其他CPU在本指令结束前访问内存,这就防止了竞争条件(当某个过程的结果取决于逻辑执行流的调度顺序时,即存在竞争条件) 注意:锁住内存总线不同于屏蔽中断。屏蔽中断是指进程在刚刚进入临界区时屏蔽所有中断,这其中就包括了时钟中断,而CPU只有发送时钟中断或其他中断时才会进行进程切换。这就保证了,在单处理器系统中,屏蔽中断就可以实现互斥。但是在多处理器中,屏蔽中断并不管用,其他CPU仍然可以进入共享内存。因此引入了锁住内存总线,这样其他CPU也不能访问内存。
2.CALL thread_yield这个指令妙处在于不用通过忙等待浪费CPU,而去将CPU分配给另一个线程。当该线程下次分得时间片运行时,再一次对锁进行测试。
另外要提的是
快速用户区互斥量futex
随着并行的增加,有效的同步和锁机制对性能而言非常重要。如果等待时间短的话,完全可以用自旋锁忙等待,因为如果阻塞进程,陷入内核的开销远远比其大。但如果等待时间长,则会浪费CPU周期。如果有很多竞争,那么阻塞该进程,并仅当锁被释放的时候让内核接触阻塞会更加有效。然而,这却带来了相反的问题:竞争不激烈时,那么不断地内核切换将花销太大。
因此引入了futex,他是linux系统的一个特性,结合了以上两者之所长。它实现了基本的锁,但避免陷入内核,除非它是真的不得不这样做。因为来回切换到内核花销很大,所以这样做可以十分可观的改善性能。一个futex包含两个部分:一个内核服务和一个用户库。内核服务提供一个等待队列,它允许多个进程在一个锁上等待,直到内核对他们解除阻塞。将一个进程放到内核等待队列需要系统调用,而系统调用将陷入内核,这会非常花费时间,我们应该尽量避免他们。因此,在没有竞争时,futex完全在用户空间工作,进程共享一个锁变量,假设锁初始态是释放状态,初始值为1.线程要进入临界区前,通过执行原子操作"减少并检验"来获得锁,接下来,这个线程检查结果,看锁是否被释放,如果为处于被锁状态,线程成功获取锁。如果该锁被其他线程持有,则进行系统调用将该线程投入内核等待队列。当一个线程将要退出临界区时,进行原子操作"增加并检验"来释放锁,并检查结果,看是否扔有进程阻塞在内核等待队列上。如果有,通知内核可以对等待队列里的一个或个线程解除阻塞。
参考文献:《现代操作系统》