本文继续分析OpenHarmony LiteOS-A内核的源代码,接下来会分析IPC的信号量。信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数。以同步为目的的信号量和以互斥为目的的信号量在使用上存在差异。本文通过分析鸿蒙轻内核信号量模块的源码,掌握信号量使用上的差异。本文中所涉及的源码,以OpenHarmony LiteOS-A内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_a 获取。如果涉及开发板,则默认以hispark_taurus为例。
接下来,我们看下信号量的结构体,信号量初始化,信号量常用操作的源代码。
1、信号量结构体定义和常用宏定义
信号量模块涉及2个头文件,分别是kernel\include\los_sem.h、kernel\base\include\los_sem_pri.h,前者定义了对外的宏定义和接口;后者声明Event模块内部使用的函数接口、结构体。
(1) 信号量结构体定义
在文件kernel\base\include\los_sem_pri.h定义的信号量控制块结构体为LosSemCB,结构体源代码如下。信号量状态.semStat取值OS_SEM_UNUSED、OS_SEM_USED,注意该成员的类型为UINT8,和M核的是不同的,M核的信号量状态使用的类型为UINT16。其他成员变量的注释见注释部分。
typedef struct {
UINT8 semStat; /**< 信号量状态 */
UINT16 semCount; /**< 可用的信号量数量 */
UINT16 maxSemCount; /**< 可用的信号量最大数量 */
UINT32 semID; /**< 信号量Id */
LOS_DL_LIST semList; /**< 阻塞在该信号量的任务链表 */
} LosSemCB;
(2) 信号量常用宏定义
系统支持创建多少信号量是根据开发板情况使用宏LOSCFG_BASE_IPC_SEM_LIMIT定义的,每一个信号量semId是UINT32类型的,取值为[0,LOSCFG_BASE_IPC_SEM_LIMIT),表示信号量池中各个的信号量的编号。
⑴、⑵处宏表示信号量未使用、使用状态。⑶处根据信号量阻塞任务双向链表中的链表节点指针ptr获取信号量控制块结构体指针。⑷处从信号量池中获取指定信号量semId对应的信号量控制块。 A核信号量核M核在信号量编号处理上稍有不同,A核的信号量编号包含2部分,如⑸所示,32位的编号分为高低16位,高16位表示信号量被使用的次数,也就是信号量创建删除的次数;低16位表示信号量编号。⑹处组合信号量编号,包含计数count和编码semId。⑺、⑻分别获取信号量编码、计数。
⑴ #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))
/**
* @ingroup los_sem
* COUNT | INDEX split bit
*/
⑸ #define SEM_SPLIT_BIT 16
/**
* @ingroup los_sem
* Set the semaphore id
*/
⑹ #define SET_SEM_ID(count, semID) (((count) << SEM_SPLIT_BIT) | (semID))
/**
* @ingroup los_sem
* get the semaphore index
*/
⑺ #define GET_SEM_INDEX(semID) ((semID) & ((1U << SEM_SPLIT_BIT) - 1))
/**
* @ingroup los_sem
* get the semaphore count
*/
⑻ #define GET_SEM_COUNT(semID) ((semID) >> SEM_SPLIT_BIT)
2、信号量初始化
信号量在内核中默认开启,用户可以通过宏LOSCFG_BASE_IPC_SEM进行关闭。开启信号量的情况下,在系统启动时,在kernel\common\los_config.c中调用OsSemInit()进行信号量模块初始化。
下面,我们分析下信号量初始化的代码。
⑴初始化双向循环链表g_unusedSemList,维护未使用的信号量池。⑵为信号量池申请内存,如果申请失败,则返回错误。⑶循环每一个信号量进行初始化,为每一个信号量节点指定索引semID,把.semStat设置为未使用OS_SEM_UNUSED,并执行⑷把信号量节点插入未使用信号量双向链表g_unusedSemList。
LITE_OS_SEC_TEXT_INIT UINT32 OsSemInit(VOID)
{
LosSemCB *semNode = NULL;
UINT32 index;
⑴ LOS_ListInit(&g_unusedSemList);
/* system resident memory, don't free */
⑵ 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 = SET_SEM_ID(0, index);
semNode->semStat = OS_SEM_UNUSED;
⑷ LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);
}
if (OsSemDbgInitHook() != LOS_OK) {
return LOS_ERRNO_SEM_NO_MEMORY;
}
return LOS_OK;
}
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)
{
return OsSemCreate(count, OS_SEM_COUNT_MAX, semHandle);
}
LITE_OS_SEC_TEXT_INIT UINT32 LOS_BinarySemCreate(UINT16 count, UINT32 *semHandle)
{
return OsSemCreate(count, OS_SEM_BINARY_COUNT_MAX, 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);
}
SCHEDULER_LOCK(intSave);
⑴ if (LOS_ListEmpty(&g_unusedSemList)) {
SCHEDULER_UNLOCK(intSave);
OsSemInfoGetFullDataHook();
OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_ALL_BUSY);
}
⑵ unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);
LOS_ListDelete(unusedSem);
SCHEDULER_UNLOCK(intSave);
semCreated = GET_SEM_LIST(unusedSem);
semCreated->semCount = count;
semCreated->semStat = OS_SEM_USED;
semCreated->maxSemCount = maxCount;
⑶ LOS_ListInit(&semCreated->semList);
⑷ *semHandle = semCreated->semID;
OsHookCall(LOS_HOOK_TYPE_SEM_CREATE, semCreated);
OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count);
return LOS_OK;
ERR_HANDLER:
OS_RETURN_ERROR_P2(errLine, errNo);
}
(2) 信号量删除
我们可以使用函数LOS_semDelete(UINT32 semHandle)来删除信号量,下面通过分析源码看看如何删除信号量的。
⑴处判断信号量semHandle是否超过LOSCFG_BASE_IPC_SEM_LIMIT,如果超过则返回错误码。如果信号量编号没有问题,获取信号量控制块LosSemCB *semDeleted。⑵处判断要删除的信号量的状态,如果处于未使用状态,则跳转到错误标签ERR_HANDLER:进行处理。⑶如果信号量的阻塞任务列表不为空,不允许删除,跳转到错误标签进行处理。⑷处如果信号量可用删除,则会把.semStat设置为未使用OS_SEM_UNUSED,并把信号量节点插入未使用信号量双向链表g_unusedSemList。这里和M核稍有不同,删除的信号量节点会被放入空闲链表的尾部。
LITE_OS_SEC_TEXT_INIT UINT32 LOS_SemDelete(UINT32 semHandle)
{
UINT32 intSave;
LosSemCB *semDeleted = NULL;
UINT32 errNo;
UINT32 errLine;
⑴ if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {
OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_INVALID);
}
semDeleted = GET_SEM(semHandle);
SCHEDULER_LOCK(intSave);
⑵ if ((semDeleted->semStat == OS_SEM_UNUSED) || (semDeleted->semID != semHandle)) {
SCHEDULER_UNLOCK(intSave);
OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_INVALID);
}
⑶ if (!LOS_ListEmpty(&semDeleted->semList)) {
SCHEDULER_UNLOCK(intSave);
OS_GOTO_ERR_HANDLER(LOS_ERRNO_SEM_PENDED);
}
⑷ LOS_ListTailInsert(&g_unusedSemList, &semDeleted->semList);
semDeleted->semStat = OS_SEM_UNUSED;
semDeleted->semID = SET_SEM_ID(GET_SEM_COUNT(semDeleted->semID) + 1, GET_SEM_INDEX(semDeleted->semID));
OsHookCall(LOS_HOOK_TYPE_SEM_DELETE, semDeleted);
OsSemDbgUpdateHook(semDeleted->semID, NULL, 0);
SCHEDULER_UNLOCK(intSave);
return LOS_OK;
ERR_HANDLER:
OS_RETURN_ERROR_P2(errLine, errNo);
}
(3) 信号量申请
我们可以使用函数UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)来请求信号量,需要的2个参数,分别是信号量semHandle和等待时间timeout,取值范围为[0, LOS_WAIT_FOREVER],单位为Tick。下面通过分析源码看看如何请求信号量的。
申请信号量时首先会进行信号量编号、参数的合法性校验。⑴处代码表示信号量如果大于配置的最大值,则返回错误码。⑵处表示如果处于中断处理期间,处于锁任务调度期间,则返回错误码。⑶处理系统任务不能请求信号量的情况。⑷处校验信号量状态核编码,如果校验不通过,跳转到OUT:标签停止信号量的申请。⑸如果信号量计数大于0,信号量计数减1,返回申请成功的结果。⑹如果信号量计数等于0,并且零等待时间timeout,则返回结果码LOS_ERRNO_SEM_UNAVAILABLE。⑺处如果锁调度了,则返回错误码。如果申请的信号量被全部占用,需要等待时,会执行⑻把当前任务阻塞的信号量.taskSem标记为申请的信号量,然后把当前任务状态设置为阻塞状态,加入信号量的阻塞链表.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 = GET_SEM(semHandle);
UINT32 retErr = LOS_OK;
LosTaskCB *runTask = NULL;
⑴ if (GET_SEM_INDEX(semHandle) >= (UINT32)LOSCFG_BASE_IPC_SEM_LIMIT) {
OS_RETURN_ERROR(LOS_ERRNO_SEM_INVALID);
}
⑵ if (OS_INT_ACTIVE) {
PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_INTERR!!!\n");
OsBackTrace();
return LOS_ERRNO_SEM_PEND_INTERR;
}
⑶ runTask = OsCurrTaskGet();
if (runTask->taskStatus & OS_TASK_FLAG_SYSTEM_TASK) {
OsBackTrace();
return LOS_ERRNO_SEM_PEND_IN_SYSTEM_TASK;
}
SCHEDULER_LOCK(intSave);
⑷ if ((semPended->semStat == OS_SEM_UNUSED) || (semPended->semID != semHandle)) {
retErr = LOS_ERRNO_SEM_INVALID;
goto OUT;
}
/* Update the operate time, no matter the actual Pend success or not */
OsSemDbgTimeUpdateHook(semHandle);
⑸ if (semPended->semCount > 0) {
semPended->semCount--;
OsHookCall(LOS_HOOK_TYPE_SEM_PEND, semPended, runTask, timeout);
goto OUT;
⑹ } else if (!timeout) {
retErr = LOS_ERRNO_SEM_UNAVAILABLE;
goto OUT;
}
⑺ if (!OsPreemptableInSched()) {
PRINT_ERR("!!!LOS_ERRNO_SEM_PEND_IN_LOCK!!!\n");
OsBackTrace();
retErr = LOS_ERRNO_SEM_PEND_IN_LOCK;
goto OUT;
}
OsHookCall(LOS_HOOK_TYPE_SEM_PEND, semPended, runTask, timeout);
⑻ OsTaskWaitSetPendMask(OS_TASK_WAIT_SEM, semPended->semID, timeout);
retErr = runTask->ops->wait(runTask, &semPended->semList, timeout);
⑼ if (retErr == LOS_ERRNO_TSK_TIMEOUT) {
retErr = LOS_ERRNO_SEM_TIMEOUT;
}
OUT:
SCHEDULER_UNLOCK(intSave);
return retErr;
}
(4) 信号量释放
我们可以使用函数UINT32 LOS_semPost(UINT32 semHandle)来释放信号量,下面通过分析源码看看如何释放信号量的。该函数会继续调用OsSemPostUnsafe来实现信号量的释放。OsSemPostUnsafe函数中,释放信号量时首先会进行信号量编号、参数的合法性校验,这些比较简单,自行阅读即可。⑴处验判断是否信号量溢出。⑵如果信号量的任务阻塞链表不为空,执行⑷从阻塞链表中获取第一个任务,设置.taskSem为NULL,不再阻塞信号量。并把获取到信号量的任务调整其状态,并加入就行队列,然后执行⑸处标记需要调度。⑹如果信号量的任务阻塞链表为空,则把信号量的计数加1。函数LOS_SemPost中,⑺处如果需要触发任务调度,则主动触发任务调度进行任务切换。
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;
}
LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched)
{
LosTaskCB *resumedTask = NULL;
LosSemCB *semPosted = GET_SEM(semHandle);
if ((semPosted->semID != semHandle) || (semPosted->semStat == OS_SEM_UNUSED)) {
return LOS_ERRNO_SEM_INVALID;
}
/* Update the operate time, no matter the actual Post success or not */
OsSemDbgTimeUpdateHook(semHandle);
⑴ if (semPosted->semCount == OS_SEM_COUNT_MAX) {
return LOS_ERRNO_SEM_OVERFLOW;
}
⑵ if (!LOS_ListEmpty(&semPosted->semList)) {
⑶ resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));
⑷ OsTaskWakeClearPendMask(resumedTask);
resumedTask->ops->wake(resumedTask);
if (needSched != NULL) {
⑸ *needSched = TRUE;
}
} else {
⑹ semPosted->semCount++;
}
OsHookCall(LOS_HOOK_TYPE_SEM_POST, semPosted, resumedTask);
return LOS_OK;
}
LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle)
{
UINT32 intSave;
UINT32 ret;
BOOL needSched = FALSE;
if (GET_SEM_INDEX(semHandle) >= LOSCFG_BASE_IPC_SEM_LIMIT) {
return LOS_ERRNO_SEM_INVALID;
}
SCHEDULER_LOCK(intSave);
ret = OsSemPostUnsafe(semHandle, &needSched);
SCHEDULER_UNLOCK(intSave);
⑺ if (needSched) {
LOS_MpSchedule(OS_MP_CPU_ALL);
LOS_Schedule();
}
return ret;
}
4、信号量使用总结
(1) 计数信号量、二值信号量和互斥锁
计数信号量和二值信号量唯一的区别就是信号量的初始数量不一致,二值信号量初始数量只能为0和1,计数信号量的初始值可以为0和大于1的整数。
互斥锁可以理解为一种特性的二值信号量,在实现实现对临界资源的独占式处理、互斥场景时,没有本质的区别。比对下二值的结构体,互斥锁的成员变量.muxCount表示加锁的次数,信号量的成员变量.semCount表示信号量的计数,含义稍有不同。
(2) 信号量的互斥和同步
信号量可用用于互斥和同步两种场景,以同步为目的的信号量和以互斥为目的的信号量在使用上,有如下不同:
用于互斥的信号量
初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。对信号量的申请和释放,需要成对出现,在同一个任务里完成申请和释放。
用于同步的信号量
多任务同时访问同一份共享资源时,会导致冲突,这时候就需要引入任务同步机制使得各个任务按业务需求一个一个的对共享资源进行有序访问操作。任务同步的实质就是任务按需进行排队。
用于同步的信号量,初始信号量计数值为0。任务1申请信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。信号量的能不能申请成功,依赖其他任务是否释放信号量,申请和释放在不同的任务里完成。
小结
本文带领大家一起剖析了鸿蒙轻内核的信号量模块的源代码,包含信号量的结构体、信号量池初始化、信号量创建删除、申请释放等。