空闲任务及其钩子函数
介绍
空闲任务(Idle任务)的作用之一:释放被删除的任务的内存。
除了上述目的之外,为什么必须要有空闲任务? 这是一个良好的程序,它的任务都是事件驱动的:平时大部分时间处于阻塞状态。有可能我们自己创建的所有任务都无法执行,但是调度器必须能找到一个可以运行的任务:所以,我们要提供空闲任务。在使用vTaskStartScheduler()函数来创建、启动调度器时,这个函数内部会创建空闲任务:
- 空闲任务优先级为0:它不能阻碍用户任务运行
- 空闲任务要么处于就绪态,要么处于运行态,永远不会阻塞
空闲任务的优先级为0,这意味着一旦某个用户的任务变为就绪态,那么空闲任务马上被切换出去,让这个用户任务运行。在这种情况下,我们说用户任务"抢占"(pre-empt)了空闲任务,这是由调度器实现的。
要注意的是:如果使用vTaskDelete()来删除任务,那么你就要确保空闲任务有机会执行,否则就无法释放被删除任务的内存。
我们可以添加一个空闲任务的钩子函数(Idle Task Hook Functions),空闲任务的循环每执行一次,就会调用一次钩子函数。钩子函数的作用有这些:
- 执行一些低优先级的、后台的、需要连续执行的函数
- 测量系统的空闲时间:空闲任务能被执行就意味着所有的高优先级任务都停止了,所以测量空闲任务占据的时间,就可以算出处理器占用率。
- 让系统进入省电模式:空闲任务能被执行就意味着没有重要的事情要做,当然可以进入省电模式了。
- 空闲任务的钩子函数的限制:
- 不能导致空闲任务进入阻塞状态、暂停状态
- 如果你会使用vTaskDelete()来删除任务,那么钩子函数要非常高效地执行。如果空闲任务移植卡在钩子函数里的话,它就无法释放内存。
使用钩子函数的前提
在FreeRTOS\Source\tasks.c中,可以看到如下代码,所以前提就是:
- 把这个宏定义为1:configUSE_IDLE_HOOK
- 实现vApplicationIdleHook函数
实际操作
创建一个任务的时候,传递了一个函数,假设这个函数只执行10次,不是死循环
如果这里不是死循环,执行完成之后,直接返回,就进入了错误处理的函数
关闭所有中断,进入死循环, 其他所有的任务也不能再次执行
任务能够调度,都是依赖Tick中断,现在中断都关闭了,那么所有的任务都没有办法再次切换了
如果想退出这个任务,必须自杀
任务如何退出?
任务如何退出?
- 自杀 vTaskDelete(NULL)
- 他杀 vTaskDelete(handle) //传入任务的句柄
这里复制第九个程序,改成十个程序
修改这一段代码,先注释vTaskDelete(NULL);,然后再取消注释,对比前后的效果
/**********************************************************************
* 函数名称: Led_Test
* 功能描述: Led测试程序
* 输入参数: 无
* 输出参数: 无
* 无
* 返 回 值: 0 - 成功, 其他值 - 失败
* 修改日期 版本号 修改人 修改内容
* -----------------------------------------------
* 2023/08/03 V1.0 韦东山 创建
***********************************************************************/
void Led_Test(void)
{
Led_Init();
//while (1)
for (unsigned char i = 0; i < 3; i++)
{
Led_Control(LED_GREEN, 1);
mdelay(500);
Led_Control(LED_GREEN, 0);
mdelay(500);
}
vTaskDelete(NULL);
}
- 任务自杀之后,LED就不再闪烁了,全彩LED正常运行
如果是任务A杀死任务B,需要任务A给任务B收尸,如果B自杀,则由空闲任务收尸
收尸就是做一些清除工作,需要释放TCB结构体、释放栈~
如果有很多任务自杀的话,空闲任务根本没有机会执行,没有办法执行,没有办法做清除工作,可能会导致内存不够用
为了让空闲任务有机会运行,我们需要养成良好的编程习惯
- a 事件驱动
- b 调用延时函数delay的时候,要主动休眠,不要使用死循环
这样我们可以再次修改代码,使用vTaskDelay,优化代码
void Led_Test(void)
{
Led_Init();
//while (1)
for (unsigned char i = 0; i < 3; i++)
{
Led_Control(LED_GREEN, 1);
// mdelay(500);
vTaskDelay(500);
Led_Control(LED_GREEN, 0);
// mdelay(500);
vTaskDelay(500);
}
vTaskDelete(NULL);
}
我们调用vTaskDelete,会把自己从ReadyList切换到DelayTaskList中,也就是从就绪态切换为阻塞态,不会再参与调度
使用vTaskDelete,进入了阻塞状态,让出了CPU资源,这样空闲任务才有机会运行,帮助自杀的任务回收资源
IDLE函数
看看空闲任务的函数,从main.c里找,启动调度器
osKernelStart();
进入函数,找到
vTaskStartScheduler();
创建了空闲任务,记录人空闲任务 prvIdleTask
xIdleTaskHandle = xTaskCreateStatic( prvIdleTask,
configIDLE_TASK_NAME,
ulIdleTaskStackSize,
( void * ) NULL, /*lint !e961. The cast is not redundant for all compilers. */
( tskIDLE_PRIORITY | portPRIVILEGE_BIT ),
pxIdleTaskStackBuffer,
pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
检查自杀的任务,做一些清理工作
prvCheckTasksWaitingTermination();
还会调用HOOK函数,钩子函数,每次都可以执行这个函数,我们可以从中添加一些打印信息,统计程序运行的状态。
vApplicationIdleHook();