LiteOS内核源码分析系列八 信号量Semaphore

LiteOS内核源码分析系列八 信号量Semaphore

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数。以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同。本文通过分析LiteOS信号量模块的源码,掌握信号量使用上的差异。

LiteOS信号量模块的源代码,均可以在LiteOS开源站点https://gitee.com/LiteOS/LiteOS 获取。信号量源代码、开发文档,示例程序代码如下:


接下来,我们看下信号量的结构体,信号量初始化,信号量常用操作的源代码。

1、信号量结构体定义和常用宏定义

1.1 信号量结构体定义

在文件kernel\base\include\los_sem_pri.h定义的信号量控制块结构体为LosSemCB,结构体源代码如下。信号量状态.semStat取值OS_SEM_UNUSEDOS_SEM_USED,信号量类型.semType取值OS_SEM_COUNTINGOS_SEM_BINARY,分别表示计数信号量和二值信号量。计数信号量的最大数量为OS_SEM_COUNT_MAX,二值信号量的最大数量为OS_SEM_BINARY_COUNT_MAX

typedef struct {
    UINT8 semStat;       /**< 信号量状态 */
    UINT8 semType;       /**< 信号量类型 */
    UINT16 semCount;     /**< 可用的信号量数量 */
    UINT32 semId;        /**< 信号量Id,高低16位分别为COUNT(UINT16)|INDEX(UINT16) */
    LOS_DL_LIST semList; /**< 阻塞在该信号量的任务链表 */
} LosSemCB;

1.2 信号量常用宏定义

系统支持创建多少信号量是根据开发板情况使用宏LOSCFG_BASE_IPC_SEM_LIMIT定义的,每一个信号量semIdUINT32类型的。信号量支持2种方式存取信号量semId,由LOSCFG_RESOURCE_ID_NOT_USE_HIGH_BITS来区分,默认不开启这个宏,开启宏的情况比较简单,自行阅读代码。开启这个宏时,信号量semId由2部分组成:countindex,分别处于高16位和低16位。创建信号量,使用后删除时,信号量回收到信号量池时,信号量semId的高16位即count值会加1,这样可以用来表示该信号量被创建删除的次数。index取值为[0,LOSCFG_BASE_IPC_SEM_LIMIT),表示信号量池中各个的信号量的编号。

⑴处的宏用来分割countsemId的位数,⑵处信号量被删除时更新信号量semId,可以看出高16位为count和低16位为index。⑶处获取信号量semId的低16位。⑷根据信号量semId获取对应的信号量被创建删除的次数count。⑸处从信号量池中获取指定信号量semId对应的信号量控制块。

⑴    #define SEM_SPLIT_BIT 16

⑵    #define SET_SEM_ID(count, index)            (((count) << SEM_SPLIT_BIT) | (index))

⑶    #define GET_SEM_INDEX(semId)                ((semId) & ((1U << SEM_SPLIT_BIT) - 1))

⑷    #define GET_SEM_COUNT(semId)                ((semId) >> SEM_SPLIT_BIT)

⑸    #define GET_SEM(semId)                      (((LosSemCB *)g_allSem) + GET_SEM_INDEX(semId))

2、信号量初始化

信号量在内核中默认开启,用户可以通过宏LOSCFG_BASE_IPC_SEM进行关闭。开启信号量的情况下,在系统启动时,在kernel\init\los_init.c中调用OsSemInit()进行信号量模块初始化。
下面,我们分析下信号量初始化的代码。

⑴为信号量申请内存,如果申请失败,则返回错误。⑵初始化双向循环链表g_unusedSemList,维护未使用的信号量。
⑶循环每一个信号量进行初始化,为每一个信号量节点指定索引semId。⑷处调用函数OsSemNodeRecycle(),把.semStat设置为未使用OS_SEM_UNUSED,并把信号量节点插入未使用信号量双向链表g_unusedSemList。⑸如果开启了信号量调测开关,则调用函数OsSemDbgInitHook()进行初始化。

LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)
{
    LosSemCB *semNode = NULL;
    UINT16 index; // support at most 65536 semaphores

 ⑴  g_allSem = (LosSemCB *)LOS_MemAlloc(m_aucSysMem0, (LOSCFG_BASE_IPC_SEM_LIMIT * sizeof(LosSemCB)));
    if (g_allSem == NULL) {
        return LOS_ERRNO_SEM_NO_MEMORY;
    }LOS_ListInit(&g_unusedSemList);for (index = 0; index < LOSCFG_BASE_IPC_SEM_LIMIT; index++) {
        semNode = ((LosSemCB *)g_allSem) + index;
        semNode->semId = (UINT32)index;OsSemNodeRecycle(semNode);
    }if (OsSemDbgInitHook() != LOS_OK) {
        return LOS_ERRNO_SEM_NO_MEMORY;
    }
    return LOS_OK;
}

3、信号量常用操作

3.1 信号量创建

我们可以使用函数LOS_SemCreate(UINT16 count, UINT32 *semHandle)来创建计数信号量,使用UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)创建二值信号量,下面通过分析源码看看如何创建信号量的。

2个函数的传入参数一样,需要传入信号量的数量count,和保存信号量编号的semHandle。计数信号量的数量不能大于OS_SEM_COUNT_MAX,二值信号量的数量不能大于OS_SEM_BINARY_COUNT_MAX。会进一步调用函数OsSemCreate()实现信号量的创建,下文继续分析。

LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemCreate(UINT16 count, UINT32 *semHandle)
{
    if (count > OS_SEM_COUNT_MAX) {
        return LOS_ERRNO_SEM_OVERFLOW;
    }
    return OsSemCreate(count, OS_SEM_COUNTING, semHandle);
}

LITE_OS_SEC_TEXT_INIT UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)
{
    if (count > OS_SEM_BINARY_COUNT_MAX) {
        return LOS_ERRNO_SEM_OVERFLOW;
    }
    return OsSemCreate(count, OS_SEM_BINARY, semHandle);
}

我们看看创建信号量的函数OsSemCreate(),需要3个参数,多了个信号量的类型type,区分计数信号量和二值信号量。

⑴判断g_unusedSemList是否为空,还有可以使用的信号量资源?如果没有可以使用的信号量,调用函数OsSemInfoGetFullDataHook()做些调测相关的检测,这个函数需要开启调测开关,后续系列专门分析。
⑵处如果g_unusedSemList不为空,则获取第一个可用的信号量节点,接着从双向链表g_unusedSemList中删除,然后调用宏GET_SEM_LIST获取LosSemCB *semCreated
,初始化创建的信号量信息,包含信号量的状态、信号量类型,信号量计数等信息。⑶初始化双向链表&semCreated->semList,阻塞在这个信号量上的任务会挂在这个链表上。⑷赋值给输出参数*semHandle,后续程序使用这个信号量编号对信号量进行其他操作。⑸开启调测时,会调用函数OsSemDbgUpdateHook()更新信号量的使用情况。

LITE_OS_SEC_TEXT_INIT STATIC UINT32 OsSemCreate(UINT16 count, UINT8 type, UINT32 *semHandle)
{
    UINT32 intSave;
    LosSemCB *semCreated = NULL;
    LOS_DL_LIST *unusedSem = NULL;

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

    SCHEDULER_LOCK(intSave);if (LOS_ListEmpty(&g_unusedSemList)) {
        SCHEDULER_UNLOCK(intSave);
        OsSemInfoGetFullDataHook();
        OS_RETURN_ERROR(LOS_ERRNO_SEM_ALL_BUSY);
    }

⑵  unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);
    LOS_ListDelete(unusedSem);
    semCreated = GET_SEM_LIST(unusedSem);
    semCreated->semStat = OS_SEM_USED;
    semCreated->semType = type;
    semCreated->semCount = count;LOS_ListInit(&semCreated->semList);*semHandle = semCreated->semId;OsSemDbgUpdateHook(semCreated->semId, OsCurrTaskGet()->taskEntry, count);

    SCHEDULER_UNLOCK(intSave);

    LOS_TRACE(SEM_CREATE, semCreated->semId, type, count);
    return LOS_OK;
}

