內核中哪些地方會用到自旋鎖?看圖:
自旋鎖顧名思義,是一把自動旋轉的鎖,這很像廁所里的鎖,進入前標記是綠色可用的,進入格子間后,手一帶,里面的鎖轉個圈,外面標記變成了紅色表示在使用,外面的只能等待.這是形象的比喻,但實際也是如此.
在多CPU核環境中,由于使用相同的內存空間,存在對同一資源進行訪問的情況,所以需要互斥訪問機制來保證同一時刻只有一個核進行操作,自旋鎖就是這樣的一種機制。
自旋鎖是指當一個線程在獲取鎖時,如果鎖已經被其它CPU中的線程獲取,那么該線程將循環等待,并不斷判斷是否能夠成功獲取鎖,直到其它CPU釋放鎖后,等鎖CPU才會退出循環。
自旋鎖的設計理念是它僅會被持有非常短的時間,鎖只能被一個任務持有,而且持有自旋鎖的CPU是不可以進入睡眠模式的,因為其他的CPU在等待鎖,為了防止死鎖上下文交換也是不允許的,是禁止發生調度的.
自旋鎖與互斥鎖比較類似,它們都是為了解決對共享資源的互斥使用問題。無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個持有者。但是兩者在調度機制上略有不同,對于互斥鎖,如果鎖已經被占用,鎖申請者會被阻塞;但是自旋鎖不會引起調用者阻塞,會一直循環檢測自旋鎖是否已經被釋放。
雖然都是共享資源競爭,但自旋鎖強調的是CPU核間的競爭,而互斥量強調的是任務(包括同一CPU核)之間的競爭.
自旋鎖長什么樣?
typedef struct Spinlock {//自旋鎖結構體
size_t rawLock;//原始鎖 #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES) // 死鎖檢測模塊開關 UINT32 cpuid; //持有鎖的CPU VOID *owner; //持有鎖任務 const CHAR *name; //鎖名稱 #endif } SPIN_LOCK_S;
結構體很簡單,里面有個宏,用于死鎖檢測,默認情況下是關閉的.所以真正的被使用的變量只有rawLock一個.但C語言代碼中找不到變量的變化過程,而是通過一段匯編代碼來實現.看完本篇會明白也只能通過匯編代碼來實現自旋鎖.
自旋鎖使用流程
自旋鎖用于多CPU核的情況,解決的是CPU之間競爭資源的問題.使用流程很簡單,三步走。
創建自旋鎖:使用LOS_SpinInit初始化自旋鎖,或者使用SPIN_LOCK_INIT初始化靜態內存的自旋鎖。
申請自旋鎖:使用接口LOS_SpinLockLOS_SpinTrylockLOS_SpinLockSave申請指定的自旋鎖,申請成功就繼續往后執行鎖保護的代碼;申請失敗在自旋鎖申請中忙等,直到申請到自旋鎖為止。
釋放自旋鎖:使用LOS_SpinUnlockLOS_SpinUnlockRestore接口釋放自旋鎖。鎖保護代碼執行完畢后,釋放對應的自旋鎖,以便其他核申請自旋鎖。
幾個關鍵函數
自旋鎖模塊由內聯函數實現,見于los_spinlock.h代碼不多,主要是三個函數.
ArchSpinLock(&lock->rawLock); ArchSpinTrylock(&lock->rawLock) ArchSpinUnlock(&lock->rawLock);
可以說掌握了它們就掌握了自旋鎖,但這三個函數全由匯編實現.見于los_dispatch.S文件 因為系列篇已有兩篇講過匯編代碼,所以很容易理解這三段代碼.函數的參數由r0記錄,即r0保存了lock->rawLock的地址,拿鎖/釋放鎖是讓lock->rawLock在0,1切換 下面逐一說明自旋鎖的匯編代碼.
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函數
看懂了這段匯編代碼就理解了自旋鎖實現的真正機制,為什么一定要用匯編來實現. 因為CPU寧愿睡眠也非拿要到鎖不可的, 注意這里可不是讓線程睡眠,而是讓CPU進入睡眠狀態,能讓CPU進入睡眠的只能通過匯編實現.C語言根本就寫不出讓CPU真正睡眠的代碼.
ArchSpinTrylock 匯編代碼
如果不看下面這段匯編代碼,你根本不可能知道 ArchSpinTrylock 和 ArchSpinLock的真正區別是什么.
FUNCTION(ArchSpinTrylock) @嘗試拿鎖,拿不到就撤 mov r1, #1 @r1=1 mov r2, r0 @r2 = r0 ldrex r0, [r2] @r2 = &lock->rawLock, 即 r0 = lock->rawLock cmp r0, #0 @r0和0比較 strexeq r0, r1, [r2] @嘗試令lock->rawLock=1,成功寫入則r0=0,否則 r0 =1 dmb @數據存儲隔離,以保證緩沖中的數據已經落實到RAM中 bx lr @跳回調用ArchSpinLock函數
比較兩段匯編代碼可知,ArchSpinTrylock即沒有循環也不會讓CPU進入睡眠,直接返回了,而ArchSpinLock會睡了醒, 醒了睡,一直守到丈夫(lock->rawLock = 0的廣播事件發生)回來才肯罷休. 筆者代碼注釋到這里那真是心潮澎湃,心碎了老一地, 真想給ArchSpinLock立一個貞節牌坊!
ArchSpinUnlock 匯編代碼
FUNCTION(ArchSpinUnlock) @釋放鎖 mov r1, #0 @r1=0 dmb @數據存儲隔離,以保證緩沖中的數據已經落實到RAM中 str r1, [r0] @令lock->rawLock = 0 dsb @數據同步隔離 sev @給各CPU廣播事件,喚醒沉睡的CPU們 bx lr @跳回調用ArchSpinLock函數
代碼中涉及到幾個不常用的匯編指令,一一說明:
匯編指令之 WFI / WFE / SEV
WFI(Wait for interrupt):等待中斷到來指令.WFI一般用于cpuidle,WFI 指令是在處理器發生中斷或類似異常之前不需要做任何事情。
在鴻蒙源碼分析系列篇(總目錄)線程篇中已說過,每個CPU都有自己的idle任務,CPU沒事干的時候就待在里面,就一個死循環守著WFI指令,有中斷來了就觸發CPU起床干活. 中斷分硬中斷和軟中斷,系統調用就是通過軟中斷實現的,而設備類的就屬于硬中斷,都能觸發CPU干活. 具體看下CPU空閑的時候在干嘛,代碼超級簡單:
LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID) //CPU沒事干的時候待在這里 { while (1) {//只有一個死循環 Wfi();//WFI指令:arm core 立即進入low-power standby state,等待中斷,進入休眠模式。 } }
WFE(Wait for event):等待事件的到來指令WFE指令是在SEV指令生成事件之前不需要執行任何操作,所以用WFE的地方,后續一定會對應一個SEV的指令去喚醒它. WFE的一個典型使用場景,是用在自旋鎖中,spinlock的功能,是在不同CPU core之間,保護共享資源。使用WFE的流程是:
開始之初資源空閑
CPU核1 訪問資源,持有鎖,獲得資源
CPU核2 訪問資源,此時資源不空閑,執行WFE指令,讓core進入low-power state(睡眠)
CPU核1 釋放資源,釋放鎖,釋放資源,同時執行SEV指令,喚醒CPU核2
CPU核2 獲得資源
另外說一下 以往的自旋鎖,在獲得不到資源時,讓CPU核進入死循環,而通過插入WFE指令,則大大節省功耗.
SEV(send event):發送事件指令,SEV是一條廣播指令,它會將事件發送到多處理器系統中的所有處理器,以喚醒沉睡的CPU.
SEV和WFE的實現很像設計模式的觀察者模式.
匯編指令之 LDREX / STREX
LDREX用來讀取內存中的值,并標記對該段內存的獨占訪問:
LDREX Rx, [Ry]上面的指令意味著,讀取寄存器Ry指向的4字節內存值,將其保存到Rx寄存器中,同時標記對Ry指向內存區域的獨占訪問。
如果執行LDREX指令的時候發現已經被標記為獨占訪問了,并不會對指令的執行產生影響。
而STREX在更新內存數值時,會檢查該段內存是否已經被標記為獨占訪問,并以此來決定是否更新內存中的值:
STREX Rx, Ry, [Rz]如果執行這條指令的時候發現已經被標記為獨占訪問了,則將寄存器Ry中的值更新到寄存器Rz指向的內存,并將寄存器Rx設置成0。指令執行成功后,會將獨占訪問標記位清除。
而如果執行這條指令的時候發現沒有設置獨占標記,則不會更新內存,且將寄存器Rx的值設置成1。
一旦某條STREX指令執行成功后,以后再對同一段內存嘗試使用STREX指令更新的時候,會發現獨占標記已經被清空了,就不能再更新了,從而實現獨占訪問的機制。
編程實例
本實例實現如下流程。
任務Example_TaskEntry初始化自旋鎖,創建兩個任務Example_SpinTask1、Example_SpinTask2,分別運行于兩個核。
Example_SpinTask1、Example_SpinTask2中均執行申請自旋鎖的操作,同時為了模擬實際操作,在持有自旋鎖后進行延遲操作,最后釋放自旋鎖。
300Tick后任務Example_TaskEntry被調度運行,刪除任務Example_SpinTask1和Example_SpinTask2。
#include "los_spinlock.h"
#include "los_task.h"
/* 自旋鎖句柄id */
SPIN_LOCK_S g_testSpinlock;
/* 任務ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;
VOID Example_SpinTask1(VOID)
{
UINT32 i;
UINTPTR intSave;
/* 申請自旋鎖 */
dprintf("task1 try to get spinlock\n");
LOS_SpinLockSave(&g_testSpinlock, &intSave);
dprintf("task1 got spinlock\n");
for(i = 0; i < 5000; i++) {
asm volatile("nop");
}
/* 釋放自旋鎖 */
dprintf("task1 release spinlock\n");
LOS_SpinUnlockRestore(&g_testSpinlock, intSave);
return;
}
VOID Example_SpinTask2(VOID)
{
UINT32 i;
UINTPTR intSave;
/* 申請自旋鎖 */
dprintf("task2 try to get spinlock\n");
LOS_SpinLockSave(&g_testSpinlock, &intSave);
dprintf("task2 got spinlock\n");
for(i = 0; i < 5000; i++) {
asm volatile("nop");
}
/* 釋放自旋鎖 */
dprintf("task2 release spinlock\n");
LOS_SpinUnlockRestore(&g_testSpinlock, intSave);
return;
}
UINT32 Example_TaskEntry(VOID)
{
UINT32 ret;
TSK_INIT_PARAM_S stTask1;
TSK_INIT_PARAM_S stTask2;
/* 初始化自旋鎖 */
LOS_SpinInit(&g_testSpinlock);
/* 創建任務1 */
memset(&stTask1, 0, sizeof(TSK_INIT_PARAM_S));
stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SpinTask1;
stTask1.pcName = "SpinTsk1";
stTask1.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
stTask1.usTaskPrio = 5;
#ifdef LOSCFG_KERNEL_SMP
/* 綁定任務到CPU0運行 */
stTask1.usCpuAffiMask = CPUID_TO_AFFI_MASK(0);
#endif
ret = LOS_TaskCreate(&g_testTaskId01, &stTask1);
if(ret != LOS_OK) {
dprintf("task1 create failed .\n");
return LOS_NOK;
}
/* 創建任務2 */
memset(&stTask2, 0, sizeof(TSK_INIT_PARAM_S));
stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SpinTask2;
stTask2.pcName = "SpinTsk2";
stTask2.uwStackSize = LOSCFG_TASK_MIN_STACK_SIZE;
stTask2.usTaskPrio = 5;
#ifdef LOSCFG_KERNEL_SMP
/* 綁定任務到CPU1運行 */
stTask1.usCpuAffiMask = CPUID_TO_AFFI_MASK(1);
#endif
ret = LOS_TaskCreate(&g_testTaskId02, &stTask2);
if(ret != LOS_OK) {
dprintf("task2 create failed .\n");
return LOS_NOK;
}
/* 任務休眠300Ticks */
LOS_TaskDelay(300);
/* 刪除任務1 */
ret = LOS_TaskDelete(g_testTaskId01);
if(ret != LOS_OK) {
dprintf("task1 delete failed .\n");
return LOS_NOK;
}
/* 刪除任務2 */
ret = LOS_TaskDelete(g_testTaskId02);
if(ret != LOS_OK) {
dprintf("task2 delete failed .\n");
return LOS_NOK;
}
return LOS_OK;
}
運行結果
task2 try to get spinlock task2 got spinlock task1 try to get spinlock task2 release spinlock task1 got spinlock task1 release spinlock
總結
自旋鎖用于解決CPU核間競爭資源的問題
因為自旋鎖會讓CPU陷入睡眠狀態,所以鎖的代碼不能太長,否則容易導致意外出現,也影響性能.
必須由匯編代碼實現,因為C語言寫不出讓CPU進入真正睡眠,核間競爭的代碼.
編輯:hfy
-
cpu
+關注
關注
68文章
10826瀏覽量
211160 -
鴻蒙系統
+關注
關注
183文章
2634瀏覽量
66220 -
自旋鎖
+關注
關注
0文章
11瀏覽量
1579
發布評論請先 登錄
相關推薦
評論