前言
本篇文章将带大家学习一下什么是空闲任务以及钩子函数,以及学习FreeRTOS中的任务调度算法,了解在FreeRTOS中任务是如何进行切换调度的。
一、空闲任务概念
空闲任务(Idle Task)是嵌入式实时操作系统(RTOS)中的一种特殊任务。它是系统中优先级最低的任务,并且在系统中没有其他任务需要执行时运行。空闲任务的目的是让处理器在没有其他工作需要执行时保持忙碌状态,从而防止处理器进入空转状态。
空闲任务在RTOS中发挥着重要的作用。当所有其他任务都没有工作要执行时,空闲任务就会运行。它通常执行一些轻量级的操作,如低功耗模式的进入、系统统计信息的更新、调度器的处理等。空闲任务的执行时间应尽量短,以充分利用处理器资源。
空闲任务可以被视为一个后备任务,确保系统始终有任务可以运行,即使没有其他任务就绪。这对于实时系统来说尤为重要,因为实时系统需要对外部事件以及响应用户请求作出快速响应。通过空闲任务,RTOS可以保证系统的连续性和响应性。
在FreeRTOS中,空闲任务是由内核自动创建和管理的。
在使用vTaskStartScheduler开启调度器后会自动的帮我们创建好空闲任务。
二、钩子函数概念
FreeRTOS 中的钩子函数(Hook Functions)是用户定义的回调函数,用于在 FreeRTOS 内核中特定事件发生时执行自定义的代码。这些钩子函数允许开发者介入 FreeRTOS 的内部操作,以便适应特定的需求、调试或性能分析。
使用钩子函数时需要先开启FreeRTOS中的configUSE_IDLE_HOOK配置宏。
FreeRTOSConfig.h:
重新实现钩子函数:
void vApplicationIdleHook( void ) { //用户自定义处理 }
三、任务调度算法
调度算法是操作系统中用于确定进程或线程在可用系统资源上执行的顺序和时间分配的一组规则和策略。调度算法的作用是合理地利用系统资源,提高系统的性能、响应性和效率。
调度算法在多任务操作系统中起着重要的作用,它决定了各个任务之间的执行顺序、分配的时间片长度以及资源的分配策略。
在 FreeRTOS 中,调度算法和行为可以通过一些宏配置来决定,包括 configUSE_PREEMPTION、configUSE_TIME_SLICING 和 configIDLE_SHOULD_YIELD。
1.configUSE_PREEMPTION:
这个宏控制是否启用任务抢占(preemption)。如果设置为 1,则允许具有更高优先级的任务中断当前正在执行的任务;如果设置为 0,则不允许任务抢占。
任务抢占允许更高优先级的任务能够立即获得处理器的控制权,以满足实时需求。然而,抢占会引入上下文切换的开销,可能会对性能产生影响。
2.configUSE_TIME_SLICING:
这个宏用于启用或禁用时间片轮转调度(time slicing)。时间片轮转调度是一种机制,当多个任务具有相同优先级并且可运行时,它们会按时间片的方式轮流使用处理器。
如果设置为 1,则启用时间片轮转调度;如果设置为 0,则禁用时间片轮转调度。
3.configIDLE_SHOULD_YIELD:
这个宏用于配置空闲任务是否应该主动让出处理器。当系统没有其他优先级较高的任务需要运行时,空闲任务会获得处理器的控制权。如果设置为 1,则空闲任务在不需要执行任何其他操作时会主动让出处理器;如果设置为 0,则空闲任务不会主动让出处理器。
如果系统中有其他低优先级任务需要执行,让空闲任务主动让出处理器可以提供更好的响应时间和系统性能。
这里使用百问网提供的一张资料图:
四、任务调度算法实验
1.实验代码
volatile int flagIdleTaskrun = 0; // 空闲任务运行时flagIdleTaskrun=1 volatile int flagTask1run = 0; // 任务1运行时flagTask1run=1 volatile int flagTask2run = 0; // 任务2运行时flagTask2run=1 volatile int flagTask3run = 0; // 任务3运行时flagTask3run=1 /*-----------------------------------------------------------*/ void vTask1( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 1; flagTask2run = 0; flagTask3run = 0; /* 打印任务的信息 */ printf("T1\r\n"); } } void vTask2( void *pvParameters ) { /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 0; flagTask2run = 1; flagTask3run = 0; /* 打印任务的信息 */ printf("T2\r\n"); } } void vTask3( void *pvParameters ) { const TickType_t xDelay5ms = pdMS_TO_TICKS( 5UL ); /* 任务函数的主体一般都是无限循环 */ for( ;; ) { flagIdleTaskrun = 0; flagTask1run = 0; flagTask2run = 0; flagTask3run = 1; /* 打印任务的信息 */ printf("T3\r\n"); // 如果不休眠的话, 其他任务无法得到执行 vTaskDelay( xDelay5ms ); } } void vApplicationIdleHook(void) { flagIdleTaskrun = 1; flagTask1run = 0; flagTask2run = 0; flagTask3run = 0; /* 故意加入打印让flagIdleTaskrun变为1的时间维持长一点 */ //printf("Id\r\n"); } int main( void ) { prvSetupHardware(); xTaskCreate(vTask1, "Task 1", 1000, NULL, 0, NULL); xTaskCreate(vTask2, "Task 2", 1000, NULL, 0, NULL); xTaskCreate(vTask3, "Task 3", 1000, NULL, 2, NULL); /* 启动调度器 */ vTaskStartScheduler(); /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */ return 0; }
2.是否抢占
抢占:
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
不抢占:
#define configUSE_PREEMPTION 0
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
这里可以发现当配置为抢占的时候,高优先级的任务就绪后就可以直接执行了。
当配置为不可抢占时,任务会一直执行下去,其他任务无法抢占执行权。
3.时间片是否轮转
支持时间片轮转:
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
不支持时间片轮转:
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 0
#define configIDLE_SHOULD_YIELD 1
当配置为支持时间片轮转时,在Tick中断会引起任务的切换。
高优先级任务就绪时会引起任务切换,高优先级任务不再运行时也会引起任务切
换。可以看到任务3就绪后可以马上执行,它运行完毕后导致任务切换。其他时间没有任务切换,
可以看到任务1、任务2都运行了很长时间。
当 FreeRTOS 配置为支持时间片轮转(即 configUSE_TIME_SLICING 设置为 1)时,任务会按照时间片的方式进行调度。每个任务被分配一个时间片,在时间片用尽之前,如果发生 Tick 中断,则会引起任务切换。
4.空闲任务让步
空闲任务让步
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
空闲任务不让步
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 0
当配置为空闲任务让步时,可以看出空闲任务运行的时间比较短,会让步给其他任务进行执行。
当配置为空闲任务不让步时,可以看出空闲任务运行的时间和任务1,任务2运行的时间差不多,并不会做出让步。
总结
本篇文章就讲解到这里。