乐趣区

关于harmonyos:鸿蒙轻内核源码分析掌握信号量使用差异

摘要: 本文率领大家一起分析鸿蒙轻内核的信号量模块的源代码,蕴含信号量的构造体、信号量池初始化、信号量创立删除、申请开释等。

本文分享自华为云社区《鸿蒙轻内核 M 核源码剖析系列十一 信号量 Semaphore》,原文作者:zhushy。

信号量(Semaphore)是一种实现工作间通信的机制,能够实现工作间同步或共享资源的互斥拜访。一个信号量的数据结构中,通常有一个计数值,用于对无效资源数的计数,示意剩下的可被应用的共享资源数。以同步为目标的信号量和以互斥为目标的信号量在应用上存在差别。本文通过剖析鸿蒙轻内核信号量模块的源码,把握信号量应用上的差别。本文中所波及的源码,以 OpenHarmony LiteOS- M 内核为例,均能够在开源站点 https://gitee.com/openharmony… 获取。

接下来,咱们看下信号量的构造体,信号量初始化,信号量罕用操作的源代码。

1、信号量构造体定义和罕用宏定义

1.1 信号量构造体定义

在文件 kernel\include\los_sem.h 定义的信号量管制块构造体为 LosSemCB,构造体源代码如下。信号量状态.semStat 取值 OS_SEM_UNUSED、OS_SEM_USED,其余成员变量的正文见正文局部。

typedef struct {
    UINT16 semStat;      /**< 信号量状态 */
    UINT16 semCount;     /**< 可用的信号量数量 */
    UINT16 maxSemCount;  /**< 可用的信号量最大数量 */
    UINT16 semID;        /**< 信号量 Id */
    LOS_DL_LIST semList; /**< 阻塞在该信号量的工作链表 */
} LosSemCB;

1.2 信号量罕用宏定义

零碎反对创立多少信号量是依据开发板状况应用宏 LOSCFG_BASE_IPC_SEM_LIMIT 定义的,每一个信号量 semId 是 UINT32 类型的,取值为 [0,LOSCFG_BASE_IPC_SEM_LIMIT),示意信号量池中各个的信号量的编号。

⑴处的宏示意二值信号量的最大值为 1,⑵处、⑶处的宏示意信号量未应用、应用状态值。⑷处依据信号量阻塞工作双向链表中的链表节点指针 ptr 获取信号量管制块构造体指针。⑸处从信号量池中获取指定信号量 semId 对应的信号量管制块。

⑴    #define OS_SEM_BINARY_MAX_COUNT     1

⑵    #define OS_SEM_UNUSED               0

⑶    #define OS_SEM_USED                 1

⑷    #define GET_SEM_LIST(ptr) LOS_DL_LIST_ENTRY(ptr, LosSemCB, semList)

⑸    #define GET_SEM(semid) (((LosSemCB *)g_allSem) + (semid))

2、信号量初始化

信号量在内核中默认开启,用户能够通过宏 LOSCFG_BASE_IPC_SEM 进行敞开。开启信号量的状况下,在系统启动时,在 kernel\src\los_init.c 中调用 OsSemInit() 进行信号量模块初始化。

上面,咱们剖析下信号量初始化的代码。

⑴初始化双向循环链表 g_unusedSemList,保护未应用的信号量池。⑵为信号量池申请内存,如果申请失败,则返回谬误。⑶循环每一个信号量进行初始化,为每一个信号量节点指定索引 semID,把.semStat 设置为未应用 OS_SEM_UNUSED,并执行⑷把信号量节点插入未应用信号量双向链表 g_unusedSemList。

LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)
{
    LosSemCB *semNode = NULL;
    UINT16 index;

⑴  LOS_ListInit(&g_unusedSemList);

    if (LOSCFG_BASE_IPC_SEM_LIMIT == 0) {return LOS_ERRNO_SEM_MAXNUM_ZERO;}

⑵  g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));
    if (g_allSem == NULL) {return LOS_ERRNO_SEM_NO_MEMORY;}

