引言
- 本章节我们将来实现事件机制,听起来比较抽象,引用生活中的例子,比如我们想要吃饭,需要等待饭做好了还要等待菜炒好才能开始吃饭
- 从任务的角度来理解:任务 A 煮饭,任务 B 炒菜,等到饭菜都做好后,任务 C 才能开始吃饭
- 而我们当前的任务调度策略中,各任务之间并没有什么关系,任务是并行执行的(当然了,微观上依旧是串行执行),我们有办法去控制不同任务中的代码执行顺序吗?
用互斥锁实现事件
- 其实以我们目前实现的系统,是可以实现控制不同任务中的代码执行顺序的,用互斥锁就可以达到这种效果
- 直接看下面的示例代码:
void TaskA(void) { gMutexRice = MutexCreat(); SetCursorPos(0, 8); print("cook rice:"); MutexLock(gMutexRice); while (1) { static U32 cnt = 0; if(cnt < 20) { SetCursorPos(12, 8); print("%d\n", cnt++); Delay(999999); } else break; } MutexUnLock(gMutexRice); } void TaskB(void) { gMutexDish = MutexCreat(); SetCursorPos(0, 10); print("cook dish:"); MutexLock(gMutexDish); while (1) { static U32 cnt = 0; if(cnt < 30) { SetCursorPos(12, 10); print("%d\n", cnt++); Delay(999999); } else break; } MutexUnLock(gMutexDish); } void TaskC(void) { MutexLock(gMutexRice); MutexLock(gMutexDish); SetCursorPos(0, 12); print("have dinner\n"); MutexUnLock(gMutexRice); MutexUnLock(gMutexDish); }
- 完整代码见:app.c
- 运行效果,可以看到,任务 C 在等待任务 A 和任务 B 都结束后才打印出 "have dinner"
- 从上面的实验代码可以看出,使用互斥锁貌似也可以实现事件,然而,这么做合适吗?
实现事件机制的系统调用
- 使用互斥锁也能实现我们想要的等待效果,但是,上面的任务中,我们想保护的临界资源是什么?貌似并没有需要保护的临界资源。所以使用互斥锁实现等待显然是不合理的,那么接下来我们就专门实现事件机制吧
- 既然使用互斥锁能实现等待的效果,那么我们完全可以参见互斥锁的做法来实现等待机制嘛
- 创建 “event.c” 和 “event.h” 文件,用于实现等待机制
- 先实现与用户层到内核层的系统调用
内核 | 用户 | 功能 |
SYS_EventCreat | EventCreat | 创建事件 |
SYS_WaitEvent | WaitEvent | 事件 |
SYS_SetEvent | SetEvent | 设置事件 |
SYS_ClearEvent | ClearEvent | 清除事件 |
SYS_EventDestory | EventDestory | 销毁事件 |
- 关于系统调用的实现前面已经讲过好几遍了,这里就不再详细描述了,具体详见:再论系统调用
- 内核层现在并没有实现具体功能,仅打印函数名替代,用以检测系统的调用实现。具体实现见:event.c、event.h、syscall.c、u_syscall.c、u_syscall.h
- 在 “app.c” 中测试一下等待相关函数系统调用是否成功,具体见:app.c
EVENT* gEvent = NULL; void TaskA(void) { gEvent = EventCreat(); SetEvent(gEvent); WaitEvent(gEvent); ClearEvent(gEvent); EventDestory(gEvent); while (1); }
- 测试结果如下:
事件机制的具体实现
- 接下来自然就是要具体实现事件机制的相关功能函数了
- 具体实现的相关代码见:event.c、schedule.c、schedule.h
- 由于事件机制的实现与互斥锁的实现很相似,所以大部分代码可以直接参考互斥锁的实现,比如 EventInit()、SYS_EventCreat()、CheckEvent()、SYS_EventDestory() 这几个函数可以说是跟互斥锁中对应的函数一样,这里就不做详细说明了,直接给出源码
// 初始化事件 void EventInit(void) { ListInit(&EVENT_LIST); } // 创建事件 EVENT* SYS_EventCreat(void) { EVENT* event = (EVENT *)Malloc(sizeof(EVENT)); if(NULL == event) return NULL; event->event = 0; // 事件未生产状态 ListAddHead(&EVENT_LIST, (LIST_NODE *)event); // 头插 QueueInit(&event->wait); // 初始化事件中的等待队列 return event; } // 检查事件是否合法有效 static BOOL CheckEvent(EVENT* event) { LIST_NODE* pListNode = NULL; EVENT* nodeTmp = NULL; LIST_FOR_EACH(&EVENT_LIST, pListNode) { nodeTmp = (EVENT *)LIST_NODE(pListNode, EVENT, node); if(event == nodeTmp) return 1; } return 0; } // 销毁事件 E_RET SYS_EventDestory(EVENT* event) { // 检查参数合法性 if(NULL == event || !CheckEvent(event)) return E_ERR; // 删除链表节点并释放内存 ListDelNode(&event->node); Free(event); return E_OK; }
- 还剩下几个函数,与互斥锁的实现稍微有点区别,然而,区别也不大,比如 SYS_WaitEvent()、SYS_SetEvent()、SYS_ClearEvent()
- 先看 SYS_WaitEvent() 函数,其主要功能是将当前任务节点从任务就绪队列中转移到事件的等待队列中,与 MutexLock() 函数相比,其逻辑简单了很多,这里的 event->event 状态只有 0 和 1 两种,0 代表事件未发生,1代表事件发生
E_RET SYS_WaitEvent(EVENT* event) { // 检查参数合法性 if(NULL == event || !CheckEvent(event)) return E_ERR; // 将当前任务节点从任务就绪队列中转移到等待时间的等待队列中 if(0 == event->event) EventSuspend(event); return E_OK; }
- 再来看看 SYS_SetEvent() 函数,其主要功能是将事件 event 中的等待队列中的任务转移到任务就绪队列中
E_RET SYS_SetEvent(EVENT* event) { // 检查参数合法性 if(NULL == event || !CheckEvent(event)) return E_ERR; // 将事件的状态设置为 1,表明事件发生 event->event = 1; // 将事件 event 中的等待队列中的任务转移到任务就绪队列中 EventResume(event); return E_OK; }
- 以上这两个函数分别调用了 EventSuspend() 函数和 EventResume() 函数,其功能实现与 MutexSuspend() 函数和 MutexResume() 函数的实现逻辑也是差不多的。我们把它们也放到 “schedule.c” 中实现,实现原理可以参考互斥锁相关章节,这里也不做详细描述了
// 将当前任务节点从任务就绪队列中转移到事件的等待队列中 E_RET EventSuspend(EVENT* event) { QUEUE_NODE* nodeTmp = NULL; // 把当前任务节点从就绪任务队列中取出,当前任务节点在队列尾,取出后添加到事件 event 的 wait 等待队列中 nodeTmp = QueueTailRemove(&TASK_READY_QUEUE); if(NULL == nodeTmp) return E_ERR; QueueAdd(&event->wait, nodeTmp); // 从就绪任务队列中取出一个任务节点并执行该任务,再将该任务节点重新添加到就绪任务队列中 nodeTmp = QueueRemove(&TASK_READY_QUEUE); if(NULL == nodeTmp) return E_ERR; current_task = (volatile TASK *)QUEUE_NODE(nodeTmp, TASK, node); QueueAdd(&TASK_READY_QUEUE, nodeTmp); TSS* tss = (TSS*)(*(U32*)TSS_ENTRY_ADDR); // 找到 TSS tss->esp0 = (U32)(¤t_task->reg) + sizeof(current_task->reg); // TSS.esp0 指向任务上下文数据结构 reg 的末尾 current_reg = (U32)(¤t_task->reg); // current_reg 指向任务上下文数据结构 reg 的起始位置 SWITCH_TO(current_task); return E_OK; } // 将事件 event 中的等待队列中的任务转移到任务就绪队列中 E_RET EventResume(EVENT* event) { QUEUE_NODE* nodeTmp = NULL; // 从事件的等待队列中取出所有任务,并将其添加到就绪队列中 while(event->wait.length) { nodeTmp = QueueTailRemove(&event->wait); if(NULL == nodeTmp) return E_ERR; QueueHeadAdd(&TASK_READY_QUEUE, nodeTmp); } return E_OK; }
- 最后还有一个 SYS_ClearEvent() 函数,其作用是将事件 event->event 的状态设置为 0,表明事件已被处理
E_RET SYS_ClearEvent(EVENT* event) { // 检查参数合法性 if(NULL == event || !CheckEvent(event)) return E_ERR; // 将事件的状态设置为 0,表明事件已被处理 event->event = 0; return E_OK; }
- 最后的最后,在 “main.c” 中,不要忘记初始化事件
S32 main(void) { ... EventInit(); // 初始化事件 ... }
实验一
- 既然已经实现了事件机制,那么我们还是回到 “吃饭问题” 上,替换掉互斥锁,重新用事件机制来解决 “吃饭问题”
- 实验代码见:app.c
- 其中, A、B、C 三个任务实现如下:
EVENT* gEventRice = NULL; EVENT* gEventDish = NULL; void TaskA(void) { gEventRice = EventCreat(); SetCursorPos(0, 8); print("cook rice:"); while (1) { static U32 cnt = 0; if(cnt < 20) { SetCursorPos(12, 8); print("%d\n", cnt++); Delay(999999); } else { break; } } SetEvent(gEventRice); } void TaskB(void) { gEventDish = EventCreat(); SetCursorPos(0, 10); print("cook dish:"); while (1) { static U32 cnt = 0; if(cnt < 30) { SetCursorPos(12, 10); print("%d\n", cnt++); Delay(999999); } else { break; } } SetEvent(gEventDish); } void TaskC(void) { WaitEvent(gEventRice); WaitEvent(gEventDish); SetCursorPos(0, 12); print("have dinner\n"); }
- 由于效果跟用互斥锁实现的一样,所以这里就不放成果展示了
实验二
- 再来一个实验测试一下我们实现的事件机制
- 实验目标:任务 A 每打印计数累加 20 次,任务 B 打印计数加 1
- 完整代码见:app.c
- 实验核心代码如下,当任务 A 调用 SetEvent(),这时候任务 B 就会被从等待队列中重新放入就绪队列调度执行,但其会被一直调度执行,想要任务 B 只执行一遍,可以调用 ClearEvent() 函数清掉标志,当任务 B 再次循环执行到 WaitEvent() 函数后就会被挂起,从而达到只执行一次的效果
EVENT* gEvent = NULL; void TaskA(void) { static U32 cnt = 0; gEvent = EventCreat(); while (1) { SetCursorPos(0, 10); print("TASK A: %d\n", cnt++); if(cnt%20 == 0) SetEvent(gEvent); Delay(999999); if(cnt > 200) break; } EventDestory(gEvent); } void TaskB(void) { static U32 cnt = 0; while (1) { WaitEvent(gEvent); ClearEvent(gEvent); SetCursorPos(0, 12); print("TASK B: %d\n", cnt++); Delay(999999); } }
- 最终成果展示:
思考
- 事件机制虽然跟互斥锁很相似,但是并没有像互斥锁那样严格限制,为什么?
- 因为使用互斥锁的目的是为了保护临界资源,有些资源就不可以多任务同时访问,比如非法操作互斥锁可能会导致某些硬件工作异常;而等待机制并没有临界资源的概念,也不需要保护什么,其本质上就是在不同任务间传递一个状态变量,就算是不合理操作了,也没有什么严重的后果产生,仅仅只能造成程序逻辑上不符合预期罢了
死锁
- 现在,我们的系统中已经实现了互斥锁以及任务之间等待机制,那么自然就引出了多任务设计中的经典死锁问题,直接看下面的代码
- 单独看 TaskA 任务,程序首先获取 gMutexA 互斥锁,再获取 gMutexB 互斥锁,然后释放 gMutexB 互斥锁,最后释放 gMutexA 互斥锁,没啥毛病,但是,如果此时还有一个 TaskB 任务并行执行,那么问题就来了
- 假设程序在执行 TaskA 任务的 ① 语句,TaskA 获取到 gMutexA 互斥锁,然后可能调度到 TaskB 的 ② 语句处执行,TaskB 获取到 gMutexB 互斥锁,再然后程序可能又调度回 TaskA ③ 处执行,此时由于 gMutexB 被 TaskB 加锁占用,那么 TaskA 就会阻塞挂起,再接着程序可能被调度到 ④ 处执行,由于 gMutexA 被 TasKA 加锁占用,那么 TaskB 也会被阻塞挂起,最终 TaskA 和 TaskB 这两个任务都被挂起了,没有人可以释放 gMutexA 和 gMutexB 互斥锁,于是 TaskA 和 TaskB 永远都不可能被唤醒参与调度执行,对于这种现象,我们也称之为死锁
- 再来看一个经典死锁示例
- TaskA 任务由于在等待 gEventB 事件,可能导致 TaskA 任务被阻塞挂起,此时 TaskB 任务又在等待 gEventA 事件,那么 TaskB 任务也有可能被阻塞挂起,TaskA 和 TaskB 任务都被阻塞挂起,于是 TaskA 无法设置 gEventA,那么就可能无法唤醒 TaskB,而在 TaskB 中又无法设置 gEventB,那么 TaskA 也同样无法被唤醒。这样子也可能造成死锁
- 总结一下死锁现象:任务因执行所需的资源无法获得而进入无限阻塞的状态
- 对于多任务程序来说,死锁是非常可怕的,因为死锁的现象不一定能复现,调试起来非常困难,所以我么在设计多任务程序的时候,一定要注意避免死锁的发生
- 对此,我么可以整理一下死锁的发生条件
- 任务获取了临界资源,但同时又在等待其它临界资源
- 任务之间相互等待
- 任务获取了资源后不能被抢夺,必须由自己释放
- 怎样避免死锁呢?这个就只能靠程序员小心、精心设计程序了