FreeRTOS临界资源保护(临界区保护)

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

  有一种问题相信大家都不愿碰上,就是低概率偶现问题,这类问题的根源,根据作者的经验常见于以下四种情况:

  • 野指针引用
  • 数组越界
  • 栈溢出
  • 临界资源未保护

  今天我们说说其中之一的原因:临界资源未保护。
  我们先看个例子,假如有一个5个节点单向链表,如下结构:

head->1->2->3->4->5->NULL

  有一任务A在一个单向链表的2->3节点之间插入一个新的2a节点,已经将2->2a,还未将2a->3,此时,链表变为两个未完整的部分,如下结构:

head->1->2->2a->?
3->4->5->NULL

  如此时,任务A被更高优先级的任务B抢占了,并任务B开始访问该链表,那么任务B此次访问就会获得一个错误的结果。

  上面的情景就是临界资源未保护导致错误的一种情况,解决这类问题我们通常有两种方法,一种是互斥锁保护,一种是暂时屏蔽中断。
  借助这个例子我们先了解几个与临界资源保护相关的概念。

相关概念

  临界资源
  临界资源指会被多个任务(或中断)访问到的公共资源。包含软件资源和硬件资源,软件资源指全局变量,如上面例子中的链表,也可能是一个整型数。硬件资源指硬件相关的资源,如spi,I2c总线等。临界资源的访问是需要保证完整的,否则就会出现错误的结果。为了描述方便,下文中出现的‘任务’同时指代任务和中断。
  临界区
  会访问临界资源的代码片段,在上面的例子中就是指对链表进行增删改查的代码片段,或对硬件总线的访问的代码片段。
  可重入
  某个函数在执行时可被其他任务打断再次进入执行的函数或代码片段。根据重入性质可将函数分为以下三种类型。

函数(代码段)类型是否引用临界资源是否保护例子
不可重入暂无
任务(线程)安全xTaskNotifyGive(FreeRTOS任务通知函数)
可重入/memcpy

  临界区保护
   通过某种互斥手段保护临界区代码不会被打断及重入,比如屏蔽会引用临界资源的中断,或者进入临界区前先获取互斥锁,操作未完成前其他任务无法进入。
  临界资源保护
   这个好理解,就是保护共享资源的完整性访问,防止出现不完整的数据,一个临界资源的通常具有几个临界区。比如一个链表具有增、删、改、查四个接口,就具有四个临界区,当采用互斥锁(或信号量等)作为保护手段时,同一个临界资源必须使用同一个互斥锁对象进行保护。

如何判断采用何种保护类型

   下表中例举了当前任务被其他任务抢占时的不同场景及应该使用何种互斥方式。

当前任务类型抢占任务类型保护方式/
任务任务互斥锁 或 屏蔽中断
任务中断及任务屏蔽中断
中断中断屏蔽中断

  总的来说,临界区的保护有互斥锁和屏蔽中断两种类型,采用互斥锁需要更多资源,但是可提高的系统的实时性,屏蔽中断需要的资源更少,但是会影响系统的实时性。

如何减少临界资源保护的情形出现

  1. 将公共变量尽可能定义为static
  2. 仅将必要的对外接口定义为extern,其他接口都定义为static
  3. 确认extern接口是否需要进行临界区保护并依据需要执行

如何检查项目中是否存在未进行保护的临界区

  1. 找出临界资源
  2. 查找所有引用该资源的代码
  3. 检查每个代码片段是否进行了必要的保护
基础数据类型的临界区保护

  前面描述的临界资源主要是指复合数据类型,如结构体、链表、i2c总线等。如果临界资源是基本数据类型的话,不一定每个引用的代码片段都需要进行保护的,如对一个整型数的读取是不需要进行保护的,因为读取一个整形数只需一个时钟周期即可完成,不存在被打断的情况,一个时钟周期已经是cpu的最小执行单位了。对基本数据类型的写操作也不一定需要进行保护,如FreeRTOS中挂起所有任务的函数就没有进行保护:

void vTaskSuspendAll( void )
{
	++uxSchedulerSuspended;
}

  因为即使被打断,并高优先级任务会访问该变量,那也会是一次“零和访问”。即高级任务访问结束后该变量的值还是会与打断低级任务时的值时一样的,也就是高级任务对该变量的所有操作的和为零。在FreeRTOS中vTaskSuspendAllxTaskResumeAll是成对访问的,一个执行++,一个执行- -,和为0。
  哪种情况需要进行保护呢? 非“零和访问”操作就需要保护,实际应用场景中的多数是非“零和访问”的,基础类型的保护手段比复合类型的多一种,这种方式叫原子操作。原子操作时由芯片指令支持的操作,可确保一次“读-改-写”过程不会被打断,如ARMv6(cortex-m3核使用的armv7-m架构是支持的)及以上就提供了ldrex和strex这对指令来实现原子操作,当cpu执行了ldrex从内存加载寄存器后该内存地址就被标记为独占状态,直到执行了strex指令将数据存回内存中。

### FreeRTOS临界区保护方法及实现机制 #### 一、什么是临界区 临界段代码也称为临界区,用于保护那些必须完整执行,不能被打断的代码段[^1]。 #### 二、FreeRTOS中的临界区保护方式 在FreeRTOS中,进入临界段时会关闭中断,当退出临界段时又重新打开中断。这种操作确保了在同一时刻只有一个任务能够访问共享资源或执行特定的操作序列。 对于任务级别的临界区,可以使用`taskENTER_CRITICAL()` 和 `taskEXIT_CRITICAL()` 函数来包围需要被保护的部分: ```c void exampleTaskFunction(void *pvParameters) { // 进入临界区前的状态保存 portBASE_TYPE uxSavedInterruptStatus; // 关闭中断并记录当前状态 uxSavedInterruptStatus = taskENTER_CRITICAL(); { // 此处放置要保护的关键代码 } // 恢复之前的中断状态 taskEXIT_CRITICAL(uxSavedInterruptStatus); } ``` 而对于中断服务程序内的临界区,则应采用专门针对ISR环境设计的一对宏——`taskENTER_CRITICAL_FROM_ISR()` 和 `taskEXIT_CRITICAL_FROM_ISR()` 来管理: ```c // 假设这是某个硬件定时器触发后的中断处理函数 void vAnExampleISRHandler(void) { BaseType_t xHigherPriorityTaskWoken; uint32_t ulOriginalState; // 获取并存储当前的中断屏蔽位设置 ulOriginalState = taskENTER_CRITICAL_FROM_ISR(); { // 执行必要的实时响应动作... } // 返回原始的中断使能配置 taskEXIT_CRITICAL_FROM_ISR(ulOriginalState); // 如果在此期间唤醒了一个更高优先级的任务,则传递给调度器此信息 if (xHigherPriorityTaskWoken != pdFALSE ) { portYIELD_FROM_ISR(xHigherPriorityTaskWoken ); } } ``` 上述例子展示了如何通过临时禁用全局中断的方式来创建一个安全区域,在这个区域内不会发生抢占式的上下文切换事件,从而保障数据一致性以及算法逻辑正确性[^4]。 此外,除了直接控制中断外,FreeRTOS还提供了其他几种同步原语如信号量(semaphore),互斥锁(mutex)等工具帮助开发者更灵活有效地构建复杂的并发控制系统[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值