原子操作和原子指令

引子

考虑如下的简单程序,全局变量x初始值为0:

int x = 0;

void thread1_func() {
  x++;
  print(x);
}

void thread2_func() {
  x++;
  print(x);
}

程序输出 1 2 或 2 2很容易理解,但也有可能输出为1 1。 Why?

原因便是x++不是原子操作,如果把它转为CPU指令形式,则很容易理解:

(1) Load x

(2) Inc x

(3) Store x

当第一个线程运行完第一步时,第二个线程也运行到此,这时它们得到的值都是0,然后将值加1再存回去,这时两个线程运行完时,x的值是1。

原子操作

最简单的解决方式便是使用原子操作,Linux中提供了以atomic_开头的原子操作函数,例如:

#define atomic_inc(v)
#define atomic_dec(v)
#define atomic_add(i, v)
...

v是atomic_t类型的变量,定义如下:

typedef struct {
  inc counter;
} atomic_t;

原子操作需要靠硬件实现,我们以arm64平台为例,看看atomic_add函数是如何实现的。

<arch/arm64/include/asm/atomic_lse.h>

#define ATOMIC_OP(op, asm_op)  static  inline  void  atomic_##op(int i, atomic_t *v) {     register  int w0 asm ("w0") = i;     register  atomic_t *x1 asm ("x1") = v;           \
   asm  volatile(ARM64_LSE_ATOMIC_INSN(__LL_SC_ATOMIC(op), "  " #asm_op " %w[i], %[v]\n")   : [i] "+r" (w0), [v] "+Q" (v->counter)  : "r" (x1)  : __LL_SC_CLOBBERS); }
ATOMIC_OP(add, stadd)

GCC内联汇编

在介始具体实现前,我们先了解一下GCC内联汇编,GCC内联汇编的格式如下:

asm volatile(指令部:输出部:输入部:损坏部)

  • 指令部中,数字加上前缀%,例如%0,%1,表示需要使用寄存器的操作数。为了提高可读性,可以使用汇编符号名来替代%前缀表示的操作数。
  • 输出部用于规定输出变量的约束条件,通常以=号开头,接着一个字母表示操作数类型的说明,然后是关于变量结合的约束
  • 输入部描述输入操作数,用逗号隔开
  • 损坏部一般以memory开头,告诉编译器指令改变了内存中的值,在执行完汇编代码后重新加载该值,目的是防止编译乱序。clobber list描述了汇编代码对寄存器的修改情况。为何要有clober list?我们的c代码是gcc来处理的,当遇到嵌入汇编代码的时候,gcc会将这些嵌入式汇编的文本送给gas进行后续处理。这样,gcc需要了解嵌入汇编代码对寄存器的修改情况,否则有可能会造成大麻烦。例如:gcc对c代码进行处理,将某些变量值保存在寄存器中,如果嵌入汇编修改了该寄存器的值,又没有通知gcc的话,那么,gcc会以为寄存器中仍然保存了之前的变量值,因此不会重新加载该变量到寄存器,而是直接使用这个被嵌入式汇编修改的寄存器,这时候,我们唯一能做的就是静静的等待程序的崩溃。还好,在output operand list 和 input operand list中涉及的寄存器都不需要体现在clobber list中(gcc分配了这些寄存器,当然知道嵌入汇编代码会修改其内容),因此,大部分的嵌入式汇编的clobber list都是空的,或者只有一个cc,通知gcc,嵌入式汇编代码更新了condition code register。

    有了基本的了解后,现在开始解读上面的代码:

  • 将变量i存放到寄存器w0中
  • 将atomic_t类型的指针v存放到寄存器x1中
  • 指令部使用原子指令stadd把变量i的值加到v->counter中,w表示位宽是32bit, x表示64bit
  • 输出部[i]表示汇编符号为i的变量,+表示可读可写,r表示变量放入寄存器,Q表示需要通过指针间接寻址
  • 输入部, r表示输入量在寄存器x1中
  • 损坏部,通知GCC有资源更新

原子指令

上节中我们发现原子add操作是通过原子指令stadd实现的,在不同的架构上实现的方式可能不一样。

Bus Lock(锁总线)

CPU执行原子指令时,给总线上锁,这样在释放前,可以防止其它CPU的内存操作。

Cache Lock

除了和IO紧密相关的(如MMIO),大部分的内存都是可以被cache的,由前面介绍的cache一致性原理,我们知道由cacheline处于Exclusive或Modified时,该变量只有当前CPU缓存了数据,因此当进行原子操作时,发出Read Invalidate消息,使其它CPU上的缓存无效,cacheline变成Exclusive状态然后将该cacheline上锁,接着就可以取数据,修改并写入cacheline,如果这时有其它CPU也进行原子操作,发出read invalidate消息,但由于当前CPU的cacheline是locked状态,因此暂时不会回复消息,这样其它CPU就一直在等待,直到当前CPU完成,使cacheline变为unlocked状态。

LL/SC

在ARMv8.1之前,为实现RMW的原子操作的方法主要是LL/SC(Load-link/Store-condition).ARMv7中实现的指令是LDREX/STREX,原理如下:

假设CPU0进行load操作,标记变量V所在的内存地址为exclusive, 在CPU0进行store前,这时CPU1也对变量V进行了load操作,这时exclusive标记属于CPU1而不再属于CPU0,在CPU0进行store时会测试该地址的exclusive标记是不是自己的,如果不是,store失败。CPU1进行store, 因为exclusive标记是自己的,所以store成功,同时exclusive失效,这时CPU0会再次尝试一试LL/SC操作,直天成功为止。

如果CPU之间竞争比较激烈,可能导致重试的次数比较多,因此从2014年ARMv8.1开始,ARM推出了原子操作的LSE(Large System Extention)指令集扩展,新增的指令包括CAS, SWP和LD, ST等,其中可以是ADD, CLR, EOR, SET等,如例子中的stadd。

原文地址:https://www.cnblogs.com/miaolong/p/12587812.html

时间: 2024-10-05 03:10:12

原子操作和原子指令的相关文章

深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则[转]

在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到锁机制,因此此小节中会适当引入锁的概念. 在Java Concurrency in Practice中是这样定义线程安全的: 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安全的. 显然只有资源竞争时才会导致线程不安全,因此无状态对象永远是线程安全的. 原子操作的描述是: 多个线程执行一个操作时,其

深入浅出 Java Concurrency (4): 原子操作 part 3 指令重排序与happens-before法则

转: http://www.blogjava.net/xylz/archive/2010/07/03/325168.html 在这个小结里面重点讨论原子操作的原理和设计思想. 由于在下一个章节中会谈到锁机制,因此此小节中会适当引入锁的概念. 在Java Concurrency in Practice中是这样定义线程安全的: 当多个线程访问一个类时,如果不用考虑这些线程在运行时环境下的调度和交替运行,并且不需要额外的同步及在调用方代码不必做其他的协调,这个类的行为仍然是正确的,那么这个类就是线程安

原子操作&amp;优化和内存屏障

原子操作 假定运行在两个CPU上的两个内核控制路径试图执行非原子操作同时"读-修改-写"同一存储器单元.首先,两个CPU都试图读同一单元,但是存储器仲裁器插手,只允许其中的一个访问而让另一个延迟.然而,当第一个读操作已经完成后,延迟的CPU从那个存储器单元正好读到同一个(旧)值.然后,两个CPU都试图向那个存储器单元写一新值,总线存储器访问再一次被存储器仲裁器串行化,最终,两个写操作都成功.但是,全局的结果是不对的,因为两个CPU写入同一(新)值.因此,两个交错的"读-修改-

java线程:Atomic(原子的)

一.何谓Atomic? Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位.计算机中的Atomic是指不能分割成若干部分的意思.如果一段代码被认为是Atomic,则表示这段代码在执行过程中,是不能被中断的.通常来说,原子指令由硬件提供,供软件来实现原子方法(某个线程进入该方法后,就不会被中断,直到其执行完成) 在x86 平台上,CPU提供了在指令执行期间对总线加锁的手段.CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过

大话Linux内核中锁机制之原子操作、自旋锁

转至:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 很多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实是由于操作系统中存在多进程对共享资源的并发访问,从而引起了进程间的竞态.这其中包括了我们所熟知的SMP系统,多核间的相互竞争资源,单CPU之间的相互竞争,中断和进程间的相互抢占等诸多问题. 通常情况下,如图1所示,对于一段程序,我们的理想是总是美好的,希望它能够这样执行:进程1先对临界区完成操作,

linux系统原子操作

一.概念 原子操作提供了指令原子执行,中间没有中断.就像原子被认为是不可分割颗粒一样,原子操作(atomic operation)是不可分割的操作.      c语言中一个变量的自加1操作,看起来很简单,好像只需要一条指令而不被打断.但这个操作实现起来,CPU的执行是有一个过程的,分为读取到寄存器,寄存器数学运算,回写到内存.这个实际情况,会给我们程序编写时带来隐患,举例来说明. Thread 1                  Thread 2      ------------------

原子操作类(一)原子操作类详细介绍

引言 ??Java从JDK1.5开始提供了java.util.concurrent.atomic包,方便程序员在多线程环境下,无锁的进行原子操作.原子变量的底层使用了处理器提供的原子指令,但是不同的CPU架构可能提供的原子指令不一样,也有可能需要某种形式的内部锁,所以该方法不能绝对保证线程不被阻塞. ??因为变量的类型有很多种,所以在Atomic包里一共提供了13个类,可分为4种类型的原子更新方式,分别是 原子更新基本类型 原子更新数组 原子更新引用 原子更新属性(字段) Atomic包里的类基

第31课 std::atomic原子变量

一. std::atomic_flag和std::atomic (一)std::atomic_flag 1. std::atomic_flag是一个bool类型的原子变量,它有两个状态set和clear,对应着flag为true和false. 2. std::atomic_flag使用前必须被ATOMIC_FLAG_INIT初始化,此时的flag为clear状态,相当于静态初始化. 3. 三个原子化操作 (1)test_and_set():检查当前flag是否被设置.若己设置直接返回true,若

Linux 同步方法剖析--内核原子,自旋锁和相互排斥锁

在学习 Linux® 的过程中,您或许接触过并发(concurrency).临界段(critical section)和锁定,可是怎样在内核中使用这些概念呢?本文讨论了 2.6 版内核中可用的锁定机制,包含原子运算符(atomic operator).自旋锁(spinlock).读/写锁(reader/writer lock)和内核信号量(kernel semaphore). 本文还探讨了每种机制最适合应用到哪些地方.以构建安全高效的内核代码. 本文讨论了 Linux 内核中可用的大量同步或锁定