原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
多核系统中,单个的机器指令就不是原子操作,因为多核系统里是多指令流并行运行的,一个核在执行一个指令时,其他核同时执行的指令有可能操作同一块内存区域,从而出现数据竞争现象。多核系统中的原子操作通常使用内存栅障(memory barrier)来实现,即一个CPU核在执行原子操作时,其他CPU核必须停止对内存操作或者不对指定的内存进行操作,这样才能避免数据竞争问题。
在开发中,我们的经常会遇到这样的程序逻辑操作:
1.读一个位于内存中的全局变量(值读取到寄存器中)。
2.修改该变量的值(也就是修改寄存器中的值)。
3.将修改的值回写到全部变量(将寄存器中的数值写回内存中的变量值)。
如果这个操作序列是串行化的操作(在一个thread中串行执行),那么一切没问题,但在线程与中断或多个CPU内核控制路径同时并行执行上面操作序列,有可能发生下面的场景:
线程调用的控制路径 | 中断handler控制路径 |
读操作 | |
读操作 | |
修改 | |
写操作 | |
修改 | |
写操作 |
线程调用的控制路径上,完成读操作后,硬件触发中断,开始执行中断handler。这种场景下,中断handler控制路径的写回的操作被系统调用控制路径上的写回覆盖了,结果也是错误的。
CPU1上的操作 | CPU2上的操作 |
读操作 | |
读操作 | |
修改 | 修改 |
写操作 | |
写操作 |
多个CPUs和memory chip是通过总线互联的,在任意时刻,只能有一个总线设备访问该memory chip。来自两个CPU上的读memory操作被串行化执行,分别获得了同样的旧值。完成修改后,两个CPU都想进行写操作,把修改的值写回到memory。但是硬件限制使得CPU的写回必须是串行化的,因此CPU1首先获得了访问权,进行写回动作,随后,CPU2完成写回动作。在这种情况下,CPU1的对memory的修改被CPU2的操作覆盖了,因此执行结果是错误的。
对于那些有多个控制路径进行read-modify-write的变量,我们使用ldrex和strex这两条汇编指令来实现原子操作,ldr和str这两条指令大家应该熟悉,后缀的ex表示Exclusive,是ARMv7提供的为了实现同步的汇编指令。下面看看这两条指令的使用方式。
LDREX <Rt>, [<Rn>]
<Rn>是base register,保存memory的address,LDREX指令从base register中获取memory address,并且将memory的内容加载到<Rt>(destination register)中。这些操作和ldr的操作是一样的,那么如何体现exclusive呢?其实,在执行这条指令的时候,会有监视器local monitor观察状态
STREX <Rd>, <Rt>, [<Rn>]
STREX跟LDREX指令类似,<Rn>是base register,保存memory的address,STREX指令从base register中获取memory address,并且将<Rt> (source register)中的内容加载到该memory中。这里的<Rd>保存了memeory 更新成功或者失败的结果,0表示memory更新成功,1表示失败。STREX指令是否能成功执行是和local monitor状态相关的。下面的表格可以描述这种情况。
thread 1 | thread 2 | local monitor的状态 |
Open Access state | ||
LDREX | Exclusive Access state | |
LDREX | Exclusive Access state | |
Modify | Exclusive Access state | |
STREX | Open Access state | |
Modify | Open Access state | |
STREX | 在Open Access state的状态下,执行STREX指令会导致该指令执行失败 | |
保持Open Access state,直到下一个LDREX指令 |
开始的时候,local monitor处于Open Access state的状态,thread 1执行LDREX 命令后,local monitor的状态迁移到Exclusive Access state(标记本地CPU对xxx地址进行了LDREX的操作),这时候,中断发生了,在中断handler中,又一次执行了LDREX ,这时候,local monitor的状态保持不变,直到STREX指令成功执行,local monitor的状态迁移到Open Access state的状态(清除xxx地址上的LDREX的标记)。返回thread 1的时候,在Open Access state的状态下,执行STREX指令会导致该指令执行失败(没有LDREX的标记,何来STREX),说明有其他的内核控制路径做了对内存的操作。
大概的原理已经描述完毕,下面回到具体实现。我们定义一个特殊的类型atomic_t,具体定义如下:
struct os_atomic
{
volatile os_int32_t counter;
};
typedef struct os_atomic os_atomic_t;
os_atomic_t实际上就是一个os_int32_t 类型的counter,不过定义这样特殊的类型os_atomic_t是有其思考的:内核定义了若干接口API函数,这些函数只会接收os_atomic_t类型的参数。可以确保os_atomic_xxx的接口函数只会操作os_atomic_t类型的数据。同样的,如果你定义了os_atomic_t类型的变量(你期望用os_atomic_xxx的接口API函数操作它),这些变量也不会被那些普通的、非原子变量操作的API函数接受,下面看看原子操作函数的一些实现:
/* 给一个原子变量mem增加value */
__asm void os_atomic_add(os_atomic_t *mem, os_int32_t value)
{
Loop_add
LDREX R2, [R0]
ADD R2, R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_add_exit
B Loop_add
Loop_add_exit
BX LR
}
/* 给一个原子变量mem增加value,将最新的mem值返回 */
__asm os_int32_t os_atomic_add_return(os_atomic_t *mem, os_int32_t value)
{
Loop_add_ret
LDREX R2, [R0]
ADD R2, R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_add_ret_exit
B Loop_add_ret
Loop_add_ret_exit
MOV R0, R2
BX LR
}
/* 给一个原子变量mem减去value */
__asm void os_atomic_sub(os_atomic_t *mem, os_int32_t value)
{
Loop_sub
LDREX R2, [R0]
SUB R2, R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_sub_exit
B Loop_sub
Loop_sub_exit
BX LR
}
/* 给一个原子变量mem减去value,将最新减去的值返回 */
__asm os_int32_t os_atomic_sub_return(os_atomic_t *mem, os_int32_t value)
{
Loop_sub_ret
LDREX R2, [R0]
SUB R2, R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_sub_ret_exit
B Loop_sub_ret
Loop_sub_ret_exit
MOV R0, R2
BX LR
}
/* 原子变量mem的值加一 */
__asm void os_atomic_inc(os_atomic_t *mem)
{
Loop_inc
LDREX R2, [R0]
ADD R2, R2, #1
STREX R3, R2, [R0]
CBZ R3, Loop_inc_exit
B Loop_inc
Loop_inc_exit
BX LR
}
/* 原子变量mem的值加一并将最新值返回 */
__asm os_int32_t os_atomic_inc_return(os_atomic_t *mem)
{
Loop_inc_ret
LDREX R2, [R0]
ADD R2, R2, #1
STREX R3, R2, [R0]
CBZ R3, Loop_inc_ret_exit
B Loop_inc_ret
Loop_inc_ret_exit
MOV R0, R2
BX LR
}
/* 原子变量mem的值减一 */
__asm void os_atomic_dec(os_atomic_t *mem)
{
Loop_dec
LDREX R2, [R0]
SUB R2, R2, #1
STREX R3, R2, [R0]
CBZ R3, Loop_dec_exit
B Loop_dec
Loop_dec_exit
BX LR
}
/* 原子变量mem的值减一并将最新的值返回 */
__asm os_int32_t os_atomic_dec_return(os_atomic_t *mem)
{
Loop_dec_ret
LDREX R2, [R0]
SUB R2, R2, #1
STREX R3, R2, [R0]
CBZ R3, Loop_dec_ret_exit
B Loop_dec_ret
Loop_dec_ret_exit
MOV R0, R2
BX LR
}
/* 将value的值写入到原子变量mem中 */
__asm os_int32_t os_atomic_xchg(os_atomic_t* mem, os_int32_t value)
{
Loop_xchg
LDREX R2, [R0]
STREX R3, R1, [R0]
CBZ R3, Loop_xchg_exit
B Loop_xchg
Loop_xchg_exit
MOV R0, R2
BX LR
}
/* 比较old和原子变量mem中的值,如果相等,那么就把new值赋给原子变量。返回旧的原子变量mem中的值 */
__asm os_bool_t os_atomic_cmpxchg(os_atomic_t* mem, os_int32_t old, os_int32_t new)
{
PUSH {R4}
Loop_cmpxchg
LDREX R3, [R0]
MOV R4, #0
TEQ R3, R1
BNE Loop_cmpxchg_exit
STREX R3, R2, [R0]
MOV R4, #1
CBZ R3, Loop_cmpxchg_exit
B Loop_cmpxchg
Loop_cmpxchg_exit
MOV R0, R4
POP {R4}
BX LR
}
/* 原子变量mem与value进行按位与操作 */
__asm void os_atomic_and(os_atomic_t* mem, os_int32_t value)
{
Loop_and
LDREX R2, [R0]
AND R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_and_exit
B Loop_and
Loop_and_exit
BX LR
}
/* 原子变量mem与value进行按位或操作 */
__asm void os_atomic_or(os_atomic_t* mem, os_int32_t value)
{
Loop_or
LDREX R2, [R0]
ORR R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_or_exit
B Loop_or
Loop_or_exit
BX LR
}
/* 原子变量mem与value进行异或操作 */
__asm void os_atomic_xor(os_atomic_t* mem, os_int32_t value)
{
Loop_xor
LDREX R2, [R0]
EOR R2, R1
STREX R3, R2, [R0]
CBZ R3, Loop_xor_exit
B Loop_xor
Loop_xor_exit
BX LR
}
/* 判断原子变量mem的某一位是否等于1 */
__asm os_bool_t os_atomic_test_bit(os_atomic_t* mem, os_int32_t nr)
{
PUSH {R4, R5}
MOV R4, #1
LSL R4, R1
Loop_test
LDREX R2, [R0]
AND R5, R2, R4
LSR R5, R1
STREX R3, R2, [R0]
CBZ R3, Loop_test_exit
B Loop_test
Loop_test_exit
MOV R0, R5
POP {R4, R5}
BX LR
}
/* 原子变量mem的值某一位置1 */
__asm void os_atomic_set_bit(os_atomic_t* mem, os_int32_t nr)
{
PUSH {R4}
MOV R4, #1
LSL R4, R1
Loop_set
LDREX R2, [R0]
ORR R2, R4
STREX R3, R2, [R0]
CBZ R3, Loop_set_exit
B Loop_set
Loop_set_exit
POP {R4}
BX LR
}
/* 原子变量mem的值某一位清零 */
__asm void os_atomic_clear_bit(os_atomic_t* mem, os_int32_t nr)
{
PUSH {R4}
MOV R4, #1
LSL R4, R1
Loop_clear
LDREX R2, [R0]
BIC R2, R4
STREX R3, R2, [R0]
CBZ R3, Loop_clear_exit
B Loop_clear
Loop_clear_exit
POP {R4}
BX LR
}
__asm void os_atomic_change_bit(os_atomic_t* mem, os_int32_t nr)
{
PUSH {R4}
MOV R4, #1
LSL R4, R1
Loop_change
LDREX R2, [R0]
EOR R2, R4
STREX R3, R2, [R0]
CBZ R3, Loop_change_exit
B Loop_change
Loop_change_exit
POP {R4}
BX LR
}