⑶  for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) {semNode = ((LosSemCB *)g_allSem) + index;
        semNode->semID = index;
        semNode->semStat = OS_SEM_UNUSED;
⑷      LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);
    }
    return LOS_OK;
}

3、信号量罕用操作

3.1 信号量创立

咱们能够应用函数 LOS_SemCreate(UINT16 count, UINT32 semHandle) 来创立计数信号量,应用 UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 semHandle) 创立二值信号量,上面通过剖析源码看看如何创立信号量的。

2 个函数的传入参数一样,须要传入信号量的数量 count,和保留信号量编号的 semHandle。计数信号量的最大数量为 OS_SEM_COUNTING_MAX_COUNT,二值信号量的最大数量为 OS_SEM_BINARY_MAX_COUNT。会进一步调用函数 OsSemCreate() 实现信号量的创立,下文持续剖析。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemCreate(UINT16 count, UINT32 *semHandle)
{return OsSemCreate(count, OS_SEM_COUNTING_MAX_COUNT, semHandle);
}

LITE_OS_SEC_TEXT_INIT UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)
{return OsSemCreate(count, OS_SEM_BINARY_MAX_COUNT, semHandle);
}

咱们看看创立信号量的函数 OsSemCreate(),须要 3 个参数,创立的信号量的数量,最大数量,以及信号量编号。

⑴判断 g_unusedSemList 是否为空,还有能够应用的信号量资源?如果没有能够应用的信号量,调用函数 OsSemInfoGetFullDataHook() 做些调测相干的检测,这个函数须要开启调测开关,后续系列专门剖析。

⑵处如果 g_unusedSemList 不为空,则获取第一个可用的信号量节点,接着从双向链表 g_unusedSemList 中删除,而后调用宏 GET_SEM_LIST 获取 LosSemCB *semCreated
,初始化创立的信号量信息,蕴含信号量的状态、信号量数量,信号量最大数量等信息。⑶初始化双向链表 &semCreated->semList,阻塞在这个信号量上的工作会挂在这个链表上。⑷赋值给输入参数 *semHandle,后续程序应用这个信号量编号对信号量进行其余操作。

LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle)
{
    UINT32 intSave;
    LosSemCB *semCreated = NULL;
    LOS_DL_LIST *unusedSem = NULL;
    UINT32 errNo;
    UINT32 errLine;

    if (semHandle == NULL) {return LOS_ERRNO_SEM_PTR_NULL;}

    if (count > maxCount) {OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_OVERFLOW);
    }

    intSave = LOS_IntLock();

⑴  if (LOS_ListEmpty(&g_unusedSemList)) {LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_ALL_BUSY);
    }

⑵  unusedSem = LOS_DL_LIST_FIRST(&(g_unusedSemList));
    LOS_ListDelete(unusedSem);
    semCreated = (GET_SEM_LIST(unusedSem));
    semCreated->semCount = count;
    semCreated->semStat = OS_SEM_USED;
    semCreated->maxSemCount = maxCount;
⑶  LOS_ListInit(&semCreated->semList);
⑷  *semHandle = (UINT32)semCreated->semID;
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_SEM_CREATE, semCreated);
    return LOS_OK;

ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

3.2 信号量删除

咱们能够应用函数 LOS_semDelete(UINT32 semHandle) 来删除信号量,上面通过剖析源码看看如何删除信号量的。

⑴处判断信号量 semHandle 是否超过 LOSCFG_BASE_IPC_SEM_LIMIT,如果超过则返回错误码。如果信号量编号没有问题,获取信号量管制块 LosSemCB *semDeleted。⑵处判断要删除的信号量的状态,如果处于未应用状态,则跳转到谬误标签 ERR_HANDLER: 进行解决。⑶如果信号量的阻塞工作列表不为空,不容许删除,跳转到谬误标签进行解决。⑷处如果信号量可用删除,则会把.semStat 设置为未应用 OS_SEM_UNUSED,并把信号量节点插入未应用信号量双向链表 g_unusedSemList。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemDelete(UINT32 semHandle)
{
    UINT32 intSave;
    LosSemCB *semDeleted = NULL;
    UINT32 errNo;
    UINT32 errLine;

⑴  if (semHandle >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_INVALID);
    }

    semDeleted = GET_SEM(semHandle);
    intSave = LOS_IntLock();
