LiteOS:SpinLock自旋鎖及LockDep死鎖檢測

摘要:除了多核的自旋鎖機制,本文會介紹下LiteOS 5.0引入的LockDep死鎖檢測特性。

2020年12月發佈的LiteOS 5.0推出了全新的內核,支持SMP多核調度功能。想學習SMP多核調度功能,須要瞭解下SpinLock自旋鎖。除了多核的自旋鎖機制,本文還會介紹下LiteOS 5.0引入的LockDep死鎖檢測特性。git

本文中所涉及的LiteOS源碼,都可以在LiteOS開源站點https://gitee.com/LiteOS/LiteOS 獲取。數組

自旋鎖SpinLock源代碼、開發文檔,LockDep死鎖檢測特性代碼文檔列表以下:架構

  • kernel\include\los_spinlock.h 自旋鎖頭文件ide

    網頁獲取自旋鎖源碼 https://gitee.com/LiteOS/LiteOS/blob/master/kernel/include/los_spinlock.h。函數

  • spinlock.S、arch/spinlock.h 自旋鎖彙編代碼文件及頭文件源碼分析

    針對不一樣的CPU架構,有兩套代碼。因爲自旋鎖適用於多核,M核架構arch\arm\cortex_m下不包含自旋鎖的彙編文件。以下:性能

    • arch\arm\cortex_a_r架構學習

      • 彙編代碼文件
        https://gitee.com/LiteOS/LiteOS/blob/master/arch/arm/cortex_a_r/src/spinlock.S。ui

      • 頭文件
        https://gitee.com/LiteOS/LiteOS/blob/master/arch/arm/cortex_a_r/include/arch/spinlock.h。url

    • arch\arm64架構

      • 彙編代碼文件 https://gitee.com/LiteOS/LiteOS/blob/master/arch/arm64/src/spinlock.S。

      • 頭文件
        https://gitee.com/LiteOS/LiteOS/blob/master/arch/arm64/include/arch/spinlock.h。

  • 開發指南自旋鎖文檔

    在線文檔
    https://gitee.com/LiteOS/LiteOS/blob/master/doc/LiteOS_Kernel_Developer_Guide.md#%E8%87%AA%E6%97%8B%E9%94%81。

  • LockDep死鎖檢測

    死鎖檢測代碼包含:

    • 頭文件
      https://gitee.com/LiteOS/LiteOS/blob/master/kernel/include/los_lockdep.h
    • C代碼文件
      https://gitee.com/LiteOS/LiteOS/blob/master/kernel/base/los_lockdep.c。

咱們首先來看看自旋鎖。

一、SpinLock 自旋鎖

在多核環境中,因爲使用相同的內存空間,存在對同一資源進行訪問的狀況,因此須要互斥訪問機制來保證同一時刻只有一個核進行操做。自旋鎖就是這樣的一種機制。

自旋鎖是指當一個線程在獲取鎖時,若是鎖已經被其它線程獲取,那麼該線程將循環等待,並不斷判斷是否可以成功獲取鎖,直到獲取到鎖纔會退出循環。所以建議保護耗時較短的操做,防止對系統總體性能有明顯的影響。

自旋鎖與互斥鎖比較相似,它們都是爲了解決對共享資源的互斥使用問題。不管是互斥鎖,仍是自旋鎖,在任什麼時候刻,最多隻能有一個持有者。可是二者在調度機制上略有不一樣,對於互斥鎖,若是鎖已經被佔用,鎖申請者會被阻塞;可是自旋鎖不會引發調用者阻塞,會一直循環檢測自旋鎖是否已經被釋放。自旋鎖用於多核不一樣CPU覈對資源的互斥訪問,互斥鎖用於同一CPU核內不一樣任務對資源的互斥訪問。

自旋鎖SpinLock核心的代碼都在kernel\include\los_spinlock.h頭文件中,包含struct Spinlock結構體定義、一些inline內聯函數LOS_SpinXXX,還有一些LockDep死鎖檢測相關的宏定義LOCKDEP_XXXX。

1.1 Spinlock 自旋鎖結構體

