基本概念
在支持多任務的操作系統中,修改一塊內存區域的數據需要“讀取-修改-寫入”三個步驟。然而同一內存區域的數據可能同時被多個任務訪問,如果在修改數據的過程中被其他任務打斷,就會造成該操作的執行結果無法預知。
使用開關中斷的方法固然可以保證多任務執行結果符合預期,但這種方法顯然會影響系統性能。
ARMv6
架構引入了LDREX
和STREX
指令,以支持對共享存儲器更縝密的非阻塞同步。由此實現的原子操作能確保對同一數據的“讀取-修改-寫入”操作在它的執行期間不會被打斷,即操作的原子性。
有多個任務對同一個內存數據進行加減或交換操作時,使用原子操作保證結果的可預知性。
看過自旋鎖篇的應該對LDREX和STREX指令不陌生的,自旋鎖的本質就是對某個變量的原子操作,而且一定要通過匯編代碼實現,也就是說LDREX
和STREX
指令保證了原子操作的底層實現.
回顧下自旋鎖申請和釋放鎖的匯編代碼.
ArchSpinLock 申請鎖代碼
FUNCTION(ArchSpinLock) @死守,非要拿到鎖
mov r1, #1 @r1=1
1: @循環的作用,因SEV是廣播事件.不一定lock->rawLock的值已經改變了
ldrex r2, [r0] @r0 = &lock->rawLock, 即 r2 = lock->rawLock
cmp r2, #0 @r2和0比較
wfene @不相等時,說明資源被占用,CPU核進入睡眠狀態
strexeq r2, r1, [r0]@此時CPU被重新喚醒,嘗試令lock->rawLock=1,成功寫入則r2=0
cmpeq r2, #0 @再來比較r2是否等于0,如果相等則獲取到了鎖
bne 1b @如果不相等,繼續進入循環
dmb @用DMB指令來隔離,以保證緩沖中的數據已經落實到RAM中
bx lr @此時是一定拿到鎖了,跳回調用ArchSpinLock函數
ArchSpinUnlock 釋放鎖代碼
FUNCTION(ArchSpinUnlock) @釋放鎖
mov r1, #0 @r1=0
dmb @數據存儲隔離,以保證緩沖中的數據已經落實到RAM中
str r1, [r0] @令lock->rawLock = 0
dsb @數據同步隔離
sev @給各CPU廣播事件,喚醒沉睡的CPU們
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
//對內存數據做加法
STATIC INLINE INT32 LOS_AtomicAdd(Atomic *v, INT32 addVal)
{
INT32 val;
UINT32 status;
do {
__asm__ __volatile__("ldrex %1, [%2]\n"
"add %1, %1, %3\n"
"strex %0, %1, [%2]"
: "=&r"(status), "=&r"(val)
: "r"(v), "r"(addVal)
: "cc");
} while (__builtin_expect(status != 0, 0));
return val;
}
這是一段C語言內嵌匯編,逐一解讀
- 先將
status
val
v
addVal
的值交由通用寄存器(R0~R3)接管.
- 先將
- %2代表了入參v,[%2]代表的是參數v指向地址的值,也就是 *v ,函數要獨占的就是它
- %0 ~ %3 對應
status
val
v
addVal
- %0 ~ %3 對應
- ldrex %1, [%2] 表示 val = *v ;
- add %1, %1, %3 表示 val = val + addVal;
- strex %0, %1, [%2] 表示 *v = val;
- status 表示是否更新成功,成功了置0,不成功則為 1
-
__builtin_expect是結束循環的判斷語句,將最有可能執行的分支告訴編譯器。
這個指令的寫法為:__builtin_expect(EXP, N)。意思是:EXP==N 的概率很大。
綜合理解__builtin_expect(status != 0, 0)
說的是status = 0 的可能性很大,不成功就會重新來一遍,直到strex更新成(status == 0)為止.
-
- "=&r"(val) 被修飾的操作符作為輸出,即將寄存器的值回給val,val為函數的返回值
- "cc"向編譯器聲明以上信息.
LOS_AtomicSub
//對內存數據做減法
STATIC INLINE INT32 LOS_AtomicSub(Atomic *v, INT32 subVal)
{
INT32 val;
UINT32 status;
do {
__asm__ __volatile__("ldrex %1, [%2]\n"
"sub %1, %1, %3\n"
"strex %0, %1, [%2]"
: "=&r"(status), "=&r"(val)
: "r"(v), "r"(subVal)
: "cc");
} while (__builtin_expect(status != 0, 0));
return val;
}
解讀
- 同
LOS_AtomicAdd
解讀
volatile
這里要重點說下volatile
,volatile
提醒編譯器它后面所定義的變量隨時都有可能改變,因此編譯后的程序每次需要存儲或讀取這個變量的時候,都要直接從變量地址中讀取數據。如果沒有volatile
關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。
//讀取內存數據
STATIC INLINE INT32 LOS_AtomicRead(const Atomic *v)
{
return *(volatile INT32 *)v;
}
//寫入內存數據
STATIC INLINE VOID LOS_AtomicSet(Atomic *v, INT32 setVal)
{
*(volatile INT32 *)v = setVal;
}
編程實例
調用原子操作相關接口,觀察結果:
1.創建兩個任務
- 任務一用LOS_AtomicAdd對全局變量加100次。
- 任務二用LOS_AtomicSub對全局變量減100次。
2.子任務結束后在主任務中打印全局變量的值。
#include "los_hwi.h"
#include "los_atomic.h"
#include "los_task.h"
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
Atomic g_sum;
Atomic g_count;
UINT32 Example_Atomic01(VOID)
{
int i = 0;
for(i = 0; i < 100; ++i) {
LOS_AtomicAdd(&g_sum,1);
}
LOS_AtomicAdd(&g_count,1);
return LOS_OK;
}
UINT32 Example_Atomic02(VOID)
{
int i = 0;
for(i = 0; i < 100; ++i) {
LOS_AtomicSub(&g_sum,1);
}
LOS_AtomicAdd(&g_count,1);
return LOS_OK;
}
UINT32 Example_TaskEntry(VOID)
{
TSK_INIT_PARAM_S stTask1={0};
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic01;
stTask1.pcName = "TestAtomicTsk1";
stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
stTask1.usTaskPrio = 4;
stTask1.uwResved = LOS_TASK_STATUS_DETACHED;
TSK_INIT_PARAM_S stTask2={0};
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic02;
stTask2.pcName = "TestAtomicTsk2";
stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
stTask2.usTaskPrio = 4;
stTask2.uwResved = LOS_TASK_STATUS_DETACHED;
LOS_TaskLock();
LOS_TaskCreate(&g_testTaskId01, &stTask1);
LOS_TaskCreate(&g_testTaskId02, &stTask2);
LOS_TaskUnlock();
while(LOS_AtomicRead(&g_count) != 2);
dprintf("g_sum = %d\n", g_sum);
return LOS_OK;
}
結果驗證
g_sum = 0
寫在最后
- 如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:
- 點贊,轉發,有你們的 『點贊和評論』,才是我創造的動力。
- 關注小編,同時可以期待后續文章ing??,不定期分享原創知識。
- 想要獲取更多完整鴻蒙最新學習知識點,請移步前往小編:
https://gitee.com/MNxiaona/733GH/blob/master/jianshu