⑵  if (semDeleted->semStat == OS_SEM_UNUSED) {LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_INVALID);
    }

⑶  if (!LOS_ListEmpty(&semDeleted->semList)) {LOS_IntRestore(intSave);
        OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_PENDED);
    }

⑷  LOS_ListAdd(&g_unusedSemList, &semDeleted->semList);
    semDeleted->semStat = OS_SEM_UNUSED;
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_SEM_DELETE, semDeleted);
    return LOS_OK;
ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

3.3 信号量申请

咱们能够应用函数 UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout) 来申请信号量,须要的 2 个参数别离是信号量 semHandle 和等待时间 timeout,取值范畴为 [0, LOS_WAIT_FOREVER],单位为 Tick。上面通过剖析源码看看如何申请信号量的。

申请信号量时首先会进行信号量编号、参数的合法性校验。⑴处代码示意信号量如果大于配置的最大值,则返回错误码。⑵处获取要申请的信号量管制块 semPended。⑶处调用函数对信号量管制块进行校验,如果信号量未创立,处于中断解决期间,处于锁任务调度期间,则返回错误码。⑷处如果校验不通过,跳转到 ERROR_SEM_PEND: 标签进行信号量的申请。

⑸如果信号量计数大于 0,信号量计数减 1,返回申请胜利的后果。⑹如果信号量计数等于 0,并且零等待时间 timeout,则返回后果码 LOS_ERRNO_SEM_UNAVAILABLE。⑺如果申请的信号量被全副占用,须要期待时,把当前任务阻塞的信号量.taskSem 标记为申请的信号量,而后调用函数 OsSchedTaskWait(),该函数具体代码上文已剖析,把当前任务状态设置为阻塞状态,退出信号量的阻塞链表.semList。如果不是永恒期待 LOS_WAIT_FOREVER,还须要更改工作状态为 OS_TASK_STATUS_PEND_TIME,并且设置 waitTimes 等待时间。⑻处触发任务调度进行工作切换,临时不执行后续代码。

如果等待时间超时,信号量还不可用,本工作获取不到信号量时,继续执行⑼,更改工作状态,返回错误码。如果信号量可用,执行⑽,本工作获取到信号量,返回申请胜利。

LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)
{
    UINT32 intSave;
    LosSemCB *semPended = NULL;
    UINT32 retErr;
    LosTaskCB *runningTask = NULL;

⑴  if (semHandle >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);
    }

⑵  semPended = GET_SEM(semHandle);
    intSave = LOS_IntLock();

⑶  retErr = OsSemValidCheck(semPended);
    if (retErr) {⑷      goto ERROR_SEM_PEND;}

⑸  if (semPended->semCount > 0) {
        semPended->semCount--;
        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_SEM_PEND, semPended, runningTask);
        return LOS_OK;
    }

⑹  if (!timeout) {
        retErr = LOS_ERRNO_SEM_UNAVAILABLE;
        goto ERROR_SEM_PEND;
    }

⑺  runningTask = (LosTaskCB *)g_losTask.runTask;
    runningTask->taskSem = (VOID *)semPended;
    OsSchedTaskWait(&semPended->semList, timeout);
    LOS_IntRestore(intSave);
    OsHookCall(LOS_HOOK_TYPE_SEM_PEND, semPended, runningTask);
⑻  LOS_Schedule();

    intSave = LOS_IntLock();
⑼  if (runningTask->taskStatus & OS_TASK_STATUS_TIMEOUT) {runningTask->taskStatus &= (~OS_TASK_STATUS_TIMEOUT);
        retErr = LOS_ERRNO_SEM_TIMEOUT;
        goto ERROR_SEM_PEND;
    }

    LOS_IntRestore(intSave);
⑽  return LOS_OK;

ERROR_SEM_PEND:
    LOS_IntRestore(intSave);
    OS_RETURN_ERROR(retErr);
}

3.4 信号量开释

