RTOS 中的任务调度与三种任务模型
概述
任务的执行通过任务调度器来完成。通俗地讲,任务调度器就像一个导演,这位大导演在系统启动时就自动开始工作了,他的主要工作就是定时看下剧本,决定让哪个任务获取 CPU 进行表演:
实际使用中,任务调度器是一个较高优先级的中断,每一个 SysTick 时间到达或系统主动触发了任务调度器工作时,就检查就绪的任务中谁的优先级高,然后允许优先级高的任务使用 CPU, 直到下次触发任务调度器再次工作,重新决定哪个 Task 获取 CPU 得到执行权。
上图显示了任务的调度有两个策略:
1)任务被创建后将进入就绪状态,当两个任务的优先级不一样时,优先级高的任务先获取 CPU 的使用权得到运行。上图中,就绪的 Task0、Task1、Task2中,因 Task0 的优先级最高,因此获取了 SysTick1-SysTick2 之间的 CPU 使用权。
2)当两个任务的优先级一样时,若它们的优先级最高,则它们执行时间片调度,在 FreeRTOS 中时间片调度即循环地获取每一个 SysTick 时间内的 CPU 使用权。如上图中,Task1、Task2 的优先级相同,因此他们共享 SysTick2-SysTick4 之间的 CPU 使用权,在这两个 Tick 内,每个任务都获取其中的 1个 Tick 内的 CPU 使用权(其他的一些 RTOS 可以定义一个时间片的大小为几个 SysTick,FreeRTOS 的时间片大小是 1个 SysTick)。
需求及功能解析
本小节主要引出任务调度器的概念,在此基础上,介绍了任务函数的三种策略:
1)单次运行的任务 task3:创建一次,运行一次:
static void task3_process(void *arg)
{
static const char *TASK3_TAG = "TASK3";
ESP_LOGI(TASK3_TAG, "task3_flag = %d, arg3 = %s", task3_flag, (char *)arg);
task3_flag++;
vTaskDelete(NULL); // 删除任务
}
采用创建任务的方式来启动任务,可以省去用通信手段触发任务的麻烦,在不需要该任务时,还可以节省内存资源。此外,可以通过创建任务时的参数 *para,使得每次启动任务时可以让任务以不同的方式来执行。通常可以对 设备升级任务采取这种设计模式。当然,这种方式也有不好的地方,创建任务比较耗费时间,因此其仅适用对实时性要求不高的事情,如控制键盘启动任务。
2)周期执行的任务 task2:创建一次,周期运行:
static void task2_process(void *arg)
{
static const char *TASK2_TAG = "TASK2";
while (1) {
ESP_LOGI(TASK2_TAG, "task2_flag = %d, arg2 = %s", task2_flag, (char *)arg);
task2_flag++;
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
具备周期性的需求,往往有多种方案,我们将在后续深入讨论这些知识。
3)事件触发执行的任务 task1:创建后,等到某个事件的触发,相关事件触发后,自动运行一次:
static void task1_process(void *arg)
{
static const char *TASK1_TAG = "TASK1";
while (1) {
if(wait_event()) { // 等待事件
process_event(); // 处理事件
notify_other_tasks();
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
根据 RTOS 的任务调度机制,事件触发的编程模型能最大限度提高CPU的使用效率(不必立即创建新任务,也不涉及延时),保证程序的实时性。
另外,周期性和事件触发执行的任务,其 TaskCode 都包含 while(1) 循环。他们的 TaskCode 的组成主要是:进行准备工作的代码、循环代码、异常处理代码。我们将在后续的学习中进一步熟悉这点。
示例解析
示例输出:
This is esp32 chip with 2 CPU core(s), WiFi/BT/BLE, Minimum free heap size: 294424 bytes
Wait event!
Process event!
Notify other tasks!
I (336) TASK2: task2_flag = 0, arg2 = 2
I (336) TASK3: task3_flag = 0, arg3 = 3
如上所述,单次运行的任务只运行一次(打印)就结束了;周期运行的任务一直打印;事件触发运行的任务也在循环打印,我们这里先阐述事件驱动任务编程的思路,后续将深入研究这种任务机制。
讨论
-
可以改变函数的优先级探究任务调度器的使用。
-
典型的任务调度机制有哪些?
1)抢占式调度( (Preemptive Scheduling) ),即总是期望优先级最高的任务获取 CPU 资源得到运行。每个任务都有不同的优先级,任务会一直运行直到被高优先级任务抢占或者遇到延时或者等待。
2)时间片调度(Time Slice),即每个任务都拥有相同的优先级,任务会运行固定的时间片个数(除非任务主动延时或者进入等待状态)。
3)协助式调度(Cooperative Scheduling,也称为合作式调度),只要一个任务不主动 yield 交出 CPU 使用权,它就会一直执行下去。
FreeRTOS 支持三种调度方式的,但最后一种调度方式几乎很少用了,读者只需了解它即可,本课程的内容仅涉及前两种调度方式。
- 触发任务调度(任务切换)的场景主要有哪些?
1)系统 SysTick 时间到达时触发任务调度器检查是否需要执行任务切换。
2)程序中主动调用系统调用函数。常见的系统调用函数如 taskYIELD()、portYIELD()、portYIELD_FROM_ISR() 在程序中被调用时也会触发任务调度器检查是否需要执行任务切换。
总结
1)任务调度器是 FreeRTOS 系统已经实现的任务管理组件,用于分配 CPU 的使用权,获取到 CPU 使用权的任务将得到执行。
2)任务调度器有两个调度策略:高优先级在 SysTick 到达时可抢占 CPU 使用权,同优先级可以共享一断时间内的 CPU 使用权(称为时间片轮转)。
3)任务根据其运行情况,可以分为单次运行、周期运行、事件驱动运行三种模型,其中事件驱动模型可以最大限度提高 CPU 的使用效率,并且实时性好。
4)更多优先级的设计,以及调度情况我们将在后续的章节中深入讨论。
资源链接
1)Learning-FreeRTOS-with-esp32 系列博客介绍
2)对应示例的 code 链接 (点击直达代码仓库)
3) 下一篇:RTOS 暂停任务-任务挂起与恢复