v27.05 鸿蒙内核源码分析(互斥锁篇) | 比自旋锁丰满的互斥锁 | 百篇博客分析OpenHarmony源码

子曰:“狂而不直,侗而不愿,悾悾而不信,吾不知之矣。” 《论语》:泰伯篇

在这里插入图片描述

百篇博客系列篇.本篇为:

v27.xx 鸿蒙内核源码分析(互斥锁篇) | 比自旋锁丰满的互斥锁

进程通讯相关篇为:

  • v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志
  • v27.05 鸿蒙内核源码分析(互斥锁) | 比自旋锁丰满的互斥锁
  • v28.04 鸿蒙内核源码分析(进程通讯) | 九种进程间通讯方式速揽
  • v29.05 鸿蒙内核源码分析(信号量) | 谁在负责解决任务的同步
  • v30.07 鸿蒙内核源码分析(事件控制) | 任务间多对多的同步方案
  • v33.03 鸿蒙内核源码分析(消息队列) | 进程间如何异步传递大数据

本篇说清楚互斥锁

内核中哪些地方会用到互斥锁?看图:
v27.05 鸿蒙内核源码分析(互斥锁篇) | 比自旋锁丰满的互斥锁 | 百篇博客分析OpenHarmony源码_第1张图片

图中是内核有关模块对互斥锁初始化,有文件,有内存,用消息队列等等,使用面非常的广.其实在给内核源码加注的过程中,会看到大量的自旋锁和互斥锁,它们的存在有序的保证了内核和应用程序的正常运行.是非常基础和重要的功能.

概述

自旋锁互斥锁 虽都是锁,但解决的问题不同, 自旋锁解决用于CPU核间共享内存的竞争,而互斥锁解决线程(任务)间共享内存的竞争.

自旋锁的特点是死守共享资源,拿不到锁,CPU选择忙等(busy waiting),等待其他CPU释放资源.所以共享代码段不能太复杂,否则容易死锁,休克.

互斥锁的特点是拿不到锁往往原任务阻塞,切换到新任务运行.CPU是会一直跑的.这样很容易会想到几个问题:

第一:会出现很多任务在等同一把锁的情况出现,因为切换新任务也可能因要同一把锁而被阻塞,CPU又被调去跑新新任务了.这样就会出现一个等锁的链表.

第二:持有锁的一方再申请同一把锁时还能成功吗? 答案是可以的,这种锁叫递归锁,是鸿蒙内核默认方式.

第三:当优先级很高的A任务要锁失败,主动让出CPU进入睡眠,而如果持有锁的B任务优先级很低, 迟迟等不到调度不到B任务运行,无法释放锁怎么办?
答案是会临时调整B任务的优先级,调到A一样高,这样B能很快的被调度到,等B释放锁后其优先级又会被打回原形.所以一个任务的优先级会看情况时高时低.

第四:B任务释放锁之后要主动唤醒等锁的任务链表,使他们能加入就绪队列,等待被调度.调度算法是一视同仁的,它只看优先级.

带着这些问题,进入鸿蒙内核互斥锁的实现代码,本篇代码量较大, 每行代码都一一注解说明.

互斥锁长什么样?

enum {
    LOS_MUX_PRIO_NONE = 0,  //线程的优先级和调度不会受到互斥锁影响,先来后到,普通排队.
    LOS_MUX_PRIO_INHERIT = 1, //当高优先级的等待低优先级的线程释放锁时,低优先级的线程以高优先级线程的优先级运行。
           //当线程解锁互斥量时,线程的优先级自动被将到它原来的优先级
    LOS_MUX_PRIO_PROTECT = 2 //详见:OsMuxPendOp中的注解,详细说明了LOS_MUX_PRIO_PROTECT的含义
};
enum {
    LOS_MUX_NORMAL = 0,  //非递归锁 只有[0.1]两个状态,不做任何特殊的错误检,不进行deadlock detection(死锁检测)
    LOS_MUX_RECURSIVE = 1, //递归锁 允许同一线程在互斥量解锁前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁,别的线程就无法加锁此互斥量。
    LOS_MUX_ERRORCHECK = 2, //进行错误检查,如果一个线程企图对一个已经锁住的mutex进行relock或对未加锁的unlock,将返回一个错误。
    LOS_MUX_DEFAULT = LOS_MUX_RECURSIVE //鸿蒙系统默认使用递归锁
};
typedef struct { //互斥锁的属性
    UINT8 protocol;  //协议
    UINT8 prioceiling; //优先级上限
    UINT8 type;   //类型属性
    UINT8 reserved;  //保留字段
} LosMuxAttr;

