http://www.epubit.com.cn/book/onlinechapter/6887 https://my.oschina.net/youranhongcha/blog/492591

原子操作的底层实现

Android的原子操作函数

// 加法
int32_t android_atomic_add(int32_t value, volatile int32_t* addr);

// 自增和自减
int32_t android_atomic_inc(volatile int32_t* addr);
int32_t android_atomic_dec(volatile int32_t* addr);

// 与和或
int32_t android_atomic_and(int32_t value, volatile int32_t* addr);
int32_t android_atomic_or(int32_t value, volatile int32_t* addr);

// 比较并交换
int android_atomic_acquire_cas(int32_t oldvalue, int32_t newvalue, volatile int32_t* addr);
int android_atomic_release_cas(int32_t oldvalue, int32_t newvalue, volatile int32_t* addr);

原子操作的实现原理

Android原子操作的实现方式和CPU的架构有密切关系,现在的原子操作一般都是在CPU指令级别实现的。这种实现方式不但简单,而且效率非常高。

虽然原子操作的接口函数有很多,但是,只有两个函数通过汇编代码真正实现了原子操作,它们是函数android_atomic_add()和android_atomic_cas(),其他函数都只是在内部调用它们而已。这两个函数的原理差不多。

ARM上的代码实现:

extern ANDROID_ATOMIC_INLINE
int32_t android_atomic_add(int32_t increment, volatile int32_t *ptr)
{
    int32_t prev, tmp, status;
    android_memory_barrier();
    do {
        __asm__ __volatile__ ("ldrex %0, [%4]\n"
                              "add %1, %0, %5\n"
                              "strex %2, %1, [%4]"
                              : "=&r" (prev), "=&r" (tmp),
                                "=&r" (status), "+m" (*ptr)
                              : "r" (ptr), "Ir" (increment)
                              : "cc");
    } while (__builtin_expect(status != 0, 0));
    return prev;
}
  • android_memory_barrier内存屏障。
  • __asm__ __volatile__是内嵌汇编,伪代码的意思如下:

    do {
      ldrex  prev,[ptr]
      add  tmp,  prev,  increment
      strex  status,  tmp, [ptr]
    } while(status != 0)
    

    在add指令的前后有两条看上去比较陌生的指令:ldrex和strex,这两条是AMRV6新引入的同步指令。ldrex指令的作用是将指针ptr指向的内容放到prev变量中,同时给执行处理器做一个标记(tag),标记上指针ptr的地址,表示这个内存地址已经有一个CPU正在访问。当执行到strex指令时,它会检查是否存在ptr的地址标记,如果标记存在,strex指令会把add指令执行的结果写入指针ptr指向的地址,并且返回0,然后清除该标记。返回的结果0将保存在status变量中,这样循环结束,函数返回结果。

    如果在strex指令执行前发生了线程的上下文切换,在切换回来后,ldrx指令设置的标志将会被清除。这时再执行strex指令时,由于没有了这个标志,strex指令将不会完成对ptr指针的存储操作,而且status变量中的返回结果将是1。因此,循环将重新开始执行,直到成功为止。

独占访问指令LDREX和STREX的原理

LDREX用来读取内存中的值,并标记对该段内存的独占访问:

LDREX Rx, [Ry]

读取寄存器Ry指向的内存值,将其保存到Rx寄存器中,同时标记对Ry指向内存区域的独占访问。如果执行LDREX指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。

STREX在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:

STREX Rx, Ry, [Rz]

如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器Ry中的值更新到寄存器Rz指向的内存,并将寄存器Rx设置成0。指令执行成功后,会将独占访问标记位清除。

内存屏障和编译屏障

现代 CPU中指令的执行次序不一定按顺序执行,没有相关性的指令可以打乱次序执行,以充分利用 CPU的指令流水线,提高执行速度。同时,编译器也会对指令进行优化,例如,调整指令顺序来利用CPU的指令流水线。这些优化方式,大部分时候都工作良好,但是在一些比较复杂的情况可能会出现错误,例如,执行同步代码时就有可能因为优化导致同步原语之后的指令在同步原语前执行。

内存屏障和编译屏障就是用来告诉CPU和编译器停止优化的手段。编译屏障是指使用伪指令“memory”告诉编译器不能把“memory”执行前后的代码混淆在一起,这时“memory”起到了一种优化屏障的作用。内存屏障是在代码中使用一些特殊指令,如ARM中的dmb、dsb和isb指令,x86中的sfence、lfence和mfence指令。CPU遇到这些特殊指令后,要等待前面的指令执行完成才执行后面的指令。这些指令的作用就好像一道屏障把前后指令隔离开了,防止CPU把前后两段指令颠倒执行。

ARM平台的内存屏障指令。

  • dsb:数据同步屏障指令。它的作用是等待所有前面的指令完成后再执行后面的指令。
  • dmb:数据内存屏障指令。它的作用是等待前面访问内存的指令完成后再执行后面访问内存的指令。
  • isb:指令同步屏障。它的作用是等待流水线中所有指令执行完成后再执行后面的指令。

编译屏障:

void android_compiler_barrier() {
    asm_volatile_("" : : : "memory");
}

内存屏障:

void android_memory_barrier()
{
#if ANDROID_SMP == 0
    android_compiler_barrier();
#else
    __asm__volatile_("dmb" : : : "memory");
#endif
}
void android_memory_store_barrier()
{
#if ANDROID_SMP == 0
    android_compiler_barrier();
#else
    __asm_volatile_("dmb st" : : : "memory");
#endif
}

results matching ""

    No results matching ""