Skip to content

48_事件控制篇

kuangyufei edited this page Sep 5, 2022 · 1 revision

本篇关键词:、、、

下载 >> 离线文档.鸿蒙内核源码分析(百篇博客分析.挖透鸿蒙内核).pdf

通讯机制相关篇为:

本篇说清楚事件(Event)

读本篇之前建议先读 v08.xx 鸿蒙内核源码分析(总目录) 其他篇。

官方概述

先看官方对事件的描述。

事件(Event)是一种任务间通信的机制,可用于任务间的同步。

多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。

  • 一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。

  • 多对多同步模型:多个任务等待多个事件的触发。

鸿蒙提供的事件具有如下特点:

  • 任务通过创建事件控制块来触发事件或等待事件。
  • 事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。
  • 事件仅用于任务间的同步,不提供数据传输功能。
  • 多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。
  • 多个任务可以对同一事件进行读写操作。
  • 支持事件读写超时机制。

再看事件图

注意图中提到了三个概念 事件控制块 事件 任务 接下来结合代码来理解事件模块的实现。

事件控制块长什么样?

typedef struct tagEvent {
    UINT32 uwEventID;        /**< Event mask in the event control block,//标识发生的事件类型位,事件ID,每一位标识一种事件类型
                                  indicating the event that has been logically processed. */
    LOS_DL_LIST stEventList; /**< Event control block linked list *///读取事件任务链表
} EVENT_CB_S*PEVENT_CB_S;

简单是简单,就两个变量,如下: uwEventID:用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共31种事件类型,第25位系统保留。

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

事件控制块<>事件<>任务 三者关系

一定要搞明白这三者的关系,否则搞不懂事件模块是如何运作的。

  • 任务是事件的生产者,通过 LOS_EventWrite,向外部广播发生了XX事件,并唤醒此前已在事件控制块中登记过的要等待XX事件发生的XX任务。

  • 事件控制块EVENT_CB_S 是记录者,只干两件事件:

    1.uwEventID按位记录哪些事件发生了,它只是记录,怎么消费它不管的。

    2.stEventList记录哪些任务在等待事件,但任务究竟在等待哪些事件它也是不记录的

  • 任务也是消费者,通过 LOS_EventRead消费,只有任务自己清楚要以什么样的方式,消费什么样的事件。 先回顾下任务结构体 LosTaskCB 对事件部分的描述如下:

    typedef struct {
        //...去掉不相关的部分
        VOID            *taskEvent;  //和任务发生关系的事件控制块
        UINT32          eventMask;   //对哪些事件进行屏蔽
        UINT32          eventMode;   //事件三种模式(LOS_WAITMODE_AND,LOS_WAITMODE_OR,LOS_WAITMODE_CLR)
    } LosTaskCB;    

    taskEvent 指向的就是 EVENT_CB_S

    eventMask 屏蔽掉 事件控制块 中的哪些事件

    eventMode 已什么样的方式去消费事件,三种读取模式

    #define LOS_WAITMODE_AND                    4U 	
    #define LOS_WAITMODE_OR                     2U 	
    #define LOS_WAITMODE_CLR                    1U	
    • 所有事件(LOS_WAITMODE_AND):逻辑与,基于接口传入的事件类型掩码eventMask,只有这些事件都已经发生才能读取成功,否则该任务将阻塞等待或者返回错误码。

    • 任一事件(LOS_WAITMODE_OR):逻辑或,基于接口传入的事件类型掩码eventMask,只要这些事件中有任一种事件发生就可以读取成功,否则该任务将阻塞等待或者返回错误码。

    • 清除事件(LOS_WAITMODE_CLR):这是一种附加读取模式,需要与所有事件模式或任一事件模式结合使用(LOS_WAITMODE_AND | LOS_WAITMODE_CLRLOS_WAITMODE_OR | LOS_WAITMODE_CLR)。在这种模式下,当设置的所有事件模式或任一事件模式读取成功后,会自动清除事件控制块中对应的事件类型位。

  • 一个事件控制块EVENT_CB_S中的事件可以来自多个任务,多个任务也可以同时消费事件控制块中的事件,并且这些任务之间可以没有任何关系!

