鴻蒙內(nèi)核源碼分析(原子操作篇) | 是誰(shuí)在為原子操作保駕護(hù)航?
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)
本篇說(shuō)清楚原子操作
讀本篇之前建議先讀鴻蒙內(nèi)核源碼分析(總目錄)系列篇.
基本概念
在支持多任務(wù)的操作系統(tǒng)中,修改一塊內(nèi)存區(qū)域的數(shù)據(jù)需要“讀取-修改-寫入”三個(gè)步驟。然而同一內(nèi)存區(qū)域的數(shù)據(jù)可能同時(shí)被多個(gè)任務(wù)訪問(wèn),如果在修改數(shù)據(jù)的過(guò)程中被其他任務(wù)打斷,就會(huì)造成該操作的執(zhí)行結(jié)果無(wú)法預(yù)知。
使用開關(guān)中斷的方法固然可以保證多任務(wù)執(zhí)行結(jié)果符合預(yù)期,但這種方法顯然會(huì)影響系統(tǒng)性能。
ARMv6架構(gòu)引入了LDREX和STREX指令,以支持對(duì)共享存儲(chǔ)器更縝密的非阻塞同步。由此實(shí)現(xiàn)的原子操作能確保對(duì)同一數(shù)據(jù)的“讀取-修改-寫入”操作在它的執(zhí)行期間不會(huì)被打斷,即操作的原子性。
有多個(gè)任務(wù)對(duì)同一個(gè)內(nèi)存數(shù)據(jù)進(jìn)行加減或交換操作時(shí),使用原子操作保證結(jié)果的可預(yù)知性。
看過(guò)鴻蒙內(nèi)核源碼分析(總目錄)自旋鎖篇的應(yīng)該對(duì)LDREX和STREX指令不陌生的,自旋鎖的本質(zhì)就是對(duì)某個(gè)變量的原子操作,而且一定要通過(guò)匯編代碼實(shí)現(xiàn),也就是說(shuō)LDREX和STREX指令保證了原子操作的底層實(shí)現(xiàn). 回顧下自旋鎖申請(qǐng)和釋放鎖的匯編代碼.
ArchSpinLock 申請(qǐng)鎖代碼
- FUNCTION(ArchSpinLock) @死守,非要拿到鎖
- mov r1, #1 @r1=1
- 1: @循環(huán)的作用,因SEV是廣播事件.不一定lock->rawLock的值已經(jīng)改變了
- ldrex r2, [r0] @r0 = &lock->rawLock, 即 r2 = lock->rawLock
- cmp r2, #0 @r2和0比較
- wfene @不相等時(shí),說(shuō)明資源被占用,CPU核進(jìn)入睡眠狀態(tài)
- strexeq r2, r1, [r0]@此時(shí)CPU被重新喚醒,嘗試令lock->rawLock=1,成功寫入則r2=0
- cmpeq r2, #0 @再來(lái)比較r2是否等于0,如果相等則獲取到了鎖
- bne 1b @如果不相等,繼續(xù)進(jìn)入循環(huán)
- dmb @用DMB指令來(lái)隔離,以保證緩沖中的數(shù)據(jù)已經(jīng)落實(shí)到RAM中
- bx lr @此時(shí)是一定拿到鎖了,跳回調(diào)用ArchSpinLock函數(shù)
ArchSpinUnlock 釋放鎖代碼
- FUNCTION(ArchSpinUnlock) @釋放鎖
- mov r1, #0 @r1=0
- dmb @數(shù)據(jù)存儲(chǔ)隔離,以保證緩沖中的數(shù)據(jù)已經(jīng)落實(shí)到RAM中
- str r1, [r0] @令lock->rawLock = 0
- dsb @數(shù)據(jù)同步隔離
- sev @給各CPU廣播事件,喚醒沉睡的CPU們
- bx lr @跳回調(diào)用ArchSpinLock函數(shù)
運(yùn)作機(jī)制
鴻蒙通過(guò)對(duì)ARMv6架構(gòu)中的LDREX和STREX進(jìn)行封裝,向用戶提供了一套原子操作接口。
● LDREX Rx, [Ry] 讀取內(nèi)存中的值,并標(biāo)記對(duì)該段內(nèi)存為獨(dú)占訪問(wèn):
◊ 讀取寄存器Ry指向的4字節(jié)內(nèi)存數(shù)據(jù),保存到Rx寄存器中。
◊ 對(duì)Ry指向的內(nèi)存區(qū)域添加獨(dú)占訪問(wèn)標(biāo)記。
● STREX Rf, Rx, [Ry] 檢查內(nèi)存是否有獨(dú)占訪問(wèn)標(biāo)記,如果有則更新內(nèi)存值并清空標(biāo)記,否則不更新內(nèi)存:
◊ 有獨(dú)占訪問(wèn)標(biāo)記
◊ 將寄存器Rx中的值更新到寄存器Ry指向的內(nèi)存。
◊ 標(biāo)志寄存器Rf置為0。
◊ 沒(méi)有獨(dú)占訪問(wèn)標(biāo)記
◊ 不更新內(nèi)存。
◊ 標(biāo)志寄存器Rf置為1。
● 判斷標(biāo)志寄存器 標(biāo)志寄存器為0時(shí),退出循環(huán),原子操作結(jié)束。 標(biāo)志寄存器為1時(shí),繼續(xù)循環(huán),重新進(jìn)行原子操作。
功能列表
原子數(shù)據(jù)包含兩種類型Atomic(有符號(hào)32位數(shù))與 Atomic64(有符號(hào)64位數(shù))。原子操作模塊為用戶提供下面幾種功能,接口詳細(xì)信息可以查看源碼。
此處講述 LOS_AtomicAdd , LOS_AtomicSub,LOS_AtomicRead,LOS_AtomicSet 理解了函數(shù)的匯編代碼是理解的原子操作的關(guān)鍵.
LOS_AtomicAdd
- //對(duì)內(nèi)存數(shù)據(jù)做加法
- 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語(yǔ)言內(nèi)嵌匯編,逐一解讀
● 1.先將 val status v addVal的值交由通用寄存器(R0~R3)接管.
● 2.%2代表了入?yún),[%2]代表的是參數(shù)v指向地址的值,也就是 *v ,函數(shù)要獨(dú)占的就是它
● 3.%0 ~ %3 對(duì)應(yīng) 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是結(jié)束循環(huán)的判斷語(yǔ)句,將最有可能執(zhí)行的分支告訴編譯器。 這個(gè)指令的寫法為:
__builtin_expect(EXP, N)。
意思是:EXP==N 的概率很大。
綜合理解__builtin_expect(status != 0, 0)
說(shuō)的是status = 1失敗的可能性很大,不成功就重新來(lái)一遍,直到strex更新成(status == 0)為止.
● 9."=&r"(val) 被修飾的操作符作為輸出,即將寄存器的值回給val,val為函數(shù)的返回值
● 10."cc"向GCC編譯器聲明以上信息.
LOS_AtomicSub
- //對(duì)內(nèi)存數(shù)據(jù)做減法
- 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
這里要重點(diǎn)說(shuō)下volatile,volatile 提醒編譯器它后面所定義的變量隨時(shí)都有可能改變,因此編譯后的程序每次需要存儲(chǔ)或讀取這個(gè)變量的時(shí)候,都要直接從變量地址中讀取數(shù)據(jù)。如果沒(méi)有volatile關(guān)鍵字,則編譯器可能優(yōu)化讀取和存儲(chǔ),可能暫時(shí)使用寄存器中的值,如果這個(gè)變量由別的程序更新了的話,將出現(xiàn)不一致的現(xiàn)象。
- //讀取內(nèi)存數(shù)據(jù)
- STATIC INLINE INT32 LOS_AtomicRead(const Atomic *v)
- {
- return *(volatile INT32 *)v;
- }
- //寫入內(nèi)存數(shù)據(jù)
- STATIC INLINE VOID LOS_AtomicSet(Atomic *v, INT32 setVal)
- {
- *(volatile INT32 *)v = setVal;
- }
編程實(shí)例
調(diào)用原子操作相關(guān)接口,觀察結(jié)果:
1.創(chuàng)建兩個(gè)任務(wù)
● 任務(wù)一用LOS_AtomicAdd對(duì)全局變量加100次。
● 任務(wù)二用LOS_AtomicSub對(duì)全局變量減100次。
2.子任務(wù)結(jié)束后在主任務(wù)中打印全局變量的值。
- #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;
- }
結(jié)果驗(yàn)證
- g_sum = 0
想了解更多內(nèi)容,請(qǐng)?jiān)L問(wèn):
51CTO和華為官方合作共建的鴻蒙技術(shù)社區(qū)