介绍spin-lock的一篇杂文

[引] http://apps.hi.baidu.com/share/detail/39279882

在Linux的内核中,spin lock用在多处理器环境中。当一个CPU访问一个临界资源 
(critical section)的时候,需要预先取得spin lock,如果取不到的话,它就在空循环 
等待,直到另外的CPU释放spin lock。由于涉及到多个处理器,spin lock的效率非常重要。
 
因为在等待spin lock的过程,处理器只是不停的循环检查,并不执行其他指令。但即使这样
, 
一般来说,spn lock的开销还是比进程调度(context switch)少得多。这就是spin lock 
被广泛应用在多处理器环境的原因。 

1. spin lock的数据结构 

/* include/asm-i386/spinlock.h */ 

typedef struct { 
volatile unsigned int lock; 
} spinlock_t; 

spin lock的数据结构很简单,只是一个整数变量lock, 如果lock等于1的话,表示 
这个spin lock是自由的;如果lock小于等于0的话,则表示spin lock已经被其他CPU所 
获取。 

2. spin lock的实现 

#define spin_lock_string 
"n1:t" 
"lock ; decb %0nt" 
"js 2fn" 
".section .text.lock,"ax"n" 
"2:t" 
"cmpb $0,%0nt" 
"rep;nopnt" 
"jle 2bnt" 
"jmp 1bn" 
".previous" 


#define spin_unlock_string 
"movb $1,%0" 
:"=m" (lock->lock) : : "memory" 

static inline void spin_lock(spinlock_t *lock) 

__asm__ __volatile__( 
spin_lock_string 
:"=m" (lock->lock) : : "memory"); 


static inline void spin_unlock(spinlock_t *lock) 

char oldval = 1; 

__asm__ __volatile__( 
spin_unlock_string 
); 


如果将上面的语句转化成纯汇编的话,则是这样: 

spin_lock(lock) 

1: 
lock ; decb %0 
js 2f 

.section .text.lock, "ax" 
2: cmpb $0,%0 
rep;nop 
jle 2b 
jmp 1b 
.previous 


其中%0就是函数参数传进来的lock->lock,下面详细地解释一下每一条 
汇编指令: 
* lock ; decb %0 
decb将lock->lock减1,它前边的lock指令表示在执行decb的时候,要锁住 
内存总线(memory bus),另外的CPU不能访问内存,以保证decb指令的原子性。 
注意,decb并不是原子操作(atomic operation),它需要将变量从内存读出来, 
放入寄存器(register),减1,再写入内存。如果在这时候另外的CPU也进行同样的操作的
 
时候,那么decb的执行结果就会不确定,也就是说,操作的原子性遭到了破坏。 

* js 2f 
如果decb的结果小于0,表示无法取得spin lock,则跳到标签为2的指令(f表示向前跳)。
 
如果decb的结果等于0,表示已经获得spin lock,执行下一条指令,则跳出整段代码,函数
返回。 
注意, "j2 2f"的下一条指令并不是"cmpb $0,%0"。 

* .section .text.lock, "ax" 
.previous 
从.section到.previous的这一段代码被用来检测spin lock何时被释放。linux定义了一个 
专门的区(.text.lock)来存放这段代码。它们和前边的"js 2f"并不在一个区(section)里
, 
    所以说"js 2f"的下一条指令并不是"cmpb $0,%0"。 
    之所以定义成一个单独的区,原因是在大多数情况下,spin lock是能获取成功的,
从.section 
    到.previous的这一段代码并不经常被调用,如果把它跟别的常用指令混在一起,会
浪费指令 
    缓存的空间。从这里也可以看出,linux内核的实现,要时时注意效率。 

* 2: cmpb $0,%0 
  rep;nop 
jle 2b 
jmp 1b 
检查lock->lock,和0比较,如果小于等于0(jle 2b),则跳回到标签2的指令,重新比较 
(b表示往回跳)。如果大于0,表示spin lock已经被释放,则往回跳回到标签1,重新试图 
     取得spin lock。 

  * rep;nop 