函数列表

事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。

其中读懂 OsEventWriteOsEventRead 就明白了事件模块。

事件初始化 -> LOS_EventInit

//初始化一个事件控制块
LITE_OS_SEC_TEXT_INIT UINT32 LOS_EventInit(PEVENT_CB_S eventCB)
{
    UINT32 intSave;
    intSave = LOS_IntLock();//锁中断
    eventCB->uwEventID = 0; //其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生)
    LOS_ListInit(&eventCB->stEventList);//事件链表初始化
    LOS_IntRestore(intSave);//恢复中断
    return LOS_OK;
}

代码解读:

  • 事件是共享资源,所以操作期间不能产生中断。
  • 初始化两个记录者 uwEventID stEventList

事件生产过程 -> OsEventWrite

LITE_OS_SEC_TEXT VOID OsEventWriteUnsafe(PEVENT_CB_S eventCBUINT32 eventsBOOL onceUINT8 *exitFlag)
{
    LosTaskCB *resumedTask = NULL;
    LosTaskCB *nextTask = NULL;
    BOOL schedFlag = FALSE;

    eventCB->uwEventID |= events;//对应位贴上标签
    if (!LOS_ListEmpty(&eventCB->stEventList)) {//等待事件链表判断,处理等待事件的任务
        for (resumedTask = LOS_DL_LIST_ENTRY((&eventCB->stEventList)->pstNextLosTaskCBpendList);
             &resumedTask->pendList != &eventCB->stEventList;) {//循环获取任务链表
            nextTask = LOS_DL_LIST_ENTRY(resumedTask->pendList.pstNextLosTaskCBpendList);//获取任务实体
            if (OsEventResume(resumedTaskeventCBevents)) {//是否恢复任务
                schedFlag = TRUE;//任务已加至就绪队列,申请发生一次调度
            }
            if (once == TRUE) {//是否只处理一次任务
                break;//退出循环
            }
            resumedTask = nextTask;//检查链表中下一个任务
        }
    }

    if ((exitFlag != NULL) && (schedFlag == TRUE)) {//是否让外面调度
        *exitFlag = 1;
    }
}
//写入事件
LITE_OS_SEC_TEXT STATIC UINT32 OsEventWrite(PEVENT_CB_S eventCBUINT32 eventsBOOL once)
{
    UINT32 intSave;
    UINT8 exitFlag = 0;

    SCHEDULER_LOCK(intSave);	//禁止调度
    OsEventWriteUnsafe(eventCBeventsonce&exitFlag);//写入事件
    SCHEDULER_UNLOCK(intSave);	//允许调度

    if (exitFlag == 1) { //需要发生调度
        LOS_MpSchedule(OS_MP_CPU_ALL);//通知所有CPU调度
        LOS_Schedule();//执行调度
    }
    return LOS_OK;
}

代码解读:

  1. 给对应位贴上事件标签,eventCB->uwEventID |= events; 注意uwEventID是按位管理的。每个位代表一个事件是否写入,例如 uwEventID = 00010010 代表产生了 1,4 事件

  2. 循环从stEventList链表中取出等待这个事件的任务判断是否唤醒任务。 OsEventResume

//事件恢复,判断是否唤醒任务
LITE_OS_SEC_TEXT STATIC UINT8 OsEventResume(LosTaskCB *resumedTaskconst PEVENT_CB_S eventCBUINT32 events)
{
    UINT8 exitFlag = 0;//是否唤醒

    if (((resumedTask->eventMode & LOS_WAITMODE_OR) && ((resumedTask->eventMask & events) != 0)) ||
        ((resumedTask->eventMode & LOS_WAITMODE_AND) &&
        ((resumedTask->eventMask & eventCB->uwEventID) == resumedTask->eventMask))) {//逻辑与 和 逻辑或 的处理
        exitFlag = 1; 

        resumedTask->taskEvent = NULL;
        OsTaskWake(resumedTask);//唤醒任务,加入就绪队列
    }

    return exitFlag;
}

