鸿蒙内核源码分析(原子操作篇) | 是谁在为原子操作保驾护航?

开发
文章由鸿蒙社区产出,想要了解更多内容请前往:51CTO和华为官方战略合作共建的鸿蒙技术社区https://harmonyos.51cto.com

[[390980]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

本篇说清楚原子操作

读本篇之前建议先读鸿蒙内核源码分析(总目录)系列篇.

基本概念

在支持多任务的操作系统中,修改一块内存区域的数据需要“读取-修改-写入”三个步骤。然而同一内存区域的数据可能同时被多个任务访问,如果在修改数据的过程中被其他任务打断,就会造成该操作的执行结果无法预知。

使用开关中断的方法固然可以保证多任务执行结果符合预期,但这种方法显然会影响系统性能。

ARMv6架构引入了LDREX和STREX指令,以支持对共享存储器更缜密的非阻塞同步。由此实现的原子操作能确保对同一数据的“读取-修改-写入”操作在它的执行期间不会被打断,即操作的原子性。

有多个任务对同一个内存数据进行加减或交换操作时,使用原子操作保证结果的可预知性。

看过鸿蒙内核源码分析(总目录)自旋锁篇的应该对LDREX和STREX指令不陌生的,自旋锁的本质就是对某个变量的原子操作,而且一定要通过汇编代码实现,也就是说LDREX和STREX指令保证了原子操作的底层实现. 回顾下自旋锁申请和释放锁的汇编代码.

ArchSpinLock 申请锁代码

  1. FUNCTION(ArchSpinLock)  @死守,非要拿到锁 
  2.     mov     r1, #1      @r1=1 
  3. 1:                      @循环的作用,因SEV是广播事件.不一定lock->rawLock的值已经改变了 
  4.     ldrex   r2, [r0]    @r0 = &lock->rawLock, 即 r2 = lock->rawLock 
  5.     cmp     r2, #0      @r2和0比较 
  6.     wfene               @不相等时,说明资源被占用,CPU核进入睡眠状态 
  7.     strexeq r2, r1, [r0]@此时CPU被重新唤醒,尝试令lock->rawLock=1,成功写入则r2=0 
  8.     cmpeq   r2, #0      @再来比较r2是否等于0,如果相等则获取到了锁 
  9.     bne     1b          @如果不相等,继续进入循环 
  10.     dmb                 @用DMB指令来隔离,以保证缓冲中的数据已经落实到RAM中 
  11.     bx      lr          @此时是一定拿到锁了,跳回调用ArchSpinLock函数 

ArchSpinUnlock 释放锁代码

  1. FUNCTION(ArchSpinUnlock)    @释放锁 
  2.      mov     r1, #0          @r1=0                
  3.      dmb                     @数据存储隔离,以保证缓冲中的数据已经落实到RAM中 
  4.      str     r1, [r0]        @令lock->rawLock = 0 
  5.      dsb                     @数据同步隔离 
  6.      sev                     @给各CPU广播事件,唤醒沉睡的CPU们 
  7.      bx      lr              @跳回调用ArchSpinLock函数 

运作机制

鸿蒙通过对ARMv6架构中的LDREX和STREX进行封装,向用户提供了一套原子操作接口。

● LDREX Rx, [Ry] 读取内存中的值,并标记对该段内存为独占访问:

◊ 读取寄存器Ry指向的4字节内存数据,保存到Rx寄存器中。

◊ 对Ry指向的内存区域添加独占访问标记。

● STREX Rf, Rx, [Ry] 检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存:

◊ 有独占访问标记

◊ 将寄存器Rx中的值更新到寄存器Ry指向的内存。

◊ 标志寄存器Rf置为0。

◊ 没有独占访问标记

◊ 不更新内存。

◊ 标志寄存器Rf置为1。

● 判断标志寄存器 标志寄存器为0时,退出循环,原子操作结束。 标志寄存器为1时,继续循环,重新进行原子操作。

功能列表

原子数据包含两种类型Atomic(有符号32位数)与 Atomic64(有符号64位数)。原子操作模块为用户提供下面几种功能,接口详细信息可以查看源码。

此处讲述 LOS_AtomicAdd , LOS_AtomicSub,LOS_AtomicRead,LOS_AtomicSet 理解了函数的汇编代码是理解的原子操作的关键.

LOS_AtomicAdd

  1. //对内存数据做加法 
  2. STATIC INLINE INT32 LOS_AtomicAdd(Atomic *v, INT32 addVal)   
  3.     INT32 val; 
  4.     UINT32 status; 
  5.  
  6.     do { 
  7.         __asm__ __volatile__("ldrex   %1, [%2]\n" 
  8.                              "add   %1, %1, %3\n"  
  9.                              "strex   %0, %1, [%2]" 
  10.                              : "=&r"(status), "=&r"(val) 
  11.                              : "r"(v), "r"(addVal) 
  12.                              : "cc"); 
  13.     } while (__builtin_expect(status != 0, 0)); 
  14.  
  15.     return val; 

这是一段C语言内嵌汇编,逐一解读

● 1.先将 val status v addVal的值交由通用寄存器(R0~R3)接管.

● 2.%2代表了入参v,[%2]代表的是参数v指向地址的值,也就是 *v ,函数要独占的就是它

● 3.%0 ~ %3 对应 val status v addVal

● 4.ldrex %1, [%2] 表示 val = *v ;

● 5.add %1, %1, %3 表示 val = val + addVal;

● 6.strex %0, %1, [%2] 表示 *v = val;

● 7.status 表示是否更新成功,成功了置0,不成功则为 1

● 8.__builtin_expect是结束循环的判断语句,将最有可能执行的分支告诉编译器。 这个指令的写法为:

__builtin_expect(EXP, N)。

意思是:EXP==N 的概率很大。

综合理解__builtin_expect(status != 0, 0)

说的是status = 1失败的可能性很大,不成功就重新来一遍,直到strex更新成(status == 0)为止.

● 9."=&r"(val) 被修饰的操作符作为输出,即将寄存器的值回给val,val为函数的返回值

● 10."cc"向GCC编译器声明以上信息.

LOS_AtomicSub

  1. //对内存数据做减法 
  2. STATIC INLINE INT32 LOS_AtomicSub(Atomic *v, INT32 subVal)   
  3.     INT32 val; 
  4.     UINT32 status; 
  5.  
  6.     do { 
  7.         __asm__ __volatile__("ldrex   %1, [%2]\n" 
  8.                              "sub   %1, %1, %3\n" 
  9.                              "strex   %0, %1, [%2]" 
  10.                              : "=&r"(status), "=&r"(val) 
  11.                              : "r"(v), "r"(subVal) 
  12.                              : "cc"); 
  13.     } while (__builtin_expect(status != 0, 0)); 
  14.  
  15.     return val; 

解读

● 同 LOS_AtomicAdd解读

volatile

这里要重点说下volatile,volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都要直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

  1. //读取内存数据 
  2. STATIC INLINE INT32 LOS_AtomicRead(const Atomic *v)  
  3.     return *(volatile INT32 *)v; 
  4. //写入内存数据 
  5. STATIC INLINE VOID LOS_AtomicSet(Atomic *v, INT32 setVal)    
  6.     *(volatile INT32 *)v = setVal; 

编程实例

调用原子操作相关接口,观察结果:

1.创建两个任务

● 任务一用LOS_AtomicAdd对全局变量加100次。

● 任务二用LOS_AtomicSub对全局变量减100次。

2.子任务结束后在主任务中打印全局变量的值。

  1. #include "los_hwi.h" 
  2. #include "los_atomic.h" 
  3. #include "los_task.h" 
  4.  
  5. UINT32 g_testTaskId01; 
  6. UINT32 g_testTaskId02; 
  7. Atomic g_sum; 
  8. Atomic g_count; 
  9.  
  10. UINT32 Example_Atomic01(VOID) 
  11.     int i = 0; 
  12.     for(i = 0; i < 100; ++i) { 
  13.         LOS_AtomicAdd(&g_sum,1); 
  14.     } 
  15.  
  16.     LOS_AtomicAdd(&g_count,1); 
  17.     return LOS_OK; 
  18.  
  19. UINT32 Example_Atomic02(VOID) 
  20.     int i = 0; 
  21.     for(i = 0; i < 100; ++i) { 
  22.         LOS_AtomicSub(&g_sum,1); 
  23.     } 
  24.  
  25.     LOS_AtomicAdd(&g_count,1); 
  26.     return LOS_OK; 
  27.  
  28. UINT32 Example_TaskEntry(VOID) 
  29.     TSK_INIT_PARAM_S stTask1={0}; 
  30.     stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic01; 
  31.     stTask1.pcName       = "TestAtomicTsk1"
  32.     stTask1.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE; 
  33.     stTask1.usTaskPrio   = 4; 
  34.     stTask1.uwResved     = LOS_TASK_STATUS_DETACHED; 
  35.  
  36.     TSK_INIT_PARAM_S stTask2={0}; 
  37.     stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic02; 
  38.     stTask2.pcName       = "TestAtomicTsk2"
  39.     stTask2.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE; 
  40.     stTask2.usTaskPrio   = 4; 
  41.     stTask2.uwResved     = LOS_TASK_STATUS_DETACHED; 
  42.  
  43.     LOS_TaskLock(); 
  44.     LOS_TaskCreate(&g_testTaskId01, &stTask1); 
  45.     LOS_TaskCreate(&g_testTaskId02, &stTask2); 
  46.     LOS_TaskUnlock(); 
  47.  
  48.     while(LOS_AtomicRead(&g_count) != 2); 
  49.     dprintf("g_sum = %d\n", g_sum); 
  50.  
  51.     return LOS_OK; 

结果验证

  1. g_sum = 0 

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2014-01-09 09:45:41

原子飞原子

2020-11-27 06:44:22

原子加锁x86

2021-01-26 11:54:10

Mysql 操作日志

2023-09-19 23:26:15

++i原子操作C++

2013-01-10 11:32:12

阿里云双十一云计算

2015-08-19 10:06:21

2021-06-29 10:07:24

Javalong原子操作

2021-04-02 09:42:54

鸿蒙HarmonyOS应用

2021-04-08 09:32:17

鸿蒙HarmonyOS应用

2021-06-30 09:11:17

KubernetesDevtronK8S

2012-09-12 09:40:36

云服务GIS技术弹性云计算

2010-06-14 23:32:04

综合布线机场西蒙

2014-03-13 14:55:45

云计算新媒体虚拟化

2009-10-29 10:22:22

2011-12-16 11:11:24

戴尔

2023-06-27 08:45:19

原子操作Golang

2012-05-23 12:49:58

Java自增操作原子性

2016-08-23 15:05:33

华为

2021-03-11 11:14:39

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号