typedef struct OsMux { //互斥锁结构体
    UINT32 magic;        /**< magic number */  //魔法数字
    LosMuxAttr attr;     /**< Mutex attribute */ //互斥锁属性
    LOS_DL_LIST holdList; /**< The task holding the lock change */ //当有任务拿到本锁时,通过holdList节点把锁挂到该任务的锁链表上
    LOS_DL_LIST muxList; /**< Mutex linked list */ //等这个锁的任务链表,上面挂的都是任务,注意和holdList的区别.
    VOID *owner;         /**< The current thread that is locking a mutex */ //当前拥有这把锁的任务
    UINT16 muxCount;     /**< Times of locking a mutex */ //锁定互斥体的次数,递归锁允许多次
} LosMux;

这互斥锁长的明显的比自旋锁丰满多啦,还记得自旋锁的样子吗,就一个变量,单薄到令人心疼.

初始化

LITE_OS_SEC_TEXT UINT32 LOS_MuxInit(LosMux *mutex, const LosMuxAttr *attr)
{   //...
    SCHEDULER_LOCK(intSave);        //拿到调度自旋锁
    mutex->muxCount = 0;            //锁定互斥量的次数
    mutex->owner = NULL;            //持有该锁的任务
    LOS_ListInit(&mutex->muxList);  //初始化等待该锁的任务链表
    mutex->magic = OS_MUX_MAGIC;    //固定标识,互斥锁的魔法数字
    SCHEDULER_UNLOCK(intSave);      //释放调度自旋锁
    return LOS_OK;
}

留意mutex->muxList,这又是一个双向链表, 双向链表是内核最重要的结构体,不仅仅是鸿蒙内核,在linux内核中(list_head)又何尝不是,牢牢的寄生在宿主结构体上.muxList上挂的是未来所有等待这把锁的任务.

三种申请模式

申请互斥锁有三种模式:无阻塞模式、永久阻塞模式、定时阻塞模式。

无阻塞模式:即任务申请互斥锁时,入参timeout等于0。若当前没有任务持有该互斥锁,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功,否则立即返回申请失败。

永久阻塞模式:即任务申请互斥锁时,入参timeout等于0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则,任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行。

定时阻塞模式:即任务申请互斥锁时,0

如果有任务阻塞于该互斥锁,则唤醒被阻塞任务中优先级最高的,该任务进入就绪态,并进行任务调度。
如果没有任务阻塞于该互斥锁,则互斥锁释放成功。

申请互斥锁主函数 OsMuxPendOp

