FreeRTOS 教程指南 学习笔记 第七章 资源管理
一、简介
在多任务处理系统中,如果一个任务开始访问一个资源,但在切换出运行状态之前没有完成其访问,则可能会出现错误。如果任务使资源处于不一致的状态,那么通过任何其他任务或中断访问同一资源都可能导致数据损坏或其他类似问题。
以下是一些示例:
- 访问外设请考虑以下场景,其中两个任务试图写入液晶显示器(LCD)。
1.1 任务A执行并开始将字符串“Hello world”写入LCD。
1.2 任务A在输出字符串“Hello w”后被任务B抢占。
1.3 任务B在进入阻塞状态之前将“Abort, Retry, Fail?”写入LCD。
1.4 任务A从它被抢占的点开始继续,并完成其字符串的其余字符-“orld”的输出。液晶显示器现在显示损坏的字符串 “Hello wAbort, Retry, Fail?orld”。 - 读取、修改、写入操作
清单111显示了一行C代码,以及一个关于C代码通常如何被转换为汇编代码的示例。可以看出,PORTA的值首先从内存中读取到一个寄存器中,在寄存器中进行修改,然后再写回内存中。这被称为读、修改、写操作。
这是一个“非原子”操作,因为它需要多个指令来完成,并且可以被中断。考虑下面的场景,其中两个任务试图更新一个名为PORTA的内存映射寄存器。
2.1 任务A将PORTA的值加载到一个寄存器中——操作的读取部分。
2.2 任务A在完成修改之前,被任务B抢占,并执行了同样修改和写入操作。
2.3 任务B更新PORTA的值,然后进入已阻止的状态。
2.4 任务A从它被抢占的点开始继续。它修改了保存在寄存器中的PORTA的副本,然后将此值更新回PORTA。
/* The C code being compiled. */
PORTA |= 0x01;
/* The assembly code produced when the C code is compiled. */
LOAD R1,[#PORTA] ; Read a value from PORTA into R1
MOVE R2,#0x01 ; Move the absolute constant 1 into R2
OR R1,R2 ; Bitwise OR R1 (PORTA) with R2 (constant 1)
STORE R1,[#PORTA] ; Store the new value back to PORTA
在此场景中,任务A将一个过期值更新并写入了PORTA。任务B在任务A获取PORTA的副本后,且在任务A将其修改的值写会PORTA之前,修改了PORTA的值,并写入PORTA的寄存器。当任务A写回PORTA时,任务B已经对PORTA进行了修改覆盖,有效的破话了PORTA的寄存器值。
本示例使用外围寄存器,但在对变量执行读、修改、写操作时也适用同样的原理。
- 非原子访问变量
更新一个结构的多个成员,或更新一个大于该体系结构的word大小的变量(例如,更新一个16位机器上的32位变量),都是非原子操作的示例。如果它们被中断,它们可能会导致数据丢失或损坏。 - 函数重入
如果一个函数可以安全的从多个任务调用,或者从任务和中断中调用,则该函数为“重入函数”。重入函数也被称为“线程安全的”,因为它们可以从多个执行线程中访问,而不会有数据或逻辑操作被损坏的风险。
每个任务都维护自己的栈和自己的一组处理器(硬件)寄存器值(任务栈和任务寄存器)。如果一个函数不访问 除 任务栈或任务寄存器 以外 的任何数据,那么该函数是重入的,线程安全的。清单112是一个可重入式函数的示例。清单113是一个不可重入的函数的示例。
//Listing 112. An example of a reentrant function
/* A parameter is passed into the function. This will either be passed on the stack, or in a processor register. Either way is safe as each task or interrupt that calls the function maintains its own stack and its own set of register values, so each task or interrupt that calls the function will have its own copy of lVar1. */
long lAddOneHundred( long lVar1 )
{
/* This function scope variable will also be allocated to the stack or a register, depending on the compiler and optimization level. Each task or interrupt that calls this function will have its own copy of lVar2. */
long lVar2;
lVar2 = lVar1 + 100;
return lVar2;
}
//Listing 113. An example of a function that is not reentrant
/* In this case lVar1 is a global variable, so every task that callslNonsenseFunction will access he same single copy of the variable. */
long lVar1;
long lNonsenseFunction( void )
{
/* lState is static, so is not allocated on the stack. Each task that calls this function will access the same single copy of the variable. */
static long lState = 0;
long lReturn;
switch( lState )
{
case 0 : lReturn = lVar1 + 10;
lState = 1;
break;
case 1 : lReturn = lVar1 + 20;
lState = 0;
break;
}
}
互斥锁
为了确保始终保持数据一致性,要访问在任务之间或任务与中断之间共享的资源,必须使用“互斥”技术进行管理。其目标是确保一旦任务开始访问不可重入且非线程安全的共享资源,同一任务就对该资源具有独占访问权限,直到该资源返回到一致状态。
FreeRTOS提供了几个可以用来实现互斥的特性,但最好的互斥方法是(只要可能,因为它通常不实用)设计应用程序,使资源不共享,每个资源只能从一个任务访问。
本章旨在让读者很好地理解:
- 何时和为什么资源管理和控制是必要的。
- 什么是一个关键的部分。
- 相互排斥的意思。
- 挂起调度程序意味着什么。
- 如何使用互斥锁。
- 如何创建和使用一个叫“守门人”的任务。
- 什么是优先级反转,以及优先级继承如何可以减少(但不是消除)其影响。
二、关键部分和挂起调度器
基本关键部分
基本的关键部分是分别被对宏taskENTER_CRITICAL()和taskEXIT_CRITICAL()的调用所包围的代码区域。关键部分也被称为关键区域。
taskENTER_CRITICAL()和taskEXIT_CRITICAL()不接受任何参数,或返回一个值。清单114展示了它们的使用情况。
//Listing 114. Using a critical section to guard access to a register
/* Ensure access to the PORTA register cannot be interrupted by placing it within a critical section. Enter the critical section. */
taskENTER_CRITICAL();
/* A switch to another task cannot occur between the call to taskENTER_CRITICAL() and the call to taskEXIT_CRITICAL(). Interrupts may still execute on FreeRTOS ports that allow interrupt nesting, but only interrupts whose logical priority is above the value assigned to the onfigMAX_SYSCALL_INTERRUPT_PRIORITY constant – and thoseinterrupts are not permitted to call FreeRTOS API functions. */
PORTA |= 0x01;
/* Access to PORTA has finished, so it is safe to exit the critical section. */
taskEXIT_CRITICAL();
本书的示例项目使用一个名为vPrintString()的函数将字符串写入标准输出——这是使用FreeRTOS窗口端口时的终端窗口。从许多不同的任务中调用了vPrintString();因此,在理论上,它的实现可以使用一个关键的部分来保护对标准输出的访问,如清单115所示。
void vPrintString( const char *pcString )
{
/* Write the string to stdout, using a critical section as a crude method of mutual exclusion. */
taskENTER_CRITICAL();
{
printf( "%s", pcString );
fflush( stdout );
}
taskEXIT_CRITICAL();
}
以这种方式实现的关键部分是提供互斥的一种非常粗糙的方法。它们的工作方式是完全禁用中断,或者达到configMAX_SYSCALL_INTERRUPT_PRIORITY设置的优先级。任务抢占的上下文切换只能中断中产生,因此,只要中断仍然处于禁用状态,调用taskENTER_CRITICAL()的任务就可以保证保持在运行状态,直到关键部分退出。
基本的关键部分必须保持非常短,否则它们将对中断响应时间并产生不利影响。每个对taskENTER_CRITICAL()的每个调用都必须与对taskEXIT_CRITICAL()的调用紧密配对。因此,标准输出(标准输出,或计算机写入其输出数据的流)不应该使用关键部分(如清单115所示)进行保护,因为写入终端可能是一个相对较长的操作。本章中的示例探讨了替代的解决方案。
关键部分嵌套是安全的,因为内核会计算嵌套深度。只有当嵌套深度返回到零时,关键部分才会退出——即对之前对taskENTER_CRITICAL()的每次调用都会执行对taskEXIT_CRITICAL()的一次调用。
调用taskENTER_CRITICAL()和taskEXIT_CRITICAL()是一个任务改变运行FreeRTOS的处理器的中断启用状态的唯一合法方式。通过任何其他方式改变中断启用状态都将使宏的嵌套计数无效。
taskENTER_CRITICAL()和taskEXIT_CRITICAL()不会以“FromISR”结尾,因此不能从中断服务例程中调用。taskENTER_CRITICAL_FROM_ISR()是taskENTER_CRITICAL()的中断安全版本,而taskEXIT_CRITICAL_FROM_ISR()是taskEXIT_CRITICAL()的中断安全版本。中断安全版本只提供给允许中断嵌套的FreeRTOS分支——它们在不允许中断嵌套的端口中将会废弃。
taskENTER_CRITICAL_FROM_ISR()返回一个必须传递到对taskEXIT_CRITICAL_FROM_ISR()的匹配调用中的值。清单116说明了这一点。
void vAnInterruptServiceRoutine( void )
{
/* Declare a variable in which the return value from taskENTER_CRITICAL_FROM_ISR() will be saved. */
UBaseType_t uxSavedInterruptStatus;
/* This part of the ISR can be interrupted by any higher priority interrupt. */
/* Use taskENTER_CRITICAL_FROM_ISR() to protect a region of this ISR. Save the value returned from taskENTER_CRITICAL_FROM_ISR() so it can be passed into the matching call to taskEXIT_CRITICAL_FROM_ISR(). */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* This part of the ISR is between the call to taskENTER_CRITICAL_FROM_ISR() and taskEXIT_CRITICAL_FROM_ISR(), so can only be interrupted by interrupts that have a priority above that set by the configMAX_SYSCALL_INTERRUPT_PRIORITY constant. */
/* Exit the critical section again by calling taskEXIT_CRITICAL_FROM_ISR(), passing in the value returned by the matching call to taskENTER_CRITICAL_FROM_ISR(). */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
/* This part of the ISR can be interrupted by any higher priority interrupt. */
}
与执行进入和退出关键部分的代码相比,要执行更多的代码是很浪费的。基本的关键部分进入得非常快,退出得非常快,而且总是确定性的,因此当被保护的代码区域非常短时,它们的使用是理想的。
暂停(或锁定)调度器
也可以通过挂起调度程序来创建关键部分。暂停调度程序有时也被称为“锁定”调度程序。
基本关键部分保护代码区域不受其他任务和中断的访问。通过暂停调度程序而实现的关键部分只保护一个代码区域不被其他任务访问,因为中断仍然处于启用状态。
如果通过简单地禁用中断来实现的关键部分太长,而可以通过挂起调度器来实现。但是,在暂停调度程序时,中断活动可能使调度程序恢复(或“取消暂停”)成为一个相对较长的操作,因此必须考虑在每种情况下使用哪一种是最佳方法。
The vTaskSuspendAll() API Function
//Listing 117. The vTaskSuspendAll() API function prototype
void vTaskSuspendAll( void );
调度程序通过调用vTaskSuspendAll()而挂起。暂停调度程序可以防止发生上下文切换,且中断保持启用状态。如果中断在调度程序挂起时请求上下文切换,则该请求将保持挂起,并且仅在调度程序恢复(未挂起)时执行。
当调度程序挂起时,不能调用FreeRTOS API函数。
The xTaskResumeAll() API Function
//Listing 118. The xTaskResumeAll() API function prototype
BaseType_t xTaskResumeAll( void );
/*返回值:在调度程序挂起时请求的上下文切换将保持挂起,并且仅在调度程序恢复时执行。如果在xTaskResumeAll()返回之前执行了挂起的上下文切换,则返回pdTRUE。否则将返回pdFALSE。*/
通过调用xTaskResumeAll()来恢复调度程序(未暂停)。
调用vTaskSuspendAll()和xTaskResumeAll()进行嵌套是安全的,因为内核保持嵌套深度的计数。只有当嵌套深度返回到零时,调度程序才会恢复——每次对vTaskSuspendAll()的调用后都调用一次xTaskResumeAll()时。
清单119显示了vPrintString()的实际实现,它会挂起调度器以保护对终端输出的访问。
//Listing 119. The implementation of vPrintString()
void vPrintString( const char *pcString )
{
/* Write the string to stdout, suspending the scheduler as a method of mutual
exclusion. */
vTaskSuspendScheduler();
{
printf( "%s", pcString );
fflush( stdout );
}
xTaskResumeScheduler();
}
三、互斥锁(和二进制信号量)
互斥锁是一种特殊类型的二进制信号量,用于控制对在两个或多个任务之间共享的资源的访问。MUTEX这个词源于“MUTual EXclusion”。configUSE_MUTEXES必须在FreeRTOSConfig.h中设置为1,才能可用。
当在互斥场景中使用时,可以将互斥锁视为与被共享的资源相关联的令牌。要合法访问资源的任务,必须首先成功“take”令牌(作为令牌持有者)。当令牌持有者完成了资源时,它必须“give”令牌。只有在返回该令牌时,另一个任务才能成功地获取该令牌,然后安全地访问相同的共享资源。一个任务不允许访问共享资源,除非它持有该令牌。该机制如图63所示。
尽管互斥体和二进制信号量有许多共同的特征,但图63中所示的场景(其中互斥体用于互斥)与图53中所示的场景(其中二进制信号量用于同步)完全不同。主要的区别是信号获得后发生什么:
- 用于互斥的信号量必须总是返回。
- 用于同步的信号量通常被丢弃而不返回。
该机制纯粹通过应用程序作者的行为来工作。一个任务没有理由不能在任何时候访问该资源,但每个任务都“同意”不这样做,除非它能够成为互斥锁的持有者。
The xSemaphoreCreateMutex() API Function
互斥体是一种信号量类型。对所有各种类型的FreeRTOS信号量的处理都存储在一个SemaphoreHandle_t类型的变量中。
在可以使用互斥体之前,必须先创建它。要创建互斥体类型的信号量,请使用xSemaphoreCreateMutex()API函数。
//Listing 120. The xSemaphoreCreateMutex() API function prototype
SemaphoreHandle_t xSemaphoreCreateMutex( void );
/*返回值:如果返回NULL,则无法创建互斥数据,因为FreeRTOS没有足够的堆内存来分配互斥数据结构。第2章提供了关于堆内存管理的更多信息。
非NULL返回值表示互斥锁已成功创建。返回的值应该存储为所创建的互斥锁的句柄。*/
Example 20. Rewriting vPrintString() to use a semaphore
这个示例创建了一个新版本的vPrintString(),称为prvNewPrintString(),然后从多个任务中调用这个新函数。prvNewPrintString()在功能上与vPrintString()相同,但是使用互斥锁来控制对标准输出的访问,而不是通过锁定调度程序。prvNewPrintString()的实现如清单121所示。
//Listing 121. The implementation of prvNewPrintString()
static void prvNewPrintString( const char *pcString )
{
/* The mutex is created before the scheduler is started, so already exists by the time this task executes.
Attempt to take the mutex, blocking indefinitely to wait for the mutex if it is not available straight away. The call to xSemaphoreTake() will only return when the mutex has been successfully obtained, so there is no need to check the function return value. If any other delay period was used then the code must check that xSemaphoreTake() returns pdTRUE before accessing the shared resource (which in this case is standard out). As noted earlier in this book, indefinite time outs are not recommended for production code. */
xSemaphoreTake( xMutex, portMAX_DELAY );
{
/* The following line will only execute once the mutex has been successfully obtained. Standard out can be accessed freely now as only one task can have the mutex at any one time. */
printf( "%s", pcString );
fflush( stdout );
/* The mutex MUST be given back! */
}
xSemaphoreGive( xMutex );
}
static void prvPrintTask( void *pvParameters )
{
char *pcStringToPrint;
const TickType_t xMaxBlockTimeTicks = 0x20;
/* Two instances of this task are created. The string printed by the task is passed into the task using the task’s parameter. The parameter is cast to the required type. */
pcStringToPrint = ( char * ) pvParameters;
for( ;; )
{
/* Print out the string using the newly defined function. */
prvNewPrintString( pcStringToPrint );
/* Wait a pseudo random time. Note that rand() is not necessarily reentrant, but in this case it does not really matter as the code does not care what value is returned. In a more secure application a version of rand() that is known to be reentrant should be used - or calls to rand() should be protected using a critical section. */
vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
}
}
int main( void )
{
/* Before a semaphore is used it must be explicitly created. In this example a mutex type semaphore is created. */
xMutex = xSemaphoreCreateMutex();
/* Check the semaphore was created successfully before creating the tasks. */
if( xMutex != NULL )
{
/* Create two instances of the tasks that write to stdout. The string they write is passed in to the task as the task’s parameter. The tasks are created at different priorities so some pre-emption will occur. */
xTaskCreate( prvPrintTask, "Print1", 1000,
"Task 1 ***************************************\r\n", 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000,
"Task 2 ---------------------------------------\r\n", 2, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
/* If all is well then main() will never reach here as the scheduler will now be running the tasks. If main() does reach here then it is likely that there was insufficient heap memory available for the idle task to be created. Chapter 2 provides more information on heap memory management. */
for( ;; );
}
优先级反转
图65展示了使用互斥锁来提供互斥的一个潜在缺陷。所描述的执行序列显示了优先级较高的任务2必须等待优先级较低的任务1放弃对互斥锁的控制。以这种方式被较低优先级任务延迟的较高优先级任务称为“优先级反转”。如果当一个高优先级任务在等待信号量时,一个中等优先级的任务开始执行,结果将是:高优先级任务在等待低优先级任务,而低优先级任务却没有机会执行。这个最坏的情况场景如图66所示。
优先级反转可能是一个重要问题,但在小型嵌入式系统中,通常可以通过在系统设计时考虑如何对资源的访问来避免。
优先权继承
FreeRTOS的互斥体和二进制信号量非常相似——不同之处在于,互斥体包含了一个基本的“优先级继承”机制,而二进制信号量则没有。优先级继承是一种最小化优先级反转的负面影响的方案。它并不“修复”优先级反转,而只是通过确保反转总是有时间限制来减少其影响。然而,优先级继承使系统时序分析变得复杂,依靠它纠正系统操作并不是一个很好的方法。
优先级继承的工作原理是,将互斥锁持有者的优先级暂时提升到试图获得相同互斥锁的最高优先级任务的优先级。持有互斥锁的低优先级任务“继承”等待互斥锁的任务的优先级。这一点如图67所示。当它返回互斥锁时,互斥锁持有人的优先级将被自动重置为其原始值。
如刚才所述,优先级继承功能会影响正在使用互斥锁的任务的优先级。因此,不能从中断服务例程中使用互斥关系。
死锁(或致命拥抱)
“死锁”是使用互斥语言进行互斥的另一个潜在陷阱。死锁有时也被称为更有戏剧性的名字“致命的拥抱”。
当两个任务都无法继续等待对方持有的资源时,就会发生死锁。考虑以下场景,其中任务A和任务B都需要获取互斥X和互斥Y才能执行操作:
- 任务A执行并成功获取互斥X。
- 任务A被任务抢占 B。
- 任务B在尝试获取互斥X之前成功获取互斥Y,但互斥X被任务A保留,因此X对任务B不可用。任务B选择进入“总是”状态,等待互斥X释放。
- 任务A继续执行。它试图获取互斥锁Y,但互斥锁Y被任务B持有,因此Y对任务A不可用。任务A选择进入阻塞状态,等待互斥锁Y被释放。
在这个场景结束时,任务A等待任务B持有的互斥锁,而任务B等待任务A持有的互斥锁,因此发生了死锁。
与优先级反转一样,避免死锁的最佳方法是在设计时考虑其潜在风险,并设计系统以确保不会出现死锁的情况。特别是,正如本书中前面所述,对一个任务无限期地等待(没有超时)来获得互斥锁通常是一种糟糕的做法。相反,使用比预期时间多一点的超时机制,那么在这段时间内无法获得互斥锁将是设计错误的症状,这可能是死锁。
在实践中,死锁在小型嵌入式系统中并不是一个大问题,因为系统设计者可以对整个应用程序有很好的理解,因此可以识别并删除可能发生死锁的区域。
递归性互斥锁
一个任务也有可能与自身发生死锁。如果一个任务多次尝试使用相同的互斥锁,而没有首先返回互斥锁,就会发生这种情况。请考虑以下情形:
- 任务已成功获得互斥锁。
- 当保持互斥锁时,该任务调用一个库函数。
- 库函数的实现尝试使用相同的互斥锁,并进入阻塞状态,等待互斥锁成为可用状态。
在此场景结束时,任务处于阻塞状态,等待互斥锁返回,但任务已经是互斥锁保持者。发生死锁,因为任务处于阻塞状态,等待自身。
可以通过使用递归互斥锁来代替标准互斥锁来避免这种类型的死锁。递归互斥锁可以被同一任务多次“take”,并且只有在对之前每次“获取”递归互斥锁的调用执行一次“give”递归互斥锁之后,才会返回。
标准的互斥体和递归的互斥体也以类似的方式创建和使用:
- 标准互斥体是使用xSemaphoreCreateMutex()创建的。递归互斥是使用xSemaphoreCreateRecursiveMutex()创建的。这两个API函数具有相同的原型。
- 标准互斥锁是使用xSemaphoreTake()“take”的。递归互斥使用xSemaphoreTakeRecursive()“take”。这两个API函数具有相同的原型。
- 标准的互变量是使用xSemaphoreGive()来“give”的。递归互斥使用xSemaphoreGiveRecursive()来“give”。这两个API函数具有相同的原型。
/* Recursive mutexes are variables of type SemaphoreHandle_t. */
SemaphoreHandle_t xRecursiveMutex;
/* The implementation of a task that creates and uses a recursive mutex. */
void vTaskFunction( void *pvParameters )
{
const TickType_t xMaxBlock20ms = pdMS_TO_TICKS( 20 );
/* Before a recursive mutex is used it must be explicitly created. */
xRecursiveMutex = xSemaphoreCreateRecursiveMutex();
/* Check the semaphore was created successfully. configASSERT() is described in section 11.2. */
configASSERT( xRecursiveMutex );
/* As per most tasks, this task is implemented as an infinite loop. */
for( ;; )
{
/* ... */
/* Take the recursive mutex. */
if( xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms ) == pdPASS )
{
/* The recursive mutex was successfully obtained. The task can now access the resource the mutex is protecting. At this point the recursive call count (which is the number of nested calls to xSemaphoreTakeRecursive()) is 1, as the recursive mutex has only been taken once. */
/* While it already holds the recursive mutex, the task takes the mutex again. In a real application, this is only likely to occur inside a sub-function called by this task, as there is no practical reason to knowingly take the same mutex more than once. The calling task is already the mutex holder, so the second call to xSemaphoreTakeRecursive() does nothing more than increment the recursive call count to 2. */
xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms );
/* ... */
/* The task returns the mutex after it has finished accessing the resource the mutex is protecting. At this point the recursive call count is 2, so the first call to xSemaphoreGiveRecursive() does not return the mutex. Instead, it simply decrements the recursive call count back to 1. */
xSemaphoreGiveRecursive( xRecursiveMutex );
/* The next call to xSemaphoreGiveRecursive() decrements the recursive call count to 0, so this time the recursive mutex is returned.*/
xSemaphoreGiveRecursive( xRecursiveMutex );
/* Now one call to xSemaphoreGiveRecursive() has been executed for every proceeding call to xSemaphoreTakeRecursive(), so the task is no longer the mutex holder.
}
}
}
互斥锁和任务调度
如果两个优先级不同的任务使用相同的互斥锁,那么FreeRTOS调度策略保证任务执行的顺序清晰;在能够运行状态的任务中,选择最高优先级的任务进入运行状态。例如,如果高优先级任务处于“阻塞”状态,等待低优先级任务所持有的互斥锁,那么一旦低优先级任务返回互斥锁,高优先级任务将抢占低优先级任务。高优先级的任务将成为互斥锁的持有者。这个场景已经在图67中看到了。
然而,当任务具有相同的优先级时,通常会对任务执行的顺序做出错误的假设。如果任务1和任务2具有相同的优先级,并且任务1处于阻塞状态,等待任务2所持有的互斥锁,那么当任务2“give”互斥锁时,任务1将不会抢占任务2。相反,任务2将保持“运行”状态,任务1将从阻塞状态移动到“就绪”状态。这个场景如图68所示,其中垂直线标记了滴答中断发生的时间。
在图68所示的场景中,当互斥锁可用时,FreeRTOS调度器就并不会立即使Task 1进入正在运行的状态,因为:
- 任务1和任务2具有相同的优先级,所以除非任务2进入阻塞状态,否则不应该切换到任务1,直到下一次时间片(假设configUSE_TIME_SLICING在FreeRTOSConfig.h中设置为1)。
- 如果一个任务在紧密循环中使用互斥锁,并且每次任务“give”互斥锁时都会发生上下文切换,那么该任务只会在很短的时间内处于运行状态。如果两个或更多的任务在一个紧密的循环中使用相同的互斥锁,那么在任务之间的快速切换将会浪费处理时间。
如果一个互斥体在一个紧密的循环中被多个任务使用,并且使用该互斥体的任务具有相同的优先级,那么必须注意确保这些任务接收到的处理时间大致相等。图69说明了任务可能无法接收到相同数量的处理时间的原因,它显示了如果清单125所示的任务的两个实例以相同的优先级创建时可能发生的执行序列。
//Listing 125. A task that uses a mutex in a tight loop
/* The implementation of a task that uses a mutex in a tight loop. The task creates a text string in a local buffer, then writes the string to a display. Access to the display is protected by a mutex. */
void vATask( void *pvParameter )
{
extern SemaphoreHandle_t xMutex;
char cTextBuffer[ 128 ];
for( ;; )
{
/* Generate the text string – this is a fast operation. */
vGenerateTextInALocalBuffer( cTextBuffer );
/* Obtain the mutex that is protecting access to the display. */
xSemaphoreTake( xMutex, portMAX_DELAY );
/* Write the generated text to the display – this is a slow operation. */
vCopyTextToFrameBuffer( cTextBuffer );
/* The text has been written to the display, so return the mutex. */
xSemaphoreGive( xMutex );
}
}
清单125中的注释注意到,创建字符串是一个快速的操作,而更新显示则是一个缓慢的操作。因此,当在更新显示时保持互斥锁时,该任务将在其大部分运行时间内保持互斥锁。
在图69中,垂直线标记了滴答中断发生的时间。
图69中的步骤7显示了任务1重新进入阻塞状态——这发生xSemaphoreTake()API函数中。
图69表明,任务1将被阻止获得互斥锁,直到一个时间片的开始q且任务2不是互斥锁持有者,这非常短且巧合的时间窗口。
图69中所示的场景可以通过在调用xSemaphoreGive()后添加对taskYIELD()的调用来避免。清单126演示了这一点,当任务保持互斥锁时,如果滴答计数发生变化,则调用taskYIELD()。
//Listing 126. Ensuring tasks that use a mutex in a loop receive a more equal amount of processing time, while also ensuring processing time is not wasted by switching between tasks too rapidly
void vFunction( void *pvParameter )
{
extern SemaphoreHandle_t xMutex;
char cTextBuffer[ 128 ];
TickType_t xTimeAtWhichMutexWasTaken;
for( ;; )
{
/* Generate the text string – this is a fast operation. */
vGenerateTextInALocalBuffer( cTextBuffer );
/* Obtain the mutex that is protecting access to the display. */
xSemaphoreTake( xMutex, portMAX_DELAY );
/* Record the time at which the mutex was taken. */
xTimeAtWhichMutexWasTaken = xTaskGetTickCount();
/* Write the generated text to the display – this is a slow operation. */
vCopyTextToFrameBuffer( cTextBuffer );
/* The text has been written to the display, so return the mutex. */
xSemaphoreGive( xMutex );
/* If taskYIELD() was called on each iteration then this task would only ever remain in the Running state for a short period of time, and processing time would be wasted by rapidly switching between tasks. Therefore, only call taskYIELD() if the tick count changed while the mutex was held. */
if( xTaskGetTickCount() != xTimeAtWhichMutexWasTaken )
{
taskYIELD();
}
}
}
四、守门员任务
守门人任务提供了一种干净的实现互斥的方法,而没有优先级倒置或死锁的风险。守门员任务是对资源拥有唯一所有权的任务。只有守门员任务被允许直接访问资源——需要访问资源的任何其他任务只能通过使用看门人的服务来间接地访问资源。
Example 21. Re-writing vPrintString() to use a gatekeeper task
示例21为vPrintString()提供了另一种替代实现。这一次,一个守门人任务被用于管理对标准输出的访问。当任务想要将消息写入标准输出时,它不会直接调用打印函数,而是将消息发送到守门员。
“守门员”任务使用FreeRTOS队列来序列化对标准输出的访问。任务的内部实现不必考虑互斥,因为它是唯一允许直接访问标准的任务。
守门员任务大部分时间处于阻塞状态,等待消息到达队列。当消息到达时,守门员只需将消息写入标准输出,然后返回到阻塞状态等待下一个消息。守门员任务的实现如清单128所示。
中断可以发送到队列,因此中断服务例程也可以安全地使用守门员的服务向终端写入消息。在本例中,使用滴答回调函数每200个滴答周期写出一条消息。
滴答回调是内核在每次滴答中断期间调用的函数。要使用滴答回调功能:
- 在FreeRTOSConfig.h中将configUSE_TICK_HOOK设置为1。
- 使用清单127中所示的确切函数名和原型,提供回调函数的实现。
//Listing 127. The name and prototype for a tick hook function
void vApplicationTickHook( void );
回调函数在滴答中断的上下文中执行,因此必须保持非常短,必须只使用适量的栈空间,并且不能调用任何不以“FromISR()”结尾的FreeRTOS API函数。
调度程序总是在滴答回调函数之后立即执行,因此从滴答回调中调用的中断安全FreeRTOS API函数不需要使用其pxHigherPriorityTaskWoken参数,并且该参数可以设置为NULL。
//Listing 128. The gatekeeper task
static void prvStdioGatekeeperTask( void *pvParameters )
{
char *pcMessageToPrint;
/* This is the only task that is allowed to write to standard out. Any other task wanting to write a string to the output does not access standard out directly, but instead sends the string to this task. As only this task accesses standard out there are no mutual exclusion or serialization issues to consider within the implementation of the task itself. */
for( ;; )
{
/* Wait for a message to arrive. An indefinite block time is specified so there is no need to check the return value – the function will only return when a message has been successfully received. */
xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
/* Output the received string. */
printf( "%s", pcMessageToPrint );
fflush( stdout );
/* Loop back to wait for the next message. */
}
}
//Listing 129. The print task implementation for Example 21
static void prvPrintTask( void *pvParameters )
{
int iIndexToString;
const TickType_t xMaxBlockTimeTicks = 0x20;
/* Two instances of this task are created. The task parameter is used to pass an index into an array of strings into the task. Cast this to the required type. */
iIndexToString = ( int ) pvParameters;
for( ;; )
{
/* Print out the string, not directly, but instead by passing a pointer to the string to the gatekeeper task via a queue. The queue is created before the scheduler is started so will already exist by the time this task executes for the first time. A block time is not specified because there should always be space in the queue. */
xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );
/* Wait a pseudo random time. Note that rand() is not necessarily reentrant, but in this case it does not really matter as the code does not care what value is returned. In a more secure application a version of rand() that is known to be reentrant should be used - or calls to rand() should be protected using a critical section. */
vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
}
}
//Listing 130. The tick hook implementation
void vApplicationTickHook( void )
{
static int iCount = 0;
/* Print out a message every 200 ticks. The message is not written out directly,
but sent to the gatekeeper task. */
iCount++;
if( iCount >= 200 )
{
/* As xQueueSendToFrontFromISR() is being called from the tick hook, it is
not necessary to use the xHigherPriorityTaskWoken parameter (the third
parameter), and the parameter is set to NULL. */
xQueueSendToFrontFromISR( xPrintQueue,
&( pcStringsToPrint[ 2 ] ),
NULL );
/* Reset the count ready to print out the string again in 200 ticks time. */
iCount = 0;
}
}
//Listing 131. The implementation of main() for Example 21
/* Define the strings that the tasks and interrupt will print out via the
gatekeeper. */
static char *pcStringsToPrint[] =
{
"Task 1 ****************************************************\r\n",
"Task 2 ----------------------------------------------------\r\n",
"Message printed from the tick hook interrupt ##############\r\n"
};
/*-----------------------------------------------------------*/
/* Declare a variable of type QueueHandle_t. The queue is used to send messages
from the print tasks and the tick interrupt to the gatekeeper task. */
QueueHandle_t xPrintQueue;
/*-----------------------------------------------------------*/
int main( void )
{
/* Before a queue is used it must be explicitly created. The queue is created to hold a maximum of 5 character pointers. */
xPrintQueue = xQueueCreate( 5, sizeof( char * ) );
/* Check the queue was created successfully. */
if( xPrintQueue != NULL )
{
/* Create two instances of the tasks that send messages to the gatekeeper. The index to the string the task uses is passed to the task via the task parameter (the 4th parameter to xTaskCreate()). The tasks are created at different priorities so the higher priority task will occasionally preempt the lower priority task. */
xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );
/* Create the gatekeeper task. This is the only task that is permitted to directly access standard out. */
xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
/* If all is well then main() will never reach here as the scheduler will now be running the tasks. If main() does reach here then it is likely that there was insufficient heap memory available for the idle task to be created. Chapter 2 provides more information on heap memory management. */
for( ;; );
}
守门员任务的优先级低于打印任务,因此发送到守门员的消息将保持在队列中,直到两个打印任务都处于“阻塞”状态。在某些情况下,为守门员分配更高的优先级是合适的,以便立即处理消息——但这样做的代价是守门员将延迟较低优先级的任务,直到它完成访问受保护的资源。