FreeRTOS之任务管理
1、任务管理
任务或者说进程是一个操作系统的基本概念,该书并没有去说明什么是任务,而是从应用的角度去介绍怎么在FreeRTOS中去创建一个任务并管理它。
1.1 任务函数
FreeRTOS中的任务是以一个函数的形式存在的,具有统一的函数原型,如下:
void TaskFunction(void *pvParameters);
其必须返回void且带有一个void指针参数,任务函数体内通常有一个死循环,决不能有一条return语句,也不能执行到函数尾部,如果某个任务不再需要,可以显式的将其删除。
1.2任务状态
当MCU只有一个核,应用程序又包含多个任务,那么只有一个任务正在执行,其他任务都处于等待状态,如下:
这是最简单的模型,那么CPU选择哪一个任务运行呢?在早期CPU非常昂贵和稀有,许多用户排队等待CPU资源,那个时候的任务调度算法侧重“公平共享”处理器时间,所有的任务地位平等,调度器给每个任务一个固定的时间获得CPU资源,时间一到运行的任务就必须将CPU让出来给其他任务,这种调度算法成为时间片轮转调度。但是在实际的应用场景中,不同的任务紧急程度不同,好比你正在洗衣服手机响了,那么你应该放下手中的衣服并去接听电话,当电话说完了再去接着洗衣服。于是时间片轮转调度发展出一些变种,比如将优先级相同的任务放在同一个队列,并且优先级越高的队列其时间片也越短。
随着处理器功能越来越强大,价格越来越便宜,单个用户可以独占一个处理器,可以使用户同时运行多个应用程序,比如用户可以一边听音乐一边看网页。随着系统中的任务越来越多,应用场景也越来越复杂,原来只有运行态和等待态的模型已经不能满足要求。例如:一个已获得除CPU之外的所有资源(如内存空间等)的任务A,和一个已经在CPU中运行,但是因为等待某些事件(比如等待用户输入)而被中断运行的任务B,这两个任务此时都在内存中且没有在CPU上运行,那么这两个任务是否都可以归为等待态呢?显然不行,试想一下,A和B都在等待态,且B的优先级更高,那么调度器就会将CPU交给B,运行B时发现它在等待事件,将B移出CPU如此往复,直到B等待的事件发生,在这个过程中CPU实际没有做有效工作,浪费了CPU资源。如此只能将A和B置为不同的状态,FreeRTOS就将A的状态定义为就绪态,而B的状态成为阻塞态。就绪态就是已经准备好,只要CPU一空闲立马就可以运行的状态,而阻塞态是等待某一事件,只有该事件发生才能继续运行的状态,一些操作系统是将已等到事件发生的阻塞态任务转为就绪态,如下:
就绪态和运行态之间之所以是双向箭头,因为一些支持抢占式的操作系统中,当就绪态中有了一个优先级更高的任务时,会抢占正在运行的低优先级的任务,并将低优先级的任务置为就绪态或者因为运行的任务已运行超过允许的时间而被移出运行态进入就绪态。
随着任务进一步增多,而内存因为成本等原因,空间增长跟不上任务的增长,另外CPU的速度远高于IO等设备的速度,内存中可能会出现许多处于阻塞态的任务。这个时候就有必要将一些阻塞态任务从内存中移到磁盘,将内存让给处于运行态和就绪态的任务,如下图:
挂起态之所以能够转化为就绪态,是因为在任务挂起时,如果它等待的事件发生了,那当调度器将其移入内存时,它的状态就变为就绪态了。下图是FreeRTOS中的完整任务状态机模型。
为了避免反复下载到板子等繁琐操作带来的麻烦,我使用了FreeRTOS在Windows下的模拟器,在PC机上做实验,模拟器下载地址是
https://sourceforge.net/projects/freertos/
压缩包里面有Visual Studio环境的工程文件。
1.3 实验一:创建任务
//每隔1秒,打印一次字符串
static void MyFirstTask( void *pvParameters )
{
while(1)
{
printf("\r\nThis is MyFirstTask!\r\n");
vTaskDelay(1000);//延时1000ms
}
}
void Test1_CreateTask( void )
{
//创建一个任务
xTaskCreate( MyFirstTask, //任务函数
"MyFirstTask", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
NULL, //传入任务函数的参数
5, //任务优先级
NULL ); //任务句柄
vTaskStartScheduler();
for( ;; ); //任务调度器启动失败会进入这里
}
FreeRTOS中任务实例是一个永不返回的函数,函数原型固定为:
void ATaskFunction( void *pvParameters );
创建任务使用FreeRTOS 的API 函数xTaskCreate():
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask );
pvTaskCode :任务只是永不退出的C 函数,实现常通常是一个死循环。参数
pvTaskCode 只一个指向任务的实现函数的指针(效果上仅仅是函数名)。
pcName: 具有描述性的任务名。这个参数不会被FreeRTOS 使用。其只是单纯地用于辅助调试。识别一个具有可读性的名字总是比通过句柄来识别容易得多。
应用程序可以通过定义常量 config_MAX_TASK_NAME_LEN 来定义任务名的最大长度——包括’\0’结束符。如果传入的字符串长度超过了这个最大值,字符串将会自动被截断。
usStackDepth: 当任务创建时,内核会分为每个任务分配属于任务自己的唯一状态。usStackDepth 值用于告诉内核为它分配多大的栈空间。这个值指定的是栈空间可以保存多少个字(word),而不是多少个字节(byte)。比如说,如果是32 位宽的栈空间,传入的usStackDepth值为100,则将会分配400 字节的栈空间(100 * 4bytes)。栈深度乘以栈宽度的结果千万不能超过一个size_t 类型变量所能表达的最大值。应用程序通过定义常量 configMINIMAL_STACK_SIZE 来决定空闲
任务任用的栈空间大小。在FreeRTOS 为微控制器架构提供的Demo 应用程序中,赋予此常量的值是对所有任务的最小建议值。如果你的任务会使用大量栈空间,那么你应当赋予一个更大的值。没有任何简单的方法可以决定一个任务到底需要多大的栈空间。计算出来虽然是可能的,但大多数用户会先简单地赋予一个自认为合理的值,然后利用FreeRTOS 提供的特性来确证分配的空间既不欠缺也不浪费。
pvParameters: 任务函数接受一个指向void 的指针(void*)。pvParameters 的值即是传递到任务中的值。这篇文档中的一些范例程序将会示范这个参数可以如何使用。
uxPriority: 指定任务执行的优先级。优先级的取值范围可以从最低优先级0 到
最高优先级(configMAX_PRIORITIES – 1)。configMAX_PRIORITIES 是一个由用户定义的常量。优先级号并没有上限(除了受限于采用的数据类型和系统的有效内存空间),但最好使用实际需要的最小数值以避免内存浪费。如果uxPriority 的值超过了(configMAX_PRIORITIES – 1),将会导致实际赋给任务的优先级被自动封顶到最大合法值。
pxCreatedTask pxCreatedTask: 用于传出任务的句柄。这个句柄将在API 调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。如果应用程序中不会用到这个任务的句柄,则pxCreatedTask 可以被设为NULL。
返回值: 有两个可能的返回值:
1. pdTRUE
表明任务创建成功。
2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY
由于内存堆空间不足,FreeRTOS 无法分配足够的空间来保存任务结构数据和任务栈,因此无法创建任务。
该实验的运行效果如下:
实验一:创建任务
在任务中调用的vTaskDelay需要在FreeRTOSConfig.h文件中将“INCLUDE_vTaskDelay”宏开关打开。调用vTaskDelay的任务会被阻塞,并进行一次任务切换。
那么在任务MyFirstTask被阻塞的这1000ms时间里,CPU在干啥?CPU是一直处于运行状态的,FreeRTOS在启动任务调度器vTaskStartScheduler()时,会自动创建一个空闲任务:
//如果需要空闲任务的句柄,就会进入该分支,并将空闲任务句柄存到xIdleTaskHandle中
#if ( INCLUDE_xTaskGetIdleTaskHandle == 1 )
{
xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), &xIdleTaskHandle );
}
#else
{
xReturn = xTaskCreate( prvIdleTask, ( signed char * ) "IDLE", tskIDLE_STACK_SIZE, ( void * ) NULL, ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), NULL );
}
#endif
空闲任务的优先级为默认为0,是最低优先级,执行的是循环指令(如果有任务被删除,空闲任务要负责回收资源),为的就是当所有任务都被阻塞时,CPU“有事可做”。
当然也可以在FreeRTOSConfig.h文件中将“configUSE_IDLE_HOOK”宏开关打开,然后在空闲任务钩子函数vApplicationIdleHook()中搞一些“小事情”:
l 执行低优先级,后台或需要不停处理的功能代码。
l 测试系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时间)。
l 将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能需要处理的时候,系统自动进入省电模式。
因为空闲任务随时都可能被抢断,所以空闲任务钩子函数不能完成较复杂的功能而且永远不能被阻塞或挂起,以防没有任务处于就绪态使得CPU“无事可做”。
1.4实验二:创建两个任务
static char strTask1[] = "\r\nThis is Task1!\r\n";
static char strTask2[] = "\r\nThis is Task2!\r\n";
//打印字符串
static void PrintString( void *pvParameters )
{
//将传入的任务参数强制转为char *型
char *strPrintf = (char *)pvParameters;
int i ,j;
while(1)
{
printf("\r\n%s\r\n",strPrintf);
for(i=0;i<10000;i++)
for(j=0;j<10000;j++);
}
}
void Test2_CreateTwoTask( void )
{
//创建一个任务
xTaskCreate( PrintString, //任务函数
"PrintString1", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
strTask1, //传入任务函数的参数
5, //任务优先级
NULL ); //任务句柄
//创建一个任务
xTaskCreate( PrintString, //任务函数
"PrintString2", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
strTask2, //传入任务函数的参数
5, //任务优先级
NULL ); //任务句柄
vTaskStartScheduler();
for( ;; ); //任务调度器启动失败会进入这里
}
这里创建的两个任务几乎完全一样,只是打印的字符串不一样,因此可以共用一个任务函数来创建两个任务实例,只是传入任务函数的参数不同,两个任务实例在调度器的控制下独立运行。
运行效果如下:
实验二:创建两个任务
可以看出同优先级的任务,即使自身没有阻塞或挂起,依然会被交替执行,这叫“时间片轮转”,每个任务执行一个“时间片”后退出运行态,然后调度器选择另一个同优先级的任务。时间片的长度可以通过在FreeRTOSConfig.h文件中配置宏“configTICK_RATE_HZ”,比如将其配置为100(Hz),那么时间片的长度就是10ms,时间片的长度要合适,不能太长也不能太短。
如果将上述源码中任务1的优先级从5提高到6,可以看到运行效果如下:
调度器每次选择就绪列表中优先级最高的任务,所以任务2得不到被执行的机会,称之为“饿死”。为了避免多任务系统中低优先级的任务被饿死,高优先级的任务要主动将自己阻塞或者挂起,比如调用前文提到的vTaskDelay()。同时需要注意的是,FreeRTOS是“可抢占式调度”,意思是如果有优先级更高的任务被创建出来,调度器会进行一次任务切换,运行更高优先级的任务,即使当前任务的“时间片”没有用完。
1.5实验三:修改任务的优先级
FreeRTOS常用的管理任务优先级的有这样几个API:
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask )
功能:获得某个任务当前的优先级;
参数:
xTask:需要获取优先级的任务的句柄,当值为NULL时表示获取当前任务的优先级;
UBaseType_t uxTaskPriorityGetFromISR( TaskHandle_t xTask )
功能:与uxTaskPriorityGet完成的功能是一样的,唯一的区别是uxTaskPriorityGetFromISR是中断安全的API。必须说明的是,只有以”FromISR”或”FROM_ISR”结束的API 函数或宏才可以在中断服务例程中。
参数:
xTask:需要获取优先级的任务的句柄,当值为NULL时表示获取当前任务的优先级;
void vTaskPrioritySet( TaskHandle_t xTask, UBaseType_t uxNewPriority )
功能:设置任务的优先级
参数:
xTask:需要设置优先级的任务的句柄,当值为NULL时表示获取当前任务的优先级;
uxNewPriority:新优先级
实验源码如下:
TaskHandle_t taskHandle_Task2;//任务2的句柄
//任务1函数
static void Task1( void *pvParameters )
{
int i,j;
while(1)
{ //将任务2的优先级设置成与本任务优先级一致
vTaskPrioritySet(taskHandle_Task2,uxTaskPriorityGet(NULL));
printf("\r\nTask2 is Running!\r\n");
for(i=0;i<1000;i++)
for(j=0;j<1000;j++);
}
}
//任务2 函数
static void Task2( void *pvParameters )
{
int i,j;
while(1)
{
//降低本任务优先级
vTaskPrioritySet(NULL,uxTaskPriorityGet(NULL)-1);
printf("\r\nTask2 Cannot Run!\r\n");
for(i=0;i<1000;i++)
for(j=0;j<1000;j++);
}
}
void Test3_CreateTwoTask( void )
{
//创建任务1
xTaskCreate( Task1, //任务函数
"Task1", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
NULL, //传入任务函数的参数
5, //任务优先级
NULL ); //任务句柄
//创建任务2
xTaskCreate( Task2, //任务函数
"Task2", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
NULL, //传入任务函数的参数
5, //任务优先级
&taskHandle_Task2 ); //任务句柄
vTaskStartScheduler();
for( ;; ); //任务调度器启动失败会进入这里
}
运行效果:
实验三:修改任务的优先级
1.6实验四:删除任务
用到的API是void vTaskDelete( TaskHandle_t xTaskToDelete ),
功能:删除指定的任务
参数:
xTaskToDelete:要删除的任务句柄
实验源码如下:
TaskHandle_t taskHandle_Task2;//任务2的句柄
//任务1函数
static void Task1( void *pvParameters )
{
vTaskDelete(taskHandle_Task2);//删除任务2
while(1)
{
printf("\r\nTHis is Task1!\r\n");
vTaskDelay(1000);
}
}
//任务2函数
static void Task2( void *pvParameters )
{
while(1)
{
printf("\r\nTHis is Task2!\r\n");
vTaskDelay(1000);
}
}
void Test4_CreateTwoTask( void )
{
//创建任务1
xTaskCreate( Task1, //任务函数
"Task1", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
NULL, //传入任务函数的参数
4, //任务优先级
NULL ); //任务句柄
//创建任务2
xTaskCreate( Task2, //任务函数
"Task2", //任务名
configMINIMAL_STACK_SIZE, //任务栈深度
NULL, //传入任务函数的参数
5, //任务优先级
&taskHandle_Task2 ); //任务句柄
vTaskStartScheduler();
for( ;; ); //任务调度器启动失败会进入这里
}
运行效果如下:
任务2的优先级高于任务1,但是在任务2自我阻塞时,任务1开始运行并删除了任务2。需要主要的是,任务1删除任务2并自我阻塞后,空闲任务开始运行,空闲任务会回收任务2占用的资源。