3.2 信号量删除

我们可以使用函数LOS_semDelete(UINT32 semHandle)来删除信号量,下面通过分析源码看看如何删除信号量的。

⑴处调用函数OsSemGetCBWithCheck()判断信号量semHandle是否超过LOSCFG_BASE_IPC_SEM_LIMIT,如果超过则返回错误码。如果信号量编号没有问题,获取信号量控制块LosSemCB *semDeleted。⑵处调用函数OsSemStateVerify()判断要删除的信号量Id是否有问题,或者要删除的信号量处于未使用状态,则跳转到错误标签OUT:进行处理。⑶如果信号量的阻塞任务列表不为空,不允许删除,跳转到错误标签进行处理。⑷处调用函数OsSemNodeRecycle(),把.semStat设置为未使用OS_SEM_UNUSED,并把信号量节点插入未使用信号量双向链表g_unusedSemList。⑸如果开启了信号量调测开关,则调用函数OsSemDbgUpdateHook()刷新。

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

⑴  ret = OsSemGetCBWithCheck(semHandle, &semDeleted);
    if (ret != LOS_OK) {
        return ret;
    }

    SCHEDULER_LOCK(intSave);

⑵  ret = OsSemStateVerify(semHandle, semDeleted);
    if (ret != LOS_OK) {
        goto OUT;
    }if (!LOS_ListEmpty(&semDeleted->semList)) {
        ret = LOS_ERRNO_SEM_PENDED;
        goto OUT;
    }

#ifndef LOSCFG_RESOURCE_ID_NOT_USE_HIGH_BITS
    semDeleted->semId = SET_SEM_ID(GET_SEM_COUNT(semDeleted->semId) + 1, GET_SEM_INDEX(semDeleted->semId));
#endifOsSemNodeRecycle(semDeleted);OsSemDbgUpdateHook(semDeleted->semId, NULL, 0);

OUT:
    SCHEDULER_UNLOCK(intSave);

    LOS_TRACE(SEM_DELETE, semHandle, ret);
    return ret;
}

3.3 信号量申请

我们可以使用函数UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)来请求信号量,需要的2个参数分别是信号量semHandle和等待时间timeout,取值范围为[0, LOS_WAIT_FOREVER],单位为Tick
下面通过分析源码看看如何请求信号量的。

申请信号量时首先会进行信号量编号、参数的合法性校验,这些比较简单。⑴处代码表示不能在中断处理时申请信号量。⑵判断申请信号量的是否系统任务,如果是系统任务,则输出警告信息。⑶处表示不能在系统锁调度期间申请信号量。⑷判断信号量的状态,未创建的信号量不能申请。

⑸如果信号量计数大于0,信号量计数减1,返回申请成功的结果。⑹如果信号量计数等于0,并且零等待时间timeout,则返回结果码LOS_ERRNO_SEM_UNAVAILABLE。⑺如果申请的信号量被全部占用,需要等待时,把当前任务阻塞的信号量.taskSem标记为申请的信号量,然后调用函数OsTaskWait(),把当前任务状态设置为阻塞状态,加入信号量的阻塞链表.semList。如果不是永久等待LOS_WAIT_FOREVER,还需要把当前任务加入定时器排序链表。⑻处触发任务调度进行任务切换,暂时不执行后续代码。

如果等待时间超时,信号量还不可用,本任务获取不到信号量时,继续执行⑼,返回错误码。如果信号量可用,本任务获取到信号量,返回申请成功。

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

    ret = OsSemGetCBWithCheck(semHandle, &semPended);
    if (ret != LOS_OK) {
        return ret;
    }

    LOS_TRACE(SEM_PEND, semHandle, semPended->semCount, timeout);if (OS_INT_ACTIVE) {
        return LOS_ERRNO_SEM_PEND_INTERR;
    }

    runTask = OsCurrTaskGet();if (runTask->taskFlags & OS_TASK_FLAG_SYSTEM) {
        PRINT_DEBUG("Warning: DO NOT recommend to use %s in system tasks.\n", __FUNCTION__);
    }if (!OsPreemptable()) {
        return LOS_ERRNO_SEM_PEND_IN_LOCK;
    }

    SCHEDULER_LOCK(intSave);

