原子操作
查看以下C语言代码:
1 2 3 4 5 6 7 8 9
| #include <stdio.h>
int val = 169;
int main() { val = 275; printf("val %d \n", val); return 0; }
|
通过GCC生成的arm64汇编如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| .arch armv8-a .file "main.c" .text .global val .data .align 2 .type val, %object .size val, 4 val: .word 169 .section .rodata .align 3 .LC0: .string "val %d \n" .text .align 2 .global main .type main, %function main: stp x29, x30, [sp, -16]! add x29, sp, 0 adrp x0, val add x0, x0, :lo12:val mov w1, 275 str w1, [x0] adrp x0, val add x0, x0, :lo12:val ldr w1, [x0] adrp x0, .LC0 add x0, x0, :lo12:.LC0 bl printf mov w0, 0 ldp x29, x30, [sp], 16 ret .size main, .-main .ident "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note.GNU-stack,"",@progbits
|
在现代计算机体系结构中,对变量的赋值过程是分为三步的,以上文的val为例:
1 2 3
| adrp x0, val mov w1, 275 str w1, [x0]
|
为了加速访问,程序在运行过程中访问的变量,通常会放置在寄存器中。如果在多线程环境中,某个线程中该三步执行的间隙,该变量被用于其他线程,这就带来了脏值问题。
为了解决这一问题,可以通过加锁解决。加锁是一种比较廉价且低效的方法,在体系结构中,提供了三个操作作为一个整体执行的原子操作。
ARM
SWP指令
在比较早的arm指令集中,提供SWP和SWPB指令,用于进行原子操作。用法如下:
SWP{B}{cond} Rt, Rt2, [Rn]
关于该指令的更多用法,参考这里
由于该指令的效率比较低,会降低整体系统的性能,在ARMv6及之后的指令集中已经不再建议使用。
LDREX/STREX指令
在ARMv6及之后的指令集中,引入了两条指令 – LDREX/STREX 来提供原子操作。
这两条操作,都会引起exclusive monitor(s)状态的变化,下文将举一个简单的例子。
查看如下用法:
LDREX R1, [R0]
将R0地址的值加载到R1,并对相应的物理地址设置相应的标志,该标志由exclusive monitors进行管理。
STREX R2, R1, [R0]
根据物理地址的状态,决定是否要进行值的写会操作。R0是将要写入的物理地址,R1中存储需要写会的值,R2则存储了这一操作的运行结果,成功返回0,失败则返回1。
关于exclusive monitor,在arm的手册中,被描述为一个简单的状态机。LDREX被称之为Load-Exclusive指令,STREX被称之为Store-Exclusive指令。对于一个Store-Exclusive指令来说,访问中涉及到的物理内存都被标记为exclusive才能够写回成功。
更多关于exclusive monitor的描述,可以查看参考一。
在Load/Store-Exclusive的基础上,原子操作可以用如下代码实现(实现来自Linux):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #define ATOMIC_OP(op, c_op, asm_op) \ static inline void atomic_##op(int i, atomic_t *v) \ { \ unsigned long tmp; \ int result; \ \ prefetchw(&v->counter); \ __asm__ __volatile__("@ atomic_" #op "\n" \ "1: ldrex %0, [%3]\n" \ " " #asm_op " %0, %0, %4\n" \ " strex %1, %0, [%3]\n" \ " teq %1, #0\n" \ " bne 1b" \ : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) \ : "r" (&v->counter), "Ir" (i) \ : "cc"); \ }
ATOMIC_OP(atomic_add)
|
更多关于原子操作,可以查看Linux arch源码里的atomic.h
X86
X86的指令支持”lock”前缀,对于支持该前缀的cpu指令,使用该前缀后,能够保证是原子执行的。
参考
- DHT0008A_arm_synchronization_primitives.pdf
- LDREX/STREX(arm)
- Atomic operations in ARM(StackOverflow)