深入FreeRTOS内核------第三章、任务管理
文章目录
- 深入FreeRTOS内核------第三章、任务管理
- 前言
- 一、任务函数
- 二、任务的顶级状态
- 三、任务创建
-
- [3.1 xTaskCreate()函数](#3.1 xTaskCreate()函数)
- 四、任务优先级
-
- [4.1 通用调度器](#4.1 通用调度器)
- [4.2 架构优化调度器](#4.2 架构优化调度器)
- 五、时间测量和时钟中断
- 六、扩展非运行状态
-
- [6.1 阻塞状态](#6.1 阻塞状态)
- [6.2 挂起状态](#6.2 挂起状态)
- [6.3 就绪状态](#6.3 就绪状态)
- [6.4 完成状态转换图](#6.4 完成状态转换图)
- [6.5 vTaskDelayUntil()函数](#6.5 vTaskDelayUntil()函数)
- 七、空闲任务和空闲钩子
-
- [7.1 空闲任务钩子函数](#7.1 空闲任务钩子函数)
- [7.2 实现空闲任务钩子函数的限制](#7.2 实现空闲任务钩子函数的限制)
- 八、改变任务优先级
-
- [8.1 vTaskPrioritySet()函数](#8.1 vTaskPrioritySet()函数)
- [8.2 uxTaskPriorityGet()函数](#8.2 uxTaskPriorityGet()函数)
- 九、删除任务
-
- [9.1 vTaskDelete()函数](#9.1 vTaskDelete()函数)
- 十、线程本地存储和可重入性
-
- [10.1 C Runtime TLS实现](#10.1 C Runtime TLS实现)
- [10.2 Custom C Runtime TLS](#10.2 Custom C Runtime TLS)
- [10.3 应用TLS](#10.3 应用TLS)
- 十一、调度算法
-
- [11.1 任务状态和事件的回顾](#11.1 任务状态和事件的回顾)
- [11.2 调度算法的选择](#11.2 调度算法的选择)
- [11.3 优先级抢占式调度加上时间片轮转](#11.3 优先级抢占式调度加上时间片轮转)
- [11.4 没有事件轮转的优先级抢占式调度](#11.4 没有事件轮转的优先级抢占式调度)
- [11.5 合作式调度](#11.5 合作式调度)
- 总结
前言
本章内容
- 任务时间分配:本章介绍了FreeRTOS如何为应用程序中的每个任务分配处理时间。这是通过任务调度器来实现的,调度器决定哪些任务在何时运行。
- 任务选择执行:介绍了FreeRTOS是如何决定在任何给定时间应该执行哪个任务的。这通常基于任务的优先级和它们的状态(例如,就绪、阻塞等)。
- 任务优先级影响:讨论了每个任务的相对优先级如何影响系统行为。在FreeRTOS中,高优先级的任务会优先获得处理时间。
- 任务存在的状态:解释了任务可能处于的不同状态,例如运行态、就绪态、阻塞态等。
- 任务实现:本章还讨论了如何实现任务,包括如何编写任务函数。
- 任务实例创建:介绍了如何创建一个或多个任务实例,即如何复制或重复使用任务代码。
- 任务参数使用:讨论了如何使用任务参数,即如何向任务传递数据。
- 任务优先级变更:解释了如何改变已经创建的任务的优先级,这允许动态调整任务的执行顺序。
- 任务删除:介绍了如何删除不再需要的任务,释放其占用的资源。
- 周期性处理实现:讨论了如何使用任务实现周期性处理,例如通过在任务中使用延时函数来周期性执行任务。
- 空闲任务执行和使用:解释了何时执行空闲任务(idle task),以及如何利用空闲任务执行一些后台任务。
本章我们要学习的概念对于理解如何使用FreeRTOS以及FreeRTOS应用程序的行为至关重要,书中最详细的章节。原书链接放在尾部。
一、任务函数
任务(可能是操作系统或者并发编程中的一个概念)是通过C语言函数来实现的,并且这些函数需要遵循一个特定的接口规范,即它们需要接受一个void指针参数,并且不返回任何值(返回类型为void)。
c
void vATaskFunction( void * pvParameters );
任务作为小程序 :每个任务就像是一个小型的程序,它有自己的入口点(即任务函数的开始)。通常情况下,任务会永远运行在一个无限循环中,并且不会退出。
任务的执行和退出:在FreeRTOS中,任务不允许通过任何方式从实现它的函数中返回。这意味着任务函数中不能包含return语句,也不能允许执行到实现函数的末尾。如果一个任务不再需要,应该明确地删除它
c
void vATaskFunction( void * pvParameters )
{
/*
* Stack-allocated variables can be declared normally when inside a function.
* Each instance of a task created using this example function will have its
* own separate instance of lStackVariable allocated on the task's stack.
*/
long lStackVariable = 0;
/*
* In contrast to stack allocated variables, variables declared with the `static`
* keyword are allocated to a specific location in memory by the linker.
* This means that all tasks calling vATaskFunction will share the same
* instance of lStaticVariable.
*/
static long lStaticVariable = 0;
for( ;; )
{
/* The code to implement the task functionality will go here. */
}
/*
* If the task implementation ever exits the above loop, then the task
* must be deleted before reaching the end of its implementing function.
* When NULL is passed as a parameter to the vTaskDelete() API function,
* this indicates that the task to be deleted is the calling (this) task.
*/
vTaskDelete( NULL );
}
二、任务的顶级状态
在单核处理器上,任何给定时间只能有一个任务正在执行,这意味着任务可能处于两种状态之一:运行(Running)和非运行(Not Running)。
当处理器正在执行任务的代码时,任务处于运行状态。当任务处于非运行状态时,任务被暂停,并且其状态已经被保存,以便在调度器决定它应该再次进入运行状态时能够恢复执行。当任务恢复执行时,它从它离开运行状态之前即将执行的指令处恢复执行。
当一个任务从非运行状态(Not Running state)转变为运行状态(Running state)时,我们说这个任务被"切换进来"(switched in)或者"交换进来"(swapped in)。这通常发生在任务被操作系统调度器选中执行时。
相反地,当一个任务从运行状态(Running state)转变为非运行状态(Not Running state)时,我们说这个任务被"切换出去"(switched out)或者"交换出去"(swapped out)。这可能发生在任务完成执行、被挂起或者被更高优先级的任务抢占时。
在FreeRTOS这个实时操作系统中,只有调度器(scheduler)有权力将任务从运行状态切换到非运行状态,或者从非运行状态切换到运行状态。调度器负责管理任务的执行顺序和时机,确保系统资源被合理分配和使用。
三、任务创建
FreeRTOS提供了六个API函数来创建任务,分别是xTaskCreate()
, xTaskCreateStatic()
, xTaskCreateRestricted()
, xTaskCreateRestrictedStatic()
, xTaskCreateAffinitySet()
, 和xTaskCreateStaticAffinitySet()
。
每个任务都需要两块RAM:一块用于存储任务控制块(Task Control Block, TCB),另一块用于存储任务的堆栈(stack)。
名字中包含"Static"的API函数使用预先分配的内存块,这些内存块作为参数传递给函数。
名字中不包含"Static"的API函数在运行时从系统堆(system heap)动态分配所需的RAM。
一些FreeRTOS端口支持以"受限"或"非特权"模式运行任务。名字中包含"Restricted"的API函数创建的任务执行时对系统内存的访问受限。
名字中不包含"Restricted"的API函数创建的任务以"特权模式"执行,可以访问系统的全部内存映射。
支持SMP的FreeRTOS端口允许不同的任务在同一CPU的多个核心上同时运行。对于这些端口,可以通过使用名字中包含"Affinity"的函数来指定任务将在哪个核心上运行。
FreeRTOS的任务创建API函数相当复杂。本文档中的大多数示例使用xTaskCreate()
,因为它是这些函数中最简单。
3.1 xTaskCreate()函数
xTaskCreateStatic() 函数有两个额外的参数,这两个参数分别指向预先分配的内存,用于存放任务的数据结构和堆栈。
c
BaseType_t xTaskCreate( TaskFunction_t pvTaskCode,
const char * const pcName,
configSTACK_DEPTH_TYPE usStackDepth,
void * pvParameters,
UBaseType_t uxPriority,
TaskHandle_t * pxCreatedTask );
以下是对每个参数的返回值的解释:
pvTaskCode
:这是一个指向实现任务的C函数的指针。任务通常被实现为一个无限循环,因此这个参数本质上就是任务函数的名称。pcName
:这是任务的描述性名称。FreeRTOS本身不使用这个名字,它纯粹作为调试辅助工具。使用人类可读的名称来识别任务比使用其句柄要简单得多。任务名称的最大长度由应用定义的常量configMAX_TASK_NAME_LEN
定义,包括空字符终止符。如果提供了更长的字符串,它将被截断。usStackDepth
:该参数指定为任务分配的堆栈大小。如果使用预先分配的内存而不是动态分配的内存,可以使用xTaskCreateStatic()
代替xTaskCreate()
。注意,这个值指定的是堆栈可以持有的字的数量,不是字节数。例如,如果堆栈是32位宽,usStackDepth
是128,则xTaskCreate()
分配512字节的堆栈空间(128 * 4字节)。configSTACK_DEPTH_TYPE
是一个宏,允许应用开发者指定用于保存堆栈大小的数据类型。如果未定义,configSTACK_DEPTH_TYPE
默认为uint16_t。如果堆栈深度乘以堆栈宽度大于65535(最大的16位数字),则在FreeRTOSConfig.h
中将configSTACK_DEPTH_TYPE
定义为unsigned long或size_t。pvParameters
:实现任务的函数接受一个void *类型的参数。pvParameters
是使用该参数传递给任务的值。uxPriority
:这个参数定义了任务的优先级。0是最低优先级,(configMAX_PRIORITIES -- 1)是最高优先级。如果定义的uxPriority大于(configMAX_PRIORITIES -- 1),它将被限制为(configMAX_PRIORITIES -- 1)。pxCreatedTask
:这是一个指向存储创建任务的句柄的位置的指针。这个句柄可以在未来用于API调用,例如改变任务的优先级或删除任务。pxCreatedTask
是一个可选参数,如果不需要任务的句柄,可以设置为NULL。- 返回值:
pdPASS
:表示任务创建成功。pdFAIL
:表示没有足够的堆内存可用于创建任务。
以下代码展示了如何创建两个简单的任务,然后启动这些新创建的任务。这些任务的工作是周期性地打印出一段字符串,它们通过使用一个简单的忙循环(busy loop)来创建周期性的延迟。这两个任务被创建为相同的优先级,除了它们打印出的字符串不同之外,其他都是相同的。
c
void vTask1( void * pvParameters )
{
/* ulCount is declared volatile to ensure it is not optimized out. */
volatile unsigned long ulCount;
for( ;; )
{
/* Print out the name of the current task task. */
vPrintLine( "Task 1 is running" );
/* Delay for a period. */
for( ulCount = 0; ulCount < mainDELAY_LOOP_COUNT; ulCount++ )
{
/*
* This loop is just a very crude delay implementation. There is
* nothing to do in here. Later examples will replace this crude
* loop with a proper delay/sleep function.
*/
}
}
}
c
void vTask2( void * pvParameters )
{
/* ulCount is declared volatile to ensure it is not optimized out. */
volatile unsigned long ulCount;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( "Task 2 is running" );
/* Delay for a period. */
for( ulCount = 0; ulCount < mainDELAY_LOOP_COUNT; ulCount++ )
{
/*
* This loop is just a very crude delay implementation. There is
* nothing to do in here. Later examples will replace this crude
* loop with a proper delay/sleep function.
*/
}
}
}
main()函数在scheduler(调度器)之前创建任务
c
int main( void )
{
/*
* Variables declared here may no longer exist after starting the FreeRTOS
* scheduler. Do not attempt to access variables declared on the stack used
* by main() from tasks.
*/
/*
* Create one of the two tasks. Note that a real application should check
* the return value of the xTaskCreate() call to ensure the task was
* created successfully.
*/
xTaskCreate( vTask1, /* Pointer to the function that implements the task.*/
"Task 1",/* Text name for the task. */
1000, /* Stack depth in words. */
NULL, /* This example does not use the task parameter. */
1, /* This task will run at priority 1. */
NULL ); /* This example does not use the task handle. */
/* Create the other task in exactly the same way and at the same priority.*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/*
* If all is well main() will not reach here because the scheduler will now
* be running the created tasks. If main() does reach here then there was
* not enough heap memory to create either the idle or timer tasks
* (described later in this book). Chapter 3 provides more information on
* heap memory management.
*/
for( ;; );
}
执行得到如下显示
bash
C:\Temp>rtosdemo
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
两个任务看似同时执行的情况。然而,由于这两个任务是在同一个处理器核心上执行的,它们实际上并不能真正的"同时"运行。真实的情况是,这两个任务快速地交替进入和退出运行状态。由于这两个任务具有相同的优先级,它们共享同一个处理器核心上的处理时间。
下图展示了这两个任务的实际执行模式。下图底部的箭头显示了从时间t1开始时间的流逝。不同颜色的线表示在每个时间点正在执行的任务------例如,在时间t1和t2之间,任务1正在执行。
在同一时间点上,只能有一个任务处于运行状态。因此,当一个任务进入运行状态(任务被切换进来),另一个任务则进入非运行状态(任务被切换出去)。这就是FreeRTOS调度器如何管理具有相同优先级任务的执行情况。
在一个任务内部创建另一个任务:这是一种更为灵活的方法,允许一个已经运行的任务创建新的任务。如下:
c
void vTask1( void * pvParameters )
{
const char *pcTaskName = "Task 1 is running\r\n";
volatile unsigned long ul; /* volatile to ensure ul is not optimized away. */
/*
* If this task code is executing then the scheduler must already have
* been started. Create the other task before entering the infinite loop.
*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/*
* This loop is just a very crude delay implementation. There is
* nothing to do in here. Later examples will replace this crude
* loop with a proper delay/sleep function.
*/
}
}
}
如何在FreeRTOS中通过使用任务参数来消除任务实现中的代码重复。具体来说,它解释了在示例上述中创建的两个任务几乎完全相同,唯一的区别在于它们打印出的文本字符串。为了消除这种重复,可以通过创建单个任务实现的多个实例,并使用任务参数传递字符串到每个实例中。我们只需要使用一个名位vTaskFunction()
的单一任务函数。需要注意的是,任务参数被转换为char *类型,以便获取任务应该打印出的字符串。
c
void vTaskFunction( void * pvParameters )
{
char *pcTaskName;
volatile unsigned long ul; /* volatile to ensure ul is not optimized away. */
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/* Delay for a period. */
for( ul = 0; ul < mainDELAY_LOOP_COUNT; ul++ )
{
/*
* This loop is just a very crude delay implementation. There is
* nothing to do in here. Later exercises will replace this crude
* loop with a proper delay/sleep function.
*/
}
}
}
创建了两个任务实例,这两个任务都是由vTaskFunction()函数实现的。vTaskFunction()是一个函数,它定义了任务的行为和功能。
在创建这两个任务时,通过任务的参数传递了一个不同的字符串给每个任务。这意味着两个任务虽然执行相同的函数,但它们可以有不同的输入参数,这允许它们在执行时有不同的行为。
这两个任务是独立执行的,并且它们的执行是由FreeRTOS的调度器控制的。
每个任务都有自己的栈(stack)。
c
/*
* Define the strings that will be passed in as the task parameters. These are
* defined const and not on the stack used by main() to ensure they remain
* valid when the tasks are executing.
*/
static const char * pcTextForTask1 = "Task 1 is running";
static const char * pcTextForTask2 = "Task 2 is running";
int main( void )
{
/*
* Variables declared here may no longer exist after starting the FreeRTOS
* scheduler. Do not attempt to access variables declared on the stack used
* by main() from tasks.
*/
/* Create one of the two tasks. */
xTaskCreate( vTaskFunction, /* Pointer to the function that
implements the task. */
"Task 1", /* Text name for the task. This is to
facilitate debugging only. */
1000, /* Stack depth - small microcontrollers
will use much less stack than this.*/
( void * ) pcTextForTask1, /* Pass the text to be printed into
the task using the task parameter. */
1, /* This task will run at priority 1. */
NULL ); /* The task handle is not used in
this example. */
/*
* Create the other task in exactly the same way. Note this time that
* multiple tasks are being created from the SAME task implementation
* (vTaskFunction). Only the value passed in the parameter is different.
* Two instances of the same task definition are being created.
*/
xTaskCreate( vTaskFunction,
"Task 2",
1000,
( void * ) pcTextForTask2,
1,
NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/*
* If all is well main() will not reach here because the scheduler will
* now be running the created tasks. If main() does reach here then there
* was not enough heap memory to create either the idle or timer tasks
* (described later in this book). Chapter 3 provides more information on
* heap memory management.
*/
for( ;; )
{
}
}
四、任务优先级
任务调度原则 :
FreeRTOS调度器的核心原则是确保能够运行的最高优先级的任务被选中进入运行状态。对于具有相同优先级的任务,调度器会轮流将它们转换进入和退出运行状态。
任务优先级设置:
- 任务的初始优先级由创建任务时使用的API函数中的
uxPriority
参数给出。 - 任务创建后,可以使用
vTaskPrioritySet()
API函数更改任务的优先级。
优先级范围:
- 可用的优先级数量由应用定义的编译时配置常量configMAX_PRIORITIES设置。
- 优先级数值较低表示任务的优先级较低,优先级0是可能的最低优先级。
- 因此,有效的优先级范围是从0到(configMAX_PRIORITIES -- 1)。
- 任何数量的任务可以共享相同的优先级。
调度器实现:
- FreeRTOS调度器有两种算法实现,用于选择运行状态的任务。
- 可以使用的
configMAX_PRIORITIES
的最大允许值取决于所使用的实现方法。
4.1 通用调度器
通用调度器是用C语言编写的,可以用于所有FreeRTOS的架构端口。它不对configMAX_PRIORITIES
(最大优先级数)设置上限。通常情况下,建议最小化configMAX_PRIORITIES
的值,因为更多的优先级值需要更多的RAM,并且会导致最坏情况下执行时间的增加。
4.2 架构优化调度器
这种调度器是用特定架构的汇编代码编写的,相较于通用的C语言实现,它的性能更优。
在架构优化的调度器中,最坏情况下的执行时间对于所有的configMAX_PRIORITIES
值都是相同的。
对于32位架构,configMAX_PRIORITIES
的最大值被设置为32;对于64位架构,最大值被设置为64。
与通用方法一样,建议将configMAX_PRIORITIES保持在实际需要的最低值,因为更高的值会需要更多的RAM。
通过在FreeRTOSConfig.h
文件中设置configUSE_PORT_optimized_TASK_SELECTION
为1,可以选择使用架构优化的调度器实现;设置为0则使用通用的C语言实现。
不是所有的FreeRTOS端口都有架构优化的实现。那些有架构优化实现的端口,如果在FreeRTOSConfig.h
中没有定义configUSE_PORT_optimized_TASK_SELECTION
,则默认使用架构优化实现(即默认值为1)。
那些没有架构优化实现的端口,默认configUSE_PORT_optimized_TASK_SELECTION
的值为0。
五、时间测量和时钟中断
时间片轮转(Time Slicing):
- 时间片轮转是一个可选特性,它允许任务在运行时被周期性地中断,以便其他任务可以运行。
- 在这些例子中,两个任务被创建为相同的优先级,并且它们总是能够运行。因此,每个任务执行一个"时间片",在时间片开始时进入运行状态,在时间片结束时退出运行状态。
时间片与调度器执行:
- 在每个时间片结束时,调度器执行以选择下一个要运行的任务。这是通过一个周期性中断,称为"滴答中断"(tick interrupt)来实现的。
configTICK_RATE_HZ
是一个编译时配置常量,它设置了滴答中断的频率,从而也设置了每个时间片的长度。例如,将configTICK_RATE_HZ
设置为100(Hz)会导致每个时间片持续10毫秒。- 两个滴答中断之间的时间被称为"滴答周期"(tick period),因此一个时间片等于一个滴答周期
调度器的执行与时间片 :
下图进一步展示了调度器的执行。最上面的线显示了调度器何时执行,细箭头显示了从任务到滴答中断,然后从滴答中断回到不同任务的执行顺序。
configTICK_RATE_HZ
的最优值 :
configTICK_RATE_HZ
的最优值取决于应用程序的需求,一般为100Hz。
FreeRTOS的API调用(函数)指定时间是以"tick"周期的倍数来表示的。
pdMS_TO_TICKS()
宏(macro)用于将以毫秒为单位的时间转换为以"tick"为单位的时间。
可用的时间分辨率取决于定义的tick频率。如果tick频率超过1KHz(即configTICK_RATE_HZ
大于1000),则不能使用pdMS_TO_TICKS()
宏。这是因为当tick频率过高时,毫秒到tick的转换可能会超出宏处理的范围。
以下代码展示了如何使用pdMS_TO_TICKS()宏将200毫秒的时间转换为等效的tick时间。这通常是通过一个简单的计算来完成的,计算公式是将毫秒数除以每个tick的毫秒数。
c
/*
* pdMS_TO_TICKS() takes a time in milliseconds as its only parameter,
* and evaluates to the equivalent time in tick periods. This example shows
* xTimeInTicks being set to the number of tick periods that are equivalent
* to 200 milliseconds.
*/
TickType_t xTimeInTicks = pdMS_TO_TICKS( 200 );
pdMS_TO_TICKS()
宏用于将毫秒数转换为滴答数(tick periods)。这样做的好处是,如果应用中的滴答频率(tick frequency)发生变化,之前用毫秒指定的时间值不需要改变。这意味着,无论系统的滴答频率如何变化,只要用毫秒表示的时间值不变,那么转换后的滴答数就能保持原有的时间长度。这为跨不同系统或在系统参数调整时保持时间一致性提供了便利。
任务调度器如何根据任务的优先级来决定任务的执行顺序,以及在不同优先级任务的情况下,调度器如何确保高优先级任务优先执行。
c
/*
* Define the strings that will be passed in as the task parameters.
* These are defined const and not on the stack to ensure they remain valid
* when the tasks are executing.
*/
static const char * pcTextForTask1 = "Task 1 is running";
static const char * pcTextForTask2 = "Task 2 is running";
int main( void )
{
/* Create the first task with a priority of 1. */
xTaskCreate( vTaskFunction, /* Task Function */
"Task 1", /* Task Name */
1000, /* Task Stack Depth */
( void * ) pcTextForTask1, /* Task Parameter */
1, /* Task Priority */
NULL );
/* Create the second task at a higher priority of 2. */
xTaskCreate( vTaskFunction, /* Task Function */
"Task 2", /* Task Name */
1000, /* Task Stack Depth */
( void * ) pcTextForTask2, /* Task Parameter */
2, /* Task Priority */
NULL );
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/* Will not reach here. */
return 0;
}
bash
C:\Temp>rtosdemo
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
Task 2 is running
原理图
六、扩展非运行状态
持续处理任务 :这类任务始终有处理工作要做,从不需要等待任何事件。由于它们不需要等待,它们总是能够进入运行状态。但这种任务的用途有限,因为它们只能被创建为最低优先级的任务。如果它们以更高的优先级运行,将会阻止所有低优先级任务运行。
事件驱动任务:这类任务只有在某个事件触发后才执行工作(处理)。在事件触发之前,它们不能进入运行状态。调度器总是选择可以运行的最高优先级任务。如果一个高优先级任务因为等待事件而不能被选中,调度器必须选择一个可以运行的低优先级任务。因此,编写事件驱动任务意味着可以在不同的优先级下创建任务,而不会使得最高优先级的任务独占所有处理时间,从而饿死所有低优先级任务。
6.1 阻塞状态
Blocked状态的定义 :
当一个任务正在等待某个事件的发生时,它会进入"Blocked"状态。这个状态是"Not Running"状态的一个子状态,意味着任务此时不在运行状态。
Temporal(时间相关)事件 :
这类事件与时间有关,它们可能在延迟期满或者达到某个绝对时间点时发生。例如,一个任务可能会进入Blocked状态,等待10毫秒的时间流逝。
Synchronization(同步)事件 :
这类事件通常由另一个任务或中断引起。例如,一个任务可能会进入Blocked状态,等待队列中的数据到达。同步事件涵盖了广泛的事件类型。
FreeRTOS中的同步事件 :
FreeRTOS中的队列、二进制信号量、计数信号量、互斥锁、递归互斥锁、事件组、流缓冲区、消息缓冲区以及直接到任务的通知都可以创建同步事件。后续章节将详细介绍这些特性。
带超时的同步事件阻塞 :
任务可以在同步事件上设置一个超时时间,这样它就可以同时阻塞在两种类型的事件上。例如,一个任务可能会选择等待最多10毫秒的数据到达队列。如果在10毫秒内数据到达,或者10毫秒过后数据仍未到达,任务都会离开Blocked状态。
6.2 挂起状态
"挂起"是"未运行"状态的一个子状态。处于挂起状态的任务不会被调度器调度。进入挂起状态的唯一方法是调用vTaskSuspend()
API函数,而退出挂起状态的唯一方法是调用vTaskResume()
或xTaskResumeFromISR()
API函数。
6.3 就绪状态
处于就绪状态的任务已经准备好执行,也就是说,它们已经具备了执行的所有条件,包括必要的资源和调度,但它们目前并没有被CPU执行。
6.4 完成状态转换图
下图展示了一个更详细的状态图,包含了所有非运行状态
周期性任务(Periodic Tasks):文中提到的示例中创建的任务都是"周期性"的,即它们执行一段延迟,然后打印字符串,再次延迟,如此循环。这种周期性行为是通过使用空循环(null loop)来实现延迟的,也就是任务在一个不断增加的循环计数器上进行轮询,直到达到一个固定值。
空循环的缺点:示例4.3清楚地展示了这种方法的缺点。当高优先级任务执行空循环时,它会一直占用运行状态(Running state),这导致低优先级任务得不到任何处理时间,即被"饿死"(starving)。
轮询的劣势:文中提到了轮询(polling)的几种缺点,最主要的是其效率低下。在轮询期间,任务实际上没有执行任何工作,但仍然占用最大的处理时间,从而浪费了处理器周期。
vTaskDelay() 函数:示例4.4通过替换轮询空循环为调用vTaskDelay() API函数来纠正这种行为。vTaskDelay()函数的原型在清单4.12中展示。需要注意的是,只有在FreeRTOSConfig.h中将INCLUDE_vTaskDelay设置为1时,vTaskDelay() API函数才可用。
vTaskDelay() 函数的作用:vTaskDelay()函数将调用它的任务置于阻塞状态(Blocked state),持续固定数量的滴答中断(tick interrupts)。任务在阻塞状态下不占用任何处理时间,因此只有在实际有工作要做时,任务才占用处理时间。
c
void vTaskDelay( TickType_t xTicksToDelay );
xTicksToDelay参数
:这个参数指定了调用vTaskDelay函数的任务将在阻塞状态中保持的时钟节拍数,之后任务会自动回到就绪状态。
例如,如果一个任务在时钟节拍计数为10,000时调用了vTaskDelay(100),那么它将立即进入阻塞状态,并一直保持在这个状态直到时钟节拍计数达到10,100。pdMS_TO_TICKS()宏
:这是一个宏,用于将毫秒指定的时间转换成时钟节拍数。例如,调用vTaskDelay(pdMS_TO_TICKS(100))
会导致调用任务在阻塞状态中保持100毫秒。
c
void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/*
* Delay for a period. This time a call to vTaskDelay() is used which
* places the task into the Blocked state until the delay period has
* expired. The parameter takes a time specified in 'ticks', and the
* pdMS_TO_TICKS() macro is used (where the xDelay250ms constant is
* declared) to convert 250 milliseconds into an equivalent time in
* ticks.
*/
vTaskDelay( xDelay250ms );
}
}
运行结果:
bash
C:\Temp>rtosdemo
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
Task 2 is running
Task 1 is running
即使两个任务被创建时具有不同的优先级,它们都能运行。它提到了调度器(scheduler)的执行被简化地省略了。此外,还提到了当调度器启动时,会自动识别创建一个空闲任务(idle task),以确保总是至少有一个任务可以运行(至少有一个任务处于就绪状态)。简言之,就是会在空闲时间调度低优先级的任务
使用空循环 :
任务通过一个空循环来实现延迟,这意味着它们在循环期间始终处于就绪状态(Ready state),因此可以一直运行。
这种方式导致任务之间占用了100%的可用处理器时间,因为它们在等待时仍然占用CPU资源。
使用阻塞状态:
- 任务在延迟期间进入阻塞状态,这意味着它们不会占用处理器时间,只有在真的有工作需要执行时(例如打印消息)才会使用处理器。
- 这种方式导致任务只使用了一小部分可用处理时间,因为它们在等待时不会占用CPU资源。
阻塞状态的效率:
- 在阻塞状态下,任务在离开阻塞状态后只执行了一小部分的时间片(tick period),然后再次进入阻塞状态。
- 大部分时间里,没有应用程序任务可以运行(没有任务处于就绪状态),因此没有任务可以被选为进入运行状态。在这种情况下,空闲任务(idle task)会运行。
- 分配给空闲任务的处理时间是系统备用处理能力的量度。使用RTOS可以通过允许应用程序完全事件驱动来显著增加备用处理能力。
任务状态转换 :
图中粗线显示了任务执行的状态转换,每个任务在返回到就绪状态之前都会经过阻塞状态。
6.5 vTaskDelayUntil()函数
vTaskDelay()
函数用于将任务延迟指定的时钟滴答数(tick interrupts)。这意味着,从任务调用 vTaskDelay()
开始,直到指定数量的滴答发生后,任务才会从Blocked状态转换回Ready状态。vTaskDelay()
参数指定了任务在Blocked状态中停留的时间长度,但任务离开Blocked状态的具体时间是相对于vTaskDelay()
被调用的时间而言的。
相对地,vTaskDelayUntil()
函数用于在特定的滴答计数值时将任务从Blocked状态移动到Ready状态。与vTaskDelay()
不同,vTaskDelayUntil()
要求一个绝对的时间点,而非相对于函数调用时的时间。这使得vTaskDelayUntil()
适用于需要固定执行周期的场景,即当你希望你的任务以固定频率周期性执行时,应该使用vTaskDelayUntil()
。
c
void vTaskDelayUntil( TickType_t * pxPreviousWakeTime,
TickType_t xTimeIncrement );
参数pxPreviousWakeTime
- 这个参数假设
vTaskDelayUntil()
被用来实现一个以固定频率执行的任务。在这种情况下,pxPreviousWakeTime
保存了任务最后一次离开阻塞状态(被"唤醒")的时间。这个时间点被用作参考,以计算任务下一次应该离开阻塞状态的时间。 pxPreviousWakeTime
指向的变量会在vTaskDelayUntil()
函数中自动更新;通常不会被应用程序代码修改,但在首次使用前必须初始化为当前的时钟节拍计数。
参数xTimeIncrement
- 这个参数同样是在假设
vTaskDelayUntil()
被用来实现一个以固定频率执行的任务的情况下命名的。 xTimeIncrement
的值以"节拍"为单位指定。可以使用宏pdMS_TO_TICKS()
将指定的毫秒时间转换为节拍时间。
使用
c
void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
TickType_t xLastWakeTime;
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/*
* The xLastWakeTime variable needs to be initialized with the current tick
* count. Note that this is the only time the variable is written to
* explicitly. After this xLastWakeTime is automatically updated within
* vTaskDelayUntil().
*/
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( pcTaskName );
/*
* This task should execute every 250 milliseconds exactly. As per
* the vTaskDelay() function, time is measured in ticks, and the
* pdMS_TO_TICKS() macro is used to convert milliseconds into ticks.
* xLastWakeTime is automatically updated within vTaskDelayUntil(), so
* is not explicitly updated by the task.
*/
vTaskDelayUntil( &xLastWakeTime, pdMS_TO_TICKS( 250 ) );
}
}
- 创建两个优先级为1的任务,这些任务不断地打印字符串,但不会调用任何可能导致它们进入阻塞状态的API函数,因此它们始终处于就绪(Ready)或运行(Running)状态。这种类型的任务被称为"持续处理"任务,因为它们始终有工作要做(尽管在这种情况下工作可能相当简单)。
- 接着创建了第三个优先级为2的任务,这个优先级高于前两个任务。第三个任务也是打印字符串,但是它是周期性地执行,所以它使用vTaskDelayUntil() API函数在每次打印迭代之间将自己置于阻塞状态。
c
void vContinuousProcessingTask( void * pvParameters )
{
char * pcTaskName;
/*
* The string to print out is passed in via the parameter. Cast this to a
* character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/*
* Print out the name of this task. This task just does this repeatedly
* without ever blocking or delaying.
*/
vPrintLine( pcTaskName );
}
}
c
void vPeriodicTask( void * pvParameters )
{
TickType_t xLastWakeTime;
const TickType_t xDelay3ms = pdMS_TO_TICKS( 3 );
/*
* The xLastWakeTime variable needs to be initialized with the current tick
* count. Note that this is the only time the variable is explicitly
* written to. After this xLastWakeTime is managed automatically by the
* vTaskDelayUntil() API function.
*/
xLastWakeTime = xTaskGetTickCount();
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( "Periodic task is running" );
/*
* The task should execute every 3 milliseconds exactly -- see the
* declaration of xDelay3ms in this function.
*/
vTaskDelayUntil( &xLastWakeTime, xDelay3ms );
}
}
运行演示:
bash
Continuous task 2 running
Continuous task 2 running
Periodic task is running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 2 running
Continuous task 2 running
Continuous task 2 running
Continuous task 2 running
Continuous task 2 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Continuous task 1 running
Periodic task is running
Continuous task 2 running
Continuous task 2 running
七、空闲任务和空闲钩子
任务阻塞状态:在这种状态下,任务不能运行,因此不能被调度器选中执行。
至少一个任务必须可运行 :为了保证至少有一个任务能够进入运行状态(Running state),当调用vTaskStartScheduler()
启动调度器时,调度器会自动创建一个空闲任务。
空闲任务的特性:空闲任务的优先级是最低的(优先级为零),这样它就不会阻止优先级更高的应用任务进入运行状态。然而,如果需要,应用设计者可以创建与空闲任务共享优先级的任务。
空闲任务的配置 :在FreeRTOS的配置文件FreeRTOSConfig.h
中,可以使用编译时配置常量configIDLE_SHOULD_YIELD
来防止空闲任务消耗,可以更有效地分配给其他优先级为0的应用任务的处理时间。
空闲任务的运行:由于空闲任务运行在最低优先级,因此一旦有更高优先级的任务进入就绪状态(Ready state),空闲任务就会立即从运行状态(Running state)转换出去。任务2被说成是抢占了空闲任务。抢占是自动发生的,被抢占的任务不需要知道这个过程。
任务删除和空闲任务 :如果一个任务使用vTaskDelete()
API函数自行删除,那么确保空闲任务不被剥夺处理时间是至关重要的。这是因为空闲任务负责清理被删除任务所使用的内核资源。
7.1 空闲任务钩子函数
空闲任务钩子 :
在FreeRTOS中,可以通过实现一个空闲钩子(idle hook)函数,将特定的应用功能直接添加到空闲任务中。这个空闲钩子函数会在空闲任务循环的每次迭代中自动被调用。
空闲钩子常见用途:
- 执行低优先级、后台或持续处理功能:在不需要创建额外的任务来执行这些功能时,可以利用空闲钩子来减少RAM开销。这意味着,当系统没有更高优先级的任务需要执行时,空闲任务可以执行一些不那么紧急的任务,从而有效利用处理器资源。
- 测量备用处理能力:空闲任务仅在所有更高优先级的应用任务没有工作执行时运行。因此,通过测量分配给空闲任务的处理时间,可以清晰地了解系统的备用处理时间。这有助于评估系统的处理能力是否得到充分利用。
- 将处理器置于低功耗模式:空闲钩子可以用来将处理器置于低功耗模式,从而在没有应用处理执行时自动节省能源。不过,这种节省能源的效果通常小于无滴答(tick-less)空闲模式所能达到的节能效果。
7.2 实现空闲任务钩子函数的限制
- 空闲任务钩子函数永远不应尝试阻塞或挂起自己。
解释:如果空闲任务以任何方式被阻塞,可能会导致没有任务可以进入运行状态(Running state),这可能会对系统的调度造成影响。 - 如果应用程序任务使用
vTaskDelete()
API函数自行删除,那么空闲任务钩子必须在合理的时间内返回给其调用者。
解释:空闲任务负责清理被删除任务所分配的内核资源。如果空闲任务永久停留在空闲钩子函数中,那么这项清理工作就无法进行。
c
void vApplicationIdleHook( void );
如何通过使用空闲钩子函数来有效利用由于任务阻塞而产生的空闲时间。
由于vTaskDelay()的使用,产生了很多空闲时间,如下代码将通过添加空闲钩子函数利用这些时间
c
/* Declare a variable that will be incremented by the hook function. */
volatile unsigned long ulIdleCycleCount = 0UL;
/*
* Idle hook functions MUST be called vApplicationIdleHook(), take no
* parameters, and return void.
*/
void vApplicationIdleHook( void )
{
/* This hook function does nothing but increment a counter. */
ulIdleCycleCount++;
}
在FreeRTOSConfig.h
中将configUSE_IDLE_HOOK
设置为1启用钩子函数
为了打印出 ulIdleCycleCount
的值,实现所创建任务的函数被稍微修改了一下。
c
void vTaskFunction( void * pvParameters )
{
char * pcTaskName;
const TickType_t xDelay250ms = pdMS_TO_TICKS( 250 );
/*
* The string to print out is passed in via the parameter. Cast this to
* a character pointer.
*/
pcTaskName = ( char * ) pvParameters;
/* As per most tasks, this task is implemented in an infinite loop. */
for( ;; )
{
/*
* Print out the name of this task AND the number of times
* ulIdleCycleCount has been incremented.
*/
vPrintLineAndNumber( pcTaskName, ulIdleCycleCount );
/* Delay for a period of 250 milliseconds. */
vTaskDelay( xDelay250ms );
}
}
运行结果:
c
C:\Temp>rtosdemo
Task 2 is running
ulIdleCycleCount = 0
Task 1 is running
ulIdleCycleCount = 0
Task 2 is running
ulIdleCycleCount = 3869504
Task 1 is running
ulIdleCycleCount = 3869504
Task 2 is running
ulIdleCycleCount = 8564623
Task 1 is running
ulIdleCycleCount = 8564623
Task 2 is running
ulIdleCycleCount = 13181489
Task 1 is running
ulIdleCycleCount = 13181489
Task 2 is running
ulIdleCycleCount = 17838406
Task 1 is running
ulIdleCycleCount = 17838406
Task 2 is running
八、改变任务优先级
8.1 vTaskPrioritySet()函数
它的作用是在调度器(scheduler)启动后改变一个任务(task)的优先级。vTaskPrioritySet()函数只有在FreeRTOS配置文件(FreeRTOSConfig.h
)中将INCLUDE_vTaskPrioritySet
宏定义设置为1时才可用。
c
void vTaskPrioritySet( TaskHandle_t xTask,
UBaseType_t uxNewPriority );
pxTask
这是要修改优先级的任务(被修改任务)的任务句柄。任务句柄是一个标识符,用于在FreeRTOS的API函数中指定特定的任务。你可以通过调用xTaskCreate()
API函数时的pxCreatedTask
参数获得任务句柄,或者通过xTaskCreateStatic()
API函数的返回值获得。
如果一个任务想要修改自己的优先级,可以通过传递NULL代替有效的任务句柄。
uxNewPriority
这是被修改任务要被设置的新优先级。这个值会被自动限制在最大可用优先级范围内,即(configMAX_PRIORITIES -- 1)。configMAX_PRIORITIES是一个在FreeRTOS配置文件FreeRTOSConfig.h
中设置的编译时常量,它定义了系统中可用的优先级总数。
8.2 uxTaskPriorityGet()函数
uxTaskPriorityGet()
是一个API函数,它的作用是返回一个任务的优先级。这个API函数只有在FreeRTOS配置文件FreeRTOSConfig.h
中将INCLUDE_uxTaskPriorityGet
宏定义为1时才可用。
c
UBaseType_t uxTaskPriorityGet( TaskHandle_t xTask );
pxTask
:这是一个任务句柄,用于标识要查询优先级的任务。任务句柄是任务的一个标识符,可以通过创建任务时的API函数获得。具体来说,可以在调用xTaskCreate()
API函数时通过pxCreatedTask
参数获得任务句柄,或者通过xTaskCreateStatic()
API函数的返回值获得。
查询自身优先级:如果一个任务想要查询自己的优先级,它可以传递NULL作为任务句柄,而不是一个有效的任务句柄。
返回值:查询操作的返回值是当前被查询任务的优先级。这意味着,当你使用这个API函数查询一个任务的优先级时,它会返回一个数值,表示该任务的优先级。
c
void vTask1( void * pvParameters )
{
UBaseType_t uxPriority;
/*
* This task will always run before Task 2 as it is created with the higher
* priority. Neither Task 1 nor Task 2 ever block so both will always be in
* either the Running or the Ready state.
*/
/*
* Query the priority at which this task is running - passing in NULL means
* "return the calling task's priority".
*/
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( "Task 1 is running" );
/*
* Setting the Task 2 priority above the Task 1 priority will cause
* Task 2 to immediately start running (as then Task 2 will have the
* higher priority of the two created tasks). Note the use of the
* handle to task 2 (xTask2Handle) in the call to vTaskPrioritySet().
* Listing 4.25 shows how the handle was obtained.
*/
vPrintLine( "About to raise the Task 2 priority" );
vTaskPrioritySet( xTask2Handle, ( uxPriority + 1 ) );
/*
* Task 1 will only run when it has a priority higher than Task 2.
* Therefore, for this task to reach this point, Task 2 must already
* have executed and set its priority back down to below the priority
* of this task.
*/
}
}
c
void vTask2( void * pvParameters )
{
UBaseType_t uxPriority;
/*
* Task 1 will always run before this task as Task 1 is created with the
* higher priority. Neither Task 1 nor Task 2 ever block so will always be
* in either the Running or the Ready state.
*
* Query the priority at which this task is running - passing in NULL means
* "return the calling task's priority".
*/
uxPriority = uxTaskPriorityGet( NULL );
for( ;; )
{
/*
* For this task to reach this point Task 1 must have already run and
* set the priority of this task higher than its own.
*/
/* Print out the name of this task. */
vPrintLine( "Task 2 is running" );
/*
* Set the priority of this task back down to its original value.
* Passing in NULL as the task handle means "change the priority of the
* calling task". Setting the priority below that of Task 1 will cause
* Task 1 to immediately start running again -- preempting this task.
*/
vPrintLine( "About to lower the Task 2 priority" );
vTaskPrioritySet( NULL, ( uxPriority - 2 ) );
}
}
每个任务可以查询和设置自己的优先级:在多任务系统中,每个任务都有能力查询(获取)和设置(改变)自己的优先级。
使用NULL代替有效的任务句柄:当任务要查询或设置自己的优先级时,不需要提供任务句柄(task handle,一种标识任务的唯一标识符)。在这种情况下,可以使用NULL来代替。
任务句柄只在引用其他任务时需要:只有当一个任务想要引用(查询或设置)除了自己以外的其他任务的优先级时,才需要提供那个任务的任务句柄。例如,任务1想要改变任务2的优先级。
任务句柄的获取和保存:为了让任务1能够改变任务2的优先级,必须在任务2创建时获取任务2的任务句柄,并将其保存起来。这样,任务1就可以使用这个句柄来引用任务2,并对其进行操作。
c
/* Declare a variable that is used to hold the handle of Task 2. */
TaskHandle_t xTask2Handle = NULL;
int main( void )
{
/*
* Create the first task at priority 2. The task parameter is not used
* and set to NULL. The task handle is also not used so is also set to
* NULL.
*/
xTaskCreate( vTask1, "Task 1", 1000, NULL, 2, NULL );
/* The task is created at priority 2 ______^. */
/*
* Create the second task at priority 1 - which is lower than the priority
* given to Task 1. Again the task parameter is not used so is set to NULL-
* BUT this time the task handle is required so the address of xTask2Handle
* is passed in the last parameter.
*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, &xTask2Handle );
/* The task handle is the last parameter _____^^^^^^^^^^^^^ */
/* Start the scheduler so the tasks start executing. */
vTaskStartScheduler();
/*
* If all is well main() will not reach here because the scheduler will
* now be running the created tasks. If main() does reach here then there
* was not enough heap memory to create either the idle or timer tasks
* (described later in this book). Chapter 2 provides more information on
* heap memory management.
*/
for( ;; )
{
}
}
执行结果
bash
Task1 is running
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running
About to raise the Task2 priority
Task2 is running
About to lower the Task2 priority
Task1 is running
九、删除任务
9.1 vTaskDelete()函数
vTaskDelete() API函数用于删除一个任务。这个函数只有在FreeRTOS配置文件(FreeRTOSConfig.h)中将INCLUDE_vTaskDelete宏定义为1时才可用。
在运行时连续创建和删除任务并不是一个好的实践。如果你发现自己需要使用这个函数,考虑其他设计选择,比如重用已有的任务。
被删除的任务将不再存在,并且不能再次进入运行状态。
如果一个任务使用了动态内存分配创建,并且后来删除了自己,那么空闲任务(Idle task)将负责释放为该任务分配的内存,比如任务的数据结构和栈。因此,在这种情况下,确保应用程序不会完全剥夺空闲任务的处理时间是很重要的。
注意:只有由内核本身为任务分配的内存在任务被删除时会自动释放。如果在任务实现过程中分配的任何内存或其他资源,如果不再需要,必须显式释放。
c
void vTaskDelete( TaskHandle_t xTaskToDelete );
pxTaskToDelete
是vTaskDelete()
函数的一个参数,它表示要被删除的任务的句柄(handle)。任务句柄是一个标识符,用于指向特定的任务。
要获取任务句柄,可以在创建任务时通过xTaskCreate()
函数的pxCreatedTask
参数获得,或者通过xTaskCreateStatic()
函数的返回值获得。
如果一个任务想要删除自己,可以在调用vTaskDelete()
时传入NULL代替有效的任务句柄。这样做会告诉FreeRTOS删除当前执行的任务。
下面是个代码例子和解释:
- 主函数(main())创建了一个优先级为1的任务1(Task 1)。当任务1运行时,它创建了一个优先级为2的任务2(Task 2)。由于任务2的优先级高于任务1,因此任务2会立即开始执行。
- 任务2执行的唯一操作是删除自身。它可以通过向vTaskDelete()传递NULL来删除自己,但为了演示目的,它使用了自身的任务句柄。列表4.29展示了任务2的源代码。
- 当任务2被删除后,任务1再次成为最高优先级的任务,因此它继续执行,并在这一点上调用vTaskDelay()进入阻塞状态,阻塞一段短暂的时间。
- 在任务1处于阻塞状态时,空闲任务(Idle task)执行,并释放之前分配给已删除的任务2的内存。
- 当任务1离开阻塞状态时,它再次成为最高优先级的就绪(Ready)状态任务,因此抢占了空闲任务。当它进入运行(Running)状态时,它会再次创建任务2,如此循环往复。
c
int main( void )
{
/* Create the first task at priority 1. */
xTaskCreate( vTask1, "Task 1", 1000, NULL, 1, NULL );
/* Start the scheduler so the task starts executing. */
vTaskStartScheduler();
/* main() should never reach here as the scheduler has been started. */
for( ;; )
{
}
}
c
TaskHandle_t xTask2Handle = NULL;
void vTask1( void * pvParameters )
{
const TickType_t xDelay100ms = pdMS_TO_TICKS( 100UL );
for( ;; )
{
/* Print out the name of this task. */
vPrintLine( "Task 1 is running" );
/*
* Create task 2 at a higher priority.
* Pass the address of xTask2Handle as the pxCreatedTask parameter so
* that xTaskCreate write the resulting task handle to that variable.
*/
xTaskCreate( vTask2, "Task 2", 1000, NULL, 2, &xTask2Handle );
/*
* Task 2 has/had the higher priority. For Task 1 to reach here, Task 2
* must have already executed and deleted itself.
*/
vTaskDelay( xDelay100ms );
}
}
c
void vTask2( void * pvParameters )
{
/*
* Task 2 immediately deletes itself upon starting.
* To do this it could call vTaskDelete() using NULL as the parameter.
* For demonstration purposes, it instead calls vTaskDelete() with its own
* task handle.
*/
vPrintLine( "Task 2 is running and about to delete itself" );
vTaskDelete( xTask2Handle );
}
运行结果:
bash
C:\Temp>rtosdemo
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
Task1 is running
Task2 is running and about to delete itself
十、线程本地存储和可重入性
Thread Local Storage (TLS):线程本地存储是一种存储方式,它为每个线程提供了一个独立的数据空间。这意味着每个线程都可以访问自己的数据副本,而不会与其他线程的数据发生冲突。这种存储方式常用于存储线程特定的数据,比如线程的ID、状态、堆栈等。使用TLS可以避免线程间的数据竞争,提高程序的并发性能。
Reentrancy (可重入性):可重入性是指一个函数或者代码块可以被多个任务(如线程)同时调用,而不会导致数据不一致或者其他问题。一个可重入的函数在执行过程中不会依赖于任何全局状态,也不会修改任何全局状态,因此可以安全地被多个线程同时调用。这对于多线程程序来说非常重要,因为它可以减少锁的使用,提高程序的并发性能。
10.1 C Runtime TLS实现
对于newlib库,可以通过定义宏configUSE_NEWLIB_REENTRANT来启用对newlib的线程本地存储支持。
对于picolibc库,可以通过定义宏configUSE_PICOLIBC_TLS来启用对picolibc的线程本地存储支持。
10.2 Custom C Runtime TLS
启用C运行时线程本地存储支持:
通过在FreeRTOS配置文件FreeRTOSConfig.h
中定义宏configUSE_C_RUNTIME_TLS_SUPPORT
并将其值设置为1,可以启用C运行时库的线程本地存储支持。这允许开发者在FreeRTOS中使用线程本地存储功能。
定义存储C运行时线程本地存储数据的C类型:
使用宏configTLS_BLOCK_TYPE
来定义一个C语言类型,这个类型将用于存储C运行时线程本地存储的数据。这个类型应该能够满足存储线程特定数据的需求。
定义初始化C运行时线程本地存储块的C代码:
宏configINIT_TLS_BLOCK
需要被定义为一段C代码,这段代码将在初始化C运行时线程本地存储块时执行。这通常涉及到设置初始值或者分配必要的资源。
定义在新任务切换时运行的C代码:
宏configSET_TLS_BLOCK
需要被定义为一段C代码,这段代码将在任务切换到新任务时执行。这通常涉及到更新线程本地存储以反映新任务的数据。
定义取消初始化C运行时线程本地存储块的C代码:
宏configDEINIT_TLS_BLOCK
需要被定义为一段C代码,这段代码将在取消初始化C运行时线程本地存储块时执行。这通常涉及到释放在初始化阶段分配的资源或者清理数据。
10.3 应用TLS
C运行时提供的线程本地存储功能,这是一种机制,允许每个线程存储自己的数据,这些数据对其他线程是不可见的。应用程序开发者也可以定义一组特定于应用的指针。这些指针将被包含在任务控制块中。任务控制块是FreeRTOS中用于管理任务的数据结构。这个功能通过在项目的FreeRTOSConfig.h文件中设置configNUM_THREAD_LOCAL_STORAGE_POINTERS为非零数值来启用。在项目的FreeRTOSConfig.h文件中进行设置。FreeRTOSConfig.h是FreeRTOS配置文件,开发者可以在这里设置各种配置选项。
vTaskSetThreadLocalStoragePointer和pvTaskGetThreadLocalStoragePointer这两个函数可以分别用来在运行时设置和获取每个线程本地存储指针的值。
c
void * pvTaskGetThreadLocalStoragePointer( TaskHandle_t xTaskToQuery,
BaseType_t xIndex )
void vTaskSetThreadLocalStoragePointer( TaskHandle_t xTaskToSet,
BaseType_t xIndex,
void * pvValue );
十一、调度算法
11.1 任务状态和事件的回顾
任务状态:
- 运行状态(Running state):正在使用处理器时间的任务处于运行状态。在单核处理器上,任何时候只能有一个任务处于运行状态。
- 就绪状态(Ready state):没有实际运行,但也没有被阻塞或挂起的任务处于就绪状态。就绪状态的任务可以被调度器选中进入运行状态。调度器总是选择最高优先级的就绪状态任务来进入运行状态。
- 阻塞状态(Blocked state):任务可以在此状态下等待某个事件,当事件触发时,它们会自动回到就绪状态。
事件类型:
- 时间事件(Temporal events):在特定时间点发生,例如阻塞时间到期时,通常用于实现周期性或超时行为。
- 同步事件(Synchronization events):当任务或中断服务程序使用任务通知、队列、事件组、消息缓冲区、流缓冲区或各种类型的信号量发送信息时发生。它们通常用于信号异步活动,如数据到达外设。
11.2 调度算法的选择
调度算法(Scheduling Algorithm) :调度算法是决定哪个就绪(Ready state)任务应该转换到运行(Running state)状态的软件例程。
配置常量(Configuration Constants):
configUSE_PREEMPTION
:这个配置常量用于启用或禁用抢占式调度。如果设置为1,则启用抢占式调度,即如果有一个优先级更高的任务变为就绪状态,当前运行的任务会被暂停(抢占),高优先级任务开始执行。
configUSE_TIME_SLICING
:这个配置常量用于启用或禁用时间片轮转调度。如果设置为1,则启用时间片轮转,即任务在执行一定时间后,无论是否完成,都会让出CPU给其他同优先级的任务执行。
configUSE_TICKLESS_IDLE
:这个配置常量影响调度算法,因为它可以导致滴答中断(tick interrupt)在长时间内完全关闭,从而减少功耗。这个选项默认设置为0,如果未定义,则使用默认值0。
轮询调度(Round Robin Scheduling)
:在所有可能的单核配置中,FreeRTOS调度器会轮流选择共享相同优先级的任务。这种"轮流执行"的政策通常被称为轮询调度。轮询调度算法对于具有相同优先级的任务,调度器会确保它们能够轮流获得CPU时间来执行。但是,这种轮流执行并不意味着每个任务获得的执行时间是完全相等的。
11.3 优先级抢占式调度加上时间片轮转
固定优先级(Fixed Priority) :
这种调度算法不会改变任务被调度时的优先级,但允许任务自身改变它们自己的优先级或其他任务的优先级。
抢占式(Preemptive) :
如果一个优先级更高的任务进入就绪(Ready)状态,抢占式调度算法会立即"抢占"正在运行(Running)状态的任务。被抢占意味着被迫从运行状态移动到就绪状态,而无需显式地让出(yield)或阻塞(block),以便让另一个任务进入运行状态。任务抢占可以在任何时候发生,不仅限于RTOS的时钟中断。
时间片轮转(Time Slicing) :
时间片轮转用于在优先级相同的任务之间共享处理时间,即使这些任务没有显式地让出或进入阻塞(Blocked)状态。使用时间片轮转的调度算法会在每个时间片结束时选择一个新任务进入运行状态,如果存在其他优先级与当前运行任务相同的就绪状态任务。一个时间片等于两个RTOS时钟中断之间的时间。
11.4 没有事件轮转的优先级抢占式调度
不使用时间片(Time Slicing):在这种调度模式下,FreeRTOS不使用时间片来共享处理时间。时间片是指任务在运行一定时间后,无论是否完成,都必须让出CPU给其他同优先级的任务。而不使用时间片意味着,任务只有在以下情况下才会被调度器选中,从而进入运行态:
有更高优先级的任务进入就绪态。
当前运行态的任务进入阻塞态(Blocked state)或挂起态(Suspended state)。
任务上下文切换(Task Context Switches) :当不使用时间片时,任务上下文切换的次数会比使用时间片时少。因此,关闭时间片可以减少调度器的处理开销。
处理时间差异 :然而,关闭时间片也可能导致同优先级的任务获得的处理时间差异很大。
高级技术 :因此,不使用时间片运行调度器被认为是一种高级技术,只应由经验丰富的用户使用。
11.5 合作式调度
书中主要关注的是抢占式调度,但也提到了FreeRTOS同样支持合作式调度。在合作式调度中,只有当运行态(Running state)的任务进入阻塞态(Blocked state)或者运行态的任务显式地放弃(yield),即通过调用taskYIELD()函数手动请求重新调度时,才会发生上下文切换。在合作式调度中,任务不会被抢占,因此不能使用时间片(time slicing)。
总结
这次学习了FreeRTOS的任务管理机制,包括任务的调度、优先级、状态以及创建和删除任务的方法。文章解释了如何实现任务、改变任务优先级、使用任务参数以及任务的阻塞和挂起状态。同时,探讨了FreeRTOS的调度算法,包括抢占式调度与时间片轮转调度,以及合作式调度。此外,还讨论了线程局部存储和任务删除的相关内容。
资料链接:https://github.com/FreeRTOS/FreeRTOS-Kernel-Book/blob/main/ch04.md