EidosOS实现(5)-事件组
1、摘要
本文主要讲解RTOS的事件组,为了实现一个事件通知多个任务,或者多个事件通知一个任务,RTOS不得不提供一种高效的机制来解决这一问题。但在介绍EidosOS的事件组之前,先对信号量与互斥量的操作进行回顾并提取它们的操作中的公共部分,统一成事件,以便后续扩展。
2、抽象层
回顾信号量和互斥量的等待操作和释放操作,发现其中都要去操作链表,并且操作的形式几乎一模一样。那么后续的队列等实现也有可能有这种操作。而且对于不同的等待或释放来说,它们的核心就是对链表的操作,把这些逻辑抽离到一个核心的事件层,可以增加扩展性以及可读性。
下面是公共的代码部分:
void mutexLock(Mutex_t *mutex)
{
//...
// 删除当前任务在就绪链表中的状态节点
vListRemove(¤tTCB->stateListItem);
// 将当前任务事件节点添加到事件的等待链表中,按照优先级排序
vListInsert((vList *)&mutex->waitingList, ¤tTCB->eventListItem, 0);
// 阻塞当前任务
currentTCB->state = BLOCKED;
// 切换到下一个任务
taskYield();
//...
}
void semaphoreWait(Semaphore_t *sem)
{
// 删除当前任务在就绪链表中的状态节点
vListRemove(¤tTCB->stateListItem);
// 将当前任务事件节点添加到事件的等待链表中,按照优先级排序
vListInsert((vList *)&sem->waitingList, ¤tTCB->eventListItem, 0);
// 阻塞当前任务
currentTCB->state = BLOCKED;
// 切换到下一个任务
taskYield();
}
wait代码中不同的地方在于不同机制结构体的链表
void mutexUnlock(Mutex_t *mutex)
{
//...
if (mutex->waitingList.itemNumber > 0)
{
// 从事件的等待链表中取出一个任务
ListItem_t *waitingItem = mutex->waitingList.end.next;
TCB_t *task = (TCB_t *)waitingItem->pvOwner;
// 将任务事件节点从等待链表中移除
vListRemove(waitingItem);
if (vListIsInList(&task->stateListItem)) // 如果任务状态节点还在延时链表中,先移除
{
vListRemove(&task->stateListItem);
}
// 设置任务状态为就绪
task->state = READY;
task->stateListItem.value = task->priority; // 更新节点值为优先级,便于就绪链表排序
// 将任务状态节点添加到就绪链表中
vListInsert(&readyList, &task->stateListItem, 0);
wakeTask = task;
}
//...
}
void semaphoreSignal(Semaphore_t *sem)
{
//...
if (sem->waitingList.itemNumber > 0)
{
// 从事件的等待链表中取出一个任务
ListItem_t *waitingItem = sem->waitingList.end.next;
TCB_t *task = (TCB_t *)waitingItem->pvOwner;
// 将任务事件节点从等待链表中移除
vListRemove(waitingItem);
if (vListIsInList(&task->stateListItem)) // 如果任务状态节点还在延时链表中,先移除
{
vListRemove(&task->stateListItem);
}
// 设置任务状态为就绪
task->state = READY;
task->stateListItem.value = task->priority; // 更新节点值为优先级,便于就绪链表排序
// 将任务状态节点添加到就绪链表中
vListInsert(&readyList, &task->stateListItem, 0);
wakeTask = task;
}
//...
}
相似度很高
将这些封装成函数,得到事件层的wait与wakeup。
static void eventWait(Event_t *event)
{
// 删除当前任务在就绪链表中的状态节点
vListRemove(¤tTCB->stateListItem);
// 将当前任务事件节点添加到事件的等待链表中,按照优先级排序
vListInsert((vList *)&event->waitingList, ¤tTCB->eventNode.eventListItem, 0);
// 阻塞当前任务
currentTCB->state = BLOCKED;
// 切换到下一个任务
taskYield();
}
static void eventWaitTimeout(Event_t *event, uint32_t timeout)
{
// 删除当前任务在就绪链表中的状态节点
vListRemove(¤tTCB->stateListItem);
// 将当前任务事件节点添加到事件的等待链表中,按照优先级排序
vListInsert((vList *)&event->waitingList, ¤tTCB->eventNode.eventListItem, 0);
// 阻塞当前任务
currentTCB->state = BLOCKED;
// 设置节点的延时值为绝对时间,便于在延迟链表中排序
currentTCB->stateListItem.value = currentTicks + timeout;
// 插入延迟链表,按照剩余时间排序
vListInsert(&delayList, ¤tTCB->stateListItem, 1);
// 切换到下一个任务
taskYield();
}
static TCB_t *wakeUpFromEvent(Event_t *event)
{
if (event->waitingList.itemNumber > 0)
{
// 从事件的等待链表中取出一个任务
ListItem_t *waitingItem = event->waitingList.end.next;
TCB_t *task = (TCB_t *)waitingItem->pvOwner;
// 将任务事件节点从等待链表中移除
vListRemove(waitingItem);
if (vListIsInList(&task->stateListItem)) // 如果任务状态节点还在延时链表中,先移除
{
vListRemove(&task->stateListItem);
}
// 设置任务状态为就绪
task->state = READY;
task->stateListItem.value = task->priority; // 更新节点值为优先级,便于就绪链表排序
// 将任务状态节点添加到就绪链表中
vListInsert(&readyList, &task->stateListItem, 0);
return task;
}
return NULL;
}
其中,每个参数都是event,但其实就是vlist,然后无论是信号量还是互斥量它们结构体中都换成event。最后TCB中的节点直接就是普通的链表节点,但是由于事件组的出现,它需要额外的两个变量,于是我们统一EventNode,只不过其他机制不会用到其他变量,只用到listItem。下面给出封装后的定义:
typedef struct eventNode
{
ListItem_t eventListItem;
// EventGroup 用
uint32_t waitBits;
uint8_t waitMode; // 0表示等待所有事件,1表示等待任一事件
} EventNode_t;
typedef struct TCB
{
uint32_t *sp; // 堆栈指针,必须在第一个位置,以便在上下文切换时正确保存和恢复
uint16_t tick_count; // 任务运行的系统滴答数
int priority;
task_state_t state;
ListItem_t stateListItem; // 用于将任务添加到不同的状态链表中的节点
EventNode_t eventNode; // 任务等待事件的节点
} TCB_t;
typedef struct event
{
vList waitingList;
} Event_t;
typedef struct semaphore
{
int count;
Event_t event; // 等待该信号量的任务链表
} Semaphore_t;
typedef struct mutex
{
int locked; // 0表示未锁定,1表示已锁定
TCB_t *owner; // 当前拥有该互斥锁的任务
Event_t event; // 等待该互斥锁的任务链表
}Mutex_t;
封装后修改信号量和互斥量操作的实现(以信号量为例):
void semaphoreWait(Semaphore_t *sem)
{
__disable_irq(); // 进入临界区,禁止中断
sem->count--;
if (sem->count < 0)
{
eventWait(&sem->event); // 阻塞当前任务,等待信号量事件
__enable_irq(); // 退出临界区,允许中断
}
else
{
__enable_irq(); // 退出临界区,允许中断
}
}
void semaphoreSignal(Semaphore_t *sem)
{
TCB_t *wakeTask = NULL;
__disable_irq(); // 进入临界区,禁止中断
sem->count++;
if (sem->count <= 0)
{
wakeTask = wakeUpFromEvent(&sem->event);
}
__enable_irq(); // 退出临界区,允许中断
// 如果新就绪的任务优先级高于当前正在运行的任务,则触发 PendSV 进行任务切换
if (wakeTask && wakeTask->priority > currentTCB->priority)
{
taskYield(); // 触发任务切换
}
}
事件链表的操作全部封装在核心函数event中
3、事件组
事件组结构体定义:
typedef struct eventGroup
{
uint32_t eventBits; // 当前事件位,每一位代表一个事件
Event_t event; // 等待该事件组的任务链表
} EventGroup_t;
事件组操作:
void eventGroupWaitBits(EventGroup_t *eventGroup, uint32_t waitBits, uint8_t waitMode)
{
currentTCB->eventNode.waitBits = waitBits;
currentTCB->eventNode.waitMode = waitMode;
__disable_irq(); // 进入临界区,禁止中断
eventWait(&eventGroup->event); // 阻塞当前任务,等待事件组事件
__enable_irq(); // 退出临界区,允许中断
}
void eventGroupSetBits(EventGroup_t *eventGroup, uint32_t bitsToSet)
{
TCB_t *wakeTask = NULL;
__disable_irq(); // 进入临界区,禁止中断
eventGroup->eventBits |= bitsToSet; // 设置事件位
// 检查等待链表中的任务是否满足唤醒条件
ListItem_t *currentItem = eventGroup->event.waitingList.end.next;
while (currentItem != &eventGroup->event.waitingList.end)
{
EventNode_t *waitNode = (EventNode_t *)currentItem;
TCB_t *task = (TCB_t *)waitNode->eventListItem.pvOwner;
uint32_t waitBits = waitNode->waitBits;
uint8_t waitMode = waitNode->waitMode;
int conditionMet = 0;
if (waitMode == 0) // 等待所有事件
{
conditionMet = ((eventGroup->eventBits & waitBits) == waitBits);
}
else // 等待任一事件
{
conditionMet = ((eventGroup->eventBits & waitBits) != 0);
}
if (conditionMet)
{
// 从事件的等待链表中移除当前任务
vListRemove(&waitNode->eventListItem);
if (vListIsInList(&task->stateListItem)) // 如果任务状态节点还在延时链表中,先移除
{
vListRemove(&task->stateListItem);
}
// 设置任务状态为就绪
task->state = READY;
task->stateListItem.value = task->priority; // 更新节点值为优先级,便于就绪链表排序
// 将任务状态节点添加到就绪链表中
vListInsert(&readyList, &task->stateListItem, 0);
}
currentItem = currentItem->next;
}
__enable_irq(); // 退出临界区,允许中断
// 如果新就绪的任务优先级高于当前正在运行的任务,则触发 PendSV 进行任务切换
if (wakeTask && wakeTask->priority > currentTCB->priority)
{
taskYield(); // 触发任务切换
}
}
由于事件组比较特殊,它不是从等待队列中根据优先级或者其他排序从头提取节点的,它要每次对所有节点进行判断,因为不同的等待所设置的bit可能不同。