⑷  ret = OsSemStateVerify(semHandle, semPended);
    if (ret != LOS_OK) {
        goto OUT;
    }

    OsSemDbgTimeUpdateHook(semHandle);if (semPended->semCount > 0) {
        semPended->semCount--;
        goto OUT;} else if (!timeout) {
        ret = LOS_ERRNO_SEM_UNAVAILABLE;
        goto OUT;
    }

⑺  runTask->taskSem = (VOID *)semPended;
    OsTaskWait(&semPended->semList, OS_TASK_STATUS_PEND, timeout);OsSchedResched();

    SCHEDULER_UNLOCK(intSave);
    SCHEDULER_LOCK(intSave);if (runTask->taskStatus & OS_TASK_STATUS_TIMEOUT) {
        runTask->taskStatus &= ~OS_TASK_STATUS_TIMEOUT;
        ret = LOS_ERRNO_SEM_TIMEOUT;
        goto OUT;
    }

OUT:
    SCHEDULER_UNLOCK(intSave);
    return ret;
}

3.4 信号量释放

我们可以使用函数UINT32 LOS_semPost(UINT32 semHandle)来释放信号量,下面通过分析源码看看如何释放信号量的。

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

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

    ret = OsSemGetCBWithCheck(semHandle, &semPosted);
    if (ret != LOS_OK) {
        return ret;
    }

    LOS_TRACE(SEM_POST, semHandle, semPosted->semType, semPosted->semCount);

    SCHEDULER_LOCK(intSave);

    ret = OsSemStateVerify(semHandle, semPosted);
    if (ret != LOS_OK) {
        goto OUT;
    }

    OsSemDbgTimeUpdateHook(semHandle);

⑴  maxCount = (semPosted->semType == OS_SEM_COUNTING) ? OS_SEM_COUNT_MAX : OS_SEM_BINARY_COUNT_MAX;
    if (semPosted->semCount >= maxCount) {
        ret = LOS_ERRNO_SEM_OVERFLOW;
        goto OUT;
    }if (!LOS_ListEmpty(&semPosted->semList)) {
⑶      resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));
        resumedTask->taskSem = NULL;OsTaskWake(resumedTask, OS_TASK_STATUS_PEND);

        SCHEDULER_UNLOCK(intSave);LOS_MpSchedule(OS_MP_CPU_ALL);
        LOS_Schedule();
        return LOS_OK;
    } else {
⑹      semPosted->semCount++;
    }

OUT:
    SCHEDULER_UNLOCK(intSave);
    return ret;
}

4、信号量使用总结

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

计数信号量和二值信号量唯一的区别就是信号量的初始数量不一致,二值信号量初始数量只能为0和1,计数信号量的初始值可以为0和大于1的整数。

互斥锁可以理解为一种特性的二值信号量,在实现实现对临界资源的独占式处理、互斥场景时,没有本质的区别。比对下二值的结构体,互斥锁的成员变量.muxCount表示加锁的次数,信号量的成员变量.semCount表示信号量的计数,含义稍有不同。

4.2 信号量的互斥和同步

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

  • 用于互斥的信号量

    初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。对信号量的申请和释放,需要成对出现,在同一个任务里完成申请和释放。

  • 用于同步的信号量

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

    用于同步的信号量,初始信号量计数值为0。任务1申请信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入ReadyRunning态,从而达到了任务间的同步。信号量的能不能申请成功,依赖其他任务是否释放信号量,申请和释放在不同的任务里完成。

小结

本文带领大家一起剖析了LiteOS信号量模块的源代码,包含信号量的结构体、信号量池初始化、信号量创建删除、申请释放等。感谢阅读,如有任何问题、建议,都可以留言给我们: https://gitee.com/LiteOS/LiteOS/issues 。为了更容易找到LiteOS代码仓,建议访问 https://gitee.com/LiteOS/LiteOS ,关注Watch、点赞Star、并Fork到自己账户下,如下图,谢谢。

LOS_STAR

(完)