任务管理(一)
freertos学习 仓库地址:https://gitee.com/killerp/free-rtos_-study
freertos是一个实时操作系统,其相比裸机最大的特点就是支持多任务。学习rtos,核心就是学习任务相关知识及其实现。
多任务是指多个任务轮流使用CPU,每个任务是一个死循环的函数,“调度器”会合理安排这些任务去得到CPU的使用。因此任务就有了状态的区分,正在使用CPU的任务处于运行态,等待CPU的任务处于就绪态,而等待某一资源的任务处于阻塞态。最后还有处于挂起态的任务,他不会被调度器管理。
一、双向链表
在操作系统中有多个处于不同状态的任务,就需要将他们有效的组织起来,使系统更加高效。freertos中使用双向链表来管理这些任务的状态。其代码位于list.c
链表的定义如下:
//定义链表、被调度器使用
typedef struct xLIST
{
volatile UBaseType_t uxNumberOfItems; //链表的节点数量
ListItem_t * configLIST_VOLATILE pxIndex; //用于遍历链表
MiniListItem_t xListEnd; //链表的最后一个节点
} List_t;
链表节点的定义:
链表是为了方便管理任务,那么一个节点必然对应一个任务。另外还需要节点值来帮助链表进行排序。
//链表节点定义
struct xLIST_ITEM
{
configLIST_VOLATILE TickType_t xItemValue; //节点的值、决定节点在链表中的位置
struct xLIST_ITEM * configLIST_VOLATILE pxNext;
struct xLIST_ITEM * configLIST_VOLATILE pxPrevious;
void * pvOwner; //节点的拥有者、一般是任务控制块
struct xLIST * configLIST_VOLATILE pxContainer; //节点所在的链表
};
typedef struct xLIST_ITEM ListItem_t;
同时定义了一个处于链表末尾的最小节点,他不代表任何任务,只用于辅助在链表中的查找。
freertos的链表非常简单,提供了插入、删除、查询等最基本的操作。
二、任务控制块
任务是程序员抽象出来的概念,在代码中是通过任务控制块TCB来实现。任务控制块帮助调度器管理任务,任务控制块含有一个任务的所有信息。
如下是一个精简版的任务控制块:
/*
* 任务控制块
* 任务控制块包含一个任务运行的所有信息。任务的堆栈地址、状态等
*/
typedef struct tskTaskControlBlock
{
volatile StackType_t *pxTopOfStack; //指向任务栈顶 注意需要volatile修饰
ListItem_t xStateListItem; //任务状态节点、用于将任务挂在就绪、阻塞、挂起链表
ListItem_t xEventListItem; //将任务挂在时间链表
UBaseType_t uxPriority; //任务优先级,0为最低
StackType_t *pxStack; //指向任务栈基地址
char pcTaskName[configMAX_TASK_NAME_LEN]; //任务名称
} tskTCB;
-
任务需要有一个任务栈,来保存任务的上下文环境,需要栈基和栈顶指针。
-
使用链表节点标记任务的状态、任务的等待的事件。通过节点将任务组织到对应的链表中。
-
freertos是支持抢占的实时操作系统,每个任务有优先级。
三、创建任务
很疑惑,任务不就是一个函数吗?那么函数的入口应该保存在哪里?
任务函数的入口地址应该保存在栈中,当任务开始运行时,调度器会将函数入口地址从栈中复制到PC,随后CPU就跳转到任务函数执行。
所以接下来就是创建一个任务。(这里以动态内存创建为例,freertos的内存管理详见:)
创建一个任务,需要创建一个任务控制块TCB,以及一个任务栈来保存任务的运行环境。接着初始化任务控制块以及任务栈,任务就可以投入运行了,就将任务插入到就绪链表中,等待调度器调度运行。一共三步:申请内存、初始化、插入就绪表,分别由三个函数完成:
BaseType_t xTaskCreate(TaskFunction_t pxTaskCode,
const char *const pcName,
const configSTACK_DEPTH_TYPE usStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pxCreatedTask)
{
TCB_t *pxNewTCB;
BaseType_t xReturn;
//若栈向上生长,则先分配tcb,后分配栈,防止栈覆盖了tcb(tcb地址小于栈基地址)
#if (portSTACK_GROWTH > 0)
{
pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
//申请一段内存作为栈、地址保存在tcb中,后续可释放
pxNewTCB->pxStack = (StackType_t *)pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
if (pxNewTCB->pxStack == NULL)
{
vPortFree(pxNewTCB);
pxNewTCB = NULL;
}
}
}
#else /* portSTACK_GROWTH */
{
//若栈向下生长,则先分配栈,后分配tcb,防止栈覆盖了tcb(tcb地址大于栈基地址)
StackType_t *pxStack;
pxStack = pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t)));
if (pxStack != NULL)
{
pxNewTCB = (TCB_t *)pvPortMalloc(sizeof(TCB_t));
if (pxNewTCB != NULL)
{
pxNewTCB->pxStack = pxStack;
}
else
{
vPortFree(pxStack);
}
}
else
{
pxNewTCB = NULL;
}
}
#endif /* portSTACK_GROWTH */
if (pxNewTCB != NULL)
{
//初始化新任务
prvInitialiseNewTask(pxTaskCode, pcName, (uint32_t)usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL);
//插入就绪表
prvAddNewTaskToReadyList(pxNewTCB);
xReturn = pdPASS;
}
else
{
xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY;
}
return xReturn;
}
初始化:
static void prvInitialiseNewTask(TaskFunction_t pxTaskCode,
const char *const pcName,
const uint32_t ulStackDepth,
void *const pvParameters,
UBaseType_t uxPriority,
TaskHandle_t *const pxCreatedTask,
TCB_t *pxNewTCB,
const MemoryRegion_t *const xRegions)
{
StackType_t *pxTopOfStack;
UBaseType_t x;
//设置堆栈地址
{
//初始化堆栈为默认值、帮助debug
(void)memset(pxNewTCB->pxStack, (int)tskSTACK_FILL_BYTE, (size_t)ulStackDepth * sizeof(StackType_t));
}
#if (portSTACK_GROWTH < 0)
{
//栈向下生长、故初始化后的栈顶在高地址
pxTopOfStack = &(pxNewTCB->pxStack[ulStackDepth - (uint32_t)1]);
//TODO 堆栈的边沿对齐计算方式 pxTopOfStack & 0xfff8 地址是8的倍数
pxTopOfStack = (StackType_t *)(((portPOINTER_SIZE_TYPE)pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK)));
//检测边沿对齐是否正确
configASSERT((((portPOINTER_SIZE_TYPE)pxTopOfStack & (portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK) == 0UL));
}
#else /* portSTACK_GROWTH */
{
//堆栈向上生长、初始化栈顶指针在低地址 也就是数组起始地址
pxTopOfStack = pxNewTCB->pxStack;
//TODO 为何在这里没有内存对齐?可能是内存在分配时就考虑到?
configASSERT((((portPOINTER_SIZE_TYPE)pxNewTCB->pxStack & (portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK) == 0UL));
//设置堆栈最高有效地址
pxNewTCB->pxEndOfStack = pxNewTCB->pxStack + (ulStackDepth - (uint32_t)1);
}
#endif /* portSTACK_GROWTH */
//复制任务名称到TCB
if (pcName != NULL)
{
for (x = (UBaseType_t)0; x < (UBaseType_t)configMAX_TASK_NAME_LEN; x++)
{
pxNewTCB->pcTaskName[x] = pcName[x];
if (pcName[x] == (char)0x00)
{
break;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
//确保字符串的正确结束
pxNewTCB->pcTaskName[configMAX_TASK_NAME_LEN - 1] = '\0';
}
else
{
pxNewTCB->pcTaskName[0] = 0x00;
}
//设置任务优先级
if (uxPriority >= (UBaseType_t)configMAX_PRIORITIES)
{
uxPriority = (UBaseType_t)configMAX_PRIORITIES - (UBaseType_t)1U;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
pxNewTCB->uxPriority = uxPriority;
//设置信号量优先级
{
pxNewTCB->uxBasePriority = uxPriority;
pxNewTCB->uxMutexesHeld = 0;
}
//初始化任务的节点
vListInitialiseItem(&(pxNewTCB->xStateListItem));
vListInitialiseItem(&(pxNewTCB->xEventListItem));
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB);
//设置事件节点的值,优先级越高、节点值越小,在链表中排序越靠前
listSET_LIST_ITEM_VALUE(&(pxNewTCB->xEventListItem), (TickType_t)configMAX_PRIORITIES - (TickType_t)uxPriority);
listSET_LIST_ITEM_OWNER(&(pxNewTCB->xEventListItem), pxNewTCB);
{
pxNewTCB->uxCriticalNesting = (UBaseType_t)0U;
}
#if (configUSE_TASK_NOTIFICATIONS == 1)
{
memset((void *)&(pxNewTCB->ulNotifiedValue[0]), 0x00, sizeof(pxNewTCB->ulNotifiedValue));
memset((void *)&(pxNewTCB->ucNotifyState[0]), 0x00, sizeof(pxNewTCB->ucNotifyState));
}
#endif
//初始化任务的堆栈使其看起来任务好像已经在运行了,这里需要根据硬件实现
pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters);
if (pxCreatedTask != NULL)
{
//返回任务的句柄
*pxCreatedTask = (TaskHandle_t)pxNewTCB;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/*
* 初始化任务栈 返回更新后的栈顶地址
* 需要由硬件架构决定具体的实现
*/
StackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack,
TaskFunction_t pxCode,
void * pvParameters )
{
//模拟任务已经在运行时的栈的情况
pxTopOfStack--; /* Offset added to account for the way the MCU uses the stack on entry/exit of interrupts. */
*pxTopOfStack = portINITIAL_XPSR; /* xPSR */
pxTopOfStack--;
*pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; //存放任务函数入口地址,载入PC
pxTopOfStack--;
*pxTopOfStack = ( StackType_t ) prvTaskExitError; //LR寄存器
pxTopOfStack -= 5; //R12, R3, R2 and R1
*pxTopOfStack = ( StackType_t ) pvParameters; //传递函数参数 R0
pxTopOfStack -= 8; // R11, R10, R9, R8, R7, R6, R5 and R4.
return pxTopOfStack;
}
从Cortex-M3权威指南中,我们可以查到,任务栈中一般存放的内容:(按入栈先后顺序)
xPSR ,PC,LR,R12,R3,R2,R1,R0,R11,R10,R9,R8,R7,R6,R5,R4,其中R11-R4需要通过代码入栈,其他寄存器由硬件自动操作。这一部分将在任务调度详解。
四、删除任务
有创建就有删除、而且删除基本就是创建的逆过程。当任务不需要再运行时、可删除任务,删除步骤:移出链表、释放内存、任务调度。(若当前正在运行的任务被删除,那么需要任务调度来选一个新任务运行)
void vTaskDelete(TaskHandle_t xTaskToDelete)
{
TCB_t *pxTCB;
//进入临界区
taskENTER_CRITICAL();
{
//获取即将删除的tcb
pxTCB = prvGetTCBFromHandle(xTaskToDelete);
//将任务的状态节点移除链表 并重置就绪链表
if (uxListRemove(&(pxTCB->xStateListItem)) == (UBaseType_t)0)
{
taskRESET_READY_PRIORITY(pxTCB->uxPriority);
}
else
{
mtCOVERAGE_TEST_MARKER();
}
//若任务在等待某件事件、则移除该事件链表
if (listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem)) != NULL)
{
(void)uxListRemove(&(pxTCB->xEventListItem));
}
else
{
mtCOVERAGE_TEST_MARKER();
}
uxTaskNumber++; //TODO 为何?
if (pxTCB == pxCurrentTCB)
{
//任务删除自己、需要将任务插入任务终止链表、由空闲任务来释放内存
vListInsertEnd(&xTasksWaitingTermination, &(pxTCB->xStateListItem));
++uxDeletedTasksWaitingCleanUp; //标记有任务需要被释放内存
traceTASK_DELETE(pxTCB);
/* The pre-delete hook is primarily for the Windows simulator,
* in which Windows specific clean up operations are performed,
* after which it is not possible to yield away from this task -
* hence xYieldPending is used to latch that a context switch is
* required. */
portPRE_TASK_DELETE_HOOK(pxTCB, &xYieldPending);
}
else
{
--uxCurrentNumberOfTasks; //当前任务总数减少1
traceTASK_DELETE(pxTCB);
prvDeleteTCB(pxTCB); //释放内存
//重置下次解锁时间
prvResetNextTaskUnblockTime();
}
}
//退出临界区
taskEXIT_CRITICAL();
//若刚刚删除的是正在运行的任务 则强制任务调度
if (xSchedulerRunning != pdFALSE)
{
if (pxTCB == pxCurrentTCB)
{
configASSERT(uxSchedulerSuspended == 0);
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
}
内存释放就是根据任务的内存分配方式,选择对应的释放函数。
/*
* 释放TCB、堆栈的内存
*/
static void prvDeleteTCB(TCB_t *pxTCB)
{
#if ((configSUPPORT_DYNAMIC_ALLOCATION == 1) && (configSUPPORT_STATIC_ALLOCATION == 0))
{
//任务使用动态内存分配策略,直接释放任务控制块内存、堆栈内存
vPortFree(pxTCB->pxStack);
vPortFree(pxTCB);
}
#elif (tskSTATIC_AND_DYNAMIC_ALLOCATION_POSSIBLE != 0)
{
//选择对应的内存释放函数
if (pxTCB->ucStaticallyAllocated == tskDYNAMICALLY_ALLOCATED_STACK_AND_TCB)
{
//tcb和堆栈都是动态内存分配的,所以必须释放
vPortFree(pxTCB->pxStack);
vPortFree(pxTCB);
}
else if (pxTCB->ucStaticallyAllocated == tskSTATICALLY_ALLOCATED_STACK_ONLY)
{
vPortFree(pxTCB);
}
else
{
//tcb和堆栈都是静态内存分配的 不必释放
configASSERT(pxTCB->ucStaticallyAllocated == tskSTATICALLY_ALLOCATED_STACK_AND_TCB);
mtCOVERAGE_TEST_MARKER();
}
}
#endif /* configSUPPORT_DYNAMIC_ALLOCATION */
}