1、概念
1.1 定义
原子操作是指在执行过程中不会被中断的操作,要么全部执行成功,要么全部不执行。原子操作是不可分割的,即使在多线程或并发环境下,也不会被其他操作中断。原子操作通常用于确保对共享资源的访问是线程安全的,避免竞态条件和数据不一致性。
1.2 场景
为什么要使用原子操作:
- 确保数据的一致性:在多线程或并发环境中,多个线程同时访问共享资源可能导致数据不一致的问题,使用原子操作可以避免这种情况发生。
- 避免竞态条件:原子操作可以保证对共享资源的操作是原子的,避免出现竞态条件,确保程序的正确性和稳定性。
- 提高性能:原子操作通常是底层硬件支持的操作,执行效率高,可以提高程序的性能。
1.3. 原理
原子操作的实现通常依赖于硬件的支持或者操作系统提供的原子操作指令。在现代处理器中,通常会提供一些原子操作指令,如 Compare-and-Swap (CAS) 指令,用于实现原子操作。CAS 操作是一个原子指令,它会比较内存中的值和一个期望的值,如果相等,则将新值写入内存,否则不做任何操作。
2、分析
2.1 常见接口介绍
内核中使用到的一些常见的原子操作接口:
函数名 | 函数原型 | 说明 |
---|---|---|
atomic_set | void atomic_set(atomic_t *v, int i) | 设置原子变量的值 |
atomic_read | int atomic_read(const atomic_t *v) | 读取原子变量的值 |
atomic_add | void atomic_add(int i, atomic_t *v) | 原子地将一个值加到原子变量上 |
atomic_sub | void atomic_sub(int i, atomic_t *v) | 原子地将一个值从原子变量上减去 |
atomic_inc | void atomic_inc(atomic_t *v) | 原子地增加原子变量的值 |
atomic_dec | void atomic_dec(atomic_t *v) | 原子地减少原子变量的值 |
atomic_inc_and_test | int atomic_inc_and_test(atomic_t *v) | 原子地增加原子变量的值,并检查是否为零 |
atomic_dec_and_test | int atomic_dec_and_test(atomic_t *v) | 原子地减少原子变量的值,并检查是否为零 |
atomic_add_return | int atomic_add_return(int i, atomic_t *v) | 原子地将一个值加到原子变量上,并返回加法后的值 |
atomic_sub_return | int atomic_sub_return(int i, atomic_t *v) | 原子地将一个值从原子变量上减去,并返回减法后的值 |
atomic_inc_return | int atomic_inc_return(atomic_t *v) | 原子地增加原子变量的值,并返回增加后的值 |
atomic_dec_return | int atomic_dec_return(atomic_t *v) | 原子地减少原子变量的值,并返回减少后的值 |
atomic_cmpxchg | int atomic_cmpxchg(atomic_t *v, int old, int new) | 比较并交换操作,原子地比较原子变量的值并在匹配时替换为新值 |
2.2 分析
我们以atomic_add
为例分析内核中原子操作的实现,选用4.9版本的内核源码,在文件arch/arm/include/asm/atomic.h中有以下定义:
#define ATOMIC_OPS(op, c_op, asm_op) \
ATOMIC_OP(op, c_op, asm_op) \
ATOMIC_OP_RETURN(op, c_op, asm_op) \
ATOMIC_FETCH_OP(op, c_op, asm_op)
ATOMIC_OPS(add, +=, add)
ATOMIC_OPS(sub, -=, sub)
将ATOMIC_OPS(add, +=, add)使用上面的宏定义进行解析可以获得:
ATOMIC_OP(add, +=, add)
ATOMIC_OP_RETURN(add, +=, add)
ATOMIC_FETCH_OP(add, +=, add)
以其中的第一个宏ATOMIC_OP为例,在内核中有如下代码:
// ARMV6架构以上
#if __LINUX_ARM_ARCH__ >= 6
/*
* ARMv6 UP and SMP safe atomic ops. We use load exclusive and
* store exclusive to ensure that these are atomic. We may loop
* to ensure that the update happens.
*/
#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"); \
}
……
……
……
#else /* ARM_ARCH_6 */
#define ATOMIC_OP(op, c_op, asm_op) \
static inline void atomic_##op(int i, atomic_t *v) \
{ \
unsigned long flags; \
\
raw_local_irq_save(flags); \
v->counter c_op i; \
raw_local_irq_restore(flags); \
} \
……
……
……
#endif /* __LINUX_ARM_ARCH__ */
其中上半部分是对于ArmV6架构以上,下半部分是针对ArmV6架构,先看下半部分,将宏定义中的参数带入后得到以下代码,可以看到对于ArmV6架构,在操作原子变量前进行关闭中断处理,操作完成后进行中断恢复处理,因为ArmV6是单核CPU,不支持SMP,因此不需要考虑会有其他线程抢占,只需要保证不被中断影响即可。
static inline void atomic_add(int i, atomic_t *v) \
{ \
unsigned long flags; \
\
raw_local_irq_save(flags); \
v->counter += i; \
raw_local_irq_restore(flags); \
}
对于ArmV6以上的版本,带入参数后得到的代码如下:
static inline void atomic_add(int i, atomic_t *v) \
{ \
unsigned long tmp; \
int result; \
\
prefetchw(&v->counter); \
__asm__ __volatile__("@ atomic_" add "\n" \
"1: ldrex %0, [%3]\n" \
" add %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"); \
}
代码中使用了内嵌汇编的编程,在执行读取-修改-存入三步的时候,读取和存入使用了ldrex
和strex
,这两条命令则是实现原子操作的关键:
- 当一个处理器执行
ldrex
指令时,ldrex
指令会将内存位置的值加载到寄存器中,并且标记该内存位置为“独占”状态。strex
用于尝试将一个值写回到内存中,并且会检查内存位置是否仍处于“独占”状态。如果内存位置仍处于“独占”状态,strex
操作会成功,否则会失败。 - 在一个处理器执行
ldrex
指令获取独占访问权后,另一个处理器也尝试获取独占访问权。这种情况下,如果两个处理器都执行ldrex
指令后,其中一个处理器执行了strex
指令成功,而另一个处理器在执行strex
指令时会发现独占访问已经被释放,此时第二个处理器的strex
操作会失败。这种情况下,ARM处理器提供了一种机制来解决这种并发情况,即在strex
指令执行失败时,第二个处理器可以重新执行ldrex
和strex
指令,或者采取其他处理方式。这样可以确保在并发情况下,内存的操作仍然是原子的。
Exclusive Access(独占访问)是由ARM处理器硬件实现的机制。ARM处理器提供了一些特定的指令(如ldrex
和strex
)来支持独占访问的操作。当处理器执行这些指令时,硬件会负责处理内存访问的原子性和独占状态的管理。
在上述代码中teq %1, #0和bne 1b就是对strex的返回值进行处理,如果返回失败的话就重新跳转到"1: ldrex %0, [%3]\n"处进行执行:
3、总结
本文结合内核源码简单讲解了原子操作的实现过程。