一、内存管理
1、五种内存管理模式
FreeRTOS提供了5种内存管理实现方法,可以根据不同的使用场合选择不同的模式。
关于5中内存管理模式可以看下面这篇文章。
链接: https://zhuanlan.zhihu.com/p/115276865
默认情况下,FreeRTOS内核创建的任务、队列、信号量、事件组等都是借助内存管理函数从内存堆中分配内存。对于1、2、4这三种模式,内存中的堆实际上是一个很大的数组。创建的任务TCB结构体、任务栈实际上都保存在这块大数组上。
默认堆的大小是17kb。
2、任务分配空间
可以看到,创建的任务TCB结构体和任务的栈都在堆上。使用xTaskCreate()
动态创建任务时,FreeRTOS会在堆中开辟出一块空间,存放任务的TCB结构体和栈。每个任务都有各自独立的内存空间,互相独立。调用xTaskCreateStatic()
函数在静态区建立任务。
我们目前只需要知道,除了heap_3模式使用的是标准库里的malloc和free函数,其他的都是FreeRTOS自己提供的申请、释放内存函数。
在heap_4这种常用模式,动态申请的堆实际上是从内部的一个巨大的数组中申请的。
二、TCB结构体
每个任务都会有自己的TCB结构体,用来保存自己的一些信息。下面是删除配置项之后的TCB结构体。
typedef struct tskTaskControlBlock
{
/* 指向任务堆栈最后一项位置,也就是栈顶,为了便于上下文切换操作,这里栈顶指针必须位于TCB第一项 */
volatile StackType_t * pxTopOfStack;
/* 任务状态,不同的状态会添加到不同的状态链表,如就绪链表*/
ListItem_t xStateListItem;
/* 事件链表项,会添加到不同的事件链表下 */
ListItem_t xEventListItem;
/* 任务优先级,数值越大优先级越高 */
UBaseType_t uxPriority;
/* 申请到的任务栈起始地址,也就是栈底,可以用来检测堆栈是否溢出 */
StackType_t * pxStack;
/* 任务名,默认16个字节 */
char pcTaskName[ configMAX_TASK_NAME_LEN ];
} tskTCB;
对比任务创建函数:
可以看到,TCB结构体并没有保存任务的函数指针和参数,那么创建任务时传入的函数指针和参数在哪里呢?
我们知道,每个任务都有自己的栈空间,用来保存内部寄存器的数据。在创建任务的时候,程序会修改栈空间里面对应寄存器的值,恢复运行的时,pc寄存器恢复为该任务的函数地址;R0-R3寄存器恢复为函数的参数。所以TCB结构体并不需要保存函数指针和参数。
每个任务的栈,需要自根据实际情况确定,关键是局部变量的大小和函数调用的深度。
三、任务创建流程
1、创建临时TCB结构体和返回值。
2、根据栈的增长方式决定TCB结构体和栈空间的申请顺序。
如果堆栈增长方式是从低地址向高地址增加,则先分配TCB然后分配堆栈。
如果堆栈增长方式是高地址往低地址增加,则先分配堆栈然后分配TCB,以便堆栈不会增长到TCB。
为什么要区别栈的增长方式来决定栈和TCB的分配顺序呢?
因为一个任务的任务栈和TCB是紧挨着的,TCB的大小是固定的,任务栈的大小是变化的。
申请TCB和栈都是在堆上申请的,堆上先申请低地址空间,后申请高地址空间。任务栈访问的方式有向下增长和向上增长两种方式。
如果栈向下增长,也就是栈顶在高地址,栈底子在低地址。此时,如果先申请TCB后申请任务栈,内存分布图如下:
在这种情况下,使用任务栈的时候,栈顶指针往低地址走,如果创建任务时分配的栈空间的大小不够的话,那么栈指针可能会越过栈底,去操作TCB结构体的部分,这样任务的TCB就会被破坏,导致无法估计的错误。
而如果先申请任务栈再申请TCB,内存分布图如下:
这种情况下,栈顶指针往向低地址走,即使任务栈空间不足,也不会触碰到该任务的TCB区域,这样相对安全一些。但是如果他前面还有别的任务的堆栈空间,也可能会把前面任务的堆栈空间破坏。
所以,在创建任务的时候,一定要保证任务的栈空间时足够该任务使用的。
3、初始化TCB结构体和确定返回值。
经过以上三步,任务就创建完成了。
四、任务管理
1、任务链表
被创建出来的任务保存在哪里,执行的时候又怎么取出来呢?
从上面的任务创建过程中我们知道,任务创建出来之后会被放入就绪列表中。
进入此函数内部看看。
在函数里面又调用了一个函数prvAddTaskToReadyList
,查看该函数。
根据函数名,可以推测出这个函数功能是根据新任务的优先级和任务状态把新创建的任务插入到对应的链表的最末尾。
任务的链表有三种,就绪链表、阻塞链表、挂起链表。分别存放就绪态任务、阻塞态任务、挂起态任务。其中就绪链表的数量由系统的优先级数量级决定。
综上可知,后被创建的任务会被放到对应链表的最后,也就是尾部。
注意:空闲任务函数也在就绪链表中。
任务调度就只负责调度就绪链表中的任务,根据优先级由高到低执行任务,执行完毕后将任务放到对应就绪链表尾部。
当任务状态变为阻塞态或者挂起态,任务就会从就绪链表移动到阻塞链表或者挂起链表中,CPU只执行就绪链表中的任务,不会执行其他链表中的任务,所以挂起链表和阻塞链表中的任务不占用CPU资源。当任务由阻塞链表或挂起链表移动到就绪链表后,会发起新的调度,CPU就会从上到下根据优先级搜索就绪链表中的任务然后开始执行。
2、任务调度
在每个tick中断中,会调用tick中断服务函数。在tick中断服务函数中,会取出下一个任务,然后切换任务,在切换任务时,又会保存当前任务,恢复新任务。
3、同优先级任务执行顺序
在创建任务时,新任务会被函数添加到就绪链表中,在函数内部有下面这样的判断。
如果新任务的优先级大于等于当前任务(也就是上一个被创建的任务)的优先级,那么当前任务就等于新任务。即新任务会先执行。
3.1优先级0任务执行顺序
但是如果任务的优先级是0,与空闲任务的优先级相同,并且空闲任务配置为不礼让模式,那么最先被执行的任务就是空闲任务,空闲任务先执行,然后进行调度,重新执行就绪链表中的其他任务。如果空闲任务配置为礼让模式,那么空闲任务会在其他任务执行之后被执行。
实验代码:
#define configIDLE_SHOULD_YIELD 0 //配置空闲任务不礼让
void Task1(void * param)
{
while(1)
{
Task1RunningFlag = 1;
Task2RunningFlag = 0;
Task3RunningFlag = 0;
IdleRunningFlag = 0;
printf("1");
}
}
void Task2(void * param)
{
while(1)
{
Task1RunningFlag = 0;
Task2RunningFlag = 1;
Task3RunningFlag = 0;
IdleRunningFlag = 0;
printf("2");
}
}
void Task3(void * param)
{
while(1)
{
Task1RunningFlag = 0;
Task2RunningFlag = 0;
Task3RunningFlag = 1;
IdleRunningFlag = 0;
printf("3");
}
}
void vApplicationIdleHook(void)
{
Task1RunningFlag = 0;
Task2RunningFlag = 0;
Task3RunningFlag = 0;
IdleRunningFlag = 1;
printf("0");
}
int main( void )
{
prvSetupHardware();
printf("Hello World!\r\n");
xTaskCreate(Task1,"Task1",100,NULL,0,NULL);
xTaskCreate(Task2,"Task2",100,NULL,0,NULL);
xTaskCreate(Task3,"Task3",100,NULL,0,NULL);
vTaskStartScheduler();
return 0;
}
输出结果:
可以看到,创建的三个任务优先级为0,空闲任务优先执行,然后任务1、2、3执行。
#define configIDLE_SHOULD_YIELD 1 //配置空闲任务礼让
可以看到,先执行按顺序添加的任务,然后执行空闲任务。
为什么空闲任务是否礼让会有上面两种不同的运行情况?
我们知道空闲任务是在启动调度器函数中被创建的,所以空闲任务是最后一个创建的任务应该优先执行,那为什么在礼让模式下,空闲任务反而最后执行呢?
我们进入空闲任务函数portTASK_FUNCTION
中看看。
在函数内部,我们可以看到下面这样的一个判断。
空闲任务是否礼让的配置宏作为判断条件,如果空闲任务礼让,那么条件成立,会执行下面的taskYIELD
函数重新发起一次任务调度,任务就从就绪链表中重新选择任务执行。也就是说礼让模式下,空闲任务主动放弃一次运行机会,下一次再正常运行。如果空闲任务不礼让,条件不成立,不会发起新的调度,当前任务就是空闲任务,所以空闲任务先执行,然后在执行其他函数。
这样就解释了上面的两种现象。
3.2优先级大于0任务执行顺序
当任务的优先级大于空闲任务时,后创建的任务先执行。
把main
函数中的创建任务优先级改变为1。
xTaskCreate(Task1,"Task1",100,NULL,1,NULL);
xTaskCreate(Task2,"Task2",100,NULL,1,NULL);
xTaskCreate(Task3,"Task3",100,NULL,1,NULL);
输出结果:
可以看到,最后被创建的任务3先执行,然后在从链表头顺序执行。