原子操作主要使用汇编语言实现,因为使用到总线锁和寄存器等操作。
1、锁
在x86的原子操作实现代码中,定义了LOCK宏。
因为锁是锁内存总线,单CPU无需防止其它CPU的操作。
如果是SMP,LOCK宏被扩展为lock指令;否则被定义为空 。
#ifdef CONFIG_SMP
#define LOCK "lock ; "
#else
#define LOCK ""
#endif
原子性不可能由软件单独保证--必须需要硬件的支持,因此是和架构相关的。
在x86 平台上,CPU提供了在指令执行期间对
总线
加锁的手段。CPU芯片上有一条引线#HLOCK pin,如果汇编语言的程序中在一条指令前面加上前缀"LOCK",经过汇编以后的机器代码就使CPU在执行这条指令的时候把#HLOCK pin的电位拉低,持续到这条指令结束时放开,从而把
总线
锁住,这样同一总线上别的CPU就暂时不能通过总线访问内存了,保证了这条指令在多处理器环境中的原子性。
2、类型
原子类型:atomic_t保存一个int值。在x86的某些处理器上,由于工作方式的原因,原子类型能够保证的可用范围只有24位。
typedef struct { volatile int counter; } atomic_t;
类型描述符volatile:要求编译器不要对其描述的对象作优化处理,对它的读写都需要从内存中访问。
3、算数操作
(1)初始化
* #define ATOMIC_INIT(i) { (i) }
用于在定义原子变量时,初始化为指定的值。是int类型,只是禁止寄存器对其暂存。
使用如下:
static atomic_t count = ATOMIC_INIT⑴;
(2)使用格式
原子操作的实现中,使用了带有C/C++表达式的内联汇编代码(这里的是AT&T的内联汇编,参考《AT&T ASM Syntax》,intel的有其他标准)内嵌汇编函数声明:
__asm__ __volatile__("Instruction List" : Output : Input : Clobber/Modify);
__asm__:
__asm__是GCC 关键字asm 的宏定义
#define __asm__ asm
__asm__或asm 用来声明一个内联汇编表达式
__volatile__:
指示编译器原封不动保留表达式中的汇编指令系列,不要考虑优化处理。
Instruction List :
汇编指令序列。
涉及其他约束:
⒈ 等号约束(=):只能用于输出操作表达式约束,说明括号内的左值表达式v->counter是write-only的。⒉ 内存约束(m):表示使用不需要借助寄存器,直接使用内存方式进行输入或输出。
⒊ 立即数约束(i):表示输入表达式是一个立即数(整数),不需要借助任何寄存器。
⒋ 寄存器约束(r):表示使用一个通用寄存器,由GCC在%eax/%ax/%al、%ebx/%bx/%bl、%ecx/%cx/%cl和%edx/%dx/%dl中选取一个合适的。
Clobber/Modify域:
Clobber/Modify域声明被修改的寄存器或者内存,以此来通知GCC当前内联汇编语句可能造成的修改。
一般是一个寄存器出现在"Instruction List",但却不是由Input/Output操作表达式所指定的(包括使用"r"约束时由GCC 为其选择的),但是此寄存器被"Instruction List"中的指令修改。
例如:
__asm__ ("mov R0, #0x34" : : : "R0");
寄存器R0出现在"Instruction List中",并且被mov指令修改,但却未被任何Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定"R0",以通知GCC,让GCC针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。
如果Clobber/Modify域是"memory",那么GCC会保证当某个内存的内容被装入了寄存器,内联汇编执行后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取(避免寄存器中的拷贝已经很可能和内存处的内容不一致)。
例如:
int main(int __argc, char* __argv[])
{
int* __p = (int*)__argc;
(*__p) = 1000;
__asm__("":::"memory");
if((*__p) == 1000)
return 3;
return (*__p);
}
如果没有那条内联汇编语句,GCC在优化时会认为语句(if((*__p) == 1000) )是没有作用的,而直接只生成return 5的汇编代码,也不会生成return (*__p)的相关代码。
而加入内联汇编语句后则if语句和两个return 语句都会生成。
在linux内核中内存屏障的实现也是一样的(include/asm/system.h)
# define barrier() _asm__volatile_("": : :"memory")
主要是不让GCC优化内存变量。
使用占位符:
可以在内联汇编中使用它引用输入和输出。这样可以在对于编译器方便的任何寄存器或者内存位置中声明输入和输出。
占位符是前面加%的数字。按照内联汇编中列出的每个输入值和输出值在列表中的顺序,每个值被赋予一个从0开始的数字,然后可以在汇编代码中使用占位符表示值。如:
asm ("assembly code"
: "=r"(result)
: "r"(data1), "r"(data2));
将生成如下的占位符:
%0: 表示包含变量值result的寄存器
%1: 表示包含变量值data1的寄存器
%2: 表示包含变量值data2的寄存器
(2)加
static __inline__ void atomic_add(int i,atomic_t *v)将v指向的原子变量加上i。返回void类型。
实现代码
{
__asm__ __volatile__(
LOCK "addl %1,%0"
:"=m" (v->counter)
:"ir" (i),"m" (v->counter));
}
(3)减
static __inline__ int atomic_sub_and_test(int i,atomic_t *v)从v 指向的原子变量减去i,并测试是否为0。若为0,返回真,否则返回假。
由于x86的subl指令会在结果为0时设置CPU的zero标志位,而且这个标志位是CPU私有的,不会被其它CPU影响。
因此,可以执行一次加锁的减操作,再根据CPU的zero标志位来设置本地变量c,并相应返回。
实现代码
{
unsigned char c;
__asm__ __volatile__(
LOCK "subl %2,%0; sete %1"
:"=m" (v->counter),"=qm" (c)
:"ir" (i),"m" (v->counter) : "memory");
return c;
}
(4)其他操作
#define atomic_read(v) ((v)->counter)读取v指向的原子变量的值。由于该操作本身就是原子的,只需要一次内存访问就能完成,因此定义为一个宏,并用C代码实现。
#define atomic_set(v,i) (((v)->counter) = (i))
设置v指向的原子变量的值为i。由于该操作本身就是原子的,只需要一次内存访问就能完成,因此定义为一个宏,并用C代码实现。
static __inline__ void atomic_sub(int i,atomic_t *v)
从v指向的原子变量减去i。
static __inline__ void atomic_inc(atomic_t *v)
递增v指向的原子变量。
static __inline__ void atomic_dec(atomic_t *v)
递减v指向的原子变量。
static __inline__ int atomic_dec_and_test(atomic_t *v)
递减v指向的原子变量,并测试是否为0。若为0,返回真,否则返回假。
static __inline__ int atomic_inc_and_test(atomic_t *v)
递增v指向的原子变量,并测试是否为0。若为0,返回真,否则返回假。
static __inline__ int atomic_add_negative(int i,atomic_t *v)
将v指向的原子变量加上i,并测试结果是否为负。若为负,返回真,否则返回假。这个操作可以用于实现semaphore。