开源鸿蒙内核源码分析系列 | 信号量 | 谁在解决任务间的同步(转载)

开源鸿蒙内核源码分析系列 | 信号量 | 谁在解决任务间的同步(转载)

基本概念

信号量(Semaphore) 是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。信号量是一个“锁定机构”, 代码需要获得钥匙才可以访问共享资源。占用该资源的任务不再使用该资源并释放资源时, 其它任务才能够访问这个资源 。一个信号量的数据结构中,通常有一个计数值,用于对有效资源数的计数,表示剩下的可被使用的共享资源数,抽象的来讲信号量是一个非负整数,所有获取它的任务都会将该整数减一其值的含义分两种情况:

  • 0:表示没有积累下来的释放信号量操作,或者该信号量当前不可获取,因此可能存在正在等待该信号量(阻塞)的任务。 
  • 正值:表示有一个或多个释放信号量操作。表示该信号量当前可被获取。

以同步为目的的信号量和以互斥为目的的信号量在使用上有如下不同:

  • 用作互斥时,初始信号量计数值不为0,表示可用的共享资源个数。在需要使用共享资源前,先获取信号量,然后使用一个共享资源,使用完毕后释放信号量。这样在共享资源被取完,即信号量计数减至0时,其他需要获取信号量的任务将被阻塞,从而保证了共享资源的互斥访问。另外,当共享资源数为1时,建议使用二值信号量,一种类似于互斥锁的机制。
  • 用作同步时,初始信号量计数值为0。任务1获取信号量而阻塞,直到任务2或者某中断释放信号量,任务1才得以进入Ready或Running态,从而达到了任务间的同步。

信号量运作原理

信号量初始化,为配置的N个信号量申请内存(N值可以由用户自行配置,通过 LOSCFG_BASE_IPC_SEM_LIMIT 宏实现),并把所有信号量初始化成未使用,加入到未使用链表中供系统使用。

  • 信号量创建,从未使用的信号量链表中获取一个信号量,并设定初值。
  • 信号量申请,若其计数器值大于0,则直接减1返回成功。否则任务阻塞,等待其它任务释放该信号量, 等待的超时时间可设定。当任务被一个信号量阻塞时,将该任务挂到信号量等待任务队列的队尾。
  • 信号量释放,若没有任务等待该信号量,则直接将计数器加1返回。否则唤醒该信号量等待任务队列上的第一个任务。
  • 信号量删除,将正在使用的信号量置为未使用信号量,并挂回到未使用链表。

信号量允许多个任务在同一时刻访问共享资源,但会限制同一时刻访问此资源的最大任务数目。当访问资源的任务数达到该资源允许的最大数量时,会阻塞其他试图获取该资源的任务,直到有任务释放该信号量。

The Smoke Signal by Frederic Remington 

信号量长什么样?


typedef struct {
    UINT8 semStat; /**< Semaphore state *///信号量的状态
    UINT16 semCount; /**< Number of available semaphores *///有效信号量的数量
    UINT16 maxSemCount;  /**< Max number of available semaphores *///有效信号量的最大数量
    UINT32 semID; /**< Semaphore control structure ID *///信号量索引号
    LOS_DL_LIST semList; /**< Queue of tasks that are waiting on a semaphore *///等待信号量的任务队列,任务通过阻塞节点挂上去
} LosSemCB;

semList,这又是一个双向链表, 双向链表是内核最重要的结构体, 可前往 v08.xx 鸿蒙内核源码分析(总目录) 查看双向链表篇, LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上semList上挂的是未来所有等待这个信号量的任务。

初始化信号量模块


#ifndef LOSCFG_BASE_IPC_SEM_LIMIT
#define LOSCFG_BASE_IPC_SEM_LIMIT 1024 //信号量的最大个数
#endif

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;//拿信号控制块, 可以直接g_allSem[index]来嘛
        semNode->semID = SET_SEM_ID(0, index);//保存ID
        semNode->semStat = OS_SEM_UNUSED;//标记未使用
        LOS_ListTailInsert(&g_unusedSemList, &semNode->semList);//通过semList把 信号块挂到空闲链表上
    }

    if (OsSemDbgInitHook() != LOS_OK) {
        return LOS_ERRNO_SEM_NO_MEMORY;
    }
    return LOS_OK;
}

