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

華為雲開發者社群發表於2021-02-27
摘要:除了多核的自旋鎖機制,本文會介紹下LiteOS 5.0引入的LockDep死鎖檢測特性。

2020年12月釋出的LiteOS 5.0推出了全新的核心,支援SMP多核排程功能。想學習SMP多核排程功能,需要了解下SpinLock自旋鎖。除了多核的自旋鎖機制,本文還會介紹下LiteOS 5.0引入的LockDep死鎖檢測特性。

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

自旋鎖SpinLock原始碼、開發文件,LockDep死鎖檢測特性程式碼文件列表如下:

我們首先來看看自旋鎖。

1、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);
}

2、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的結束點。如圖所示:

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

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關係示意圖:

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

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死鎖檢測

本文分享自華為雲社群《LiteOS核心原始碼分析系列二 SpinLock自旋鎖及LockDep死鎖檢測》,原文作者:zhushy。

 

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

相關文章