咱们能够应用函数 UINT32 LOS_semPost(UINT32 semHandle) 来开释信号量,上面通过剖析源码看看如何开释信号量的。

开释信号量时首先会进行信号量编号、参数的合法性校验,这些比较简单,自行浏览即可。⑴处验判断是否信号量溢出。⑵如果信号量的工作阻塞链表不为空,执行⑶从阻塞链表中获取第一个工作,设置.taskSem 为 NULL,不再阻塞信号量。执行⑷把获取到信号量的工作调整其状态,并退出就行队列。⑸触发任务调度进行工作切换。⑹如果信号量的工作阻塞链表为空,则把信号量的计数加 1。

LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle)
{
    UINT32 intSave;
    LosSemCB *semPosted = GET_SEM(semHandle);
    LosTaskCB *resumedTask = NULL;

    if (semHandle >= LOSCFG_BASE_IPC_SEM_LIMIT) {return LOS_ERRNO_SEM_INVALID;}

    intSave = LOS_IntLock();

    if (semPosted->semStat == OS_SEM_UNUSED) {LOS_IntRestore(intSave);
        OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);
    }

⑴  if (semPosted->maxSemCount == semPosted->semCount) {LOS_IntRestore(intSave);
        OS_RETURN_ERROR(LOS_ERRNO_SEM_OVERFLOW);
    }
⑵  if (!LOS_ListEmpty(&semPosted->semList)) {⑶      resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));
        resumedTask->taskSem = NULL;
⑷      OsSchedTaskWake(resumedTask);

        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_SEM_POST, semPosted, resumedTask);
⑸      LOS_Schedule();} else {
⑹      semPosted->semCount++;
        LOS_IntRestore(intSave);
        OsHookCall(LOS_HOOK_TYPE_SEM_POST, semPosted, resumedTask);
    }

    return LOS_OK;
}

4、信号量应用总结

4.1 计数信号量、二值信号量和互斥锁

计数信号量和二值信号量惟一的区别就是信号量的初始数量不统一,二值信号量初始数量只能为 0 和 1,计数信号量的初始值能够为 0 和大于 1 的整数。

互斥锁能够了解为一种个性的二值信号量,在实现实现对临界资源的独占式解决、互斥场景时,没有实质的区别。比对下二值的构造体,互斥锁的成员变量.muxCount 示意加锁的次数,信号量的成员变量.semCount 示意信号量的计数,含意稍有不同。

4.2 信号量的互斥和同步

信号量可用用于互斥和同步两种场景,以同步为目标的信号量和以互斥为目标的信号量在应用上,有如下不同:

  • 用于互斥的信号量

初始信号量计数值不为 0,示意可用的共享资源个数。在须要应用共享资源前,先获取信号量,而后应用一个共享资源,应用结束后开释信号量。这样在共享资源被取完,即信号量计数减至 0 时,其余须要获取信号量的工作将被阻塞,从而保障了共享资源的互斥拜访。对信号量的申请和开释,须要成对呈现,在同一个工作里实现申请和开释。

  • 用于同步的信号量

多任务同时拜访同一份共享资源时,会导致抵触,这时候就须要引入工作同步机制使得各个工作按业务需要一个一个的对共享资源进行有序拜访操作。工作同步的本质就是工作按需进行排队。

用于同步的信号量,初始信号量计数值为 0。工作 1 申请信号量而阻塞,直到工作 2 或者某中断开释信号量,工作 1 才得以进入 Ready 或 Running 态,从而达到了工作间的同步。信号量的能不能申请胜利,依赖其余工作是否开释信号量,申请和开释在不同的工作里实现。

小结

本文率领大家一起分析了鸿蒙轻内核的信号量模块的源代码,蕴含信号量的构造体、信号量池初始化、信号量创立删除、申请开释等。感激浏览,如有任何问题、倡议,都能够留言给咱们:https://gitee.com/openharmony…。为了更容易找到鸿蒙轻内核代码仓,倡议拜访 https://gitee.com/openharmony…,关注 Watch、点赞 Star、并 Fork 到本人账户下,谢谢。

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

退出移动版