分析如下:

  • 初始化创建了信号量池来统一管理信号量, 默认 1024 个信号量。
  • 信号ID范围从 [0,1023]。
  • 未分配使用的信号量都挂到了全局变量 g_unusedSemList 上。

小建议:鸿蒙内核其他池(如进程池,任务池)都采用free来命名空闲链表,而此处使用unused,命名风格不太严谨,有待改善。

创建信号量


LITE_OS_SEC_TEXT_INIT UINT32 OsSemCreate(UINT16 count, UINT16 maxCount, UINT32 *semHandle)
{
    unusedSem = LOS_DL_LIST_FIRST(&g_unusedSemList);//从未使用信号量池中取首个
    LOS_ListDelete(unusedSem);//从空闲链表上摘除
    semCreated = GET_SEM_LIST(unusedSem);//通过semList挂到链表上的,这里也要通过它把LosSemCB头查到。进程,线程等结构体也都是这么干的。
    semCreated->semCount = count;//设置数量
    semCreated->semStat = OS_SEM_USED;//设置可用状态
    semCreated->maxSemCount = maxCount;//设置最大信号数量
    LOS_ListInit(&semCreated->semList);//初始化链表,后续阻塞任务通过task->pendList挂到semList链表上,就知道哪些任务在等它了。
    *semHandle = semCreated->semID;//参数带走 semID
    OsSemDbgUpdateHook(semCreated->semID, OsCurrTaskGet()->taskEntry, count);
    return LOS_OK;

ERR_HANDLER:
    OS_RETURN_ERROR_P2(errLine, errNo);
}

分析如下:

  • 从未使用的空闲链表中拿首个信号量供分配使用。
  • 信号量的最大数量和信号量个数都由参数指定。
  • 信号量状态由 OS_SEM_UNUSED 变成了 OS_SEM_USED。
  • semHandle带走信号量ID,外部由此知道成功创建了一个编号为 *semHandle 的信号量。

申请信号量


LITE_OS_SEC_TEXT UINT32 LOS_SemPend(UINT32 semHandle, UINT32 timeout)
{
    UINT32 intSave;
    LosSemCB *semPended = GET_SEM(semHandle);//通过ID拿到信号体
    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) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了
        semPended->semCount--;//资源少了一个
        goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 
    } 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;
    }

    runTask->taskSem = (VOID *)semPended;//标记当前任务在等这个信号量
    retErr = OsTaskWait(&semPended->semList, timeout, TRUE);//任务进入等待状态,当前任务会挂到semList上,并在其中切换任务上下文
    if (retErr == LOS_ERRNO_TSK_TIMEOUT) {//注意:这里是涉及到task切换的,把自己挂起,唤醒其他task 
        runTask->taskSem = NULL;
        retErr = LOS_ERRNO_SEM_TIMEOUT;
    }

OUT:
    SCHEDULER_UNLOCK(intSave);
    return retErr;
}

分析如下: 

这个函数有点复杂,大量的goto,但别被它绕晕了,盯着返回值看。 先说结果只有一种情况下申请信号量能成功(即 retErr == LOS_OK)


    if (semPended->semCount > 0) {//还有资源可用,返回肯定得成功,semCount=0时代表没资源了,task会必须去睡眠了
        semPended->semCount--;//资源少了一个
        goto OUT;//注意这里 retErr = LOS_OK ,所以返回是OK的 
    }

其余申请失败的原因有:

  • 信号量ID超出范围(默认1024)
  • 中断发生期间
  • 系统任务
  • 信号量状态不对,信号量ID不匹配

以上都是异常的判断,再说正常情况下 semPended->semCount = 0时的情况,没有资源了怎么办? 任务进入 OsTaskWait 睡眠状态,怎么睡,睡多久,由参数 timeout 定 timeout 值分以下三种模式:

  • 无阻塞模式:即任务申请信号量时,入参 timeout 等于0。若当前信号量计数值不为0,则申请成功,否则立即返回申请失败。
  • 永久阻塞模式:即任务申请信号量时,入参timeout 等于0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该信号量,阻塞任务才会重新得以执行。
  • 定时阻塞模式:即任务申请信号量时,0<timeout<0xFFFFFFFF。若当前信号量计数值不为0,则申请成功。 否则,该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后, 超时前如果有其他任务释放该信号量,则该任务可成功获取信号量继续执行,若超时前未获取到信号量,接口将返回超时错误码。