3.唤醒任务OsTaskWake只是将任务重新加入就绪队列,需要立即申请一次调度 LOS_Schedule

事件消费过程 -> OsEventRead

LITE_OS_SEC_TEXT STATIC UINT32 OsEventRead(PEVENT_CB_S eventCBUINT32 eventMaskUINT32 modeUINT32 timeoutBOOL once)
{
    UINT32 ret;
    UINT32 intSave;
    SCHEDULER_LOCK(intSave);
    ret = OsEventReadImp(eventCBeventMaskmodetimeoutonce);//读事件实现函数
    SCHEDULER_UNLOCK(intSave);
    return ret;
}

//读取指定事件类型的实现函数,超时时间为相对时间:单位为Tick
LITE_OS_SEC_TEXT STATIC UINT32 OsEventReadImp(PEVENT_CB_S eventCBUINT32 eventMaskUINT32 modeUINT32 timeoutBOOL once)
{
    UINT32 ret = 0;
    LosTaskCB *runTask = OsCurrTaskGet();
    runTask->eventMask = eventMask;
    runTask->eventMode = mode;
    runTask->taskEvent = eventCB;//事件控制块
    ret = OsTaskWait(&eventCB->stEventListtimeout, TRUE);//任务进入等待状态,挂入阻塞链表
    if (ret == LOS_ERRNO_TSK_TIMEOUT) {//如果返回超时
        runTask->taskEvent = NULL;
        return LOS_ERRNO_EVENT_READ_TIMEOUT;
    }
    ret = OsEventPoll(&eventCB->uwEventIDeventMaskmode);//检测事件是否符合预期
    return ret;
}

代码解读:

  • 事件控制块是给任务使用的, 任务给出读取一个事件的条件
    1. eventMask 告诉系统屏蔽掉这些事件,对屏蔽的事件不感冒.
    2. eventMode 已什么样的方式去消费事件,是必须都满足给的条件,还是只满足一个就响应.
    3. 条件给完后,自己进入等待状态 OsTaskWait,等待多久 timeout决定,任务自己说了算.
    4. OsEventPoll检测事件是否符合预期,啥意思?看下它的代码就知道了
      //根据用户传入的事件值、事件掩码及校验模式,返回用户传入的事件是否符合预期
      LITE_OS_SEC_TEXT UINT32 OsEventPoll(UINT32 *eventIDUINT32 eventMaskUINT32 mode)
      {
          UINT32 ret = 0;//事件是否发生了
      
          LOS_ASSERT(OsIntLocked());//断言不允许中断了
          LOS_ASSERT(LOS_SpinHeld(&g_taskSpin));//任务自旋锁
      
          if (mode & LOS_WAITMODE_OR) {//如果模式是读取掩码中任意事件
              if ((*eventID & eventMask) != 0) {
                  ret = *eventID & eventMask; //发生了
              }
          } else {//等待全部事件发生
              if ((eventMask != 0) && (eventMask == (*eventID & eventMask))) {//必须满足全部事件发生
                  ret = *eventID & eventMask; //发生了
              }
          }
      
          if (ret && (mode & LOS_WAITMODE_CLR)) {//是否清除事件
              *eventID = *eventID & ~ret; 
          }
      
          return ret; 
      }

编程实例

本实例实现如下流程。

