关注公众号不迷路:DumpStack
扫码加关注

目录
- 一、数据结构
- 二、原子操作的命名规则
- 三、基础接口:基于Linux-2.6.30.4
- 3.1 atomic_read – 获取原子变量的值
- 3.2 atomic_set – 设置,不停尝试,直到成功
- 3.3 atomic_add_return – 加,不停尝试,直到成功,返回新值
- 3.4 atomic_add – 加,不停尝试,直到成功
- 3.5 atomic_sub_return – 减,不停尝试,直到成功,返回新值
- 3.6 atomic_sub – 减,不停尝试,直到成功
- 3.7 atomic_xchg - 赋值,返回操作之前的值
- 3.8 atomic_cmpxchg – 只有在旧值等于old时,才会更新为new,返回旧值
- 3.9 atomic_clear_mask – 清除指定的bit
- 3.10 atomic_add_unless - 执行加操作,返回v值是否等于指定的u
- 3.11 atomic_inc_not_zero - 加完后,判断新值是否为0
- 3.12 atomic_inc/atomic_dec - 执行加减1的操作
- 3.13 atomic_inc_and_test/atomic_dec_and_test - 加减1之后,判断是否为0
- 3.14 atomic_inc_return/atomic_dec_return - 加减1,返回新值
- 3.15 atomic_sub_and_test - 减完后,判断结果是否为0
- 3.16 atomic_add_negative - 加完后,判断值是否小于0
- 四、拓展接口:基于Linux-5.10.61
- 五、参考文档
- 关注公众号不迷路:DumpStack
注意:ARMv6之前的CPU并不支持SMP,之后的ARM架构都是支持SMP的,因此,对于ARM处理器,其原子操作分成了两个阵营,一个是支持SMP的ARMv6之后的CPU,另外一个就是ARMv6之前的,只有单核架构的CPU
另外,ARMv6之前的CPU只支持swp/swpb指令,ARMv6之后的CPU只支持ldrex/strex指令
一、数据结构
1.1 atomic_t - 实际就是一个int型变量
typedef struct { int counter; //实际上就是一个整形变量 } atomic_t; |
二、原子操作的命名规则
Linux内核中提供了各种各样的原子操作函数。除了最原始的读取和设置之外,还包括各种运算,以及位操作等等。而且有的原子操作还要返回操作过后变量的值,有的要返回操作之前变量的值,如果再牵涉到内存屏障的问题,将这些因素组合起来,有非常多的原子操作函数。这些原子操作函数看似非常杂乱,其实函数命名是有规律的。
命名规则:atomic{LEN}_{FEATCH}_{OPT}_{RETURN}_{BARRIER}
{LEN}: 指定原子操作的长度
可取值:空或"64"或"_long"分别表示32和64bit
示例: atomic_add、atomic64_add、atomic_long_add
{FEATCH}: 返回修改之前的值
可取值:featch
示例: atomic_fetch_add
{OPT}: 要完成的操作
可取值:算数操作{add,sub,inc,dec},位操作{and,or,xor,andnot},交换操作{xchg,cmpxchg,try_cmpxchg}等
示例: atomic_add、atomic_sub
注意:try关键字,用于标记期望的操作是否成功执行
{RETURN}: 返回修改之后的值
可取值:return
示例: atomic_add_return
{BARRIER}: 屏障相关
可取值: acquire、release、relaxed,前面两个表示单向屏障,后面表示没有任何屏障
示例: atomic_xchg_acquire、atomic_cmpxchg_release、atomic_add_relaxed
注意:这里的屏障,表示函数里面的操作指令会不会被重新排序,如果一个原子操作函数名有"_relaxed"后缀,表示这个函数没有被任何内存屏障保护,可以被任意重排序
所谓单向屏障,实际就是Load-Acquire(LDAR指令)和Store-Release(STLR指令)。
普通的内存屏障一般是双向的,也就是可以控制内存屏障之前的某些存储操作要在内存屏障之前完成,并且内存屏障之后的某些存储操作要在内存屏障之后才能开始。但是Load-Acquire和Store-Release却只限定了单个方向的:
Load-Acquire:这条指令之后的所有加载和存储操作一定不会被重排序到这条指令之前,但是没有要求这条指令之前的所有加载和存储操作一定不能被重排序到这条指令之后。所以,可以看出来,这条指令是个单向屏障,只挡住了后面出现的所有内存操作指令,但是没有挡住这条指令之前的所有内存操作指令。
Store-Release:这条指令之前的所有加载和存储才做一定不会被重排序到这条指令之后,但是没有要求这条指令之后的所有加载和存储操作一定不能被重排序到这条指令之前。所以,这条指令也是一个单向屏障,只挡住了前面出现的所有内存操作指令,但是没有挡住这条指令之后的所有内存操作指令
关于内存屏障,参见这篇文章:
https://blog.csdn.net/m0_47799526/article/details/110411989
三、基础接口:基于Linux-2.6.30.4
基础接口我们参考内核Linux-2.6.30.4,老的内核没有那么多弯弯绕,代码逻辑清晰的多,虽然Linux的版本不停迭代,但是基情永远不变!
3.1 atomic_read – 获取原子变量的值
多个cpu可以同时获取原子变量的值
#define atomic_read(v) ((v)->counter) |
3.2 atomic_set – 设置,不停尝试,直到成功
当ARM的版本大于ARMv6时,我们需要考虑多核场景,此时通过ldrex和strex指令实现对变量的原子访问,设置失败则进入死循环不停的尝试,直到成功为止
执行的操作:v->counter = i;
要点:不停尝试,直到成功。函数返回时,一定成功赋值
返回值:void型,因此没有返回
注意:
如果有多个cpu同时执行atomic_set操作,一个cpu上执行atomic_set(&v, 1),另一个cpu上执行atomic_set(&v, 2),则这个原子变量最终的值取决于最后执行的那个cpu,可能是1,也可能是2
#if __LINUX_ARM_ARCH__ >= 6 //ARMv6之后版本的cpu需要考虑SMP /* * 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. Writing to 'v->counter' * without using the following operations WILL break the atomic * nature of these ops. */ static inline void atomic_set(atomic_t *v, int i) { unsigned long tmp;
__asm__ __volatile__("@ atomic_set\n" "1: ldrex %0, [%1]\n" //所有cpu都能够获取原子变量的值,tmp = &v->count; " strex %0, %2, [%1]\n" //只有一个cpu能够成功修改原子变量的值,&v->count = i; " teq %0, #0\n" //检测当前cpu是否修改成功 " bne 1b" //若修改不成功,则死循环等待 : "=&r" (tmp) //"返回"&v->count的原始值 : "r" (&v->counter), "r" (i) : "cc"); }
#else //ARMv6之前版本的cpu不支持SMP
#ifdef CONFIG_SMP #error SMP not supported on pre-ARMv6 CPUs #endif
//因为不存在其他cpu,可以直接设置变量的值 #define atomic_set(v,i) (((v)->counter) = (i))
#endif |
3.3 atomic_add_return – 加,不停尝试,直到成功,返回新值
要点:不停尝试,直到成功,返回新值
执行的操作:&v->counter = &v->counter + i;
返回:修改后的值
注意:
假设在执行之前v->count的值为5,在一个cpu上执行atimic_add_return(1, &v)的操作后,返回值不一定是6,因为有可能其他的cpu上也在执行atimic_add_return(1, &v)操作,所以返回值可能是7,8,9...
#if __LINUX_ARM_ARCH__ >= 6 //ARMv6之后版本的cpu需要考虑SMP
static inline int atomic_add_return(int i, atomic_t *v) { unsigned long tmp; int result;
smp_mb(); //内存屏障
__asm__ __volatile__("@ atomic_add_return\n" "1: ldrex %0, [%2]\n" //多个cpu都能获取到原子变量的值 " add %0, %0, %3\n" //多个cpu都在自己的副本里面执行加的操作 " strex %1, %0, [%2]\n" //但是只有一个cpu能够成功更新变量的值 " teq %1, #0\n" //检查是否更新成功 " bne 1b" //更新失败则死循环 : "=&r" (result), "=&r" (tmp) : "r" (&v->counter), "Ir" (i) : "cc");
smp_mb();
return result; //返回修改后的值 }
#else //ARMv6之前版本的cpu不支持SMP
#ifdef CONFIG_SMP #error SMP not supported on pre-ARMv6 CPUs #endif
static inline int atomic_add_return(int i, atomic_t *v) { unsigned long flags; int val;
//关闭当前cpu上的中断,难道是为了防止同一个cpu上不同的线程的竞争 raw_local_irq_save(flags); val = v->counter; v->counter = val += i; raw_local_irq_restore(flags);
return val; //返回旧的值 }
#endif |
关于上面的屏障,实现如下,暂不分析:
#ifndef CONFIG_SMP #define mb() do { if (arch_is_coherent()) dmb(); else barrier(); } while (0) #define rmb() do { if (arch_is_coherent()) dmb(); else barrier(); } while (0) #define wmb() do { if (arch_is_coherent()) dmb(); else barrier(); } while (0) #define smp_mb() barrier() #define smp_rmb() barrier() #define smp_wmb() barrier() #else #define mb() dmb() #define rmb() dmb() #define wmb() dmb() #define smp_mb() dmb() #define smp_rmb() dmb() #define smp_wmb() dmb() #endif |
这里有几个遗留的问题暂时没搞明白,后面分析清楚后再补充吧
a) 对于atomic_add_return来说,为什么SMP时需要mb,而单cpu的时候不需要mb?
b) 对于atomic_add_return来说,为什么SMP时不用关中断,而单cpu的时候需要关中断?
c) 为什么atomic_add_return需要mb,但是atomic_add不需要mb?
3.4 atomic_add – 加,不停尝试,直到成功
要点:不停尝试,直到成功
执行的操作:&v->counter = &v->counter + i;
返回值:void型,无返回值
注意:
假设在执行之前v->count的值为5,在一个cpu上执行atimic_add(1, &v)的操作后,v->count最终的值不一定是6,因为有可能其他的cpu上也在执行atimic_add(1, &v)操作,所以返回值可能是7,8,9...
#if __LINUX_ARM_ARCH__ >= 6 //ARMv6之后版本的cpu需要考虑SMP
static inline void atomic_add(int i, atomic_t *v) { unsigned long tmp; int result;
__asm__ __volatile__("@ atomic_add\n" "1: ldrex %0, [%2]\n" //把v->counter的值读出来,并存入result,标记独占,result = &v->counter " add %0, %0, %3\n" //等价于 result=result+i " strex %1, %0, [%2]\n" //&v->counter = result。若&v->counter所指内存未被独占,则失败,tmp=1 " teq %1, #0\n" //tmp不等于0的话,就跳至标号1重新执行 " bne 1b" : "=&r" (result), "=&r" (tmp) : "r" (&v->counter), "Ir" (i) : "cc"); }
#else //ARMv6之前版本的cpu不支持SMP
#ifdef CONFIG_SMP #error SMP not supported on pre-ARMv6 CPUs #endif
#define atomic_add(i, v) (void) atomic_add_return(i, v)
#endif |
3.5 atomic_sub_return – 减,不停尝试,直到成功,返回新值
要点:不停尝试,直到成功,返回新值
执行的操作:&v->counter = &v->counter - i;
返回:修改后的值
注意:
假设在执行之前v->count的值为5,在一个cpu上执行atimic_sub_return(1, &v)的操作后,返回值不一定是4,因为有可能其他的cpu上也在执行atimic_sub_return(1, &v)操作,所以返回值可能是4,3,2...
#if __LINUX_ARM_ARCH__ >= 6 //ARMv6之后版本的cpu需要考虑SMP
static inline int atomic_sub_return(int i, atomic_t *v) { unsigned long tmp; int result;
smp_mb();
__asm__ __volatile__("@ atomic_sub_return\n" "1: ldrex %0, [%2]\n" //result = &v->counter; " sub %0, %0, %3\n" //result = result – i; " strex %1, %0, [%2]\n" //&v->counter = result; " teq %1, #0\n" //判断上一步的写入操作是否成功 " bne 1b" //若写入失败,则循环尝试,直到成功为止 : "=&r" (result), "=&r" (tmp) : "r" (&v->counter), "Ir" (i) : "cc");
smp_mb();
return result; //返回修改后的值 }
#else //ARMv6之前版本的cpu不支持SMP
#ifdef CONFIG_SMP #error SMP not supported on pre-ARMv6 CPUs #endif
static inline int atomic_sub_return(int i, atomic_t *v) { unsigned long flags; int val;
//关闭当前cpu的中断,这主要防止同一个cpu上不同线程之间的竞争 raw_local_irq_save(flags); val = v->counter; v->counter = val -= i; raw_local_irq_restore(flags);
return val; }
#endif |
3.6 atomic_sub – 减,不停尝试,直到成功
要点:不停尝试,直到成功
执行的操作:&v->counter = &v->counter - i;
注意:
假设在执行之前v->count的值为5,在一个cpu上执行atimic_sub(1, &v)的操作后,v->count最终的值不一定是4,因为有可能其他的cpu上也在执行atimic_sub(1, &v)操作,所以返回值可能是4,3,2...
#if __LINUX_ARM_ARCH__ >= 6 //ARMv6之后版本的cpu需要考虑SMP
static inline void atomic_sub(int i, atomic_t *v) { unsigned long tmp; int result;
__asm__ __volatile__("@ atomic_sub\n" "1: ldrex %0, [%2]\n" //result = &v->counter " sub %0, %0, %3\n" //result = result – i; " strex %1, %0, [%2]\n" //&v->counter = result; " teq %1, #0\n" //判断上一步的写入操作是否成功 " bne 1b" //若写入失败,则循环尝试,直到成功为止 : "=&r" (result), "=&r" (tmp) : "r" (&v->counter), "Ir" (i) : "cc"); }
#else //ARMv6之前版本的cpu不支持SMP
#ifdef CONFIG_SMP #error SMP not supported on pre-ARMv6 CPUs #endif
#define atomic_sub(i, v) (void) atomic_sub_return(i, v)
#endif |
3.7 atomic_xchg - 赋值,返回操作之前的值
要点:不停尝试,直到成功
执行的操作:&v->counter = new;
返回值:返回赋值之前旧的值
#define atomic_xchg(v, new) (xchg(&((v)->counter), new))
其中: #define xchg(ptr,x) \ ((__typeof__(*(ptr)))__xchg((unsigned long)(x),(ptr),sizeof(*(ptr)))) |
__xchg实现如下:
static inline unsigned long __xchg(unsigned long x, volatile void *ptr, int size) { extern void __bad_xchg(volatile void *, int); unsigned long ret; #ifdef swp_is_buggy unsigned long flags; #endif #if __LINUX_ARM_ARCH__ >= 6 unsigned int tmp; #endif
smp_mb();
switch (size) { #if __LINUX_ARM_ARCH__ >= 6 case 1: asm volatile("@ __xchg1\n" "1: ldrexb %0, [%3]\n" //ret = &ptr->counter " strexb %1, %2, [%3]\n" //&ptr->counter = x " teq %1, #0\n" //判断上一步的写入操作是否成功 " bne 1b" //若写入失败,则循环尝试,直到成功为止 : "=&r" (ret), "=&r" (tmp) : "r" (x), "r" (ptr) : "memory", "cc"); break; case 4: asm volatile("@ __xchg4\n" "1: ldrex %0, [%3]\n" " strex %1, %2, [%3]\n" " teq %1, #0\n" " bne 1b" : "=&r" (ret), "=&r" (tmp) : "r" (x), "r" (ptr) : "memory", "cc"); break; #elif defined(swp_is_buggy) #ifdef CONFIG_SMP #error SMP is not supported on this platform #endif //单核cpu执行下面操作 case 1: raw_local_irq_save(flags); //关中断 ret = *(volatile unsigned char *)ptr; //交换 *(volatile unsigned char *)ptr = x; raw_local_irq_restore(flags); break;
case 4: raw_local_irq_save(flags); ret = *(volatile unsigned long *)ptr; *(volatile unsigned long *)ptr = x; raw_local_irq_restore(flags); break; #else case 1: asm volatile("@ __xchg1\n" " swpb %0, %1, [%2]" : "=&r" (ret) : "r" (x), "r" (ptr) : "memory", "cc"); break; case 4: asm volatile("@ __xchg4\n" " swp %0, %1, [%2]" : "=&r" (ret) : "r" (x), "r" (ptr) : "memory", "cc"); break; #endif default: __bad_xchg(ptr, size), ret = 0; break; } smp_mb();
return ret; //返回修改前的值 } |
3.8 atomic_cmpxchg – 只有在旧值等于old时,才会更新为new,返回旧值
要点:只有在旧值等于old时,才将原子变量的值设置为new,不停尝试,直到设置成功;如果旧的值不等于old,则不执行任何操作,直接返回
执行的操作:if(&ptr->counter == old) &ptr->counter = new;
返回:原子变量的原始值
#if __LINUX_ARM_ARCH__ >= 6 //ARMv6之后版本的cpu需要考虑SMP
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new) { unsigned long oldval, res;
smp_mb();
do { __asm__ __volatile__("@ atomic_cmpxchg\n" "ldrex %1, [%2]\n" //oldval = &ptr->counter; "mov %0, #0\n" //res = 0; "teq %1, %3\n" //if(oldval == old) "strexeq %0, %4, [%2]\n" // &ptr->counter = new; : "=&r" (res), "=&r" (oldval) : "r" (&ptr->counter), "Ir" (old), "r" (new) : "cc"); } while (res); //若上面写入失败,则重新判断
smp_mb();
return oldval; }
#else //ARMv6之前版本的cpu不支持SMP
#ifdef CONFIG_SMP #error SMP not supported on pre-ARMv6 CPUs #endif
static inline int atomic_cmpxchg(atomic_t *v, int old, int new) { int ret; unsigned long flags;
raw_local_irq_save(flags); ret = v->counter; if (likely(ret == old)) //只有在旧的值等于old时才会更新原子变量的值 v->counter = new; raw_local_irq_restore(flags);
return ret; }
#endif |
3.9 atomic_clear_mask – 清除指定的bit
功能:对addr所指的数据指向mask屏蔽操作
要点:清除指定的bit,不停尝试,直到成功
执行的操作:[addr] = [addr] & ~mask;
void atomic_clear_mask(unsigned long mask, unsigned long *addr) { unsigned long tmp, tmp2;
__asm__ __volatile__("@ atomic_clear_mask\n" "1: ldrex %0, [%2]\n" //tmp = [addr]; " bic %0, %0, %3\n" //tmp = tmp & ~mask; " strex %1, %0, [%2]\n" //addr = tmp; " teq %1, #0\n" //判断上一步的写入操作是否成功 " bne 1b" //若写入失败,则循环尝试,直到成功为止 : "=&r" (tmp), "=&r" (tmp2) : "r" (addr), "Ir" (mask) : "cc"); } |
3.10 atomic_add_unless - 执行加操作,返回v值是否等于指定的u
功能:unless的含义为"除非"的意思,表示执行加操作,除非原子变量的值为指向的u
要点:只有在原子变量v不等于u的时候,才会将a的值加到v上面去
返回值:加之前原子变量的值是否等于指定的值u,如果等于,则完全不执行加操作
只要加成功,函数一定返回,返回值用于标记新的值是否和指定的值u相等,相等则返回true,不相等则返回false
下面来个前景分析:原子变量v的初值为0,cpua上执行atomic_add_unless(&v, 1, 5)
在t1时刻,cpua上atomic_cmpxchg操作成功,此时c为初值0,返回的old也为0,while循环不成立,退出循环,返回true
在t2时刻,cpua上atomic_cmpxchg操作成功,此时c为初值1,返回的old也为1,while循环不成立,退出循环,返回true
在t3时刻,cpua和cpub上同时执行atomic_cmpxchg操作,但是cpub抢先成功,cpua上操作失败了,此时c的值为2,而此时返回的old的值为3,while循环的条件成立,继续执行下一次循环操作;在下一次循环中atomic_cmpxchg操作成功,c的值为3,返回的old的值为3,while循环不成立,退出循环,返回true
在t4时刻,cpua上atomic_cmpxchg操作成功,此时c为初值4,返回的old也为4,while循环不成立,退出循环,返回true
在t5时刻,此时c为初值5,while循环直接就不成立了,直接退出循环,返回false
时间戳 |
cpua |
cpub |
t1 |
1 |
|
t2 |
2 |
|
t3 |
3 |
|
t3 |
4 |
|
t4 |
5 |
|
t5 |
6 |
代码实现如下:
/** * atomic_add_unless - add unless the number is already a given value * @v: pointer of type atomic_t * @a: the amount to add to v... * @u: ...unless v is equal to u. * * Atomically adds @a to @v, if @v was not already @u. * Returns true if the addition was done. */ static inline int atomic_add_unless(atomic_t *v, int a, int u) { int c, old;
c = atomic_read(v); while (c != u && (old = atomic_cmpxchg((v), c, c + a)) != c) c = old; return c != u; } |
3.11 atomic_inc_not_zero - 加完后,判断新值是否为0
功能:将原子变量v加1,并测试加完后的值是否不为零,如果不为零则返回ture
#define atomic_inc_not_zero(v) atomic_add_unless((v), 1, 0) |
3.12 atomic_inc/atomic_dec - 执行加减1的操作
#define atomic_inc(v) atomic_add(1, v) #define atomic_dec(v) atomic_sub(1, v) |
3.13 atomic_inc_and_test/atomic_dec_and_test - 加减1之后,判断是否为0
#define atomic_inc_and_test(v) (atomic_add_return(1, v) == 0) #define atomic_dec_and_test(v) (atomic_sub_return(1, v) == 0) |
3.14 atomic_inc_return/atomic_dec_return - 加减1,返回新值
#define atomic_inc_return(v) (atomic_add_return(1, v)) #define atomic_dec_return(v) (atomic_sub_return(1, v)) |
3.15 atomic_sub_and_test - 减完后,判断结果是否为0
#define atomic_sub_and_test(i, v) (atomic_sub_return(i, v) == 0) |
3.16 atomic_add_negative - 加完后,判断值是否小于0
#define atomic_add_negative(i,v) (atomic_add_return(i, v) < 0) |
四、拓展接口:基于Linux-5.10.61
4.1 atomic_try_cmpxchg - 尝试cmpxchg,返回期望的操作是否成功
注意:try是"尝试"的意思,标记期望的xchg操作是否成功,每次操作完成后,old值都会被更新为旧的值
返回值:标记是否已经把new写进原子变量,返回true表示成功写入,返回false有下面两种情况
a) v和old压根就不相等,根本就没有进行原子交换的动作
b) v和old相等,但是在进行原子交换的过程中被其他cpu抢了
static __always_inline bool atomic_try_cmpxchg(atomic_t *v, int *old, int new) { int r, o = *old; r = atomic_cmpxchg(v, o, new); if (unlikely(r != o)) *old = r; return likely(r == o); } |
五、参考文档
https://blog.csdn.net/roland_sun/article/details/107115267
https://blog.csdn.net/m0_47799526/article/details/110411989
文章评论