Chinaunix首页 | 论坛 | 博客
  • 博客访问: 51402
  • 博文数量: 16
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 112
  • 用 户 组: 普通用户
  • 注册时间: 2013-10-09 19:10
文章分类

全部博文(16)

文章存档

2015年(2)

2014年(3)

2013年(11)

我的朋友

分类: LINUX

2013-10-09 19:13:06

原文地址:Linux多CPU原子操作 作者:xiangfei01

在Linux2.6.18之后,删除了,GCC提供了内置的 原子操作函数,更适合用户态的程序使用。现在atomic.h在内核头文件中,不在gcc默认搜索路径下,即使像下面这样强行指定路径,还是会出现编译错 误。

  1. #include  

gcc从4.1.2提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作。

可以对1,2,4或8字节长度的数值类型或指针进行原子操作,其声明如下

  1. type __sync_fetch_and_add (type *ptr, type value, ...)   
  2. type __sync_fetch_and_sub (type *ptr, type value, ...)   
  3. type __sync_fetch_and_or (type *ptr, type value, ...)   
  4. type __sync_fetch_and_and (type *ptr, type value, ...)   
  5. type __sync_fetch_and_xor (type *ptr, type value, ...)   
  6. type __sync_fetch_and_nand (type *ptr, type value, ...)   
  7.           { tmp = *ptr; *ptr op= value; return tmp; }   
  8.           { tmp = *ptr; *ptr = ~tmp & value; return tmp; }   // nand   
  9.   
  10. type __sync_add_and_fetch (type *ptr, type value, ...)   
  11. type __sync_sub_and_fetch (type *ptr, type value, ...)   
  12. type __sync_or_and_fetch (type *ptr, type value, ...)   
  13. type __sync_and_and_fetch (type *ptr, type value, ...)   
  14. type __sync_xor_and_fetch (type *ptr, type value, ...)   
  15. type __sync_nand_and_fetch (type *ptr, type value, ...)   
  16.           { *ptr op= value; return *ptr; }   
  17.           { *ptr = ~*ptr & value; return *ptr; }   // nand  

这两组函数的区别在于第一组返回更新前的值,第二组返回更新后的值,下面的示例引自这里 。

  1. #include    
  2. #include    
  3. #include    
  4.   
  5. static int count = 0;   
  6.   
  7. void *test_func(void *arg)   
  8. {   
  9.         int i=0;   
  10.         for(i=0;i<20000;++i){   
  11.                 __sync_fetch_and_add(&count,1);   
  12.         }   
  13.         return NULL;   
  14. }   
  15.   
  16. int main(int argc, const char *argv[])   
  17. {   
  18.         pthread_t id[20];   
  19.         int i = 0;   
  20.   
  21.         for(i=0;i<20;++i){   
  22.                 pthread_create(&id[i],NULL,test_func,NULL);   
  23.         }   
  24.   
  25.         for(i=0;i<20;++i){   
  26.                 pthread_join(id[i],NULL);   
  27.         }   
  28.   
  29.         printf("%d\n",count);   
  30.         return 0;   
  31. }  

对于使用atomic.h的老代码,可以通过宏定义的方式,移植到高内核版本的linux系统上,例如

  1. #define atomic_inc(x) __sync_add_and_fetch((x),1)   
  2. #define atomic_dec(x) __sync_sub_and_fetch((x),1)   
  3. #define atomic_add(x,y) __sync_add_and_fetch((x),(y))   
  4. #define atomic_sub(x,y) __sync_sub_and_fetch((x),(y)) 



关于单CPU,多CPU上的原子操作  转自http://software.intel.com/zh-cn/blogs/2010/01/14/cpucpu/?cid=sw:prccsdn956

    所谓原子操作,就是"不可中断的一个或一系列操作" 。

硬件级的原子操作:
    在单处理器系统(UniProcessor)中,能够在单条指令中完成的操作都可以认为是" 原子操作",因为中断只能发生于指令之间。这也是某些CPU指令系统中引入了test_and_set、test_and_clear等指令用于临界资源互斥的原因。

    在对称多处理器(Symmetric Multi-Processor)结构中就不同了,由于系统中有多个处理器在独立地运行,即使能在单条指令中完成的操作也有可能受到干扰。

    在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把总线锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中 的原子性。
软件级的原子操作:
    软件级的原子操作实现依赖于硬件原子操作的支持。
    对于linux而言,内核提供了两组原子操作接口:一组是针对整数进行操作;另一组是针对单独的位进行操作。