自旋鎖結構體Spinlock定義以下,主要的成員變量爲size_t rawLock,這是自旋鎖是否佔用持有的成功的標記:爲0時,鎖沒有被持有,爲1時表示被成功持有。當開啓LockDep死鎖檢測調測特性時,會使能另外3個成員變量,記錄持有自旋鎖的CPU核信息、任務信息。

struct Spinlock {
    size_t      rawLock;            /**< 原始自旋鎖 */
#ifdef LOSCFG_KERNEL_SMP_LOCKDEP
    UINT32      cpuid;              /**< 死鎖檢測特性開啓時,持有自旋鎖的CPU核 */
    VOID        *owner;             /**< 死鎖檢測特性開啓時,持有自旋鎖的任務的TCB指針 */
    const CHAR  *name;              /**< 死鎖檢測特性開啓時,持有自旋鎖的任務的名稱 */
#endif
};

1.2 Spinlock 自旋鎖經常使用函數接口

LiteOS自旋鎖模塊爲用戶提供下面幾種功能,包含自旋鎖初始化,申請/釋放,查詢自旋鎖狀態等。自旋鎖相關的函數、宏定義只支持SMP - Symmetric MultiProcessor模式,當單核UP - UniProcessor時,函數不生效。接口詳細信息能夠查看API參考。

1.2.1 自旋鎖初始化

自旋鎖初始化的內聯函數以下,其中參數SPIN_LOCK_S *lock,即自旋鎖結構體指針,其中SPIN_LOCK_S是Spinlock的typedef別名,在kernel\include\los_lockdep.h文件中定義的。

自旋鎖初始時,會把自旋鎖標記爲0:lock->rawLock = 0,當開啓死鎖檢測特性時,也會作相應的初始化。

LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinInit(SPIN_LOCK_S *lock)
{
    lock->rawLock     = 0;
#ifdef LOSCFG_KERNEL_SMP_LOCKDEP
    lock->cpuid       = (UINT32)-1;
    lock->owner       = SPINLOCK_OWNER_INIT;
    lock->name        = "spinlock";
#endif
}

LOS_SpinInit()是動態初始化的自旋鎖,LiteOS還提供了靜態初始化自旋鎖的方法SPIN_LOCK_INIT(lock):

#define SPIN_LOCK_INIT(lock)  SPIN_LOCK_S lock = SPIN_LOCK_INITIALIZER(lock)

1.2.2 申請/釋放自旋鎖

初始化自旋鎖後,能夠以SPIN_LOCK_S *lock爲參數申請、釋放自旋鎖。自旋鎖的這些函數中,調用的LOCKDEP_開頭函數是死鎖檢測的函數,後文會詳細講述。核心的3個函數由彙編語言編寫,這些彙編函數存,根據不一樣的CPU架構,能夠在文件arch\arm\cortex_a_r\src\spinlock.S或arch\arm64\src\spinlock.S中查看,此文再也不詳細講述其彙編代碼。

ArchSpinLock(&lock->rawLock);  // 彙編語言編寫的 申請自旋鎖的函數
ArchSpinUnlock(&lock->rawLock); // 彙編語言編寫的 釋放自旋鎖的函數
ArchSpinTrylock(&lock->rawLock); // 彙編語言編寫的 嘗試申請自旋鎖的函數
  • STATIC INLINE VOID LOS_SpinLock(SPIN_LOCK_S *lock) 申請自旋鎖

該函數嘗試申請自旋鎖,若是自旋鎖鎖被其餘核佔用,則循環等待,直至其餘核釋放自旋鎖。

咱們看下代碼首先執行⑴處代碼,暫停任務調度,而後執行彙編函數ArchSpinLock(&lock->rawLock)申請自旋鎖。

LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinLock(SPIN_LOCK_S *lock)
{
⑴  LOS_TaskLock();
    LOCKDEP_CHECK_IN(lock);
⑵  ArchSpinLock(&lock->rawLock);
    LOCKDEP_RECORD(lock);
}
  • STATIC INLINE VOID LOS_SpinUnlock(SPIN_LOCK_S *lock) 釋放自旋鎖

釋放自旋鎖LOS_SpinUnlock(SPIN_LOCK_S *lock)須要和申請自旋鎖的函數LOS_SpinLock(SPIN_LOCK_S *lock)成對使用。執行⑴處彙編函數ArchSpinUnlock(&lock->rawLock)釋放自旋鎖,而後執行⑵恢復任務調度功能。

LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinUnlock(SPIN_LOCK_S *lock)
{
    LOCKDEP_CHECK_OUT(lock);
⑴  ArchSpinUnlock(&lock->rawLock);
⑵  LOS_TaskUnlock();
}
  • STATIC INLINE INT32 LOS_SpinTrylock(SPIN_LOCK_S *lock) 嘗試申請自旋鎖

嘗試申請指定的自旋鎖,若是沒法獲取鎖,直接返回失敗,而不會一直循環等待。用戶根據返回值,判斷是否成功申請到自旋鎖,而後再作後續業務處理。和函數LOS_SpinLock(SPIN_LOCK_S *lock)執行的彙編函數不一樣,該函數調用的彙編函數爲ArchSpinTrylock(&lock->rawLock),並有返回值。

LITE_OS_SEC_ALW_INLINE STATIC INLINE INT32 LOS_SpinTrylock(SPIN_LOCK_S *lock)
{
    LOS_TaskLock();
    LOCKDEP_CHECK_IN(lock);
⑴  INT32 ret = ArchSpinTrylock(&lock->rawLock);
    if (ret == LOS_OK) {
        LOCKDEP_RECORD(lock);
    }
    return ret;
}

1.2.3 申請/釋放自旋鎖(同時進行關中斷保護)

LiteOS 還提供一對支持關中斷保護的申請/釋放指定自旋鎖的函數,除了參數SPIN_LOCK_S *lock,還須要參數UINT32 *intSave用於關中斷、恢復中斷。LOS_SpinLockSave()和LOS_SpinUnlockRestore()必須成對使用。

  • STATIC INLINE VOID LOS_SpinLockSave(SPIN_LOCK_S *lock, UINT32 *intSave) 關中斷後,再申請指定的自旋鎖值

從代碼中,能夠看出首先執行LOS_IntLock()關中斷,而後再調用LOS_SpinLock(lock)申請自旋鎖。

LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinLockSave(SPIN_LOCK_S *lock, UINT32 *intSave)
{
    *intSave = LOS_IntLock();
    LOS_SpinLock(lock);
}
  • STATIC INLINE VOID LOS_SpinUnlockRestore(SPIN_LOCK_S *lock, UINT32 *intSave) 關中斷後,再申請指定的自旋鎖值。

從代碼中,能夠看出首先調用LOS_SpinUnlock(lock)釋放自旋鎖,而後再調用LOS_IntRestore(intSave)恢復中斷。

LITE_OS_SEC_ALW_INLINE STATIC INLINE VOID LOS_SpinUnlockRestore(SPIN_LOCK_S *lock, UINT32 intSave)

{
    LOS_SpinUnlock(lock);
    LOS_IntRestore(intSave);
}

1.2.4 獲取自旋鎖持有狀態

可使用函數BOOL LOS_SpinHeld(const SPIN_LOCK_S *lock)查詢自旋鎖的持有狀態,返回TRUE,自旋鎖鎖被持有,返回FALSE時表示沒有被持有:

LITE_OS_SEC_ALW_INLINE STATIC INLINE BOOL LOS_SpinHeld(const SPIN_LOCK_S *lock)
{
    return (lock->rawLock != 0);
}

二、LockDep 死鎖檢測調測特性

LockDep是Lock Dependency Check的縮寫,是內核的一種死鎖檢測機制。這個調測特性默認是關閉的,若是須要該調測特性,須要使能宏定義LOSCFG_KERNEL_SMP_LOCKDEP。當檢測到死鎖錯誤時,會打印發生死鎖的自旋鎖的相關信息,打印backtrace回溯棧信息。

2.1 LockDep 自旋鎖的錯誤類型及結構體定義

在文件kernel\include\los_lockdep.h中定義了死鎖的枚舉類型LockDepErrType及HeldLocks結構體。

自旋鎖的錯誤類型有double lock重複申請鎖、dead lock死鎖、unlock without lock釋放未持有的鎖、lockdep overflow死鎖檢測溢出,超出定義的MAX_LOCK_DEPTH。