在 OsTaskWait 中,任务将被挂入semList链表,semList上挂的都是等待这个信号量的任务。

释放信号量


LITE_OS_SEC_TEXT UINT32 OsSemPostUnsafe(UINT32 semHandle, BOOL *needSched)
{
    LosSemCB *semPosted = NULL;
    LosTaskCB *resumedTask = NULL;

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

    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)) {//当前有任务挂在semList上,要去唤醒任务
        resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(semPosted->semList)));//semList上面挂的都是task->pendlist节点,取第一个task下来唤醒
        resumedTask->taskSem = NULL;//任务不用等信号了,重新变成NULL值
        OsTaskWake(resumedTask);//唤醒任务,注意resumedTask一定不是当前任务,OsTaskWake里面并不会自己切换任务上下文,只是设置状态
        if (needSched != NULL) {//参数不为空,就返回需要调度的标签
            *needSched = TRUE;//TRUE代表需要调度
        }
    } else {//当前没有任务挂在semList上,
        semPosted->semCount++;//信号资源多一个
    }

    return LOS_OK;
}

LITE_OS_SEC_TEXT UINT32 LOS_SemPost(UINT32 semHandle)
{
    UINT32 intSave;
    UINT32 ret;
    BOOL needSched = FALSE;

    SCHEDULER_LOCK(intSave);
    ret = OsSemPostUnsafe(semHandle, &needSched);
        SCHEDULER_UNLOCK(intSave);
    if (needSched) {//需要调度的情况
        LOS_MpSchedule(OS_MP_CPU_ALL);//向所有CPU发送调度指令
        LOS_Schedule();////发起调度
    }

    return ret;
}

分析如下:

  • 注意看在什么情况下 semPosted->semCount 才会 ++ ,是在LOS_ListEmpty为真的时候,semList是等待这个信号量的任务。 semList上的任务是在OsTaskWait中挂入的。都在等这个信号。
  • 每次OsSemPost都会唤醒semList链表上一个任务,直到semList为空。
  • 掌握信号量的核心是理解 LOS_SemPend 和 LOS_SemPost。

编程示例

本实例实现如下功能:

  • 测试任务Example_TaskEntry创建一个信号量,锁任务调度,创建两个任务Example_SemTask1、Example_SemTask2,Example_SemTask2优先级高于Example_SemTask1,两个任务中申请同一信号量,解锁任务调度后两任务阻塞,测试任务Example_TaskEntry释放信号量。
  • Example_SemTask2得到信号量,被调度,然后任务休眠20Tick,Example_SemTask2延迟,Example_SemTask1被唤醒。
  • Example_SemTask1定时阻塞模式申请信号量,等待时间为10Tick,因信号量仍被Example_SemTask2持有,Example_SemTask1挂起,10Tick后仍未得到信号量, Example_SemTask1被唤醒,试图以永久阻塞模式申请信号量,Example_SemTask1挂起。
  • 20Tick后Example_SemTask2唤醒, 释放信号量后,Example_SemTask1得到信号量被调度运行,最后释放信号量。
  • Example_SemTask1执行完,40Tick后任务Example_TaskEntry被唤醒,执行删除信号量,删除两个任务。

/* 任务ID */
static UINT32 g_testTaskId01;
static UINT32 g_testTaskId02;
/* 测试任务优先级 */
#define TASK_PRIO_TEST  5
/* 信号量结构体id */
static UINT32 g_semId;

VOID Example_SemTask1(VOID)
{
    UINT32 ret;

    printf("Example_SemTask1 try get sem g_semId ,timeout 10 ticks.\n");
    /* 定时阻塞模式申请信号量,定时时间为10ticks */
    ret = LOS_SemPend(g_semId, 10);

    /*申请到信号量*/
    if (ret == LOS_OK) {
         LOS_SemPost(g_semId);
         return;
    }
    /* 定时时间到,未申请到信号量 */
    if (ret == LOS_ERRNO_SEM_TIMEOUT) {
        printf("Example_SemTask1 timeout and try get sem g_semId wait forever.\n");
        /*永久阻塞模式申请信号量*/
        ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);
        printf("Example_SemTask1 wait_forever and get sem g_semId .\n");
        if (ret == LOS_OK) {
            LOS_SemPost(g_semId);
            return;
        }
    }
}

