Linux中同步互斥机制研究之原子操作

  操作系统中,对共享资源的访问需要有同步互斥机制来保证其逻辑的正确性,而这一切的基础便是原子操作。

| 原子操作(Atomic Operations):

    原子操作从定义上理解,应当是类似原子的,不可再分的操作;然而实际上稍有不同,较为准确的定义应当是:不可被打断的一个或一系列操作

  在单处理器系统中,能够在单条指令中完成的操作都可以认为是“原子操作”,因为中断只发生在指令边缘。在多处理器结构中就不同了,由于系统中有多个处理器独立运行,即使能在单条指令中完成的操作也有可能受到干扰。在X86平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU上有一根引线#HLOCK pin连到北桥芯片,如果在汇编程序的指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,直到这条指令执行结束时才放开,从而将总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中对内存访问的原子性。

| 1. 为什么需要原子操作 

  我们来看一个例子,假设我们用两个线程,完成对全局变量 index 的自增至10的功能,我们这样写线程函数

  

  执行结果如下,可以看到执行了两次,但结果却不一样(不是想象的值为10),事实上每次执行的结果都可能不一样。理论上,两个线程分别同时执行5次自增操作,只要执行正确应该总是得到10的结果,但是可以看到两个线程虽然可以交叉执行,index的值却不是从1增长到10。图中mthread程序第一次执行时,线程一将index增加到5后,线程二居然得到了2的结果,这是怎么回事?

  

 

  我们知道,在计算机中,变量的值是存储在内存中的,要知道一个存储在内存中的变量的值就需要读内存,需要更新这个变量的值就必须写回去。如果这整个操作是不可打断的,那么是不会出现上面的结果的,所以中间肯定是出现了什么问题。事实上i++这种操作确实不是原子的,程序编译成汇编代码后就可以看到,i++实际上是由Read(读)-Modify(改)-Write(写) 3个操作实现的:

  

  

  如上图所示,两个线程分别在两个处理器核上执行,假设i的值刚开始是0,线程1先读取i的值,放到ax寄存器中,此时线程2也读取i的值,接着add eax,1,然后回写到i的地址中,此时i的值为1;之后线程1中再执行add eax,1,再回写到i的地址,所以i的值就是1,而不是2。如果需要将i++变成原子的就需要一些额外的方式。

| 2. Linux中原子操作的实现 

  有了上述的感官认识,现在来说实现就顺理成章了。Linux中对原子操作的实现主要在 linux-2.6.28\arch\x86\include\asm 目录下的 Atomic_32.h 中,包括了一系列内联函数(因为这些函数的代码都不长,基本都采用内嵌汇编代码编写)。首先来看原子类型的数据结构 atomic_t的定义:

1 /*
2  * Make sure gcc doesn't try to be clever and move things around
3  * on us. We need to use _exactly_ the address the user gave us,
4  * not some alias that contains the same information.
5  */
6 typedef struct {
7     int counter;
8 } atomic_t;

   接着来看如何实现原子操作:

 1 /**
 2  * atomic_add - add integer to atomic variable
 3  * @i: integer value to add
 4  * @v: pointer of type atomic_t
 5  *
 6  * Atomically adds @i to @v.
 7  */
 8 static inline void atomic_add(int i, atomic_t *v)
 9 {
10     asm volatile(LOCK_PREFIX "addl %1,%0"
11              : "+m" (v->counter)
12              : "ir" (i));
13 }

  上面这段代码截自Atomic_32.h,即原子的加操作。这个函数的实现采用了内嵌汇编的方式,首先声明LOCK前缀(实际上是一个宏定义),以原子的方式执行指令addl  %1,%0,表示将输入操作数和输出操作数编号,将输入和输出相加后输出。其中“+m”中加号表示输出是可读可写的,括号内注明了是原子变量v->counter;m表示输出存储在内存之中。“ir”中 i 表示输入是一个直接操作数,r表示这里输入 i 是存储在寄存器中的。

  原子的加有了,原子的减自然也不难:

 1 /**
 2  * atomic_sub - subtract integer from atomic variable
 3  * @i: integer value to subtract
 4  * @v: pointer of type atomic_t
 5  *
 6  * Atomically subtracts @i from @v.
 7  */
 8 static inline void atomic_sub(int i, atomic_t *v)
 9 {
10     asm volatile(LOCK_PREFIX "subl %1,%0"
11              : "+m" (v->counter)
12              : "ir" (i));
13 }

   其他的原子操作就不举例了,总之原理就是这样,在有竞争的访问中,有时需要保证操作最后执行的逻辑正确性,就必须将某些操作或者指令设置为原子的。原子操作是其他的一些同步互斥机制的基础,有了原子操作就可以实现多核系统中其他的同步互斥机制了。

 

 

  参考文献:

[1].http://edsionte.com/techblog/archives/1809

[2].http://blog.csdn.net/qb_2008/article/details/6840808

 

 

posted @ 2014-05-20 23:19  XiaoH在博客园  阅读(1238)  评论(0编辑  收藏  举报