首先来看一下源码包基本目录结构
针对各个处理器所做的移植,以及其对应的Demo,数目如此多的文件,全部都存放在同一个压缩文件里。这样做极大地简化了FreeRTOS的发布过程,但是面对这么多的源文件,也很可能会令新手望而生畏。其实,FreeRTOS源文件包的目录结构非常简洁,更出人意料的是,FreeRTOS实时内核仅仅只包含3个文件(如果需要用到software timer, event group or co-routine功能,则还得包含其他文件)。
解压FreeRTOS源文件包之后,可以看到两个子目录,FreeRTOS and FreeRTOS-Plus。如下图所示:
+-FreeRTOS-Plus Contains FreeRTOS+ components and demo projects. ¦ +-FreeRTOS Contains the FreeRTOS real time kernel source files and demo projects
FreeRTOS-Plus目录里是一些组件及其Demo,而FreeRTOS目录下,又包含如下两个子目录:
FreeRTOS ¦ +-Demo Contains the demo application projects. ¦ +-Source Contains the real time kernel source code.
与内核相关的文件仅有3个,它们分别是 tasks.c, queue.c 和 list.c. 它们位于FreeRTOS/Source目录之内。在这个目录下,还包含 timers.c and croutine.c 这两个文件,它们分别实现 software timer 和 co-routine 功能。
对于每一种架构的处理器,都有与之相关的一部分RTOS代码,它们被称之为RTOS可移植层,位于FreeRTOS/Source/Portable/[compiler]/[architecture]子目录中,其中
compiler为用户所使用的编译器,architecture是指具体某一种架构。举例如下:
那么,与 TriCore 相关的文件 (port.c),则位于 FreeRTOS/Source/Portable/GCC/TriCore_1782 目录下。 如此一来,所有 FreeRTOS/Source/Portable 目录下的子目录,除了 FreeRTOS/Source/Portable/MemMang 之外,其它的都可以一律删掉。
那么,与 RX600 相关的文件(port.c), 则位于 FreeRTOS/Source/Portable/IAR/RX600 目录下。 如此一来,所有 FreeRTOS/Source/Portable 目录下的子目录,除了 FreeRTOS/Source/Portable/MemMang 之外,其它的都可以一律删掉。
FreeRTOS/Source 目录树如下所示:
FreeRTOS ¦ +-Source The core FreeRTOS kernel files ¦ +-include The core FreeRTOS kernel header files ¦ +-Portable Processor specific code. ¦ +-Compiler x All the ports supported for compiler x +-Compiler y All the ports supported for compiler y +-MemMang The sample heap implementations
对于每一种处理器架构和编译器,FreeRTOS源文件包里都有对应的Demo应用程序。大多数Demo应用程序共用的文件,位于 FreeRTOS/Demo/Common/Minimal 目录下。(FreeRTOS/Demo/Common/Full 目录下的文件无需理会,这些文件在将FreeRTOS移植至PC时才会用到)。
FreeRTOS/Demo 目录下的其它子目录,每一个都对应某一处架构的处理器及编译器。这一点可以从其目录名可以看出来,举例如下:
那么,与 TriCore 相应的Demo应用程序则位于 FreeRTOS/Demo/TriCore_TC1782_TriBoard_GCC 目录,如此一来,所有 FreeRTOS/Demo 目录下的子目录(除开Common子目录外)都可以被忽略或删除。
那么,与 TriCore 相应的Demo应用程序则位于 FreeRTOS/Demo/RX600_RX62N-RDK_IAR 目录,如此一来,所有 FreeRTOS/Demo 目录下的子目录(除开Common子目录外)都可以被忽略或删除。
FreeRTOS/Demo 目录树如下所示:
FreeRTOS ¦ +-Demo ¦ +-Common The demo application files that are used by all the demos. +-Dir x The demo application build files for port x +-Dir y The demo application build files for port y
对于新手,建议首先运行相应的Demo,然后在此基础之上,逐步将Demo内的应用程序文件替换成自己的应用程序文件。
小型多任务嵌入式系统简介
不同的多任务系统有不同的侧重点。以工作站和桌面电脑为例:
早期的处理器非常昂贵,所以那时的多任务用于实现在单处理器上支持多用户。这
类系统中的调度算法侧重于让每个用户”公平共享”处理器时间。
随着处理器功能越来越强大,价格却更偏宜,所以每个用户都可以独占一个或多个
处理器。这类系统的调度算法则设计为让用户可以同时运行多个应用程序,而计算
机也不会显得反应迟钝。例如某个用户可能同时运行了一个字处理程序,一个电子
表格,一个邮件客户端和一个 WEB 浏览器,并且期望每个应用程序任何时候都能
对输入有足够快的响应时间。
桌面电脑的输入处理可以归类为”软实时”。为了保证用户的最佳体验,计算机对每
个输入的响应应当限定在一个恰当的时间范围——但是如果响应时间超出了限定范围,
并不会让人觉得这台电脑无法使用。比如说,键盘操作必须在键按下后的某个时间内作
出明显的提示。但如果按键提示超出了这个时间,会使得这个系统看起来响应太慢,而
不致于说这台电脑不能使用。
仅仅从单处理器运行多线程这一点来说,实时嵌入式系统中的多任务与桌面电脑的
多任务从概念上来讲是相似的。但实时嵌入式系统的侧重点却不同于桌面电脑——特别
是当嵌入式系统期望提供”硬实时”行为的时候。
硬实时功能必须在给定的时间限制之内完成——如果无法做到即意味着整个系统
的绝对失败。汽车的安全气囊触发机制就是一个硬实时功能的例子。安全气囊在撞击发
生后给定时间限制内必须弹出。如果响应时间超出了这个时间限制,会使得驾驶员受到
伤害,而这原本是可以避免的。
大多数嵌入式系统不仅能满足硬实时要求,也能满足软实时要求。
本章的目的是让读者充分了解:
在应用程序中, FreeRTOS 如何为各任务分配处理时间。
在任意给定时刻, FreeRTOS 如何选择任务投入运行。
任务优先级如何影响系统行为。
任务存在哪些状态。
此外,还期望能够让读者解:
如何实现一个任务。
如何创建一个或多个任务的实例。
如何使用任务参数。
如何改变一个已创建任务的优先级。
如何删除任务。
如何实现周期性处理。
空闲任务何时运行,可以用来干什么。
本章所介绍的概念是理解如何使用 FreeRTOS 的基础,也是理解基于 FreeRTOS
的应用程序行为方式的基础——因此,本章也是这本书中最为详尽的一章。
任务是由 C 语言函数实现的。唯一特别的只是任务的函数原型,其必须返回 void,
而且带有一个 void 指针参数。其函数原型
void ATaskFunction( void *pvParameters );
每个任务都是在自己权限范围内的一个小程序。其具有程序入口,通常会运行在一
个死循环中,也不会退出。一个典型的任务结构如程序清单 2 所示。
FreeRTOS 任务不允许以任何方式从实现函数中返回——它们绝不能有一
条”return”语句,也不能执行到函数末尾。如果一个任务不再需要,可以显式地将其删
除。这也在程序清单 2 展现。
一个任务函数可以用来创建若干个任务——创建出的任务均是独立的执行实例,拥
有属于自己的栈空间,以及属于自己的自动变量(栈变量),即任务函数本身定义的变量
void ATaskFunction( void *pvParameters )
{
/* 可以像普通函数一样定义变量。用这个函数创建的每个任务实例都有一个属于自己的iVarialbleExample变
量。但如果iVariableExample被定义为static,这一点则不成立 – 这种情况下只存在一个变量,所有的任务实
例将会共享这个变量。 */
int iVariableExample = 0;
/* 任务通常实现在一个死循环中。 */
for( ;; )
{
/* 完成任务功能的代码将放在这里。 */
}
/* 如果任务的具体实现会跳出上面的死循环,则此任务必须在函数运行完之前删除。传入NULL参数表示删除
的是当前任务 */
vTaskDelete( NULL );
}
程序清单 2 典型的任务函数结构
任务从非运行态转移到运行态被称为”切换入或切入(switched in)”或”交换入
(swapped in)”。相反,任务从运行态转移到非运行态被称为”切换出或切出(switched
out)”或”交换出(swapped out)”。 FreeRTOS 的调度器是能让任务切入切出的唯一实体
xTaskCreate() API 函数
描述用到的数据类型和命名。
portBASE_TYPE xTaskCreate( pdTASK_CODE pvTaskCode,
const signed portCHAR * const pcName,
unsigned portSHORT usStackDepth,
void *pvParameters,
unsigned portBASE_TYPE uxPriority,
xTaskHandle *pxCreatedTask );
程序清单 3 xTaskCreate() API 函数原型
表 1 xTaskCreate()参数与返回值
参数名 |
描述 |
pvTaskCode |
任务只是永不退出的 C 函数,实现常通常是一个死循环。参数 pvTaskCode 只一个指向任务的实现函数的指针(效果上仅仅是函数名)。 |
pcName |
具有描述性的任务名。这个参数不会被 FreeRTOS 使用。其只是单纯地用于辅助调试。识别一个具有可读性的名字总是比通过句柄来识别容易得多。 应用程序可以通过定义常量 config_MAX_TASK_NAME_LEN 来定义 任务名的最大长度——包括’\0’结束符。如果传入的字符串长度超过了这个最大值,字符串将会自动被截断。 |
usStackDepth |
当任务创建时,内核会分为每个任务分配属于任务自己的唯一状态。 usStackDepth 值用于告诉内核为它分配多大的栈空间。 这个值指定的是栈空间可以保存多少个字(word),而不是多少个字 节(byte)。比如说,如果是 32 位宽的栈空间,传入的 usStackDepth 值为 100,则将会分配 400 字节的栈空间(100 * 4bytes)。栈深度乘以栈宽度的结果千万不能超过一个 size_t 类型变量所能表达的最大值。 应用程序通过定义常量 configMINIMAL_STACK_SIZE 来决定空闲 任务任用的栈空间大小。在 FreeRTOS 为微控制器架构提供的 Demo 应用程序中,赋予此常量的值是对所有任务的最小建议值。 如果你的任务会使用大量栈空间,那么你应当赋予一个更大的值。 没有任何简单的方法可以决定一个任务到底需要多大的栈空间。计 算出来虽然是可能的,但大多数用户会先简单地赋予一个自认为合 理的值,然后利用 FreeRTOS 提供的特性来确证分配的空间既不欠缺也不浪费。第六章包括了一些信息,可以知道如何去查询任务使用了多少栈空间。 |
pvParameters |
任务函数接受一个指向 void 的指针(void*)。 pvParameters 的值即是传递到任务中的值。这篇文档中的一些范例程序将会示范这个参数可以如何使用。 |
uxPriority |
指定任务执行的优先级。优先级的取值范围可以从最低优先级 0 到最高优先级(configMAX_PRIORITIES – 1)。 configMAX_PRIORITIES 是一个由用户定义的常量。优先级号并没有上限(除了受限于采用的数据类型和系统的有效内存空间),但最好使用实际需要的最小数值以避免内存浪费。如果 uxPriority 的值超过了(configMAX_PRIORITIES – 1),将会导致实际赋给任务的优先级被自动封顶到最大合法值。 |
pxCreatedTask |
pxCreatedTask 用于传出任务的句柄。这个句柄将在 API 调用中对该创建出来的任务进行引用,比如改变任务优先级,或者删除任务。 如果应用程序中不会用到这个任务的句柄,则 pxCreatedTask 可以被设为 NULL。 |
返回值 |
有两个可能的返回值: 1. pdTRUE 表明任务创建成功。 2. errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 由于内存堆空间不足, FreeRTOS 无法分配足够的空间来保存任务 结构数据和任务栈,因此无法创建任务。 第五章将提供更多有关内存管理方面的信息 |
这两个任务只是周期性地打印输出字
符串,采用原始的空循环方式来产生周期延迟。两者在创建时指定了相同的优先级,并
且在实现上除输出的字符串外完全一样——程序清单 4 和程序清单 5 是这两个任务对应
的实现代码
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
/* 和大多数任务一样,该任务处于一个死循环中。 */
for( ;; )
{
/* Print out the name of this task. */打印任务名字
vPrintString( pcTaskName );
/* 延迟,以产生一个周期 */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* 这个空循环是最原始的延迟实现方式。在循环中不做任何事情。后面的示例程序将采用
delay/sleep函数代替这个原始空循环。 */
}
}
}
程序清单4 例1中的第一个任务实现代码
void vTask2( void *pvParameters )
{
const char *pcTaskName = "Task 2 is running\r\n";
volatile unsigned long ul;
/* 和大多数任务一样,该任务处于一个死循环中。 */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 延迟,以产生一个周期 */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* 这个空循环是最原始的延迟实现方式。在循环中不做任何事情。后面的示例程序将采用
delay/sleep函数代替这个原始空循环。 */
}
}
}
程序清单 5 例 1 中的第二个任务实现代码
int main( void )
{
/* 创建第一个任务。 需要说明的是一个实用的应用程序中应当检测函数xTaskCreate()的返回值,以确保任务创建成功。 */
xTaskCreate( vTask1, /* 指向任务函数的指针 */
"Task 1", /* 任务的文本名字,只会在调试中用到 */
1000, /* 栈深度 – 大多数小型微控制器会使用的值会比此值小得多 */
NULL, /* 没有任务参数 */
1, /* 此任务运行在优先级1上. */
NULL ); /* 不会用到任务句柄 */
/* Create the other task in exactly the same way and at the same priority. */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* 启动调度器,任务开始执行 */
vTaskStartScheduler();
/* 如果一切正常, main()函数不应该会执行到这里。但如果执行到这里,很可能是内存堆空间不足导致空闲任务无法创建。第五章有讲述更多关于内存管理方面的信息 */
for( ;; );
}
主函数
图 2 中看到两个任务在同时运行,但实际上这两个任务运行在同一个处理器上,所
以不可能会同时运行。事实上这两个任务都迅速地进入与退出运行态。由于这两个任务
运行在同一个处理器上,所以会平等共享处理器时间(优先级还一样)。真实的执行流程所图 3 所示
在t1时刻,任务1进入运行状态,执行到t2时刻 t2时刻,任务2进入“运行中”状态,执行任务,直到t3时刻,任务1重新进入“运行中”状态
在任何时刻只可能有一个任务处于运行态。所以一个任务进入运行态后(切入),另
一个任务就会进入非运行态(切出)。
例 1 中, main()函数在启动调度器之前先完成两个任务的创建。当然也可以从一个
任务中创建另一个任务。我们可以先在 main()中创建任务 1,然后在任务 1 中创建任务
2。如果我们需要这样做,则任务 1 代码就应当修改成程序清单 7 所示的样子。这样,
在调度器启动之前,任务 2 还没有被创建,但是整个程序运行的输出结果还是相同的
void vTask1( void *pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul;
/* 如果已经执行到本任务的代码,表明调度器已经启动。在进入死循环之前创建另一个任务。 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
for( ;; )
{
/* 打印任务名字 */
vPrintString( pcTaskName );
/* 延时一段时间 */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/*这个循环只是一个非常粗糙的延迟实现。在这里没什么事可做。后面的示例将取代此。有适当延迟/睡眠功能的循环*/
}
}
}
程序清单 7 在一个任务中创建另一个任务 —— 在调度器启动之后】
程序清单 8 包含了例 2 中用到的唯一一个任务函数代码(vTaskFunction)。这一个
任务函数代替了例 1 中的两个任务函数(vTask1 与 vTask2)。这个函数的任务参数被强
制转化为 char*以得到任务需要打印输出的字符串
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
volatile unsigned long ul;
/* 需要打印输出的字符串从入口参数传入。强制转换为字符指针。 */
pcTaskName = ( char * ) pvParameters;
/* *与大多数任务一样,这个任务在一个无限循环中实现。* */
for( ;; )
{
/* /*打印此任务的名称。*. */
vPrintString( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/* 这个循环只是一个非常粗糙的延迟实现。这里没什么可做的。稍后的练习将用适当的延迟/睡眠功能取代这个粗糙的循环。 */
}
}
}
程序清单 8 例 2 中用于创建两个任务实例的任务函数
尽管现在只有一个任务实现代码(vTaskFunction),但是可以创建多个任务实例。每
个任务实例都可以在 FreeRTOS 调度器的控制下独运行。
传递给 API 函数 xTaskCreate()的参数 pvPrameters 用于传入字符串文本。如程序
清单 9 所示
/* 定义将要通过任务参数传递的字符串。定义为const,且不是在栈空间上,以保证任务执行时也有效。 */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main( void )
{
/* Create one of the two tasks. */
xTaskCreate( vTaskFunction, /* 指向任务函数的指针. */
"Task 1", /* 任务名. */
1000, /* 栈深度. */
(void*)pcTextForTask1, /* 通过任务参数传入需要打印输出的文本. */
1, /* 此任务运行在优先级1上. */
NULL ); /* 不会用到此任务的句柄. */
/* 同样的方法创建另一个任务。至此,由相同的任务代码(vTaskFunction)创建了多个任务,仅仅是传入的参数不同。同一个任务创建了两个实例。 */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 1, NULL );
/* Start the scheduler so our tasks start executing. */
vTaskStartScheduler();
/*如果一切正常,那么main()将永远不会像调度程序一样到达这里现在运行这些任务。如果main()到达这里,那么很可能是没有足够的堆内存可供空闲任务创建 */
for( ;; );
}
程序清单 9 例 2 中的 main()函数实现代码
例 2 的运行输出结果与例 1 完全一样,参见图 2
xTaskCreate() API 函数的参数 uxPriority 为创建的任务赋予了一个初始优先级。这
个侁先级可以在调度器启动后调用 vTaskPrioritySet() API 函数进行修改。
应 用 程 序 在 文 件 FreeRTOSConfig.h 中 设 定 的 编 译 时 配 置 常 量
configMAX_PRIORITIES 的值,即是最多可具有的优先级数目。 FreeRTOS 本身并没
有限定这个常量的最大值,但这个值越大,则内核花销的内存空间就越多。所以总是建
议将此常量设为能够用到的最小值
对于如何为任务指定优先级, FreeRTOS 并没有强加任何限制。任意数量的任务可
以共享同一个优先级——以保证最大设计弹性。当然,如果需要的话,你也可以为每个
任务指定唯一的优先级(就如同某些调度算法的要求一样),但这不是强制要求的
低优先级号表示任务的优先级低,优先级号 0 表示最低优先级。有效的优先级号范
围从 0 到(configMAX_PRIORITES – 1)
调度器保证总是在所有可运行的任务中选择具有最高优先级的任务,并使其进入运
行态。如果被选中的优先级上具有不止一个任务,调度器会让这些任务轮流执行。这种
行为方式在之前的例子中可以明显看出来。两个测试任务被创建在同一个优先级上,并
且一直是可运行的。所以每个任务都执行一个”时间片”,任务在时间片起始时刻进入运
行态,在时间片结束时刻又退出运行态。 图 3 中 t1 与 t2 之间的时段就等于一个时间片。
要能够选择下一个运行的任务,调度器需要在每个时间片的结束时刻运行自己本
身。一个称为心跳(tick,有些地方被称为时钟滴答, 本文中一律称为时钟心跳)中断的
周期性中断用于此目的。时间片的长度通过心跳中断的频率进行设定,心跳中断频率由
FreeRTOSConfig.h 中的编译时配置常量 configTICK_RATE_HZ 进行配置。比如说,
如果 configTICK_RATE_HZ 设为 100(HZ),则时间片长度为 10ms。可以将图 3 进行
扩展,将调度器本身的执行时间在整个执行流程中体现出来。请参见图 4。
需要说明的是, FreeRTOS API 函数调用中指定的时间总是以心跳中断为单位(通
常的提法为心跳”ticks”)。常量 portTICK_RATE_MS 用于将以心跳为单位的时间值转化
为以毫秒为单位的时间值。有效精度依赖于系统心跳频率。
翻译:
心跳中断发生
内核在tick中断中运行以选择下一个任务
当tick中断完成时,新选择的任务将运行
图 4 中红色的线段表时内核本身在运行。黑色箭头表示任务到中断,中断再到另一
个任务的执行顺序
到目前为止的示例程序中,两个任务都创建在相同的优先级上。所以这两个任务轮番进
入和退出运行态。本例将改变例 2 其中一个任务的优先级,看一下倒底会发生什么。现
在第一个任务创建在优先级 1 上,而另一个任务创建在优先级 2 上。创建这两个任务的
代码参见程序清单 10
/* 定义将要通过任务参数传递的字符串。定义为const,且不是在栈空间上,以保证任务执行时也有效。 */
static const char *pcTextForTask1 = “Task 1 is running\r\n”;
static const char *pcTextForTask2 = “Task 2 is running\t\n”;
int main( void )
{
/* 第一个任务创建在优先级1上。优先级是倒数第二个参数。 */
xTaskCreate( vTaskFunction, "Task 1", 1000, (void*)pcTextForTask1, 1, NULL );
/* 第二个任务创建在优先级2上。 */
xTaskCreate( vTaskFunction, "Task 2", 1000, (void*)pcTextForTask2, 2, NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
return 0;
}
程序清单 10 两个任务创建在不同的优先级上
调度器总是选择具有最高优先级的可运行任务来执行。任务 2 的优先级比任务 1
高,并且总是可运行,因此任务 2 是唯一一个一直处于运行态的任务。而任务 1 不可能
进入运行态,所以不可能输出字符串。这种情况我们称为任务 1 的执行时间被任务 2”
饿死(starved)”了。
任务 2 之所以总是可运行,是因为其不会等待任何事情——它要么在空循环里打
转,要么往终端打印字符串
翻译:
心跳中断发生
调度程序在tick中断中运行,但选择相同的任务(任务2)。任务2始终处于“正在运行”状态,任务1始终处于“未运行”状态
为了使我们的任务切实有用,我们需要通过某种方式来进行事件驱动。一个事件驱
动任务只会在事件发生后触发工作(处理),而在事件没有发生时是不能进入运行态的。
调度器总是选择所有能够进入运行态的任务中具有最高优先级的任务。一个高优先级但
不能够运行的任务意味着不会被调度器选中,而代之以另一个优先级虽然更低但能够运
行的任务。因此,采用事件驱动任务的意义就在于任务可以被创建在许多不同的优先级
上,并且最高优先级任务不会把所有的低优先级任务饿死
阻塞状态
如果一个任务正在等待某个事件,则称这个任务处于”阻塞态(blocked)”。阻塞态是
非运行态的一个子状态。
任务可以进入阻塞态以等待以下两种不同类型的事件:
1. 定时(时间相关)事件——这类事件可以是延迟到期或是绝对时间到点。比如
说某个任务可以进入阻塞态以延迟 10ms。
2. 同步事件——源于其它任务或中断的事件。比如说,某个任务可以进入阻塞
态以等待队列中有数据到来。同步事件囊括了所有板级范围内的事件类型。
FreeRTOS 的队列,二值信号量,计数信号量,互斥信号量(recursive semaphore,
递归信号量,本文一律称为互斥信号量, 因为其主要用于实现互斥访问)和互斥量都可
以用来实现同步事件。第二章和第三章涵盖了有关这些的详细内容。
任务可以在进入阻塞态以等待同步事件时指定一个等待超时时间,这样可以有效地
实现阻塞状态下同时等待两种类型的事件。比如说,某个任务可以等待队列中有数据到
来,但最多只等 10ms。如果 10ms 内有数据到来,或是 10ms 过去了还没有数据到来,
这两种情况下该任务都将退出阻塞态。
挂起状态
“挂起(suspended)”也是非运行状态的子状态。处于挂起状态的任务对调度器而言
是不可见的。让一个任务进入挂起状态的唯一办法就是调用 vTaskSuspend() API 函数;
而 把 一 个 挂 起 状 态 的 任 务 唤 醒 的 唯 一 途 径 就 是 调 用 vTaskResume() 或
vTaskResumeFromISR() API 函数。大多数应用程序中都不会用到挂起状态。
就绪状态
如果任务处于非运行状态,但既没有阻塞也没有挂起,则这个任务处于就绪(ready,
准备或就绪)状态。处于就绪态的任务能够被运行,但只是”准备(ready)”运行,而当前
尚未运行
完整的状态转移图
例 4. 利用阻塞态实现延迟
之前的示例中所有创建的任务都是”周期性”的——它们延迟一个周期时间,打印输
出字符串,再一次延迟,如此周而复始。而产生延迟的方法也相当原始地使用了空循环
——不停地查询并递增一个循环计数直至计到某个指定值。例 3 明确的指出了这种方法
的缺点。一直保持在运行态中执行空循环,可能将其它任务饿死。
其实以任何方式的查询都不仅仅只是低效,还有各种其它方面的缺点。在查询过程
中,任务实际上并没有做任何有意义的事情,但它依然会耗尽所有处理时间,对处理器
周期造成浪费。 例 4 通过调用 vTaskDelay() API 函数来代替空循环,对这种”不良行为”
进行纠正。 vTaskDelay()的函数原型见程序清单 11,而新的任务实现见程序清单 12。
void vTaskDelay( portTickType xTicksToDelay );
程序清单 11 vTaskDelay() API 函数原型
参数名 |
描述 |
xTicksToDelay |
延迟多少个心跳周期。调用该延迟函数的任务将进入阻塞态,经延迟指定的心跳周期数后,再转移到就绪态。 举个例子,当某个任务调用 vTaskDelay( 100 )时,心跳计数值为 10,000,则该任务将保持在阻塞态,直到心跳计数计到10,100。 常数 portTICK_RATE_MS 可以用来将以毫秒为单位的时间值转换为以心跳周期为单位的时间值 |
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
/* T打印出来的字符串通过参数传递进来。将其转换为字符指针*/
pcTaskName = ( char * ) pvParameters;
/* *与大多数任务一样,这个任务在一个无限循环中实现。*. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 延迟一个循环周期。 调用vTaskDelay()以让任务在延迟期间保持在阻塞态。延迟时间以心跳周期为
单位,常量portTICK_RATE_MS可以用来在毫秒和心跳周期之间相换转换。本例设定250毫秒的循环周期。 */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
程序清单 12 调用 vTaskDelay()来代替空循环实现延迟
尽管两个任务实例还是创建在不同的优先级上,但现在两个任务都可以得到执行。
例 4 的运行输出结果参见图 8
图 9 所示的执行流程可以解释为什么此时不同优先级的两个任务竟然都可以得到
执行。图中为了简便,忽略了内核自身的执行时间
空闲任务是在调度器启动时自动创建的,以保证至少有一个任务可运行(至少有一
个任务处于就绪态)。本章第 7 节会对空闲任务进行更详细的描述。
翻译:
1-任务2有更高优先级因此先运行,它打印出自己的字符串后,调用vTaskDelay,这样做进入Blocked状态,允许低优先级的Task 1开始执行。
2-Task 1打印出它的字符串,然后它通过调用vTaskDelay进入Blocked状态
3-此时,两个应用程序任务都处于阻塞状态,所以空闲任务运行
4-当延迟结束时,调度器将任务移回就绪状态,在这里,它们都会再次执行,然后再次调用vTaskDelay,导致它们重新进入Blocked状态。Task 2优先执行,因为它有更高的优先级。
图 4 展现的是当任务采用空循环进行延迟时的执行流程——结果就是任务总是可
运行并占用了大量的机器周期。从图 9 中的执行流程中可以看到,任务在整个延迟周期
内都处于阻塞态,只在完成实际工作的时候才占用处理器时间(本例中任务的实际工作
只是简单地打印输出一条信息)。
在图 9 所示的情形中,任务离开阻塞态后,仅仅执行了一个心跳周期的一个片段,
然后又再次进入阻塞态。所以大多数时间都没有一个应用任务可运行(即没有应用任务
处于就绪态),因此没有应用任务可以被选择进入运行态。这种情况下,空闲任务得以
执行。空间任务可以获得的执行时间量,是系统处理能力裕量的一个度量指标。
vTaskDelayUntil() API 函数
vTaskDelayUntil()类似于 vTaskDelay()。和范例中演示的一样,函数 vTaskDelay()
的参数用来指定任务在调用 vTaskDelay()到切出阻塞态整个过程包含多少个心跳周期。
任务保持在阻塞态的时间量由 vTaskDelay()的入口参数指定,但任务离开阻塞态的时刻
实际上是相对于 vTaskDelay()被调用那一刻的。 vTaskDelayUntil()的参数就是用来指定
任务离开阻塞态进入就绪态那一刻的精确心跳计数值。 API 函数 vTaskDelayUntil()可以
用于实现一个固定执行周期的需求(当你需要让你的任务以固定频率周期性执行的时
候)。由于调用此函数的任务解除阻塞的时间是绝对时刻,比起相对于调用时刻的相对
时间更精确(即比调用 vTaskDelay()可以实现更精确的周期性)。
void vTaskDelayUntil( portTickType * pxPreviousWakeTime, portTickType xTimeIncrement );
程序清单 13 vTaskDelayUntil() API 函数原型
表 3 vTaskDelayUntil()参数
参数名 |
描述 |
pxPreviousWakeTime |
此参数命名时假定 vTaskDelayUntil()用于实现某个任务以 固定频率周期性执行。这种情况下 pxPreviousWakeTime 保存了任务上一次离开阻塞态(被唤醒)的时刻。这个时刻 被用作一个参考点来计算该任务下一次离开阻塞态的时 刻。 pxPreviousWakeTime 指 向 的 变 量 值 会 在 API 函 数 vTaskDelayUntil()调用过程中自动更新,应用程序除了该 变量第一次初始化外,通常都不要修改它的值。程序清单 14 展示了这个参数的使用方法。 |
xTimeIncrement |
此参数命名时同样是假定 vTaskDelayUntil()用于实现某个 任 务 以 固 定 频 率 周 期 性 执 行 —— 这 个 频 率 就 是 由 xTimeIncrement 指定的。 xTimeIncrement 的 单 位 是 心 跳 周 期 , 可 以 使 用 常 量 portTICK_RATE_MS 将毫秒转换为心跳周期。 |
例 5. 转换示例任务使用 vTaskDelayUntil()
例 4 中的两个任务是周期性任务,但是使用 vTaskDelay()无法保证它们具有固定的
执行频率,因为这两个任务退出阻塞态的时刻相对于调用 vTaskDelay()的时刻。通过调
用 vTaskDelayUntil()代替 vTaskDelay(),把这两个任务进行转换,以解决这个潜在的问
题。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
portTickType xLastWakeTime;
/* The string to print out is passed in via the parameter. Cast this to a character pointer. */
pcTaskName = ( char * ) pvParameters;
/* 变量xLastWakeTime需要被初始化为当前心跳计数值。说明一下,这是该变量唯一一次被显式赋值。之后,xLastWakeTime将在函数vTaskDelayUntil()中自动更新。 */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( pcTaskName );
/* 本任务将精确的以250毫秒为周期执行。同vTaskDelay()函数一样,时间值是以心跳周期为单位的,可以使用常量portTICK_RATE_MS将毫秒转换为心跳周期。变量xLastWakeTime会在
vTaskDelayUntil()中自动更新,因此不需要应用程序进行显示更新。 */
vTaskDelayUntil( &xLastWakeTime, ( 250 / portTICK_RATE_MS ) );
}
}
程序清单 14 使用 vTaskDelayUntil()实现示例任务
例 6. 合并阻塞与非阻塞任务
本例通过合并这两种方案的执行流程,再次实现具有既定预期的系统行为。
在优先级 1 上创建两个任务。这两个任务只是不停地打印输出字符串,然它什么
事情也不做。
这两个任务没有调用任何可能导致它们进入阻塞态的 API 函数,所以这两个任务
要么处于就绪态,要么处于运行态。具有这种性质的任务被称为”不停处理(或持
续处理, continuous processing)”任务,因为它们总是有事情要做,虽然在本例
中的它们做的事情没什么意义。持续处理任务的源代码参见程序清单 15。
第三个任务创建在优先级 2 上,高于另外两个任务的优先级。这个任务虽然也是
打印输出字符串,但它是周期性的,所以调用了 vTaskDelayUntil(),在每两次打
印之间让自己处于阻塞态。
周期性任务的实现代码参见程序清单 16
void vContinuousProcessingTask( void *pvParameters )
{
char *pcTaskName;
/* 打印输出的字符串由任务参数传入,强制转换为char* */
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* 打印输出任务名,无阻塞,也无延迟。 */
vPrintString( pcTaskName );
}
}
程序清单 15 例6中持续处理任务的实现代码
void vPeriodicTask( void *pvParameters )
{
portTickType xLastWakeTime;
/* 初始化xLastWakeTime,之后会在vTaskDelayUntil()中自动更新。 */
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Periodic task is running\r\n" );
/* The task should execute every 10 milliseconds exactly. */
vTaskDelayUntil( &xLastWakeTime, ( 10 / portTICK_RATE_MS ) );
}
}
程序清单 16 例6中周期任务的实现代码
结果如下图
处理器总是需要代码来执行——所以至少要有一个任务处于运行态。为了保证这
一点,当调用 vTaskStartScheduler()时,调度器会自动创建一个空闲任务。空闲任务是
一个非常短小的循环——和最早的示例任务十分相似,总是可以运行。
空闲任务拥有最低优先级(优先级 0)以保证其不会妨碍具有更高优先级的应用任务
进入运行态——当然,没有任何限制说是不能把应用任务创建在与空闲任务相同的优先
级上;如果需要的话,你一样可以和空闲任务一起共享优先级。
运行在最低优先级可以保证一旦有更高优先级的任务进入就绪态,空闲任务就会立
即切出运行态。这一点可以在图 9 的 tn 时刻看出来,当任务 2 退出阻塞态时,空闲任
务立即切换出来以让任务 2 执行。任务 2 被看作是抢占(pre-empted)了空闲任务。抢占
是自动发生的,也并不需要通知被抢占任务
空闲任务钩子函数
通过空闲任务钩子函数(或称回调, hook, or call-back),可以直接在空闲任务中添
加应用程序相关的功能。空闲任务钩子函数会被空闲任务每循环一次就自动调用一次。
通常空闲任务钩子函数被用于:
执行低优先级,后台或需要不停处理的功能代码。
测试处系统处理裕量(空闲任务只会在所有其它任务都不运行时才有机会执行,所
以测量出空闲任务占用的处理时间就可以清楚的知道系统有多少富余的处理时
间)。
将处理器配置到低功耗模式——提供一种自动省电方法,使得在没有任何应用功能
需要处理的时候,系统自动进入省电模式。
空闲任务钩子函数的实现限制
空闲任务钩子函数必须遵从以下规则
1. 绝不能阻塞或挂起。空闲任务只会在其它任务都不运行时才会被执行(除非有应用任
务共享空闲任务优先级)。以任何方式阻塞空闲任务都可能导致没有任务能够进入
运行态!
2. 如果应用程序用到了 vTaskDelete() API 函数,则空闲钩子函数必须能够尽快返回。
因为在任务被删除后,空闲任务负责回收内核资源。如果空闲任务一直运行在钩
子函数中,则无法进行回收工作。
空闲任务钩子函数必须具有程序清单 17 所示的函数名和函数原型。
void vApplicationIdleHook( void );
程序清单 17 空闲任务钩子函数原型
例 7. 定义一个空闲任务钩子函数
例 4 调用了带阻塞性质的 vTaskDelay() API 函数,会产生大量的空闲时间——在
这期间空闲任务会得到执行,因为两个应用任务均处于阻塞态。本例通过空闲钩子函数
来使用这些空间时间。具体源代码参见程序清单 18。
/* 声明一个将被钩子函数递增的变量。. */
unsigned long ulIdleCycleCount = 0UL;
/* 空闲钩子函数必须命名为vApplicationIdleHook(),无参数也无返回值。 */
void vApplicationIdleHook( void )
{
/*这个钩子函数除了增加一个计数器什么都不做 */
ulIdleCycleCount++;
}
程序清单 18 一个非常简单的空闲钩子函数
FreeRTOSConfig.h 中的配置常量 configUSE_IDLE_HOOK 必须定义为 1,这样空
闲任务钩子函数才会被调用。
对应用任务实现函数进行了少量的修改,用以打印输出变量 ulIdleCycleCount 的
值,如程序清单 19 所示。
void vTaskFunction( void *pvParameters )
{
char *pcTaskName;
/* 要打印的字符串通过参数传入。将此转换为字符指针。. */
pcTaskName = ( char * ) pvParameters;
/* *和大多数任务一样,这个任务是在一个无限循环中实现的. */
for( ;; )
{
/* 打印输出任务名,以及调用计数器ulIdleCycleCount的值。 */
vPrintStringAndNumber( pcTaskName, ulIdleCycleCount );
/* •/*延时250毫秒。 */
vTaskDelay( 250 / portTICK_RATE_MS );
}
}
程序清单 19 示例任务现在用于打印输出 ulIdleCycleCount 的值
例 7 的输出结果参见图 13。空闲任务钩子函数在应用任务的每次循环过程中被调用了(非常)接近 4.5 million 次
vTaskPrioritySet() API 函数
API 函数 vTaskPriofitySet()可以用于在调度器启动后改变任何任务的优先级。
void vTaskPrioritySet( xTaskHandle pxTask, unsigned portBASE_TYPE uxNewPriority );
程序清单 20 vTaskPrioritySet() API 函数原型
表 4 vTaskPrioritySet() 参数
参数名 |
描述 |
pxTask |
被修改优先级的任务句柄(即目标任务)——参考 xTaskCreate() API函数的参数 pxCreatedTask 以了解如何得到任务句柄方面的信息。 任务可以通过传入 NULL 值来修改自己的优先级。 |
uxNewPriority |
目标任务将被设置到哪个优先级上。如果设置的值超过了最大可用优先级(configMAX_PRIORITIES – 1),则会被自动封顶为最大值。常量 configMAX_PRIORITIES 是在 FreeRTOSConfig.h 头文件中设置的一个编译时选项。 |
uxTaskPriorityGet() API 函数
uxTaskPriorityGet() API 函数用于查询一个任务的优先级。
unsigned portBASE_TYPE uxTaskPriorityGet( xTaskHandle pxTask );
程序清单 21 uxTaskPriorityGet() API 函数原型
表 5 uxTaskPriorityGet()参数及返回值
参数名 |
描述 |
pxTask |
被查询任务的句柄(目标任务) ——参考 xTaskCreate() API 函数的参数 pxCreatedTask 以了解如何得到任务句柄方面的信息。 任务可以通过传入 NULL 值来查询自己的优先级。 |
返回值 |
被查询任务的当前优先级 |
例 8. 改变任务优先级
调度器总是在所有就绪态任务中选择具有最高优先级的任务,并使其进入运行态。
本例即是通过调用 vTaskPrioritySet() API 函数来改变两个任务的相对优先级,以达到
对调度器这一行为的演示。
在不同的优先级上创建两个任务。这两个任务都没有调用任何会令其进入阻塞态的
API 函数,所以这两个任务要么处于就绪态,要么处于运行态——这种情形下,调度器
选择具有最高优先级的任务来执行。
例 8 具有以下行为方式:
任务 1(程序清单 22)创建在最高优先级,以保证其可以最先运行。任务 1 首先
打印输出两个字符串,然后将任务 2(程序清单 23)的优先级提升到自己之上。
任务 2 一旦拥有最高优先级便启动执行(进入运行态)。由于任何时候只可能有
一个任务处于运行态,所以当任务 2 运行时,任务 1 处于就绪态。
任务 2 打印输出一个信息,然后把自己的优先级设回低于任务 1 的初始值。
任务 2 降低自己的优先级意味着任务 1 又成为具有最高优先级的任务,所以任
务 1 重新进入运行态,任务 2 被强制切入就绪态。
void vTask1( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
/* 本任务将会比任务2更先运行,因为本任务创建在更高的优先级上。任务1和任务2都不会阻塞,所以两者要么处于就绪态,要么处于运行态。
查询本任务当前运行的优先级 – 传递一个NULL值表示说“返回我自己的优先级”。 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Task1 is running\r\n" );
/* 把任务2的优先级设置到高于任务1的优先级,会使得任务2立即得到执行(因为任务2现在是所有任务中具有最高优先级的任务)。注意调用vTaskPrioritySet()时用到的任务2的句柄。程序清单24将展示
如何得到这个句柄。 */
vPrintString( "About to raise the Task2 priority\r\n" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
/* 本任务只会在其优先级高于任务2时才会得到执行。因此,当此任务运行到这里时,任务2必然已经执行过了,并且将其自身的优先级设置回比任务1更低的优先级。 */
}
}
程序清单 22 例 8 中任务 1 的实现代码
void vTask2( void *pvParameters )
{
unsigned portBASE_TYPE uxPriority;
/* 任务1比本任务更先启动,因为任务1创建在更高的优先级。任务1和任务2都不会阻塞,所以两者要么处于就绪态,要么处于运行态。
查询本任务当前运行的优先级 – 传递一个NULL值表示说“返回我自己的优先级”。 */
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* 当任务运行到这里,任务1必然已经运行过了,并将本身务的优先级设置到高于任务1本身。 */
vPrintString( "Task2 is running\r\n" );
/* 将自己的优先级设置回原来的值。传递NULL句柄值意味“改变我己自的优先级”。把优先级设置到低
于任务1使得任务1立即得到执行 – 任务1抢占本任务。 */
vPrintString( "About to lower the Task2 priority\r\n" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
程序清单 23 例 8 中的任务 2 实现代码
任务在查询和修改自己的优先级时,并没有使用一个有效的句柄——使用 NULL代替。只有在某个任务需要引用其它任务的时候才会用到任务句柄。好比任务 1 想要改变任务 2 的优先级,为了让任务 1 能够使用到任务 2 的句柄,在任务 2 被创建时其句柄就被获得并保存下来,就像程序清单 24 注释中重点提示的一样。
/* 声明变量用于保存任务2的句柄。 */
xTaskHandle xTask2Handle;
int main( void )
{
/* 任务1创建在优先级2上。任务参数没有用到,设为NULL。任务句柄也不会用到,也设为NULL */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
/* The task is created at priority 2 ______^. */
/* 任务2创建在优先级1上 – 此优先级低于任务1。任务参数没有用到,设为NULL。但任务2的任务句柄会被用到,故将xTask2Handle的地址传入。 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
/**任务句柄是最后一个参数*/
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
for( ;; );
}
程序清单 24 例 8 中 main()函数实现代码
图 14 展示了例 8 的执行流程, 例 8 的运行输出结果参见图 15。
翻译:1- Task1优先级最高,优先运行
2-每次运行Task2, Task1将Task2的优先级设为最高
3- 当Task2将自己的优先级降低到Task1优先级以下时,Task1将再次运行,依此类推
4-空闲任务永远不会运行,因为两个应用程序任务总是能够运行,而且它们的优先级总是高于空闲优先级
vTaskDelete() API 函数
任务可以使用 API 函数 vTaskDelete()删除自己或其它任务。
任务被删除后就不复存在,也不会再进入运行态。
空闲任务的责任是要将分配给已删除任务的内存释放掉。因此有一点很重要,那就
是使用 vTaskDelete() API 函数的任务千万不能把空闲任务的执行时间饿死。
需要说明一点,只有内核为任务分配的内存空间才会在任务被删除后自动回收。任
务自己占用的内存或资源需要由应用程序自己显式地释放。
void vTaskDelete( xTaskHandle pxTaskToDelete );
程序清单 25 vTaskDelete() API 函数原型
表 6 vTaskDelete()参数
参数名 |
描述 |
pxTaskToDelete |
被删除任务的句柄(目标任务) —— 参考 xTaskCreate() API 函数的 参数 pxCreatedTask 以了解如何得到任务句柄方面的信息。 任务可以通过传入 NULL 值来删除自己。 |
例 9. 删除任务
这是一个非常简单的范例,其行为如下:
任务 1 则 main()创建在优先级 1 上。任务 1 运行时,以优先级 2 创建任务 2。现
在任务 2 具有最高优先级,所以会立即得到执行。 main()函数的源代码参见程
序清单 26,任务 1 的实现代码参见程序清单 27。
任务 2 什么也没有做,只是删除自己。可以通过传递 NULL 值以 vTaskDelete()
来删除自己,但是为了纯粹的演示,传递的是任务自己的句柄。任务 2 的实现
源代码见程序清单 28。
当任务 2 被自己删除之后,任务 1 成为最高优先级的任务,所以继续执行,调用
vTaskDelay()阻塞一小段时间。
当任务 1 进入阻塞状态后,空闲任务得到执行的机会。空闲任务会释放内核为已
删除的任务 2 分配的内存。
任务 1 离开阻塞态后,再一次成为就绪态中具有最高优先级的任务,因此会抢占
空闲任务。又再一次创建任务 2,如此往复。
int main( void )
{
/* 任务1创建在优先级1上 */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
/* The task is created at priority 1 ______^. */
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/* main() should never reach here as the scheduler has been started. */
for( ;; );
}
程序清单 26 例 9 中的 main()函数实现
void vTask1( void *pvParameters )
{
const portTickType xDelay100ms = 100 / portTICK_RATE_MS;
for( ;; )
{
/* Print out the name of this task. */
vPrintString( "Task1 is running\r\n" );
/* 创建任务2为最高优先级。 */
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
/* 因为任务2具有最高优先级,所以任务1运行到这里时,任务2已经完成执行,删除了自己。任务1得以
执行,延迟100ms */
vTaskDelay( xDelay100ms );
}
}
程序清单 27 例 9 中任务 1 的实现代码
void vTask2( void *pvParameters )
{
/* 任务2什么也没做,只是删除自己。删除自己可以传入NULL值,这里为了演示,还是传入其自己的句柄。 */
vPrintString( "Task2 is running and about to delete itself\r\n" );
vTaskDelete( xTask2Handle );
}
程序清单 28 例 9 中的任务 2 实现代码
翻译:1-任务1运行并创建任务2。任务2立即开始运行,因为它有更高的优先级
2-Task 2除了删除自身之外什么也不做,允许执行返回Task 1
3-任务1调用VTaskDelay0。允许空闲任务运行到延迟时间到期,然后重复整个序列。
优先级抢占式调度
本章的示例程序已经演示了 FreeRTOS 在什么时候以及以什么方式选择一个什么
样的任务来执行。
每个任务都赋予了一个优先级。
每个任务都可以存在于一个或多个状态。
在任何时候都只有一个任务可以处于运行状态。
调度器总是在所有处于就绪态的任务中选择具有最高优先级的任务来执行。
这种类型的调度方案被称为”固定优先级抢占式调度”。所谓”固定优先级”是指每个
任务都被赋予了一个优先级,这个优先级不能被内核本身改变(只能被任务修改)。 ”抢占
式”是指当任务进入就绪态或是优先级被改变时,如果处于运行态的任务优先级更低,
则该任务总是抢占当前运行的任务。
任务可以在阻塞状态等待一个事件,当事件发生时其将自动回到就绪态。时间事件
发生在某个特定的时刻,比如阻塞超时。时间事件通常用于周期性或超时行为。任务或
中断服务例程往队列发送消息或发送任务一种信号量,都将触发同步事件。同步事件通
常用于触发同步行为,比如某个外围的数据到达了。
图 18 通过图示某个应用程序的执行流程展现了抢占式调度的行为方式。
翻译:Task 2抢占Task3
Task 1抢占Task 2
任务3抢占空闲任务
任务2抢占空闲任务
事件处理被延迟,直到更高优先级的任务阻塞
如图 18 中所示:
1. 空闲任务
空闲任务具有最低优先级,所以每当有更高优先级任务处于就绪态是,空闲任
务就会被抢占 —— 如图中 t3, t5 和 t9 时刻。
2. 任务 3
任务 3 是一个事件驱动任务。其工作在一个相对较低的优先级,但优先级高于
空闲任务。其大部份时间都在阻塞态等待其关心的事件。每当事件发生时其就
从阻塞态转移到就绪态。 FreeRTOS 中所有的任务间通信机制(队列,信号量等)
都可以通过这种方式用于发送事件以及让任务解除阻塞。
事件在 t3, t5 以及 t9 至 t12 之间的某个时刻发生。发生在 t3 和 t5 时刻的事件
可以立即被处理,因为这些时刻任务 3 在所有可运行任务中优先级最高。发生
在 t9 至 t12 之间某个时刻的事件不会得到立即处理,需要一直等到 t12 时刻。
因为具有更高优先级的任务 1 和任务 2 尚在运行中,只有到了 t12 时刻,这两
个任务进入阻塞态,使得任务 3 成为具有最高优先级的就绪态任务。
3. 任务 2
任务 2 是一个周期性任务,其优先级高于任务 3 并低于任务 1。根据周期间隔,
任务 2 期望在 t1, t6 和 t9 时刻执行。
在 t6 时刻任务 3 处于运行态,但是任务 2 相对具有更高的优先级,所以会抢占
任务 3,并立即得到执行。任务 2 完成处理后,在 t7 时刻返回阻塞态。同时,
任务 3 得以重新进入运行态,继续完成处理。任务 3 在 t8 时刻进入阻塞状态。
4. 任务 1
任务 1 也是一个事件驱动任务。任务 1 在所有任务中具有最高优先级,因此可
以抢占系统中的任何其它任务。在图中看到,任务 1 的事件只是发生在在 t10
时刻,此时任务 1 抢占了任务 2。只有当任务 1 在 t11 时刻再次进入阻塞态之
后,任务 2 才得以机会继续完成处理。
选择任务优先级
从图 18 中可以看到优先级分配是如何从根本上影响应用程序行为的。
作为一种通用规则,完成硬实时功能的任务优先级会高于完成软件时功能任务的优
先级。但其它一些因素,比如执行时间和处理器利用率,都必须纳入考虑范围,以保证
应用程序不会超过硬实时的需求限制。
单调速率调度(Rate Monotonic Scheduling, RMS)是一种常用的优先级分配技术。
其根据任务周期性执行的速率来分配一个唯一的优先级。具有最高周期执行频率的任务
赋予高最优先级;具有最低周期执行频率的任务赋予最低优先级。这种优先级分配方式
被证明了可以最大化整个应用程序的可调度性(schedulability),但是运行时间不定以及
并非所有任务都具有周期性,会使得对这种方式的全面计算变得相当复杂。
协作式调度
本文专注于抢占式调度。 FreeRTOS 可以选择采用协作式调度。
采用一个纯粹的协作式调度器,只可能在运行态任务进入阻塞态或是运行态任务显
式调用 taskYIELD()时,才会进行上下文切换。任务永远不会被抢占,而具有相同优先
级的任务也不会自动共享处理器时间。协作式调度的这作工作方式虽然比较简单,但可
能会导致系统响应不够快。
实现混合调度方案也是可行的,这需要在中断服务例程中显式地进行上下文切换,
从而允许同步事件产生抢占行为,但时间事件却不行。这样做的结果是得到了一个没有
时间片机制的抢占式系统。或许这正是所期望的,因为获得了效率,并且这也是一种常
用的调度器配置