//互斥锁的主体函数,由OsMuxlockUnsafe调用,互斥锁模块最重要的几个函数之一
//最坏情况就是拿锁失败,让出CPU,变成阻塞任务,等别的任务释放锁后排到自己了接着执行. 
STATIC UINT32 OsMuxPendOp(LosTaskCB *runTask, LosMux *mutex, UINT32 timeout)
{
    UINT32 ret;
    LOS_DL_LIST *node = NULL;
    LosTaskCB *owner = NULL;

    if ((mutex->muxList.pstPrev == NULL) || (mutex->muxList.pstNext == NULL)) {//列表为空时的处理
        /* This is for mutex macro initialization. */
        mutex->muxCount = 0;//锁计数器清0
        mutex->owner = NULL;//锁没有归属任务
        LOS_ListInit(&mutex->muxList);//初始化锁的任务链表,后续申请这把锁任务都会挂上去
    }

    if (mutex->muxCount == 0) {//无task用锁时,肯定能拿到锁了.在里面返回
        mutex->muxCount++;              //互斥锁计数器加1
        mutex->owner = (VOID *)runTask; //当前任务拿到锁
        LOS_ListTailInsert(&runTask->lockList, &mutex->holdList);//持有锁的任务改变了,节点挂到当前task的锁链表
        if ((runTask->priority > mutex->attr.prioceiling) && (mutex->attr.protocol == LOS_MUX_PRIO_PROTECT)) {//看保护协议的做法是怎样的?
            LOS_BitmapSet(&runTask->priBitMap, runTask->priority);//1.priBitMap是记录任务优先级变化的位图,这里把任务当前的优先级记录在priBitMap
            OsTaskPriModify(runTask, mutex->attr.prioceiling);//2.把高优先级的mutex->attr.prioceiling设为当前任务的优先级.
        }//注意任务优先级有32个, 是0最高,31最低!!!这里等于提高了任务的优先级,目的是让其在下次调度中继续提高被选中的概率,从而快速的释放锁.
        return LOS_OK;
    }
    //递归锁muxCount>0 如果是递归锁就要处理两种情况 1.runtask持有锁 2.锁被别的任务拿走了
    if (((LosTaskCB *)mutex->owner == runTask) && (mutex->attr.type == LOS_MUX_RECURSIVE)) {//第一种情况 runtask是锁持有方
        mutex->muxCount++;  //递归锁计数器加1,递归锁的目的是防止死锁,鸿蒙默认用的就是递归锁(LOS_MUX_DEFAULT = LOS_MUX_RECURSIVE)
        return LOS_OK;      //成功退出
    }
    //到了这里说明锁在别的任务那里,当前任务只能被阻塞了.
    if (!timeout) {//参数timeout表示等待多久再来拿锁
        return LOS_EINVAL;//timeout = 0表示不等了,没拿到锁就返回不纠结,返回错误.见于LOS_MuxTrylock 
    }
    //自己要被阻塞,只能申请调度,让出CPU core 让别的任务上
    if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁)
        return LOS_EDEADLK;//返回错误,自旋锁被别的CPU core 持有
    }

    OsMuxBitmapSet(mutex, runTask, (LosTaskCB *)mutex->owner);//设置锁位图,尽可能的提高锁持有任务的优先级

    owner = (LosTaskCB *)mutex->owner;  //记录持有锁的任务
    runTask->taskMux = (VOID *)mutex;   //记下当前任务在等待这把锁
    node = OsMuxPendFindPos(runTask, mutex);//在等锁链表中找到一个优先级比当前任务更低的任务
    ret = OsTaskWait(node, timeout, TRUE);//task陷入等待状态 TRUE代表需要调度
    if (ret == LOS_ERRNO_TSK_TIMEOUT) {//这行代码虽和OsTaskWait挨在一起,但要过很久才会执行到,因为在OsTaskWait中CPU切换了任务上下文
        runTask->taskMux = NULL;// 所以重新回到这里时可能已经超时了
        ret = LOS_ETIMEDOUT;//返回超时
    }

    if (timeout != LOS_WAIT_FOREVER) {//不是永远等待的情况
        OsMuxBitmapRestore(mutex, runTask, owner);//恢复锁的位图
    }

    return ret;
}

释放锁的主体函数 OsMuxPostOp

