前言
OpenHarmony的事件提供一种任务间的同步机制,简单来说就是一个或多个任务可以通过写一个或多个不同的事件来触发内核调度让另一个等待读取事件的任务进入运行状态,从而实现任务间的同步。具体是怎么实现的呢?今天我就带大伙深入到内核当中,扒一扒事件的源代码。
关键数据结构
在解读事件的源码之前还是要先了解下事件的关键的数据结构PEVENT_CB_S,数据结构永远是内核学习绕不开的坎:
typedef struct tagEvent {
UINT32 uwEventID;
LOS_DL_LIST stEventList; /**< Event control block linked list */
} EVENT_CB_S, *PEVENT_CB_S;
- uwEventID:标记任务的事件类型,每个bit可以标识一个事件最多支持31个事件(第25bit保留)。
- stEventList:事件控制块的双向循环链表,理解这个字段是理解事件的关键。在双向循环链表中唯一不变的节点就是头节点,而这里的stEventList就是头节点。当有任务等待事件但事件还没发生时任务会被挂载到等待链表中,当事件发生时系统唤醒等待事件的任务,此时任务就会被剔出链表。
事件初始化
下面是事件初始化源码:
LITE_OS_SEC_TEXT_INIT UINT32 LOS_EventInit(PEVENT_CB_S eventCB)
{
if (eventCB == NULL) {
return LOS_ERRNO_EVENT_PTR_NULL;
}
eventCB->uwEventID = 0;
LOS_ListInit(&eventCB->stEventList);
OsHookCall(LOS_HOOK_TYPE_EVENT_INIT, eventCB);
return LOS_OK;
}
PEVENT_CB_S 相当于 EVENT_CB_S *, 因此eventCB是指针,是指针,是指针,重要的话说三遍哈。
侧面也说明事件控制块由任务自己创建,内核事件模块只负责维护。到这里大伙就知道怎么创建事件控制块了吧,任务定义自己的事件控制块变量,然后通过LOS_EventInit来初始化,此时没有事件发生,当然事件链表空空如也。
用图来表达就是:
事件写操作
任务可以通过LOS_EventWrite来写触发一个或多个事件:
LITE_OS_SEC_TEXT UINT32 LOS_EventWrite(PEVENT_CB_S eventCB, UINT32 events)
{
eventCB->uwEventID |= events; ---1
if (!LOS_ListEmpty(&eventCB->stEventList)) { ---2
for (resumedTask = LOS_DL_LIST_ENTRY((&eventCB->stEventList)->pstNext, LosTaskCB, pendList);
&resumedTask->pendList != (&eventCB->stEventList);) { -------3
nextTask = LOS_DL_LIST_ENTRY(resumedTask->pendList.pstNext, LosTaskCB, pendList);
if (((resumedTask->eventMode & LOS_WAITMODE_OR) && (resumedTask->eventMask & events) != 0) ||
((resumedTask->eventMode & LOS_WAITMODE_AND) &&
((resumedTask->eventMask & eventCB->uwEventID) == resumedTask->eventMask))) {
exitFlag = 1;
OsSchedTaskWake(resumedTask); ---4
}
resumedTask = nextTask;
}
if (exitFlag == 1) {
LOS_IntRestore(intSave);
LOS_Schedule(); ---5
return LOS_OK;
}
}
}
1处,保存事件使用的或运算操作,因此一个或多个任务可以写一个或多个事件一次或多次,当然是不同的事件,多次写同一个事件相当于只写了一次。
2处,有事件发生了就该检查是否有任务在等待事件,事件链表不为空说明有任务在等待事件。
3处,遍历事件链表,唤醒符合条件的任务。LOS_DL_LIST_ENTRY((&eventCB->stEventList)->pstNext, LosTaskCB, pendList) 前面说过头节点是空节点,第一次遍历从头节点的下一个节点开始,后续顺藤摸瓜 依次找出nextTask,直到回到头节点。
4处,针对事件读取模式,找到满足条件的任务并唤醒该任务。
5处,一旦匹配到等待事件的任务,则执行任务调度,被唤醒的任务得到执行。
写事件实际操作如下图:
事件读操作
LiteOS为用户提供了两个事件读函数:
- LOS_EventPoll():根据任务传入的事件值、掩码及校验模式,返回满足条件的事件,任务可以主动检查事件是否发生而不必被挂起。
- LOS_EventRead():读取事件,可以理解为阻塞式读,如果事件没有发生,可以指定等待时间,挂起当前任务;
下面是LOS_EventPoll()的实现:
LITE_OS_SEC_TEXT UINT32 LOS_EventPoll(UINT32 *eventID, UINT32 eventMask, UINT32 mode)
{
UINT32 ret = 0;
UINT32 intSave;
if (eventID == NULL) {
return LOS_ERRNO_EVENT_PTR_NULL;
}
intSave = LOS_IntLock();
if (mode & LOS_WAITMODE_OR) {
if ((*eventID & eventMask) != 0) { ---1
ret = *eventID & eventMask;
}
} else {
if ((eventMask != 0) && (eventMask == (*eventID & eventMask))) { ---2
ret = *eventID & eventMask;
}
}
if (ret && (mode & LOS_WAITMODE_CLR)) { ---3
*eventID = *eventID & ~(ret);
}
LOS_IntRestore(intSave);
return ret;
}
1处,如果读取模式是LOS_WAITMODE_OR,只要有一个事件发生则读取成功,返回发生的那个事件。
2处,如果读取模式LOS_WAITMODE_AND,全部检查事件发生才算读取成功,并返回全部发生事件。
3处,事件读取成功后事件控制块中的事件标记怎么处理?这里通过LOS_WAITMODE_CLR来决定是否清除事件标记。
可以看出以上实现了两种事件读取方式:多个事件只要一个发生就算发生和全部事件发生才算发生。
下面是LOS_EventRead():
LITE_OS_SEC_TEXT UINT32 LOS_EventRead(PEVENT_CB_S eventCB, UINT32 eventMask, UINT32 mode, UINT32 timeOut)
{
ret = LOS_EventPoll(&(eventCB->uwEventID), eventMask, mode); ---1
OsHookCall(LOS_HOOK_TYPE_EVENT_READ, eventCB, eventMask, mode, timeOut);
if (ret == 0) {
if (timeOut == 0) {
LOS_IntRestore(intSave);
return ret;
}
if (g_losTaskLock) {
LOS_IntRestore(intSave);
return LOS_ERRNO_EVENT_READ_IN_LOCK;
}
runTsk = g_losTask.runTask;
runTsk->eventMask = eventMask;
runTsk->eventMode = mode;
OsSchedTaskWait(&eventCB->stEventList, timeOut); ---2
LOS_IntRestore(intSave);
LOS_Schedule(); ---3
intSave = LOS_IntLock();
if (runTsk->taskStatus & OS_TASK_STATUS_TIMEOUT) {
runTsk->taskStatus &= ~OS_TASK_STATUS_TIMEOUT;
LOS_IntRestore(intSave);
return LOS_ERRNO_EVENT_READ_TIMEOUT;
}
ret = LOS_EventPoll(&eventCB->uwEventID, eventMask, mode); ---4
}
}
1处,主动查询想要的事件是否已经发生。
2处,如果事件没有发生,就把当前任务挂起到等待事件链表中。
3处,如果事件没有发生,当前读事件的任务被挂起,让出CPU。
4处,事件发生时等待事件的任务被调度再次获得CPU恢复执行,读取事件。
事件读写整个过程串起来如下图所示:
事件销毁操作
做事有始有终,事件消费完成剩下的事情当然是清除事件和等待事件的任务链表。
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_EventClear(PEVENT_CB_S eventCB, UINT32 eventMask)
{
eventCB->uwEventID &= eventMask;
}
LITE_OS_SEC_TEXT_INIT UINT32 LOS_EventDestroy(PEVENT_CB_S eventCB)
{
eventCB->stEventList.pstNext = (LOS_DL_LIST *)NULL;
eventCB->stEventList.pstPrev = (LOS_DL_LIST *)NULL;
}
在LOS_EventClear中通过使eventMask=0来清空事件,在LOS_EventDestroy中清空事件链表指针。
小结
事件模块本身并不复杂,相信看了上面的描述大伙对事件的运作机制已经有了更深刻的理解,下面我们来个总结:
- 事件控制块由任务创建,事件模块本身只维护事件控制块的内容。
- 写事件会触发读事件任务被唤醒,任务调度就这么发生了。
- 任务可以主动查询事件,也可以被动等待事件发生时来唤醒自己。
- 事件结束后根据应用场景可以有选择的清除事件ID或(和)事件链表。