有一种问题相信大家都不愿碰上,就是低概率偶现问题,这类问题的根源,根据作者的经验常见于以下四种情况:
- 野指针引用
- 数组越界
- 栈溢出
- 临界资源未保护
今天我们说说其中之一的原因:临界资源未保护。
我们先看个例子,假如有一个5个节点单向链表,如下结构:
有一任务A在一个单向链表的2->3节点之间插入一个新的2a节点,已经将2->2a,还未将2a->3,此时,链表变为两个未完整的部分,如下结构:
如此时,任务A被更高优先级的任务B抢占了,并任务B开始访问该链表,那么任务B此次访问就会获得一个错误的结果。
上面的情景就是临界资源未保护导致错误的一种情况,解决这类问题我们通常有两种方法,一种是互斥锁保护,一种是暂时屏蔽中断。
借助这个例子我们先了解几个与临界资源保护相关的概念。
临界资源
临界资源指会被多个任务(或中断)访问到的公共资源。包含软件资源和硬件资源,软件资源指全局变量,如上面例子中的链表,也可能是一个整型数。硬件资源指硬件相关的资源,如spi,I2c总线等。临界资源的访问是需要保证完整的,否则就会出现错误的结果。为了描述方便,下文中出现的‘任务’同时指代任务和中断。
临界区
会访问临界资源的代码片段,在上面的例子中就是指对链表进行增删改查的代码片段,或对硬件总线的访问的代码片段。
可重入
某个函数在执行时可被其他任务打断再次进入执行的函数或代码片段。根据重入性质可将函数分为以下三种类型。
| 函数(代码段)类型 | 是否引用临界资源 | 是否保护 | 例子 |
|---|---|---|---|
| 不可重入 | 有 | 无 | 暂无 |
| 任务(线程)安全 | 有 | 有 | xTaskNotifyGive(FreeRTOS任务通知函数) |
| 可重入 | 无 | / | memcpy |
临界区保护
通过某种互斥手段保护临界区代码不会被打断及重入,比如屏蔽会引用临界资源的中断,或者进入临界区前先获取互斥锁,操作未完成前其他任务无法进入。
临界资源保护
这个好理解,就是保护共享资源的完整性访问,防止出现不完整的数据,一个临界资源的通常具有几个临界区。比如一个链表具有增、删、改、查四个接口,就具有四个临界区,当采用互斥锁(或信号量等)作为保护手段时,同一个临界资源必须使用同一个互斥锁对象进行保护。
下表中例举了当前任务被其他任务抢占时的不同场景及应该使用何种互斥方式。
| 当前任务类型 | 抢占任务类型 | 保护方式 | / |
|---|---|---|---|
| 任务 | 任务 | 互斥锁 或 屏蔽中断 | |
| 任务 | 中断及任务 | 屏蔽中断 | |
| 中断 | 中断 | 屏蔽中断 |
总的来说,临界区的保护有互斥锁和屏蔽中断两种类型,采用互斥锁需要更多资源,但是可提高的系统的实时性,屏蔽中断需要的资源更少,但是会影响系统的实时性。
如何减少临界资源保护的情形出现
- 将公共变量尽可能定义为static
- 仅将必要的对外接口定义为extern,其他接口都定义为static
- 确认extern接口是否需要进行临界区保护并依据需要执行
如何检查项目中是否存在未进行保护的临界区
- 找出临界资源
- 查找所有引用该资源的代码
- 检查每个代码片段是否进行了必要的保护
前面描述的临界资源主要是指复合数据类型,如结构体、链表、i2c总线等。如果临界资源是基本数据类型的话,不一定每个引用的代码片段都需要进行保护的,如对一个整型数的读取是不需要进行保护的,因为读取一个整形数只需一个时钟周期即可完成,不存在被打断的情况,一个时钟周期已经是cpu的最小执行单位了。对基本数据类型的写操作也不一定需要进行保护,如FreeRTOS中挂起所有任务的函数就没有进行保护:
void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
因为即使被打断,并高优先级任务会访问该变量,那也会是一次“零和访问”。即高级任务访问结束后该变量的值还是会与打断低级任务时的值时一样的,也就是高级任务对该变量的所有操作的和为零。在FreeRTOS中vTaskSuspendAll和xTaskResumeAll是成对访问的,一个执行++,一个执行- -,和为0。
哪种情况需要进行保护呢? 非“零和访问”操作就需要保护,实际应用场景中的多数是非“零和访问”的,基础类型的保护手段比复合类型的多一种,这种方式叫原子操作。原子操作时由芯片指令支持的操作,可确保一次“读-改-写”过程不会被打断,如ARMv6(cortex-m3核使用的armv7-m架构是支持的)及以上就提供了ldrex和strex这对指令来实现原子操作,当cpu执行了ldrex从内存加载寄存器后该内存地址就被标记为独占状态,直到执行了strex指令将数据存回内存中。

本文探讨了在FreeRTOS中临界资源未保护可能导致的问题,通过一个链表操作的例子解释了临界区的概念。介绍了临界资源、临界区和可重入等概念,并提出了互斥锁和屏蔽中断两种保护方法。同时,文章建议减少临界资源的使用,检查并确保关键操作的保护,以及在某些情况下使用原子操作来确保基础数据类型的完整性。
662

被折叠的 条评论
为什么被折叠?