//是否有其他任务持有互斥锁而处于阻塞状,如果是就要唤醒它,注意唤醒一个任务的操作是由别的任务完成的
//OsMuxPostOp只由OsMuxUnlockUnsafe,参数任务归还锁了,自然就会遇到锁要给谁用的问题, 因为很多任务在申请锁,由OsMuxPostOp来回答这个问题
STATIC UINT32 OsMuxPostOp(LosTaskCB *taskCB, LosMux *mutex, BOOL *needSched)
{
    LosTaskCB *resumedTask = NULL;

    if (LOS_ListEmpty(&mutex->muxList)) {//如果互斥锁列表为空
        LOS_ListDelete(&mutex->holdList);//把持有互斥锁的节点摘掉
        mutex->owner = NULL;
        return LOS_OK;
    }

    resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(mutex->muxList)));//拿到等待互斥锁链表的第一个任务实体,接下来要唤醒任务
    if (mutex->attr.protocol == LOS_MUX_PRIO_INHERIT) {//互斥锁属性协议是继承会怎么操作?
        if (resumedTask->priority > taskCB->priority) {//拿到锁的任务优先级低于参数任务优先级
            if (LOS_HighBitGet(taskCB->priBitMap) != resumedTask->priority) {//参数任务bitmap中最低的优先级不等于等待锁的任务优先级
                LOS_BitmapClr(&taskCB->priBitMap, resumedTask->priority);//把等待任务锁的任务的优先级记录在参数任务的bitmap中
            }
        } else if (taskCB->priBitMap != 0) {//如果bitmap不等于0说明参数任务至少有任务调度的优先级
            OsMuxPostOpSub(taskCB, mutex);//
        }
    }
    mutex->muxCount = 1;//互斥锁数量为1
    mutex->owner = (VOID *)resumedTask;//互斥锁的持有人换了
    resumedTask->taskMux = NULL;//resumedTask不再等锁了
    LOS_ListDelete(&mutex->holdList);//自然要从等锁链表中把自己摘出去
    LOS_ListTailInsert(&resumedTask->lockList, &mutex->holdList);//把锁挂到恢复任务的锁链表上,lockList是任务持有的所有锁记录
    OsTaskWake(resumedTask);//resumedTask有了锁就唤醒它,因为当初在没有拿到锁时处于了pend状态
    if (needSched != NULL) {//如果不为空
        *needSched = TRUE;//就走起再次调度流程
    }

    return LOS_OK;
}

编程实例

本实例实现如下流程。

  • 任务Example_TaskEntry创建一个互斥锁,锁任务调度,创建两个任务Example_MutexTask1、Example_MutexTask2。Example_MutexTask2优先级高于Example_MutexTask1,解锁任务调度,然后Example_TaskEntry任务休眠300Tick。
  • Example_MutexTask2被调度,以永久阻塞模式申请互斥锁,并成功获取到该互斥锁,然后任务休眠100Tick,Example_MutexTask2挂起,Example_MutexTask1被唤醒。
  • Example_MutexTask1以定时阻塞模式申请互斥锁,等待时间为10Tick,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。10Tick超时时间到达后,Example_MutexTask1被唤醒,以永久阻塞模式申请互斥锁,因互斥锁仍被Example_MutexTask2持有,Example_MutexTask1挂起。
  • 100Tick休眠时间到达后,Example_MutexTask2被唤醒, 释放互斥锁,唤醒Example_MutexTask1。Example_MutexTask1成功获取到互斥锁后,释放锁。
  • 300Tick休眠时间到达后,任务Example_TaskEntry被调度运行,删除互斥锁,删除两个任务。
/* 互斥锁句柄id */
UINT32 g_testMux;
/* 任务ID */
UINT32 g_testTaskId01;
UINT32 g_testTaskId02;

VOID Example_MutexTask1(VOID)
{
    UINT32 ret;

    printf("task1 try to get  mutex, wait 10 ticks.\n");
    /* 申请互斥锁 */
    ret = LOS_MuxPend(g_testMux, 10);

    if (ret == LOS_OK) {
        printf("task1 get mutex g_testMux.\n");
        /* 释放互斥锁 */
        LOS_MuxPost(g_testMux);
        return;
    } else if (ret == LOS_ERRNO_MUX_TIMEOUT ) {
            printf("task1 timeout and try to get mutex, wait forever.\n");
            /* 申请互斥锁 */
            ret = LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);
            if (ret == LOS_OK) {
                printf("task1 wait forever, get mutex g_testMux.\n");
                /* 释放互斥锁 */
                LOS_MuxPost(g_testMux);
                return;
            }
    }
    return;
}