这是一条很有趣的指令:),咋一看,这只是一条空指令,但实际上这条指令可以降低CPU的运
行频率,减低电的消耗量,但最重要的是,提高了整体的效率。因为这段指令执行太
快的话,会生成很多读取内存变量的指令,另外的一个CPU可能也要写这个内存变量,现在的CPU经
常需要重新排序指令来提高效率,如果读指令太多的话,为了保证指令之间的依赖性,CPU会以
牺牲流水线执行(pipeline)所带来的好处。从pentium 4以后,intel引进了一条pause指令,
专门用于spin lock这种情况,据intel的文档说,加上pause可以提高25倍的效率! 

spin_unlock(lock) 
  * movb $1,%0 
spin_unlock的实现很简单,只是重新将lock->lock置1就行了。 

  还有一个问题我想谈的是,在linux 2.3以前,spin lock是用"lock; btrl $0,%0"来实
现解锁的,但是后来的版本只使用了简单的mov指令,执行时间从22个时钟周期降低到1个时钟
周期。 
但是最开始linus本人不同意这种做法,以为他以为由于intel芯片的指令重排序,会使spin lock 
的实现不稳定,但后来intel里的一个工程师出来澄清了linus的错误。这也许是open sourc
e的好处吧。 

  spin lock的实现看起来简单,但是细微之处却很复杂,如果大家需要进一步理解,请细
细读一下 kernel的mail list和intel关于pentium的文档。 

 

 

=====================

Memory Ordering (http://www.cnblogs.com/codingmylife/archive/2010/04/28/1722573.html)

Background
很久很久很久以前,CPU忠厚老实,一条一条指令的执行我们给它的程序,规规矩矩的进行计算和内存的存取。
很久很久以前, CPU学会了Out-Of-Order,CPU有了Cache,但一切都工作的很好,就像很久很久很久以前一样,而且工作效率得到了很大的提高。
很久以前,我们需要多个CPU一起工作,于是出现了传说中的SMP系统,每个CPU都有独立的Cache,都会乱序执行,会打乱内存存取顺序,于是事情变得复杂了……
Problem
由于每个CPU都有自己的Cache,内存读写不再一定需要真的作内存访问,而是直接从Cache里面操作,同时CPU可能会在合适的时候对于内存访问进行重新排序以提高效率,在只有一个CPU的时候,这很完美。
而当有多个CPU的时候—— 从Cache到内存的flush操作通常是被延迟的,所以就需要某种方法保证CPU A进行的内存写操作真的可以被CPU B读取到。 CPU可能会因为某些原因(比如某两个变量同在一个Cacheline中)而打乱 实际内存写入顺序 实际内存读取顺序 所以就需要某种方法保证在需要的时候 之前的读写操作已经完成 未来的读写操作还没开始 考虑一个例子:
Thread A:
while (flag == 0)
        ; // do nothing
printf("%d\n", data);
Thread B:
data = 523;
flag = 1;
这里data代表了某种数据,它可以像这里一样是一个简单的整数,也可能是某种复杂的数据结构,总之,我们在Thread B中对data进行了写入,并利用flag变量表示data已经准备好了。
在Thread A中,一个忙等待直到发现data已经准备好了,然后开始使用data,这里是简单的把data打印出来。
现在考虑如果CPU发现对于data和flag的写入,如果按照先写入flag后写入data的方式进行,或者考虑由于Cache的flush操作的延迟,使得内存中变量的实际修改顺序是先flag后data,那么都将导致Thread A的结果不正确。事实上,由于内存读入操作同样是可能乱序进行的,Thread A甚至可能在读入flag进行判断之前就已经完成了对data的读入操作,这同样导致错误的结果。
Solution
在这个例子中,我们的需求是,Thread A中对于flag判断时,后面的任何读入操作都没有开始,Thread B中对于flag写入时,任何之前的写入操作都已经完成。
在Linux内核中,smp_rmb()、smp_wmb()、smp_mb()就是用来解决这类问题的,mb表示memory barrier。rmb表示读操作不可跨越(注意,不是人民币的意思:-P),也就是我们这个例子中的Thread A所需要的。wmb表示写操作不可跨越,也就是这里Thread B所需要的。mb集合了rmb和wmb的能力,读写操作都不可跨越。

你可能感兴趣的:(thread,数据结构,linux,cache,OpenSource,linux内核)