一文了解原子操作
原子操作(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
);
-
指令部中数字前加
%
表示寄存器的样板操作数。@
表示注释。指令中可能会出现指令前缀比如lock:
这是避免多处理器同时修改内存中的数据。而且lock
还能够保证在完成原子操作后,同时刷新多个处理器的缓存,保证一致性。 -
输出部,输出约束用
=
开头,表示只写操作数,一般指明输出对象,接着是一个字母表示对操作类型的说明。+
表示可读可写,r
表示使用通用寄存器,&
表示只用于输出。 -
clobber resgister,这部分主要是要告诉编译器汇编代码所做的修改。一般以
"memory"
结束,目的是告诉GCC
编译器汇编指令改变了内存中的值,强制要求编译器在执行汇编代码前存储所有缓存的值,然后执行结束后重新加载该值。"cc"
表示,condition register状态寄存器。 -
操作类型说明
约束 用途 范围 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)
实例
-
in
指令从端口获取数据__asm__ __volatile__( "in %0, %1" :"=&r"(result) // 输出操作数 :"I" (PORTD) // 输入操作数 I表示正整数常量 : );
-
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" ); }
-
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" // 标记内存操作 ); }
注意
- ubuntu18.04版本内核已经没有
/usr/include/asm/atomic.h
这个库了,建议直接使用C语言库#Include<stdatomic>
头文件调用原子操作。在较新版本的Linux内核中,<asm/atomic.h>
已经被移除或不再位于GCC默认的头文件搜索路径下。这是因为在Linux 2.6.18之后,系统不再使用atomic.h
和bitops.h
,而是采用GCC提供的内建原子操作函数__sync_*
来实现原子操作。其实内部还是有atomic.h这个文件,内核源码的arch目录下。
参考
- 《奔跑吧Linux内核》--张天飞
- 《GCC-AVR Inline Assembler Cookbook》--Harald Kipp
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)