FreeRTOS任务调度(抢占式、协作式、时间片轮转)

任务调度


前言

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系统最核心的功能。

FreeRTOS中的基于优先级的抢占式任务调度是通过任务控制块(TCB)和调度器的协作来实现的。每个任务都有一个TCB,其中包含了任务的状态信息和任务控制数据。当系统初始化时,所有任务的TCB被创建并添加到就绪列表中。调度器会根据任务的优先级来决定哪个任务获得处理器时间片。 参考资源链接:[FreeRTOS实时内核实战指南](https://wenku.csdn.net/doc/6412b79ebe7fbd1778d4af28?spm=1055.2569.3001.10343)FreeRTOS中,调度器是一个非常核心的部分,它负责任务的切换和调度。当一个高优先级的任务变为就绪状态时,调度器会检查它是否具有比当前正在运行的任务更高的优先级。如果是,调度器会执行一次上下文切换,将CPU的控制权从当前任务转移到高优先级的任务。这个过程是实时操作系统响应外部事件的关键所在。 具体到源代码层面,FreeRTOS调度器的核心功能是实现于`vTaskSwitchContext`函数中。这个函数会在任务切换时被调用,其目的是根据任务优先级来选择下一个将要运行的任务。它通过比较就绪列表中任务的优先级来完成这一工作,一旦找到一个优先级更高的就绪任务,就会执行上下文切换。 例如,当一个高优先级任务完成延时等待并变为就绪状态时,它会触发调度器进行任务切换。具体代码实现可能如下: ```c void vTaskSwitchContext( void ) { // ... 省略部分代码 ... // 检查是否有优先级更高的任务就绪 if( pxReadyTasksLists[ uxTopReadyPriority ] != NULL ) { // 找到就绪列表中的第一个任务,并准备进行上下文切换 pxCurrentTCB = ( TCB_t * ) pxReadyTasksLists[ uxTopReadyPriority ]; // ... 省略部分代码 ... // 执行上下文切换,这里通常涉及到汇编语言的调用,以保存和恢复任务的上下文 portYIELD_WITHIN_API(); } } ``` 需要注意的是,实际的上下文切换过程涉及到处理器特定的寄存器保存和恢复操作,这部分代码通常是用汇编语言编写的,以确保切换过程的效率和正确性。在x86、ARM等不同架构的处理器上,这些代码会有所不同。 通过理解FreeRTOS调度策略和源代码,开发者能够更好地掌握如何在实际的嵌入式项目中利用FreeRTOS来实现高效的任务调度和系统设计。此外,为了深入学习FreeRTOS的各项功能和优化方法,建议阅读《FreeRTOS实时内核实战指南》中文版。这本指南不仅详细介绍了任务调度的原理,还提供了实用的项目案例和技巧,有助于开发者进一步提升嵌入式开发的实战能力。 参考资源链接:[FreeRTOS实时内核实战指南](https://wenku.csdn.net/doc/6412b79ebe7fbd1778d4af28?spm=1055.2569.3001.10343)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我与nano

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值