一文了解原子操作

原子操作(X86架构)

楔子

首先什么是原子,意味着不可再分。相应地,反映在计算机程序里,那么就会成为一条指令,不存在中间指令,执行过程不会被打断。这样程序在执行时,就能够保证独占访问,避免其它线程访问它。接下来接口的讲解,是以C11标准有#inlcude <stdatomic>为例。

已有接口

这里的每个接口都有另一个声明explict防止隐式转换的函数。能够实现的11个操作包括取数、存数、交换、比较交换(CAS)、加法、减法、按位或、按位异或、设置标志位(用来获取一个自旋锁)、清除标志位。

atomic_store(object, desired); // 将desired这个值读取到原子object中
atomic_load(object); // 从原子object中取值,可以用来赋给一个变量
atomic_exchange(object, desired); // 将desired存到object中,并将旧值返回
atomic_compare_exchange_strong(object, expected, desired); // 如果 object 当前的值与 expected 相同,则将 desired 存储到 object 中。如果交换成功,返回 true;否则,将 object 当前的值存储到 expected 指向的内存中,并返回 false。
atomic_compare_exchange_weak(object, expected, desired)//  这个函数发生时,若交换失败,则可能会将object当前的值存储到expected指向的内存中。
atomic_fetch_add(object, operand); // 将object加一个operand再重新赋给object。
atomic_fetch_sub(object, operand); // 将object减小一个operand再重新赋给object。
atomic_fetch_or(object, operand); // 将object按位(bitwire)或操作一个operand再重新赋给object。
atomic_fetch_xor(); // 将object按位(bitwire)异或操作一个operand再重新赋给object。
atomic_flag_test_and_set(); // 将object指向的原子标志设位非零值,并返回标志原来的值,用来获取一个自旋锁
atomic_flag_clear(); // 将object指向的原子标志,清零

GCC嵌入式汇编

GCC嵌入式汇编格式:asm指明是汇编方式,volatile指明不要进行编译器优化,假如这段部分修改的变量值,并不在后续的程序中使用,那么编译器就会移除掉这段代码。所以我们需要使用volatile告诉编译器不要进行优化。

__asm__ __volatile__( // volatile禁止编译器优化
    指令部 
   : output operand 
   : input operand
   : clobber list
);
  1. 指令部中数字前加%表示寄存器的样板操作数。@表示注释。指令中可能会出现指令前缀比如lock:这是避免多处理器同时修改内存中的数据。而且lock还能够保证在完成原子操作后,同时刷新多个处理器的缓存,保证一致性。

  2. 输出部,输出约束用=开头,表示只写操作数,一般指明输出对象,接着是一个字母表示对操作类型的说明。+表示可读可写,r表示使用通用寄存器,&表示只用于输出。

  3. clobber resgister,这部分主要是要告诉编译器汇编代码所做的修改。一般以"memory"结束,目的是告诉GCC编译器汇编指令改变了内存中的值,强制要求编译器在执行汇编代码前存储所有缓存的值,然后执行结束后重新加载该值。"cc"表示,condition register状态寄存器。

  4. 操作类型说明

    约束 用途 范围
    a 简单寄存器 r16 to r23
    b 基指针寄存器对 y,z
    d 上寄存器 r16 to r31
    e 指针寄存器对 x, y, z
    G 浮点常量 0.0
    I 6位正整数常量 0 to 63
    J 6位负整数常量 -63 to 0
    K 整数常量 2
    L 整数常量 0
    l 低位寄存器 r0 to r15
    m 内存地址
    M 8位整数常量 0 to 255
    n 16位整数常量
    N 整数常量 -1
    O 整数常量 8, 16, 24
    P 整数常量 1
    q 堆栈指针寄存器 SPH:SPL
    r 任意寄存器 r0 to r31
    t 临时寄存器 r0
    V 32位整数常量
    W 特殊上寄存器对 r24, r26, r28, r30
    x 指针寄存器对 X x (r27:r26)
    y 指针寄存器对 Y y (r29:r28)
    z 指针寄存器对 Z z (r31:r30)

实例

  1. in指令从端口获取数据

    __asm__ __volatile__(
    "in %0, %1"
    :"=&r"(result) // 输出操作数
    :"I" (PORTD) // 输入操作数 I表示正整数常量
    :
    );
    
  2. inc指令,执行增加add操作

    int inc(int* value, int add){
        int old;
        // volatile指示编译器不要对变量进行优化
        __asm__ volatile(
            "lock; xaddl %2, %1"
            : "=a" (old)  // 这里有两个寄存器
            : "m" (*value), "a" (add)
            : "cc", "memory"
        );
    }
    
  3. cas操作

    void cas(int *ptr, int expected, int desired) {
        __asm__ volatile (
            "lock; cmpxchg %2, %0\n" // 使用LOCK前缀的cmpxchg指令
            : "+m" (*ptr), "+a" (expected) // 操作数约束
            : "r" (desired) // 操作数约束
            : "memory" // 标记内存操作
        );
    }
    

注意

  1. ubuntu18.04版本内核已经没有/usr/include/asm/atomic.h这个库了,建议直接使用C语言库#Include<stdatomic>头文件调用原子操作。在较新版本的Linux内核中,<asm/atomic.h> 已经被移除或不再位于GCC默认的头文件搜索路径下。这是因为在Linux 2.6.18之后,系统不再使用 atomic.hbitops.h,而是采用GCC提供的内建原子操作函数 __sync_* 来实现原子操作。其实内部还是有atomic.h这个文件,内核源码的arch目录下。
参考
  1. 《奔跑吧Linux内核》--张天飞
  2. 《GCC-AVR Inline Assembler Cookbook》--Harald Kipp
posted @   LemHou  阅读(115)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示