乐趣区

关于死锁:LiteOSSpinLock自旋锁及LockDep死锁检测

摘要:除了多核的自旋锁机制,本文会介绍下 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死锁检测个性代码文档列表如下:

  • kernelincludelos_spinlock.h 自旋锁头文件

    网页获取自旋锁源码 https://gitee.com/LiteOS/Lite…。

  • spinlock.S、arch/spinlock.h 自旋锁汇编代码文件及头文件

    针对不同的 CPU 架构,有两套代码。因为自旋锁实用于多核,M核架构 archarmcortex_m 下不蕴含自旋锁的汇编文件。如下:

    • archarmcortex_a_r 架构

      • 汇编代码文件
        https://gitee.com/LiteOS/Lite…。
      • 头文件
        https://gitee.com/LiteOS/Lite…。
    • archarm64 架构

      • 汇编代码文件 https://gitee.com/LiteOS/Lite…。
      • 头文件
        https://gitee.com/LiteOS/Lite…。
  • 开发指南自旋锁文档

    在线文档
    https://gitee.com/LiteOS/Lite…。

  • LockDep死锁检测

    死锁检测代码蕴含:

    • 头文件
      https://gitee.com/LiteOS/Lite…
    • C 代码文件
      https://gitee.com/LiteOS/Lite…。

咱们首先来看看自旋锁。

1、SpinLock 自旋锁

在多核环境中,因为应用雷同的内存空间,存在对同一资源进行拜访的状况,所以须要互斥拜访机制来保障同一时刻只有一个核进行操作。自旋锁就是这样的一种机制。

自旋锁是指当一个线程在获取锁时,如果锁曾经被其它线程获取,那么该线程将循环期待,并一直判断是否可能胜利获取锁,直到获取到锁才会退出循环。因而倡议爱护耗时较短的操作,避免对系统整体性能有显著的影响。

自旋锁与互斥锁比拟相似,它们都是为了解决对共享资源的互斥应用问题。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个持有者。然而两者在调度机制上略有不同,对于互斥锁,如果锁曾经被占用,锁申请者会被阻塞;然而自旋锁不会引起调用者阻塞,会始终循环检测自旋锁是否曾经被开释。自旋锁用于多核不同 CPU 核查资源的互斥拜访,互斥锁用于同一 CPU 核内不同工作对资源的互斥拜访。

自旋锁 SpinLock 外围的代码都在 kernelincludelos_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_SSpinlock的 typedef 别名,在 kernelincludelos_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 架构,能够在文件 archarmcortex_a_rsrcspinlock.Sarcharm64srcspinlock.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 死锁检测调测个性

LockDepLock Dependency Check 的缩写,是内核的一种死锁检测机制。这个调测个性默认是敞开的,如果须要该调测个性,须要使能宏定义 LOSCFG_KERNEL_SMP_LOCKDEP。当检测到死锁谬误时,会打印产生死锁的自旋锁的相干信息,打印backtrace 回溯栈信息。

2.1 LockDep 自旋锁的谬误类型及构造体定义

在文件 kernelincludelos_lockdep.h 中定义了死锁的枚举类型 LockDepErrTypeHeldLocks构造体。

自旋锁的谬误类型有 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() 会全副开释当前任务持有的自旋锁。在 archarmcortex_a_rsrcfault.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/Lite…。为了更容易找到 LiteOS 代码仓,倡议拜访 https://gitee.com/LiteOS/LiteOS,关注 Watch、点赞Star、并Fork 到本人账户下,如下图,谢谢。

本文分享自华为云社区《LiteOS 内核源码剖析系列二 SpinLock 自旋锁及 LockDep 死锁检测》,原文作者:zhushy。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版