目录
临界区的概念在任何的 SoC 都存在,比如,针对一个寄存器,基本操作为:读->改->写;在不带 OS 的系统下,普通代码希望对某个寄存器进行读->改->写,此刻,一个 IRQ 打断了这个操作,也同时对这个寄存器进行 读->改->写,中断返回,后,普通代码又继续进行,这样就会导致逻辑错误;
在带 OS 的情况下,不光是有 IRQ,而且存在任务切换,这样,同一个资源在 ISR 和不同任务之间修改,这造成了临界区;临界区的资源需要保护起来,临界区保护的不是代码,而是数据;
所以,在设计阶段尽早识别哪些是临界区,以及采取对应的策略,避免后续出现很难查的问题;
FreeRTOS 针对临界区资源,存在几种保护的方式:
1、taskENTER_CRITICAL() 和 taskEXIT_CRITICAL()
2、vTaskSuspendAll() 和 xTaskResumeAll()
3、Mutexes
4、Gatekeeper Tasks
接下来就看看他们的具体概念以及用法;
1、taskENTER_CRITICAL
这是个最强悍的临界区保护调用,它总是和 taskEXIT_CRITICAL 成对出现,即:
-
taskENTER_CRITICAL();
-
{
-
............. // 临界区
-
}
-
taskEXIT_CRITICAL();
- 1
为何称之为强悍,因为它直接屏蔽了中断,OS 调度靠中断,ISR 也靠中断;
也就是说,在这之间的对数据的操作,是绝对安全!
适用场景是,临界区可能存在于中断和任务中;
使用 taskENTER_CRITICAL 的时候,尽量保证这个临界区很短小,因为它暂停了所有的活动,来满足这段临界区,外部其他的任何响应,都无法阻止他;
它的实现是
注意:
1、在 ISR 中使用 taskENTER_CRITICAL_FROM_ISR() 和 taskEXIT_CRITICAL_FROM_ISR();
2、taskENTER_CRITICAL 和 taskEXIT_CRITICAL 必须成对出现;
taskENTER_CRITICAL 和 taskEXIT_CRITICAL 支持嵌套使用,因为里面维护了一个引用计数;
2、vTaskSuspendAll
上面那种关闭中断的方式,需要尽快退出临界区,以免引起中断延时处理,任务被延时处理;
FreeRTOS 还提供了一种挂起调度器的方式的临界区,它通过调用 vTaskSuspendAll 和 xTaskResumeAll 来建立临界区:
-
vTaskSuspendAll();
-
{
-
............. // 临界区
-
}
-
xTaskResumeAll();
- 1
这种方式和 taskENTER_CRITICAL 不一样的地方在于,它仅仅是挂起了调度器,而没有去关闭中断;换言之,资源争夺的场景中,它仅仅是防止了任务之间的资源争夺,中断照样可以直接响应;
所以,挂起调度器的方式,适用于,临界区位于任务与任务之间;既不用去延时中断,又可以做到临界区的安全;
3、Mutexes
3.1、Usage
互斥量是二值信号量的特殊形式 (它也是通过 Queue 实现),与二值信号量不同,互斥量用于控制多个任务之间共享资源的访问,也就是互锁;
不同于上面两种,互斥量不但开放了中断,同时也不挂起调度器;
使用互斥量,需要定义 configUSE_MUTEXES 为 1
用于互锁的互斥量可以充当保护资源的令牌。当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问同一资源。
互斥量和信号量使用相同的API函数,因此互斥量也允许指定一个阻塞时间。阻塞时间单位为系统节拍周期时间,数目表示获取互斥量无效时最多处于阻塞状态的系统节拍周期个数。
互斥量与二进制信号量最大的不同是:互斥量具有优先级继承机制。也就是说,如果一个互斥量(令牌)正在被一个低优先级任务使用,此时一个高优先级企图获取这个互斥量,高优先级任务会因为得不到互斥量而进入阻塞状态,正在使用互斥量的低优先级任务会临时将自己的优先级提升,提升后的优先级与与进入阻塞状态的高优先级任务相同。这个优先级提升的过程叫做优先级继承。这个机制用于确保高优先级任务进入阻塞状态的时间尽可能短,以及将已经出现的“优先级翻转”影响降低到最小。
在很多场合中,某个硬件资源只有一个,当低优先级任务占用该资源的时候,即便高优先级任务也只能乖乖的等待低优先级任务释放资源。这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
为什么优先级继承能够降低优先级翻转的影响呢?举个例子,现在有任务A、任务B和任务C,三个任务的优先级顺序为任务C>任务B>任务A。任务A和任务C都要使用某一个硬件资源,并且当前任务A占有该资源。
先看没有优先级继承的情况:任务C也要使用该资源,但是此时任务A正在使用这个资源,因此任务C进入阻塞,此时三个任务的优先级顺序没有发生变化。在任务C进入阻塞之后,某硬件产生了一次中断,唤醒了一个事件,该事件可以解除任务B的阻塞状态。在中断结束后,因为任务B的优先级是大于任务A的,所以任务B抢占任务A的CPU权限。那么任务C的阻塞时间就至少为:中断处理时间+任务B的运行时间+任务A的运行时间。
再看有优先级继承的情况:任务C也要使用该资源,但是此时任务A正在使用这个资源,因此任务C进入阻塞,此时由于优先级A会继承任务C的优先级,三个任务的优先级顺序发生了变化,新的优先级顺序为:任务C=任务A>任务B。在任务C进入阻塞之后,某硬件产生了一次中断,唤醒了一个事件,该事件可以解除任务B的阻塞状态。在中断结束后,因为任务A的优先级临时被提高,大于任务B的优先级,所以任务A继续获得CPU权限。任务A完成后,处于高优先级的任务C会接管CPU。所以任务C的阻塞时间为:中断处理时间+任务A的运行时间。看,任务C的阻塞时间变小了,这就是优先级继承的优势。
优先级继承不能解决优先级反转,只能将这种情况的影响降低到最小。硬实时系统在一开始设计时就要避免优先级反转发生。
典型的,两个 Task A、Task B 都要访问同一个资源,他们通过互斥量来做互斥,
首先 A 获取到资源,进入临界区,
此刻 B 去获取资源,由于 A 还没用完资源,所有获取不到,进入阻塞;
A 执行完毕后,释放资源;
资源到位,B 解除阻塞,获取资源;
B 执行完毕,释放资源;
3.2、APIs
创建一个互斥量,使用 xSemaphoreCreateMutex
SemaphoreHandle_t xSemaphoreCreateMutex( void );
- 1
有一个返回值,如果成功创建,返回句柄,否则返回 NULL;
进入/ 退出 互斥量的临界区使用和信号量一样的接口 xSemaphoreTake 和 xSemaphoreGive,
注意:ISR 中使用带 _FromISR 版本的 API
Example:
-
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 );
-
}