VOID Example_MutexTask2(VOID)
{
    printf("task2 try to get  mutex, wait forever.\n");
    /* 申请互斥锁 */
    (VOID)LOS_MuxPend(g_testMux, LOS_WAIT_FOREVER);

    printf("task2 get mutex g_testMux and suspend 100 ticks.\n");

    /* 任务休眠100Ticks */
    LOS_TaskDelay(100);

    printf("task2 resumed and post the g_testMux\n");
    /* 释放互斥锁 */
    LOS_MuxPost(g_testMux);
    return;
}

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

    /* 创建互斥锁 */
    LOS_MuxCreate(&g_testMux);

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

    /* 创建任务1 */
    memset(&task1, 0, sizeof(TSK_INIT_PARAM_S));
    task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask1;
    task1.pcName       = "MutexTsk1";
    task1.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    task1.usTaskPrio   = 5;
    ret = LOS_TaskCreate(&g_testTaskId01, &task1);
    if (ret != LOS_OK) {
        printf("task1 create failed.\n");
        return LOS_NOK;
    }

    /* 创建任务2 */
    memset(&task2, 0, sizeof(TSK_INIT_PARAM_S));
    task2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_MutexTask2;
    task2.pcName       = "MutexTsk2";
    task2.uwStackSize  = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;
    task2.usTaskPrio   = 4;
    ret = LOS_TaskCreate(&g_testTaskId02, &task2);
    if (ret != LOS_OK) {
        printf("task2 create failed.\n");
        return LOS_NOK;
    }

    /* 解锁任务调度 */
    LOS_TaskUnlock();
    /* 休眠300Ticks */
    LOS_TaskDelay(300);

    /* 删除互斥锁 */
    LOS_MuxDelete(g_testMux);

    /* 删除任务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;
}

结果验证

task2 try to get  mutex, wait forever.
task2 get mutex g_testMux and suspend 100 ticks.
task1 try to get  mutex, wait 10 ticks.
task1 timeout and try to get mutex, wait forever.
task2 resumed and post the g_testMux
task1 wait forever,get mutex g_testMux.

总结

1.互斥锁解决的是任务间竞争共享内存的问题.

2.申请锁失败的任务会进入睡眠OsTaskWait,内核会比较持有锁的任务和申请锁任务的优先级,把持有锁的任务优先级调到尽可能的高,以便更快的被调度执行,早日释放锁.

3.释放锁的任务会在等锁链表中找一个高优先级任务,通过OsTaskWake唤醒它,并向调度算法申请调度.但要注意,调度算法只是按优先级来调度,并不保证调度后的任务一定是要唤醒的任务.

4.互斥锁篇关键是看懂 OsMuxPendOp 和 OsMuxPostOp 两个函数.

百篇博客分析.深挖内核地基

  • 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 
  • 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

基础工具 加载运行 进程管理 编译构建
双向链表
位图管理
用栈方式
定时器
原子操作
时间管理
ELF格式
ELF解析
静态链接
重定位
进程映像
进程管理
进程概念
Fork
特殊进程
进程回收
信号生产
信号消费
Shell编辑
Shell解析
编译环境
编译过程
环境脚本
构建工具
gn应用
忍者ninja
进程通讯 内存管理 前因后果 任务管理
自旋锁
互斥锁
进程通讯
信号量
事件控制
消息队列
内存分配
内存管理
内存汇编
内存映射
内存规则
物理内存
总目录
调度故事
内存主奴
源码注释
源码结构
静态站点
时钟任务
任务调度
任务管理
调度队列
调度机制
线程概念
并发并行
CPU
系统调用
任务切换
文件系统 硬件架构
文件概念
文件系统
索引节点
挂载目录
根文件系统
字符设备
VFS
文件句柄
管道文件
汇编基础
汇编传参
工作模式
寄存器
异常接管
汇编汇总
中断切换
中断概念
中断管理

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

四大码仓中文注解 . 定期同步官方代码

鸿蒙研究站( weharmonyos ) | 每天死磕一点点,原创不易,欢迎转载,请注明出处。若能支持点赞更好,感谢每一份支持。

你可能感兴趣的:(v27.05 鸿蒙内核源码分析(互斥锁篇) | 比自旋锁丰满的互斥锁 | 百篇博客分析OpenHarmony源码)