VOID Example_SemTask2(VOID)
{
    UINT32 ret;
    printf("Example_SemTask2 try get sem g_semId wait forever.\n");
    /* 永久阻塞模式申请信号量 */
    ret = LOS_SemPend(g_semId, LOS_WAIT_FOREVER);

    if (ret == LOS_OK) {
        printf("Example_SemTask2 get sem g_semId and then delay 20ticks .\n");
    }

    /* 任务休眠20 ticks */
    LOS_TaskDelay(20);

    printf("Example_SemTask2 post sem g_semId .\n");
    /* 释放信号量 */
    LOS_SemPost(g_semId);
    return;
}

UINT32 ExampleTaskEntry(VOID)
{
    UINT32 ret;
    TSK_INIT_PARAM_S task1;
    TSK_INIT_PARAM_S task2;

   /* 创建信号量 */
    LOS_SemCreate(0,&g_semId);

    /* 锁任务调度 */
    LOS_TaskLock();

    /*创建任务1*/
    (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
    task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask1;
    task1.pcName       = "TestTsk1";
    task1.uwStackSize  = OS_TSK_DEFAULT_STACK_SIZE;
    task1.usTaskPrio   = TASK_PRIO_TEST;
    ret = LOS_TaskCreate(&g_testTaskId01, &task1);
    if (ret != LOS_OK) {
        printf("task1 create failed .\n");
        return LOS_NOK;
    }

    /* 创建任务2 */
    (VOID)memset_s(&task2, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S));
    task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_SemTask2;
    task2.pcName       = "TestTsk2";
    task2.uwStackSize  = OS_TSK_DEFAULT_STACK_SIZE;
    task2.usTaskPrio   = (TASK_PRIO_TEST - 1);
    ret = LOS_TaskCreate(&g_testTaskId02, &task2);
    if (ret != LOS_OK) {
        printf("task2 create failed .\n");
        return LOS_NOK;
    }

    /* 解锁任务调度 */
    LOS_TaskUnlock();

    ret = LOS_SemPost(g_semId);

    /* 任务休眠40 ticks */
    LOS_TaskDelay(40);

    /* 删除信号量 */
    LOS_SemDelete(g_semId);

    /* 删除任务1 */
    ret = LOS_TaskDelete(g_testTaskId01);
    if (ret != LOS_OK) {
        printf("task1 delete failed .\n");
        return LOS_NOK;
    }
    /* 删除任务2 */
    ret = LOS_TaskDelete(g_testTaskId02);
    if (ret != LOS_OK) {
        printf("task2 delete failed .\n");
        return LOS_NOK;
    }

    return LOS_OK;
}

实例运行结果:


Example_SemTask2 try get sem g_semId wait forever.
Example_SemTask1 try get sem g_semId ,timeout 10 ticks.
Example_SemTask2 get sem g_semId and then delay 20ticks .
Example_SemTask1 timeout and try get sem g_semId wait forever.
Example_SemTask2 post sem g_semId .
Example_SemTask1 wait_forever and get sem g_semId .

百文说内核 | 抓住主脉络

子曰:“诗三百,一言以蔽之,曰‘思无邪’。”——《论语》:为政篇。百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在开源鸿蒙内核源码加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。百篇博客系列思维导图结构如下:

根据上图的思维导图,我们未来将要和大家一一分享以上大部分关键技术点的博客文章。

百万汉字注解.精读内核源码

如果大家觉得看文章不过瘾,想直接撸代码的话,可以去下面四大码仓围观同步注释内核源码:

gitee仓

https://gitee.com/weharmony/kernel_liteos_a_note

github仓 :

https://github.com/kuangyufei/kernel_liteos_a_note

codechina仓

https://codechina.csdn.net/kuangyufei/kernel_liteos_a_note

coding仓

https://weharmony.coding.net/public/harmony/kernel_liteos_a_note/git/files

写在最后

我们最近正带着大家玩嗨OpenHarmony。如果你有用OpenHarmony开发的好玩的东东,或者有对OpenHarmony的深度技术剖析,想通过我们平台让更多的小伙伴知道和分享的,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:

合作邮箱:zzliang@atomsource.org