結構體LockDep是任務LosTaskCB結構體的開啓LOSCFG_KERNEL_SMP_LOCKDEP時的一個成員變量,記錄該任務持有的自旋鎖、須要申請的自旋鎖的信息。結構體HeldLocks記錄持有的自旋鎖的詳細信息,各個成員變量見以下注釋:

typedef struct Spinlock SPIN_LOCK_S;

#define MAX_LOCK_DEPTH  16U

enum LockDepErrType {
    LOCKDEP_SUCEESS = 0,
    LOCKDEP_ERR_DOUBLE_LOCK, // double lock 重複申請鎖
    LOCKDEP_ERR_DEAD_LOCK,  // dead lock 死鎖
    LOCKDEP_ERR_UNLOCK_WITHOUT_LOCK, // unlock without lock 釋放未持有的鎖
    LOCKDEP_ERR_OVERFLOW, // lockdep overflow 死鎖檢測溢出
};

typedef struct {
    VOID *lockPtr; // Spinlock 自旋鎖的內存地址
    VOID *lockAddr; // 請求鎖的函數的返回地址
    UINT64 waitTime; // 搶佔申請自旋鎖的等待時間
    UINT64 holdTime;  // 持有自旋鎖的時間
} HeldLocks;

typedef struct {
    VOID *waitLock; // 任務申請佔用的自旋鎖Spinlock
    INT32 lockDepth; // 自旋鎖的深度
    HeldLocks heldLocks[MAX_LOCK_DEPTH]; // 持有的自旋鎖詳細信息數組
} LockDep;

2.2 LockDep 死鎖檢測的經常使用函數接口

LockDep 死鎖檢測特性提供了3個函數接口,在申請自旋鎖前、成功申請到自旋鎖後、釋放自旋鎖後打點調用。另外,提供了一些其餘經常使用函數接口。

咱們先看下,死鎖檢測函數如何記錄等待時間waitTime、持有時間holdTime的。在申請自旋鎖前調用OsLockDepCheckIn(),記錄waitTime的起點;成功申請到自旋鎖後,調用OsLockDepRecord()記錄waitTime的結束點,同時記錄記錄holdTime的起點;釋放自旋鎖後調用OsLockDepCheckOut()記錄holdTime的結束點。如圖所示:

2.2.1 OsLockDepCheckIn(const SPIN_LOCK_S *lock) 記錄申請自旋鎖

咱們一塊兒分析下代碼,看看申請自旋鎖前死鎖檢測特性作了哪些操做。⑴處代碼獲取請求自旋鎖的函數返回地址。⑵獲取當前任務的TCB,而後獲取它的死鎖檢測成員LockDep *lockDep。⑶、⑽處兩個函數配對使用,前者先關中斷,而後等待、佔用死鎖檢測特性、設置STATIC Atomic g_lockdepAvailable爲0,後者釋放鎖檢測特性,設置STATIC Atomic g_lockdepAvailable爲1,而後恢復中斷。

⑷處代碼判斷當前任務持有的自旋鎖是否超過死鎖檢測特性設置的自旋鎖數量的最大值MAX_LOCK_DEPTH,若是超過,則報溢出錯誤,跳轉到OUT繼續執行。⑸處代碼,若是申請的自旋鎖沒有被任何CPU核持有,能夠直接佔有,無需等待,跳轉到OUT繼續執行。⑹處代碼,若是申請的自旋鎖被當前任務持有,則報重複申請自旋鎖錯誤,跳轉到OUT繼續執行。⑺處判斷是否發生死鎖,稍後再分析函數OsLockDepCheckDependancy()。

⑻處代碼,若是檢測結果經過,能夠持有自旋鎖,則記錄相關信息,包含要申請的自旋鎖、申請鎖的函數返回地址、申請自旋鎖的開始時間。不然執行⑼處代碼,輸出死鎖錯誤信息。

