1.前言
访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用”互斥”技术以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直至这个资源又恢复到完整状态
FreeRTOS 提供了多种特性用以实现互斥,但是最好的互斥方法(如果可能的话,任何时候都当如此)还是通过精心设计应用程序,尽量不要共享资源,或者是每个资源都通过单任务访问。
2.本章内容
本章期望让读者了解以下内容:
为什么,以及在什么时候有必要进行资源管理与控制。
什么是临界区。
互斥是什么意思。
挂起调度器有什么意义。
如何使用互斥量。
如何创建与使用守护任务。
什么是优先级反转,以及优先级继承是如何减小(但不是消除)其影响的
3.为何进行资源管理与控制
可参考freeRTOS学习4--资源管理概述一文中2.并发抢占导致错误的场景
4.基本临界区(方法一)
4.1 基本临界区示例
基本临界区是指宏taskENTER_CRITICAL()与taskEXIT_CRITICAL()之间的代码区间
使用基本临界区对寄存器的访问进行保护代码示例如下:
/* 为了保证对PORTA寄存器的访问不被中断,将访问操作放入临界区。 进入临界区 */ taskENTER_CRITICAL(); /* 在taskENTER_CRITICAL() 与 taskEXIT_CRITICAL()之间不会切换到其它任务。 中断可以执行,也允许 嵌套,但只是针对优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断 – 而且这些中断不允许访问 FreeRTOS API 函数. */ PORTA |= 0x01; /* 我们已经完成了对PORTA的访问,因此可以安全地离开临界区了。 */ taskEXIT_CRITICAL(); 程
4.2 基本临界区的几点说明
(1)基本临界区是提供互斥功能的一种非常原始的实现方法,临界区的工作仅仅是简单地把中断全部关掉
或是关掉优先级在configMAX_SYSCAL_INTERRUPT_PRIORITY 及以下的中断——依赖于具体使用的FreeRTOS 移植
优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断不允许访问 FreeRTOS API 函数
(2)抢占式上下文切换只可能在某个中断中完成
所以调用taskENTER_CRITICAL()的任务可以在中断关闭的时段一直保持运行态,直到退出临界区。
(3)临界区必须只具有很短的时间
否则会反过来影响中断响应时间,在每次调用taskENTER_CRITICAL()之后,必须尽快地配套调用一个taskEXIT_CRITICAL()
(4)临界区嵌套是安全的
因为内核有维护一个嵌套深度计数。临界区只会在嵌套深度为0 时才会真正退出——即在为每个之前调用的taskENTER_CRITICAL()都配套调用了taskEXIT_CRITICAL()之后。
注:基本临界区嵌套个人理解是当前某个任务正在操作临界资源,其它任务也进入临界区来操作临界资源,内核会检查一个任务退出临界区才允许另一个任务进入临界区
内核通过taskENTER_CRITICAL()和taskEXIT_CRITICAL()的计数次数来决定什么时候真正的使能中断
5.挂起调度器(方法二)
5.1 什么是挂起调度器
挂起调度器是通过锁定调度器来创建临界区。
挂起调度器有些时候也被称为锁定调度器,挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断
5.2 与基本临界区的区别
基本临界区保护一段代码区间不被其它任务或中断打断;
挂起临界区只可以保护一段代码区间不被其它任务打断,中断是使能的
5.3 采用挂起调度器or基本临界区
如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式。
但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作。所以评估哪种是最佳方式需要结合实际情况。
5.4 挂起调度器主要API
名称 | 说明 | 参数 | 返回值 |
vTaskSuspendAll() | 挂起调度器。挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行。 在调度器处于挂起状态时,不能调用FreeRTOS API 函数 | ||
xTaskResumeAll() | 在调度器挂起过程中,上下文切换请求也会被挂起,直到调度器被唤醒后才会得到执行。 | 如果一个挂起的上下文切换请求在xTaskResumeAll()返回前得到执行,则函数返回pdTRUE。在其它情况下,xTaskResumeAll()返回pdFALSE |
注:嵌套调用vTaskSuspendAll()和xTaskResumeAll()是安全的,因为内核有维护一个嵌套深度计数。
调度器只会在嵌套深度计数为0 时才会被唤醒——即在为每个之前调用的vTaskSuspendAll()都配套调用了xTaskResumAll()之后。
千万不要在调度器挂起时调用其它API 函数
5.5 挂起调度器示例
void vPrintString( const portCHAR *pcString ) { /* Write the string to stdout, suspending the scheduler as a method of mutual exclusion. */ vTaskSuspendScheduler(); { printf( "%s", pcString ); fflush( stdout ); } xTaskResumeScheduler(); /* Allow any key to stop the application running. A real application that actually used the key value should protect access to the keyboard input too. */ if( kbhit() ) { vTaskEndScheduler(); } }
6.互斥量(二值信号量)(方法三)
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源
6.1 互斥量与二值信号量的区别
最大的区别在于信号量在被获得之后所发生的事情
- 用于互斥的信号量必须归还。
- 用于同步的信号量通常是完成同步之后便丢弃,不再归
6.2 使用互斥量的场景示例
6.3 主要API
名称 | 说明 | 参数 | 返回值 |
xSemaphoreCreateMutex() | 互斥量是一种信号量。FreeRTOS 中所有种类的信号量句柄都保存在类型为xSemaphoreHandle 的变量中。 | 如果返回NULL 表示互斥量创建失败。原因是内存堆空间不足导致FreeRTOS 无法为互斥量分配结构数据空间。第五章提供更多关于内存管理方面的信息。 |
7. 优先级反转
7.1 什么是优先级反转
高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”
优先级反转可能会产生重大问题。但是在一个小型的嵌入式系统中,通常可以在设计阶段就通过规划好资源的访问方式避免出现这个问题
7.2 优先级反转的最坏情况
当高优先级任务正等待信号量的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行
7.3. 优先级继承
(1)优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具有的最高优先级
(2)持有互斥量的低优先级任务”继承”了等待互斥量的任务的优先级
(3)互斥量持有者在归还互斥量时,优先级会自动设置为其原来的优先级
注:由于最好是优先考虑避免优先级反转,并且因为FreeRTOS 本身是面向内存有限的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务在任意时刻只会持有一个互斥量
8. 死锁
当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁
9. 守护任务(方法四)
9.1 守护任务的特点
(1)守护任务提供了一种干净利落的方法来实现互斥功能,而不用担心会发生优先级反转和死锁。
(2)守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的资源——其它任务要访问该资源只能间接地通过守护任务提供的服务。
9.2 守护任务的示例
以中断和任务中都打印字符串到输出设备为例
(1)守护任务使用了一个FreeRTOS 队列来对终端实现串行化访问。该任务内部实现不必考虑互斥,因为它是唯一能够直接访问终端的任务
(2)守护任务大部份时间都在阻塞态等待队列中有信息到来。当一个信息到达时,守护任务仅仅简单地将收到的信息写到标准输出上,然后又返回阻塞态,继续等待下一条信息地到来
(3)中断中可以写队列,所以中断服务例程也可以安全地使用守护任务提供的服务,从而把信息输出到终端
10.参考文档
[1] FreeRTOS中文实用教程