示例中,任务Example_TaskEntry创建一个任务Example_Event,Example_Event读事件阻塞,Example_TaskEntry向该任务写事件。可以通过示例日志中打印的先后顺序理解事件操作时伴随的任务切换。

  • 在任务Example_TaskEntry创建任务Example_Event,其中任务Example_Event优先级高于Example_TaskEntry。
  • 在任务Example_Event中读事件0x00000001,阻塞,发生任务切换,执行任务Example_TaskEntry。
  • 在任务Example_TaskEntry向任务Example_Event写事件0x00000001,发生任务切换,执行任务Example_Event。
  • Example_Event得以执行,直到任务结束。
  • Example_TaskEntry得以执行,直到任务结束。
#include "los_event.h"
#include "los_task.h"
#include "securec.h"

/* 任务ID */
UINT32 g_testTaskId;

/* 事件控制结构体 */
EVENT_CB_S g_exampleEvent;

/* 等待的事件类型 */
#define EVENT_WAIT 0x00000001

/* 用例任务入口函数 */
VOID Example_Event(VOID)
{
    UINT32 ret;
    UINT32 event;

    /* 超时等待方式读事件,超时时间为100 ticks, 若100 ticks后未读取到指定事件,读事件超时,任务直接唤醒 */
    printf("Example_Event wait event 0x%x \n"EVENT_WAIT);

    event = LOS_EventRead(&g_exampleEventEVENT_WAITLOS_WAITMODE_AND100);
    if (event == EVENT_WAIT) {
        printf("Example_Event,read event :0x%x\n"event);
    } else {
        printf("Example_Event,read event timeout\n");
    }
}

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

    /* 事件初始化 */
    ret = LOS_EventInit(&g_exampleEvent);
    if (ret != LOS_OK) {
        printf("init event failed .\n");
        return -1;
    }

    /* 创建任务 */
    (VOID)memset_s(&task1sizeof(TSK_INIT_PARAM_S), 0sizeof(TSK_INIT_PARAM_S));
    task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Event;
    task1.pcName       = "EventTsk1";
    task1.uwStackSize  = OS_TSK_DEFAULT_STACK_SIZE;
    task1.usTaskPrio   = 5;
    ret = LOS_TaskCreate(&g_testTaskId&task1);
    if (ret != LOS_OK) {
        printf("task create failed .\n");
        return LOS_NOK;
    }

    /* 写g_testTaskId 等待事件 */
    printf("Example_TaskEntry write event .\n");

    ret = LOS_EventWrite(&g_exampleEventEVENT_WAIT);
    if (ret != LOS_OK) {
        printf("event write failed .\n");
        return LOS_NOK;
    }

    /* 清标志位 */
    printf("EventMask:%d\n"g_exampleEvent.uwEventID);
    LOS_EventClear(&g_exampleEvent, ~g_exampleEvent.uwEventID);
    printf("EventMask:%d\n"g_exampleEvent.uwEventID);

    /* 删除任务 */
    ret = LOS_TaskDelete(g_testTaskId);
    if (ret != LOS_OK) {
        printf("task delete failed .\n");
        return LOS_NOK;
    }

    return LOS_OK;
}

运行结果

Example_Event wait event 0x1 
Example_TaskEntry write event .
Example_Eventread event :0x1
EventMask:1
EventMask:0

百文说内核 | 抓住主脉络

  • 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
  • 与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
  • 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:

基础知识 进程管理 任务管理 内存管理
双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数 调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析 任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈 内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐
通讯机制 文件系统 硬件架构 内核汇编
通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存 文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝 芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理 编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断
编译运行 调测工具
编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO 模块监控 日志跟踪 系统安全 测试用例

百万注源码 | 处处扣细节

  • 百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

  • < gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

关注不迷路 | 代码即人生

期间不断得到小伙伴的支持,有学生,有职场新人,也有老江湖,在此一并感谢,大家的支持是前进的动力。尤其每次收到学生的赞助很感慨,后生可敬。 >> 查看捐助名单

据说喜欢 点赞 + 分享 的,后来都成了大神。:)

Clone this wiki locally