VOID OsLockDepCheckIn(const SPIN_LOCK_S *lock)
{
    UINT32 intSave;
    enum LockDepErrType checkResult = LOCKDEP_SUCEESS;
⑴  VOID *requestAddr = (VOID *)__builtin_return_address(0);
⑵  LosTaskCB *current = OsCurrTaskGet();
    LockDep *lockDep = &current->lockDep;
    LosTaskCB *lockOwner = NULL;
    if (lock == NULL) {
        return;
    }
⑶   OsLockDepRequire(&intSave);
⑷  if (lockDep->lockDepth >= (INT32)MAX_LOCK_DEPTH) {
        checkResult = LOCKDEP_ERR_OVERFLOW;
        goto OUT;
    }
    lockOwner = lock->owner;
⑸  if (lockOwner == SPINLOCK_OWNER_INIT) {
        goto OUT;
    }
⑹  if (current == lockOwner) {
        checkResult = LOCKDEP_ERR_DOUBLE_LOCK;
        goto OUT;
    }
⑺  if (OsLockDepCheckDependancy(current, lockOwner) != TRUE) {
        checkResult = LOCKDEP_ERR_DEAD_LOCK;
        goto OUT;
    }
OUT:
⑻  if (checkResult == LOCKDEP_SUCEESS) {
        lockDep->waitLock = (SPIN_LOCK_S *)lock;
        lockDep->heldLocks[lockDep->lockDepth].lockAddr = requestAddr;
        lockDep->heldLocks[lockDep->lockDepth].waitTime = OsLockDepGetCycles(); /* start time */
    } else {
⑼      OsLockDepDumpLock(current, lock, requestAddr, checkResult);
    }
⑽  OsLockDepRelease(intSave);
}

咱們再分析下死鎖檢測的函數OsLockDepCheckDependancy(),循環判斷嵌套申請的自旋鎖是否會發生死鎖,包含2個參數,第一個參數是申請自旋鎖的任務LosTaskCB *current,第二個參數爲持有自旋鎖的任務LosTaskCB *lockOwner:

⑴處代碼,若是申請自旋鎖的任務和持有鎖的任務同一個,則發生死鎖。⑵處代碼,若是持有自旋鎖的任務,還在申請其餘自旋鎖,則把lockOwner指向其餘自旋鎖的任務TCB,不然退出循環。⑶若是自旋鎖被佔用則一直循環。

STATIC BOOL OsLockDepCheckDependancy(const LosTaskCB *current, const LosTaskCB *lockOwner)
{
    BOOL checkResult = TRUE;
    const SPIN_LOCK_S *lockTemp = NULL;
    do {
⑴      if (current == lockOwner) {
            checkResult = FALSE;
            return checkResult;
        }
⑵      if (lockOwner->lockDep.waitLock != NULL) {
            lockTemp = lockOwner->lockDep.waitLock;
            lockOwner = lockTemp->owner;
        } else {
            break;
        }
⑶  } while (lockOwner != SPINLOCK_OWNER_INIT);
    return checkResult;
}

死鎖檢測TCB、LockDep、Spinlock關係示意圖:

2.2.2 OsLockDepRecord(const SPIN_LOCK_S *lock) 記錄申請到的自旋鎖

咱們繼續分析,當申請自旋鎖後,死鎖檢測特性作了哪些操做。⑴處代碼獲取系統運行以來的cycle數目,而後計算waitTime,即從開始申請自旋鎖到申請到自旋鎖以前的cycle數目,同時記錄持有自旋鎖的holdTime的開始時間。⑵處代碼更新自旋鎖的信息,鎖被當前任務持有,CPU核設置爲當前核。⑶處更新死鎖檢測lockDep的信息,持有鎖的數目加1,等待鎖置空。

VOID OsLockDepRecord(SPIN_LOCK_S *lock)
{
    UINT32 intSave;
    UINT64 cycles;
    LosTaskCB *current = OsCurrTaskGet();
    LockDep *lockDep = &current->lockDep;
    HeldLocks *heldlock = &lockDep->heldLocks[lockDep->lockDepth];
    if (lock == NULL) {
        return;
    }
    OsLockDepRequire(&intSave);
⑴  cycles = OsLockDepGetCycles();
    heldlock->waitTime = cycles - heldlock->waitTime;
    heldlock->holdTime = cycles;
⑵  lock->owner = current;
    lock->cpuid = ArchCurrCpuid();
⑶  heldlock->lockPtr = lock;
    lockDep->lockDepth++;
    lockDep->waitLock = NULL;
    OsLockDepRelease(intSave);
}

2.2.3 OsLockDepCheckOut(const SPIN_LOCK_S *lock) 記錄釋放自旋鎖