2.1. 原子整数操作
    针对整数的原子操作只能对atomic_t类型的数据处理。这里没有使用C语言的int类型,主要是因为:

    1) 让原子函数只接受atomic_t类型操作数,可以确保原子操作只与这种特殊类型数据一起使用

    2) 使用atomic_t类型确保编译器不对相应的值进行访问优化

    3) 使用atomic_t类型可以屏蔽不同体系结构上的数据类型的差异。尽管Linux支持的所有机器上的整型数据都是32位,但是使用atomic_t的代 码只能将该类型的数据当作24位来使用。这个限制完全是因为在SPARC体系结构上,原子操作的实现不同于其它体系结构:32位int类型的低8位嵌入了 一个锁,因为SPARC体系结构对原子操作缺乏指令级的支持,所以只能利用该锁来避免对原子类型数据的并发访问。

    原子整数操作最常见的用途就是实现计数器。原子整数操作列表在中定义。原子操作通常是内敛函数,往往通过内嵌汇编指令来实现。如果某个函数本来就是原子的,那么它往往会被定义成一个宏。

在编写内核时,操作也简单:

    atomic_t use_cnt;

    atomic_set(&use_cnt, 2);

    atomic_add(4, &use_cnt);

    atomic_inc(use_cnt);

2.2. 原子性与顺序性

    原子性确保指令执行期间不被打断,要么全部执行,要么根本不执行。而顺序性确保即使两条或多条指令出现在独立的执行线程中,甚至独立的处理器上,它们本该执行的顺序依然要保持。

2.3. 原子位操作

    原子位操作定义在文件中。令人感到奇怪的是位操作函数是对普通的内存地址进行操作的。原子位操作在多数情况下是对一个字长的内存访问,因而位号该位于0-31之间(在64位机器上是0-63之间),但是对位号的范围没有限制。

编写内核代码,只要把指向了你希望的数据的指针给操作函数,就可以进行位操作了:

    unsigned long word = 0;

    set_bit(0, &word); /*第0位被设置*/

    set_bit(1, &word); /*第1位被设置*/

    clear_bit(1, &word); /*第1位被清空*/

    change_bit(0, &word); /*翻转第0位*/

为什么关注原子操作?
    1)在确认一个操作是原子的情况下,多线程环境里面,我们可以避免仅仅为保护这个操作在外围加上性能开销昂贵的锁。
    2)借助于原子操作,我们可以实现互斥锁。
    3)借助于互斥锁,我们可以把一些列操作变为原子操作。

GNU C中x++是原子操作吗?
    答案不是。x++由3条指令完成。x++在单CPU下不是原子操作。
    对应3条汇编指令
    movl x, %eax
    addl $1, %eax
    movl %eax, x
    在vc2005下对应
    ++x;
    004232FA mov eax,dword ptr [x]
    004232FD add eax,1
    00423300 mov dword ptr [x],eax
    仍然是3条指令。
    所以++x,x++等都不是原子操作。因其步骤包括了从内存中取x值放入寄存器,加寄存器,把值写入内存三个指令。

如何实现x++的原子性?
    在单处理器上,如果执行x++时,禁止多线程调度,就可以实现原子。因为单处理的多线程并发是伪并发。
    在多处理器上,需要借助cpu提供的Lock功能。锁总线。读取内存值,修改,写回内存三步期间禁止别的CPU访问总线。同时我估计使用Lock指令锁总线的时候,OS也不会把当前线程调度走了。要是调走了,那就麻烦了。

    在多处理器系统中存在潜在问题的原因是:
    不使用LOCK指令前缀锁定总线的话,在一次内存访问周期中有可能其他处理器会产生异常或中断,而在异常处理中有可能会修改尚未写入的地址,这样当INC操作完成后会产生无效数据(覆盖了前面的修改)。

    spinlock 用于CPU同步, 它的实现是基于CPU锁定数据总线的指令.
    当某个CPU锁住数据总线后, 它读一个内存单元(spinlock_t)来判断这个spinlock 是否已经被别的CPU锁住. 如果否, 它写进一个特定值, 表示锁定成功, 然后返回. 如果是, 它会重复以上操作直到成功, 或者spin次数超过一个设定值. 锁定数据总线的指令只能保证一个机器指令内, CPU独占数据总线.
    单CPU当然能用spinlock, 但实现上无需锁定数据总线.

    spinlock在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候spinlock会让其它process动不了.

阅读(1718) | 评论(0) | 转发(0) |
0

上一篇:没有了

下一篇:第一篇博文

给主人留下些什么吧!~~