体系结构对原子操作的实现和支持
2024-08-28 03:43:39

原子操作

查看以下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指令,使用该前缀后,能够保证是原子执行的。

参考

  1. DHT0008A_arm_synchronization_primitives.pdf
  2. LDREX/STREX(arm)
  3. Atomic operations in ARM(StackOverflow)