咱們再分析下,當釋放自旋鎖後,死鎖檢測特性作了哪些操做。⑴處代碼表示,當釋放一個沒有佔用的自旋鎖,會調用函數OsLockDepDumpLock()打印死鎖檢測錯誤信息。⑵處代碼先獲取持有鎖的任務TCB的死鎖檢測變量lockDep,而後獲取其持有鎖數組的起始地址,即指針變量heldlocks。⑶獲取持有鎖的數目,而後執行⑷,對持有的鎖進行循環遍歷,定位到自旋鎖*lock的數組索引,再執行⑸處代碼更新持有鎖的總時間。

⑹處代碼,判斷若是釋放的鎖,不是任務持有鎖數組的最後一個,則移動數組後面的元素,數組元素也須要減小1。最後,執行⑺更新自旋鎖的沒有被任何CPU核、任何任務佔用。

VOID OsLockDepCheckOut(SPIN_LOCK_S *lock)
{
    UINT32 intSave;
    INT32 depth;
    VOID *requestAddr = (VOID *)__builtin_return_address(0);  
    LosTaskCB *current = OsCurrTaskGet();
    LosTaskCB *owner = NULL;
    LockDep *lockDep = NULL;
    HeldLocks *heldlocks = NULL;
    if (lock == NULL) {
        return;
    }
    OsLockDepRequire(&intSave);
    owner = lock->owner;
⑴  if (owner == SPINLOCK_OWNER_INIT) {
        OsLockDepDumpLock(current, lock, requestAddr, LOCKDEP_ERR_UNLOCK_WITHOUT_LOCK);
        goto OUT;
    }
    lockDep = &owner->lockDep;
⑵  heldlocks = &lockDep->heldLocks[0];
⑶  depth = lockDep->lockDepth;
    while (depth-- >= 0) {
⑷      if (heldlocks[depth].lockPtr == lock) {
            break;
        }
    }
    LOS_ASSERT(depth >= 0);
⑸  heldlocks[depth].holdTime = OsLockDepGetCycles() - heldlocks[depth].holdTime;
⑹  while (depth < lockDep->lockDepth - 1) {
        lockDep->heldLocks[depth] = lockDep->heldLocks[depth + 1];
        depth++;
    }
    lockDep->lockDepth--;
⑺  lock->cpuid = (UINT32)(-1);
    lock->owner = SPINLOCK_OWNER_INIT;

OUT:
    OsLockDepRelease(intSave);
}

2.2.4 OsLockdepClearSpinlocks(VOID) 釋放持有的自旋鎖

該函數OsLockdepClearSpinlocks()會所有釋放當前任務持有的自旋鎖。在arch\arm\cortex_a_r\src\fault.c文件中,異常處理函數OsExcHandleEntry()經過調用LOCKDEP_CLEAR_LOCKS()實現對該函數的調用。

⑴處代碼獲取當前任務死鎖檢測變量lockDep,而後⑵處循環變量持有的自旋鎖,獲取自旋鎖並調用LOS_SpinUnlock()進行釋放。

VOID OsLockdepClearSpinlocks(VOID)
{
    LosTaskCB *task = OsCurrTaskGet();
⑴  LockDep *lockDep = &task->lockDep;
    SPIN_LOCK_S *lock = NULL;
    while (lockDep->lockDepth) {
⑵      lock = lockDep->heldLocks[lockDep->lockDepth - 1].lockPtr;
        LOS_SpinUnlock(lock);
    }
}

小結

本文帶領你們一塊兒剖析了SpinLock自旋鎖,LockDep死鎖檢測特性的源代碼,結合講解,參考官方示例程序代碼,本身寫寫程序,實際編譯運行一下,加深理解。

感謝閱讀,若有任何問題、建議,均可以留言給咱們: https://gitee.com/LiteOS/LiteOS/issues 。爲了更容易找到LiteOS代碼倉,建議訪問 https://gitee.com/LiteOS/LiteOS  關注Watch、點贊Star、並Fork到本身帳戶下,以下圖,謝謝。

本文分享自華爲雲社區《LiteOS內核源碼分析系列二 SpinLock自旋鎖及LockDep死鎖檢測》,原文做者:zhushy。

 

點擊關注,第一時間瞭解華爲雲新鮮技術~

相關文章
相關標籤/搜索