任务调度
文章目录
- 任务调度
- 前言
- 一、协作式
- 二、时间片轮转
- 三、抢占式
- 总结
前言
FreeRTOS 是一个开源的实时操作系统,它支持多种调度策略,包括协作式(cooperative)和抢占式(preemptive)调度。
一、协作式
在协作式调度中,一旦一个任务开始执行,它将持续运行,直到它自己放弃 CPU 控制权为止。这通常发生在任务主动调用 task delay(等待时间片到期)、task yield(放弃剩余时间片)、或者进入阻塞状态(等待事件或资源)时。协作式调度简化了任务间共享资源的管理,因为开发者可以确保在没有明确放弃 CPU 控制权的情况下,任务不会被中断。但这种模式要求每个任务都必须定期放弃 CPU 控制权,否则可能导致系统响应性能下降。
协作式的调度方式,其本质上是任务在运行一段时间后,自己放弃CPU运行权,让其他任务运行。
在FreeRTOS里,是通过taskYIELD()这个函数实现放弃CPU的。一个典型的协作式任务是在while(1){}大循环的最后,调用taskYIELD()去主动放弃CPU;这时其他处于就绪态的最高优先级的任务才可能运行;如果其他任务都不在就绪状态,那么仍然回到taskYIELD()后面继续运行原来的任务。
在FreeRTOS里taskYIELD()是一种放弃CPU执行权的方法,还可以使用延时函数vTaskDelay,以及等待信号量、消息队列等等。
这里改成0是协作式
直接看代码
/*FreeRTOS配置*/
/* START_TASK 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define START_TASK_PRIO 1 /* 任务优先级 */
#define START_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t StartTask_Handler; /* 任务句柄 */
void start_task(void *pvParameters); /* 任务函数 */
/* TASK1 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK1_PRIO 2 /* 任务优先级 */
#define TASK1_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task1Task_Handler; /* 任务句柄 */
void task1(void *pvParameters); /* 任务函数 */
/* TASK2 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK2_PRIO 4 /* 任务优先级 */
#define TASK2_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task2Task_Handler; /* 任务句柄 */
void task2(void *pvParameters); /* 任务函数 */
/* TASK3 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK3_PRIO 4 /* 任务优先级 */
#define TASK3_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task3Task_Handler; /* 任务句柄 */
void task3(void *pvParameters); /* 任务函数 */
/******************************************************************************************************/
/**
* @brief FreeRTOS入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
xTaskCreate((TaskFunction_t )start_task, /* 任务函数 */
(const char* )"start_task", /* 任务名称 */
(uint16_t )START_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )START_TASK_PRIO, /* 任务优先级 */
(TaskHandle_t* )&StartTask_Handler); /* 任务句柄 */
vTaskStartScheduler();
}
/**
* @brief start_task
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void start_task(void *pvParameters)
{
taskENTER_CRITICAL(); /* 进入临界区 */
/* 创建任务1 */
xTaskCreate((TaskFunction_t )task1, /* 任务函数 */
(const char* )"task1", /* 任务名称 */
(uint16_t )TASK1_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )TASK1_PRIO, /* 任务优先级 */
(TaskHandle_t* )&Task1Task_Handler); /* 任务句柄 */
/* 创建任务2 */
xTaskCreate((TaskFunction_t )task2, /* 任务函数 */
(const char* )"task2", /* 任务名称 */
(uint16_t )TASK2_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )TASK2_PRIO, /* 任务优先级 */
(TaskHandle_t* )&Task2Task_Handler); /* 任务句柄 */
/* 创建任务3 */
xTaskCreate((TaskFunction_t )task3, /* 任务函数 */
(const char* )"task3", /* 任务名称 */
(uint16_t )TASK3_STK_SIZE, /* 任务堆栈大小 */
(void* )NULL, /* 传入给任务函数的参数 */
(UBaseType_t )TASK3_PRIO, /* 任务优先级 */
(TaskHandle_t* )&Task3Task_Handler); /* 任务句柄 */
vTaskDelete(StartTask_Handler); /* 删除开始任务 */
taskEXIT_CRITICAL(); /* 退出临界区 */
}
/**
* @brief task1
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task1(void *pvParameters)
{
uint32_t task1_num = 0;
while (1)
{
printf("task1\r\n");
HAL_Delay(100);
}
}
/**
* @brief task2
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task2(void *pvParameters)
{
uint32_t task2_num = 0;
while (1)
{
printf("task2\r\n");
HAL_Delay(200);
}
}
/**
* @brief task3
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task3(void *pvParameters)
{
uint8_t key = 0;
while (1)
{
printf("task3\r\n");
HAL_Delay(300);
}
}
先来看这段代码的执行结果
只有task2在执行,task3和task2是相同优先级 但是task2任务先创建的,task2任务执行后没有放弃CPU使用权,所以其他任务没有机会执行。
这说明了协作式的调度,是需要任务自己放弃CPU的,否则其他任务不能得到运行机会。
然后我们修改一下代码
可以看到task2执行完之后把cpu执行权释放了,然后这个时候task3开始执行,由于task3开始执行后没有释放CPU执行权所以,task2任务无法执行
然后我们在修改一下代码
可以看到task2和task3交替执行
可以发现,现在task02和task03都有执行,而task01没有执行。
这是因为task02在执行完后,通过taskYIELD();放弃了CPU;
此时task03和task01都有机会运行,但是task03优先级高,所以task03获得了运行机会;
task03运行完之后,也是通过taskYIELD();放弃了CPU,此时task02和task01有机会运行,但是task02优先级高,task02又获得了运行权。
这样循环执行,由于task01优先级低,它总是得不到运行机会。
如果想要所有任务都执行到,一种可行的方法是使用vTaskDelay指定放弃CPU的时间,如task02中放弃1000周期、tack03中放弃1000周期,那么当这两个任务都被挂起时,task01就获得了运行权。
我们修改一下代码
可以看到执行结果
二、时间片轮转
时间片轮转的调度方法,是让相同优先级的几个任务轮流运行,每个任务运行一个时间片,任务在时间片运行完之后,操作系统自动切换到下一个任务运行;在任务运行的时间片中,也可以提前让出CPU运行权,把它交给下一个任务运行(这一点和协作式类似)。
FreeRTOS的时间片固定为一个时钟节拍,由configTICK_RATE_HZ这个宏设置
把设置改回来,然后修改一下代码
可以看到执行结果,各任务都不主动放弃CPU。可以看到,虽然各个任务都没有主动放弃CPU执行权,但是同为高优先级的task02和task03,都得到了运行的机会;只有低优先级的task01没有执行到。这是因为操作系统对两个高优先级的任务使用了时间片轮转调度,轮流得到了执行。
关闭时间片轮转调度功能,即把configUSE_TIME_SLICING宏定义去除或定义为0:
执行结果如下图所示
task03又没有机会去执行了。
三、抢占式
抢占式调度允许操作系统根据优先级来决定哪个任务应当获得 CPU 控制权。当一个高优先级任务变为就绪状态时(例如,它正在等待的事件发生了),系统会中断当前运行的较低优先级任务,把 CPU 控制权交给高优先级任务。这确保了对紧急任务的快速响应。在这种模式下,任务不需要显式放弃 CPU 控制权,因为它们可以随时被更高优先级的任务抢占。
抢占式调度,是最高优先级的任务一旦就绪,总能得到CPU的执行权;它抢占了低优先级的运行机会。在抢占式调度系统中,总是运行最高优先级的任务。
抢占式调度的特点在于,可以使得一些需要实时运行的任务,能在较短的时间内获得CPU执行权,保证这些实时任务及时执行。
在FreeRTOS中,抢占式调度,与时间片轮转可以同时存在;当有高优先级任务就绪时,运行高优先级任务;当最高优先级的任务有好几个时,这几个任务可以以时间片轮转方式调度。
我们修改一下代码
低优先级的task01一直占用CPU,不会主动放弃执行权;task02执行一次,放弃500ms的执行权;task03执行一次,放弃1000ms执行权:
然后看一下结果
首先task02和task03执行时间片轮转,各执行了一次;然后它们都放弃了CPU执行权,task01得到了执行,在500ms内,它可以执行3次;
到500ms时,task02第一次延时时间到,恢复就绪态,task02抢占了CPU执行,执行完打印后,又进行延时放弃CPU;task01得到了执行;又执行了2次,过去了1000ms,这个时候task02和task03都有机会执行,进行了一次时间片轮转,然后又回到task01执行
到100ms时,task03第一次恢复就绪,task02第二次恢复就绪,都各执行了一遍;之后又让出CPU;task01得到了执行;然后一直循环。
总结
可以看出,开启了抢占式和时间片轮转两种调度算法时,高优先级的任务一旦就绪,就能抢占低优先级的CPU执行权;如果有两个同优先级的任务都可以运行,则它们之间是时间片轮转方式调度。抢占式调度和时间片轮转两种任务的切换方式,可以说是FreeRTOS系统最核心的功能。