目录
- 1、 资源使用概况
- 2、互斥方法之一:基本临界区
- 2.1、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR()
- 3、互斥方法之二:挂起或锁定调度程序
- 3.1 vTaskSuspendAll()
- 3.2 xTaskResumeAll()
- 4 互斥方法三:互斥信号量(和二进制信号量)
- 4.1 xSemaphoreCreateMutex()
- 4.2 实例
- 4.3 什么叫优先级继承
- 4.4 关于死锁
- 4.5 递归互斥锁
- 4.6 互斥锁和任务调度
- 5、网守任务(Gatekeeper Tasks)
- 总结
1、 资源使用概况
在多任务系统中,如果一个任务开始访问资源,但在退出运行状态之前未完成其访问,则可能会出错。如果任务使资源处于不一致状态,则任何其他任务或中断对同一资源的访问都可能导致数据损坏或其他类似问题。
以上的问题是很浅显的。对外设的访问,对内存的访问等都可能出现以上的问题。
互斥
为了确保始终保持数据一致性,必须使用“互斥”技术管理对任务之间或任务与中断之间共享的资源的访问。目标是确保,一旦任务开始访问非可重入且非线程安全的共享资源,同一任务就可以独占访问该资源,直到资源返回到一致状态。
FreeRTOS提供了几个可用于实现互斥的功能,但最好的互斥方法是(在可能的情况下,因为通常不实用)以不共享资源的方式设计应用程序,并且每个资源只能从单个任务访问。
2、互斥方法之一:基本临界区
什么是临界区:基本临界区是分别被宏 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 调用包围的代码区域。
以这种方式实现的临界区是提供互斥的一种非常粗略的方法。它们的工作方式是完全禁用中断,或者禁用达到configMAX_SYSCALL_INTERRUPT_PRIORITY 设置的中断优先级(这取决于所使用的 FreeRTOS 端口)的中断。
由于抢先式上下文切换只能在中断中发生,因此,只要中断保持禁用状态,调用 taskENTER_CRITICAL() 的任务就可以保证保持在运行状态,直到退出临界区。
在调用 taskENTER_CRITICAL() 和调用 taskEXIT_CRITICAL() 之间不能切换到另一个任务。中断仍然可以在允许中断嵌套的 FreeRTOS 端口上执行,但仅限于逻辑优先级高于分配给 configMAX_SYSCALL_INTERRUPT_PRIORITY 常量的值的中断——并且不允许这些中断调用 FreeRTOS API 函数。
临界区必须保持非常短,否则将对中断响应时间产生不利影响。对taskENTER_CRITICAL()的每次调用都必须与对taskEXIT_CRITICAL的调用紧密配对。
临界区嵌套是安全的,因为内核会计算嵌套深度。只有当嵌套深度归零时,临界区才会退出——即对之前的每一次 taskENTER_CRITICAL() 调用都执行了一次 taskEXIT_CRITICAL() 调用。
调用 taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 是任务更改运行 FreeRTOS 的处理器的中断启用状态的唯一合法方法。通过任何其他方式更改中断启用状态将使宏的嵌套计数无效
2.1、taskENTER_CRITICAL_FROM_ISR() 和taskEXIT_CRITICAL_FROM_ISR()
taskENTER_CRITICAL() 和 taskEXIT_CRITICAL() 不会以“FromISR”结尾,因此不得从中断服务例程中调用。
taskENTER_CRITICAL_FROM_ISR() 是 taskENTER_CRITICAL() 的中断安全版本,taskEXIT_CRITICAL_FROM_ISR() 是 taskEXIT_CRITICAL() 的中断安全版本。中断安全版本仅适用于允许嵌套中断的 FreeRTOS 端口——它们在不允许中断嵌套的端口中已过时。
返回值
返回调用taskENTER_CRITIC_FROM_ISR()时的中断掩码状态。必须保存返回值,以便将其传递到对taskEXIT_CRITICAL_FROM_ISR()的匹配调用中。
void vAnInterruptServiceRoutine( void )
{
UBaseType_t uxSavedInterruptStatus; //这个变量存储taskENTER_CRITICAL_FROM_ISR()的返回值
/*ISR中的这部分可以被更高优先级的中断所中断. */
/* 使用 taskENTER_CRITICAL_FROM_ISR()保护ISR的这个区域。存储taskENTER_CRITICAL_FROM_ISR()返回值,并被传递给对应的 taskEXIT_CRITICAL_FROM_ISR(). */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* 这部分内容处于taskENTER_CRITICAL_FROM_ISR()与 taskEXIT_CRITICAL_FROM_ISR()之间,所以只会被优先级高于 configMAX_SYSCALL_INTERRUPT_PRIORITY 所设定的值的中断所中断。 */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
}
基本的临界区进入非常快,退出非常快,而且总是确定性的,因此当受保护的代码区域非常短时,它们的使用非常理想。
3、互斥方法之二:挂起或锁定调度程序
这是另一种建立临界区的方法。
可以通过暂停调度程序(命令vTaskSuspendAll())来创建临界区域。暂停调度程序有时也称为“锁定”调度程序。
基本临界区保护代码区域不被其他任务和中断访问。通过挂起调度程序实现的临界区仅保护代码区域不被其他任务访问,因为中断仍然启用。
如果关键部分太长,无法通过简单地禁用中断来实现,则可以通过暂停调度器来实现。然而,当调度程序被挂起时,中断活动会使恢复(或“取消挂起”)调度程序成为一个相对较长的操作,因此必须考虑在每种情况下使用哪种方法最好。
3.1 vTaskSuspendAll()
调度程序通过调用vTaskSuspendAll()被挂起。挂起调度程序可防止发生上下文切换,但会启用中断。如果在调度程序挂起时中断请求上下文切换,则该请求保持挂起状态,并且仅在调度程序恢复(未挂起)时执行。
3.2 xTaskResumeAll()
描述
本函数恢复调度器活动,跟在先前调用的vTaskSuspendAll(),使调度器由挂起状态过渡为活动状态。
返回值
pdTRUE : 调度器转为活动状态。这个转换引起了阻塞中的上下文进行切换。
pdFALSE :调度程序转换到 Active 状态并且转换没有导致发生上下文切换,或者调度程序由于对vTaskSuspendAll() 的嵌套调用而留在 Suspended 状态。
注意
调用vTaskSuspendAll()使调度器被挂起。当调度器被挂起中断仍然可用,但上下文切换不再进行。如果调度器挂起时有一个上下文切换请求,那么这个请求将被保持挂起直到调度器被恢复。
对 vTaskSuspendAll() 和 xTaskResumeAll() 的调用嵌套是安全的,因为内核会计算嵌套深度。只有当嵌套深度归零时,调度程序才会恢复——也就是说,对于之前对 vTaskSuspendAll() 的每一次调用,都会执行一次xTaskResumeAll() 调用。
4 互斥方法三:互斥信号量(和二进制信号量)
Mutex 是一种特殊类型的二进制信号量,用于控制对两个或多个任务之间共享的资源的访问。 MUTEX 一词源于“MUTual EXclusion”。 configUSE_MUTEXES 必须在 FreeRTOSConfig.h 中设置为 1 才能使用互斥锁。
在互斥场景中使用互斥锁时,可以将互斥锁视为与共享资源相关联的令牌。对于合法访问资源的任务,它必须首先成功“获取”令牌(成为令牌持有者)。当令牌持有者用完资源后,它必须“归还”令牌。只有当令牌被返回后,另一个任务才能成功获取令牌,然后安全地访问相同的共享资源。除非拥有令牌,否则不允许任务访问共享资源。
4.1 xSemaphoreCreateMutex()
描述:
该函数创建一个互斥类型的信号量,并返回一个可以引用互斥的句柄。
每个互斥类型的信号量都需要少量的RAM来保存信号量的状态。如果使用SemaphoreCreateMutex()创建互斥体,则所需的RAM将自动从FreeRTOS堆中分配。如果使用xSemaphoreCreateMutexStatic()创建互斥锁,则RAM由应用程序编写器提供,这需要额外的参数,但允许在编译时静态分配RAM。
返回值
NULL : 如果无法创建信号量则返回NULL,因为没有足够的堆内存供FreeRTOS分配信号量数据结构。
除NULL外的其它值:已成功创建信号量。返回的值是一个句柄,通过它可以引用创建的信号量。
注意事项
二进制信号量和互斥量非常相似,但确实有一些细微的区别。
互斥包含优先级继承机制,而二进制信号量不包含。这使得二进制信号量成为实现同步(任务之间或任务与中断之间)的更好选择,而互斥体则是实现简单互斥的更好选择。
二进制信号量–用于同步的二进制信号量在成功“获取”(获得)后无需“返回”。通过让一个任务或中断“给出”信号量,另一个任务“接受”信号量来实现任务同步。
互斥锁–如果另一个优先级较高的任务试图获得相同的互斥锁,则持有互斥锁的任务的优先级将提高。已经持有互斥锁的任务被称为“继承”试图“获取”同一互斥锁任务的优先级。当互斥体被返回时,继承的优先级将被“取消继承”(在持有互斥体时继承了更高优先级的任务在返回互斥体时将返回其原始优先级)。
获得互斥对象的任务必须始终返回( xSemaphoreTake())互斥对象,否则任何其他任务都无法获得相同的互斥对象。
互斥和二进制信号量都是使用SemaphoreHandle_t类型的变量引用的,并且可以在任何使用该类型参数的API函数中使用。
configSUPPORT_DYNAMIC_ALLOCATION必须在FreeRTOSConfig.h中设置为1,或者干脆不定义,才能使用此函数。
4.2 实例
此示例创建了一个名为prvNewPrintString()的vPrintString()新版本,然后从多个任务调用新函数。prvNewPrintString()在功能上与vPrintString()相同,但使用互斥锁而不是锁定调度器来控制对标准输出的访问。prvNewPrintString()的实现如下所示。
static void prvNewPrintString( const char *pcString )
{
/* 互斥信号量xMutex已在调度器启动前被创建,具体见其它代码.
这里用了portMAX_DELAY,使得如果得不到xMutex时,就一直阻塞。因此这里要么阻塞,要么获得信号量,所以这里可以不用在其后去判断获得信号量
是否成功。如果后面的阻塞等待时间参数不是portMAX_DELAY,那么就需要在其后判断获取信号量是否成功 */
xSemaphoreTake( xMutex, portMAX_DELAY );
{
/* 只有成功获取互斥对象后,才会执行以下行。标准输出现在可以自由访问,因为任何时候只有一个任务可以拥有互斥锁。 */
printf( "%s", pcString );
fflush( stdout );
}
xSemaphoreGive( xMutex );/* 互斥锁必须被主动给回。 */
}
prvNewPrintString()由prvPrintTask()实现的任务的两个实例重复调用。每个调用之间使用随机延迟时间。task参数用于向任务的每个实例传递唯一字符串。prvPrintTask()的实现如下所示。
static void prvPrintTask( void *pvParameters )
{
char *pcStringToPrint;
const TickType_t xMaxBlockTimeTicks = 0x20;
pcStringToPrint = ( char * ) pvParameters;
for( ;; )
{
prvNewPrintString( pcStringToPrint );
vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
}
}
prvPrintTask() 的两个实例以不同的优先级创建,因此较低优先级的任务有时会被较高优先级的任务抢占。由于使用互斥锁来确保每个任务都可以互斥地访问终端,因此即使发生抢占,显示的字符串也将是正确的,不会被破坏。可以通过减少任务在阻塞状态下花费的最长时间来增加抢占的频率,该时间由 xMaxBlockTimeTicks 常量设置。
int main( void )
{
xMutex = xSemaphoreCreateMutex(); //创建互斥锁,用于对标准输出资源的管理
/* Check the semaphore was created successfully before creating the tasks. */
if( xMutex != NULL )
{
/* 创建两个写入stdout任务的实例。两个实例被传入不同的字符串做为参数,用于任务的打印输出。两个任务优先级不同,使他们能互相抢占。*/
xTaskCreate( prvPrintTask, "Print1", 1000,
"Task 1 ***************************************\r\n", 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000,
"Task 2 ---------------------------------------\r\n", 2, NULL );
vTaskStartScheduler();
}
for( ;; );
}
以上代码的输出结果哪下:
4.3 什么叫优先级继承
要明白互斥信号量所具有的优先级继承概念,首先要知道什么叫优先级反转。
优先级反转
当较低优先级的任务拥有了互斥锁后,则按常理较高优先级的任务必须等待这个较低优先级的任务放弃对互斥体的控制。因此更高优先级的任务是以这种方式被较低优先级任务延迟称为“优先级反转”。如果中等优先级任务开始执行而高优先级任务正在等待信号量(结果将是高优先级任务等待低优先级任务),则这种不良行为将被进一步夸大,而低优先级任务甚至无法执行。执行。这种最坏的情况如下图 所示。
1、三个不同的优先级任务。LP,MP,HP分别指低优先级,中等优先级,最高优先级。LP在执行时在被HP抢占前就获得了互斥锁。
2、HP抢占了LP后,想要去获得互斥锁,但因为此时是LP占用互斥锁,因此HP只能进入阻塞状态去等待LP释放互斥锁。
3、LP继续执行,但是在释放互斥锁之前,又被 MP抢占了时间片。
4、这时HP继续等待LP释放互斥锁,但是LP现在却不能执行,更不能释放互斥锁,因为此时是MP在不断抢占执行。
以上的这个“优先级反转”会破坏任务间的正常调度,造成系统的不确定性。因此这里必须引入“优先级继承”的技术来解决这个问题。
优先级继承
FreeRTOS 互斥体和二进制信号量非常相似——不同之处在于互斥体包含基本的“优先级继承”机制,而二进制信号量则没有。优先级继承是一种将优先级反转的负面影响降至最低的方案。它不会“修复”优先级反转,而只是通过确保返转总是有时间限制来减轻其影响。但是,优先级继承使系统时序分析变得复杂,依赖它来进行正确的系统操作并不是一个好的做法。
优先级继承的工作原理是将互斥锁持有者的优先级临时提高到试图获得相同互斥锁的最高优先级任务的优先级。持有互斥锁的低优先级任务“继承”等待互斥锁的任务的优先级。下图 演示了这一点。互斥锁持有者的优先级在将互斥锁归还时自动重置为其原始值。
1、三个不同的优先级任务。LP,MP,HP分别指低优先级,中等优先级,最高优先级。LP在执行时在被HP抢占前就获得了互斥锁。
2、HP抢占了LP后,想要去获得互斥锁,但因为此时是LP占用互斥锁,因此HP只能进入阻塞状态去等待LP释放互斥锁。
3、 LP 任务阻止 HP 任务执行,因此继承了HP 任务的优先级。 LP 任务现在不能被 MP 任务抢占,因此优先级反转存在的时间量被最小化。当 LP 任务将互斥锁返回时,它会返回其原始优先级。
4、返回互斥锁的 LP 任务导致 HP 任务作为互斥锁持有者退出阻塞状态。当 HP 任务使用互斥体完成时,它会将其返回。 MP 任务仅在 HP 任务返回 Blocked 状态时执行,因此 MP 任务永远不会阻塞 HP 任务。
4.4 关于死锁
死锁(或致命拥抱)
“死锁”是使用互斥锁进行互斥的另一个潜在陷阱。死锁有时也以更具戏剧性的名称“致命拥抱”而闻名。
当两个任务因为都在等待对方持有的资源而无法继续时,就会发生死锁。考虑以下场景,其中任务 A 和任务 B 都需要获取互斥锁
X 和互斥锁 Y 才能执行操作:
1、 任务 A 执行并成功获取 mutex X。
2、 任务 A 被任务 B 抢占。
3、 任务 B 在尝试同时获取互斥体 X 之前成功地获取了互斥体 Y——但互斥体 X 由任务 A 持有,因此对任务 B 不可用。任务 B 选择
进入阻塞状态以等待互斥体 X 被释放。
4、 任务A继续执行。它尝试获取互斥体 Y——但互斥体 Y 由任务 B 持有,因此对任务 A 不可用。任务 A 选择进入阻塞状态以等待
互斥体 Y 被释放。
在这个场景结束时,任务 A 正在等待任务 B 持有的互斥锁,而任务 B 正在等待任务 A 持有的互斥锁。由于两个任务都无法继续,
因此发生了死锁。
与优先级倒置一样,避免死锁的最佳方法是在设计时考虑其潜力,并设计系统以确保不会发生死锁。特别是,正如前面所述,任务无限期地等待(没有超时)以获得互斥锁通常是不好的做法。
4.5 递归互斥锁
递归互斥锁
任务也可能与自身发生死锁。如果一个任务尝试多次使用同一个互斥锁,而不首先返回互斥锁,就会发生这种情况。考虑以下场
景:
1、任务成功获取互斥量。
2、 在持有互斥锁的同时,任务调用库函数。
3、库函数的实现尝试取同一个互斥体,进入阻塞状态等待互斥体可用。
在这个场景结束时,任务处于阻塞状态等待互斥锁返回,但任务已经是互斥锁持有者。发生死锁是因为任务处于阻塞状态等待自
己。
可以通过使用递归互斥锁代替标准互斥锁来避免这种类型的死锁。递归互斥锁可以被同一个任务多次“获取”,并且只有在对“
获取”递归互斥锁的每个先前调用都执行了一次“给予”递归互斥锁的调用后才会返回。标准互斥锁和递归互斥锁的创建和使用方式类似:
->使用 xSemaphoreCreateMutex() 创建标准互斥锁。递归互斥锁是使用 xSemaphoreCreateRecursiveMutex() 创建的。这两个API 函数有相同的原型。
->使用 xSemaphoreTake()“获取”标准互斥锁。递归互斥锁是使用 xSemaphoreTakeRecursive() 来“获取”的。这两个 API 函数具有相同的原型。
->使用 xSemaphoreGive() “给定”标准互斥锁。递归互斥锁是使用 xSemaphoreGiveRecursive() “给定”的。这两个 API 函数具有相同的原型。
演示了如何创建和使用递归互斥锁。
SemaphoreHandle_t xRecursiveMutex; //递归互斥锁的变量
/* 以下是创建和使用一个递归互斥锁的任务 */
void vTaskFunction( void *pvParameters )
{
const TickType_t xMaxBlock20ms = pdMS_TO_TICKS( 20 );
xRecursiveMutex = xSemaphoreCreateRecursiveMutex(); //创建递归互斥锁
configASSERT( xRecursiveMutex ); //检查互斥锁是否被创建成功
for( ;; )
{
/* ... */
if( xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms ) == pdPASS ) //获得递归互斥锁
{
/*已成功获取递归互斥对象。该任务现在可以访问互斥锁正在保护的资源。此时,递归调用计数(这是对xSemaphoreTakeRecursive()
的嵌套调用数)为1,因为递归互斥体只执行了一次。
当它已经持有递归互斥锁时,任务将再次获取互斥锁。在实际应用程序中,这只可能发生在该任务调用的子函数中,因为没有实际的理由
故意多次使用相同的互斥锁。调用任务已经是互斥体持有者,因此对xSemaphoreTakeRecursive()的第二次调用只会将递归调用计
数增加到2。*/
xSemaphoreTakeRecursive( xRecursiveMutex, xMaxBlock20ms );
/* ... */
/* 任务在完成对互斥锁所保护的资源的访问后返回互斥锁。此时,递归调用计数为2,因此对xSemaphoreGiveRecursive()的第一次
调用不会返回互斥体。相反,它只是将递归调用计数减回到1。*/
xSemaphoreGiveRecursive( xRecursiveMutex );
/* 下一次对xSemaphoreGiveRecursive()的调用将递归调用计数减为0,因此这次返回递归互斥。*/
xSemaphoreGiveRecursive( xRecursiveMutex );
/*现在,对 xSemaphoreTakeRecursive()的每一次调用都执行了一次xSemaphoreGiveRecursive(),因此该任务不再是互斥体
持有者。
}
}
}
4.6 互斥锁和任务调度
如果两个不同优先级的任务使用同一个互斥锁,那么 FreeRTOS 调度策略会明确任务执行的顺序;能够运行的优先级最高的任务将被选为进入运行状态的任务。例如,如果一个高优先级任务处于阻塞状态以等待一个低优先级任务持有的互斥锁,那么一旦低优先级任务返回互斥锁,高优先级任务就会抢占低优先级任务 . 然后,高优先级任务将成为互斥锁持有者。
然而,当任务具有相同优先级时,通常会错误地假设任务将执行的顺序。如果 Task 1 和 Task 2 具有相同的优先级,并且 Task 1 处
于 Blocked 状态以等待 Task 2 持有的互斥锁,那么当 Task 2“给予”互斥锁时,Task 1 不会抢占 Task 2。相反,任务 2 将保持在运
行状态,而任务 1 将简单地从阻塞状态移动到就绪状态。这种情况如下图所示,其中垂直线标记发生滴答中断的时间。
1、Task2运行一个时间片,在此期间拿到一个互斥锁。
2、在下一个时间片,Task1开始执行。
3、Task1试图拿到互斥锁,此时互斥锁被Task2拥有,因此task1进入阻塞等待。
4、在t2-t3剩下的时间片里,Task2运行,并且持续到下一个时间片t3-t4
5、在t3-t4时间片里,Task2交出互斥锁,这里task1立即从阻塞状态切换到ready状态。
6、task1要到t3-t4时间片结束后,才能进入running状态。
在上图 所示的场景中,一旦互斥锁可用,FreeRTOS 调度程序就不会立即使Task1 成为运行状态任务,因为:
1、 任务 1 和任务 2 具有相同的优先级,因此除非任务 2 进入 Blocked 状态,否则在下一次滴答中断之前不应切换到任务 1(假设
FreeRTOSConfig.h 中的 configUSE_TIME_SLICING 设置为 1)。
2、 如果任务在紧密循环中使用互斥锁,并且每次任务“给予”互斥锁时都会发生上下文切换,那么任务只会在短时间内保持运行状
态。如果两个或多个任务在紧密循环中使用相同的互斥锁,则在任务之间快速切换会浪费处理时间。
如果多个任务在紧密循环中使用互斥锁,并且使用互斥锁的任务具有相同的优先级,则必须注意确保任务获得大致相等的处理时间。
下图说明了任务可能无法获得相等处理时间的原因,下图显示了如果以相同优先级创建如下代码所示任务的两个实例,可能会发生的执行顺序。
/* 在紧密循环中使用互斥锁的任务的实现。该任务在本地缓冲区中创建一个文本字符串,然后将该字符串写入显示器。对显示器的访问受到互斥锁的保护*/
void vATask( void *pvParameter )
{
extern SemaphoreHandle_t xMutex;
char cTextBuffer[ 128 ];
for( ;; )
{
/* 生成字符串--这是一个快速的操作 */
vGenerateTextInALocalBuffer( cTextBuffer );
/* 获得互斥锁,用于保护显示器的访问. */
xSemaphoreTake( xMutex, portMAX_DELAY );
/* 将生成的字符串写到显示器---这是一个慢速的操作. */
vCopyTextToFrameBuffer( cTextBuffer );
/* 字符串写到显示器后,返回互斥锁 */
xSemaphoreGive( xMutex );
} }
在这个任务中,创建字符串是一项快速操作,而更新显示是一项缓慢的操作。因此,由于在更新显示时保持互斥锁,任务将在其大部分运行时间保持互斥锁。
1、Task2 在t1-t2时间片里处于运行状态,同时在此期间获得了互斥锁。
2、Task1在下一个时间片开始运行。
3、Task1运行期间,想要获取互斥锁,但task2并没有释放互斥锁,所以task1进入阻塞状态
4、task2在t2-t3的剩余时间里开始运行,并保持到下一个时间片t3-t4。
5、task2在此处返回互斥锁。立即使task1退出阻塞状态,进入ready状态。但由于进间片还未完成,所以task2继续运行。
6、在task2继续运行的进间片里,task2又获得互斥锁。
7、在t4时间片开始时,task1进入运行状态,但立即又因为拿不到互斥锁而进入阻塞状态。
上图中的第 7 步显示任务 1 重新进入阻塞状态—这发生在 xSemaphoreTake() API 函数内部。
上图 展示了任务 1 将被阻止获得互斥锁,直到时间片的开始与任务 2 不是互斥锁持有者的短周期之一重合。
通过在调用 xSemaphoreGive() 之后添加对 taskYIELD() 的调用,可以避免上图 中所示的情况。这在下面的代码中进行了演示,如
果在任务持有互斥锁时滴答计数发生变化,则调用 taskYIELD()。
void vFunction( void *pvParameter )
{
extern SemaphoreHandle_t xMutex;
char cTextBuffer[ 128 ];
TickType_t xTimeAtWhichMutexWasTaken;
for( ;; )
{
/* 生成字符串--这是一个快速的操作,时间很短。 */
vGenerateTextInALocalBuffer( cTextBuffer );
/* 获得互斥锁,用于保护对显示器的访问。 */
xSemaphoreTake( xMutex, portMAX_DELAY );
/* 记录获得互斥锁的时间。 */
xTimeAtWhichMutexWasTaken = xTaskGetTickCount();
/* 将字符串写入到显示器--这是一个慢速的操作,时间较长 */
vCopyTextToFrameBuffer( cTextBuffer );
/* 完成写入显示器后,返回互斥锁. */
xSemaphoreGive( xMutex );
/* 如果在每次迭代中调用taskYIELD(),则此任务只会在短时间内保持运行状态,并且快速切换任务会浪费处理时间。因此,
只有在互斥锁被持有时滴答计数发生变化时,才调用taskYIELD()。 */
if( xTaskGetTickCount() != xTimeAtWhichMutexWasTaken )
{
taskYIELD();
}
}
}
5、网守任务(Gatekeeper Tasks)
网守任务提供了一种实现互斥的干净方法,没有优先级反转或死锁的风险。
网守任务是对资源拥有唯一所有权的任务。只有网守任务被允许直接访问资源——任何其他需要访问资源的任务只能通过使用网守的服务间接访问。
接下来的示例 , 重写 vPrintString() 以使用网守任务。示例为 vPrintString() 提供了另一种替代实现。这一次,gatekeeper 任务用于管理对标准输出的访问。当一个任务想要将消息写入标准输出时,它不会直接调用打印函数,而是将消息发送给gatekeeper。
网守任务使用 FreeRTOS 队列来序列化对标准输出的访问。任务的内部实现不必考虑互斥,因为它是唯一允许直接访问标准的任务。
网守任务大部分时间都处于阻塞状态,等待消息到达队列。当消息到达时,网守只需将消息写入标准输出,然后返回阻塞状态以等待下一条消息。
中断可以发送到队列,因此中断服务例程也可以安全地使用网守的服务将消息写入终端。在这个例子中,一个ticks钩子函数用于每 200 个ticks写出一条消息。
tick钩子函数在tick中断的上下文中执行,因此必须保持非常短,必须仅使用适量的堆栈空间,并且不得调用任何不以“FromISR()”结尾的 FreeRTOS API 函数。
调度程序总是在tick钩子函数之后立即执行,因此从tick钩子调用的中断安全 FreeRTOS API 函数不需要使用它们的pxHigherPriorityTaskWoken 参数,该参数可以设置为 NULL。
static void prvStdioGatekeeperTask( void *pvParameters )
{
char *pcMessageToPrint;
/* 这是唯一允许写入标准输出的任务。任何其他想要将字符串写入输出的任务都不会直接访问标准输出,而是将字符串发送到此任务。
由于只有此任务访问标准输出,因此在任务本身的实现中不需要考虑互斥或串行化问题。 */
for( ;; )
{
/* 接收队列内的信息 */
xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
printf( "%s", pcMessageToPrint );
fflush( stdout );
}
}
写入队列的任务下 所示。如前所述,创建了两个单独的任务实例,并使用 task 参数将任务写入队列的字符串传递给任务。
static void prvPrintTask( void *pvParameters )
{
int iIndexToString;
const TickType_t xMaxBlockTimeTicks = 0x20;
/* 该任务的两个实例。任务的参数用于传送字符串数组的索引。*/
iIndexToString = ( int ) pvParameters;
for( ;; )
{
/* 不直接打印字符串,而是通过队列将指向字符串的指针传递给看门人任务。队列是在调度程序启动之前创建的,因此在该任务第一次
执行时已经存在。未指定块时间,因为队列中应始终有空间。*/
xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );
vTaskDelay( ( rand() % xMaxBlockTimeTicks ) );
}
}
tick钩子函数计算它被调用的次数,每次计数达到 200 时将其消息发送给网守任务。仅出于演示目的,tick钩子写入队列的前面,任务写入后面的队列。tick钩子实现如下所示。
tick钩子(或滴答回调)是内核在每次滴答中断期间调用的函数。要使用tick挂钩功能:
- 在 FreeRTOSConfig.h 中将 configUSE_TICK_HOOK 设置为 1。
- 使用的钩子函数的原型如:
void vApplicationTickHook( void )
{
static int iCount = 0;
/* 每200个tick打印一个信息。信息并不是直接输出,而是发送给了gatekeeper任务。 */
iCount++;
if( iCount >= 200 )
{
xQueueSendToFrontFromISR( xPrintQueue,
&( pcStringsToPrint[ 2 ] ),
NULL );
iCount = 0;
}
}
像往常一样, main() 创建运行示例所需的队列和任务,然后启动调度程序。 main() 的实现如清单 131 所示。
/* 用于打印的字符串数组,用于传送给网守任务 gatekeeper. */
static char *pcStringsToPrint[] =
{
"Task 1 ****************************************************\r\n",
"Task 2 ----------------------------------------------------\r\n",
"Message printed from the tick hook interrupt ##############\r\n"
};
/*-----------------------------------------------------------*/
/* 声明QueueHandle_t类型的变量。该队列用于从打印任务发送消息,并将tick中断发送到看门人任务。*/
QueueHandle_t xPrintQueue;
/*-----------------------------------------------------------*/
int main( void )
{
xPrintQueue = xQueueCreate( 5, sizeof( char * ) );
/* Check the queue was created successfully. */
if( xPrintQueue != NULL )
{
/* 创建两个向网关守卫发送消息的任务实例。任务使用的字符串的索引通过任务参数(xTaskCreate()的第四个参数)传递给任务。
任务以不同的优先级创建,因此较高优先级的任务偶尔会抢占较低优先级的任务。*/
xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );
/* 创建看门人任务。这是唯一允许直接访问标准输出的任务。 */
xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );
vTaskStartScheduler();
}
for( ;; );
}
执行示例时产生的输出如图 70 所示。可以看出,源自任务的字符串和源自中断的字符串都正确打印出来,没有损
坏。
网守任务的优先级低于打印任务,因此发送到网守的消息将保留在队列中,直到两个打印任务都处于阻塞状态。在
某些情况下,为网守分配更高的优先级是合适的,因此消息会立即得到处理——但这样做的代价是网守延迟较低优
先级的任务,直到它完成对受保护资源的访问。
总结
多任务系统中,任务对资源占用以及协调是非常重要的。即要保证占用资源的任务能不被干扰的使用,也要防止出现死锁。FreeRTOS针对不同类型的使用场景,提供了从禁用中断,禁用调度这类系统级的资源使用手段。也提供了互斥信号量,嵌套互斥信号量等应用级的资源使用手段。也提供了Gatekeeper Tasks(网守任务)方式的资源使用方案。
因为任务调度模式导至的多种使用资源可能出现死锁的场景,FreeRTOS提供了优先级继承,递归互斥锁的机制解决。分析了对于多个任务在紧密循环中使用互斥锁,并且使用互斥锁的任务具有相同的优先级时可能出现的问题以及解决的方法。