1.资源管理概述
1.1 必要性
在多任务系统中,会存在一种潜在的风险。比如,当一个任务正在使用某个资源时,被另一个任务或中断抢占访问该资源,将造成数据损坏。可能存在类似风险的场景有以下几种:
(1)访问外设
- 比如
UART I2C SPI
这些常用的公共资源,存在多个任务共享的情况
举个例子,任务A通过串口向主机发送传感器数据,任务B抢占任务A发送命令请求,这时便破坏了传感器数据的完整性。
(2)读-改-写操作
在代码被编译成汇编语言后,有些寄存器值的修改过程是分多步完成的:①从内存中读到寄存器,②在寄存器中修改数据,③然后再写回内存,即“读-改-写”。因此在这个过程中也有可能被任务或中断抢占破坏这个完整过程。
(3)变量的非原子访问
更新结构体的多个成员变量。
(4)函数重入
被多个任务调用的函数有可能是不可重入函数。
1.2 互斥机制
由于多任务系统存在以上隐患,FreeRTOS
总是希望访问资源的这段代码在执行的时候全程不要被打断,或者即使打断但是其它任务不能访问同样的资源。
全程不被打断其实过于苛刻,所以基本都是专注于能够被打断但是重入后依然没有问题的设计。任务被打断的原因有2个:中断(外部)的到来、调度器中断(内部)调度任务,可以简述为外部中断和任务中断,所以只要解决这2个问题,就可以实现资源互斥访问机制。
- 因为都是中断,所以最粗暴的方式就是直接关闭所有中断(临界区),这样任务中断和外部中断都不能打断这段代码,但是这种方法太粗暴,FreeRTOS应该要实时响应外部中断
- 所以可以只关任务中断而打开外部中断(挂起调度器),这样还是有问题,系统有时候不能关闭任务切换,比如存在周期性任务
- 那么就可以用一种申请-释放的方式访问资源(互斥量),申请的一方即使被打断,后来者也不能访问,可是申请-释放的方式是公用的,容易导致死锁和优先级反转
- 所以原则上不应该相信其它任务可以很好管理资源,于是专门派一个任务负责资源的分配和释放(守护任务),其它任务不能公用,只能间接使用。
通过以上可知,FreeRTOS
提供了4
种特性用于实现互斥机制,分别是临界区、挂起调度器、互斥量和守护任务,各有优缺点,分别应用在不同的场合。
2.资源管理方法
2.1 临界区
一种最为简单粗暴的方法,通过关闭阈值优先级以下的任务来实现互斥,具体实现过程前面讲过。
这种方式的要求是临界区之间的代码要尽可能精简,避免影响 FreeRTOS
的中断响应,应用场景有任务创建、数据连续读取等。
if (CS1259Ready()) //等待AD开始信号
{
taskENTER_CRITICAL(); //进入临界区
Z13Adc = ReadADC();
printf("%d\t\t",Z13Adc);
Z13Res = CalRes(Z13Adc);
printf("%d\r\n",Z13Res);
taskEXIT_CRITICAL(); //退出临界区
vTaskDelay(5/portTICK_RATE_MS);
}
2.2 挂起调度器
挂起调度器使得任务的执行过程不被其它任务打断,但是可以被外部中断打断。
API | 功能 |
---|---|
vTaskSuspendAll() | 挂起调度器 |
xTaskResumeAll() | 唤醒调度器 |
vTaskSuspendAll()
++uxSchedulerSuspended; //计数加1,用于嵌套和标记
xTaskResumeAll()
taskENTER_CRITICAL() //进入临界区
--uxSchedulerSuspended //计数减1,用于嵌套和标记
prvAddTaskToReadyList() //调出就绪任务
taskEXIT_CRITICAL() //退出临界区
vTaskSuspendAll()
挂起调度器只是简单地加1计数,因为这个uxSchedulerSuspended
全局变量会在Systick
中断中使用(具体到xTaskIncrementTick()
函数),如果uxSchedulerSuspended
不为0(挂起),那么xTickCount
不再计数,表达系统心跳暂时停止,于是调度器也不会进行任务切换。
2.3 互斥量
互斥量不需要关闭任何中断,它采用一种申请-释放的方式去访问资源,申请和释放的其实不是资源,而是代表资源的钥匙,在这种方式下,资源与一把钥匙绑定,要想访问资源,必须先拿到这把钥匙,没有钥匙的只能等待前者释放,所以即使拥有钥匙的一方被打断,后者也不能访问资源。
2.4 守护任务
无论是临界区、挂起调度器还是互斥量,它们的使用都带来非常多的问题,核心原因是资源是公用的,资源的所有权和使用权都是公共的,任何一个任务都可以去直接操作,为了解决这个核心原因,可以把资源私有化,所有权和使用权都在一个任务A上面,其它任务只能间接去访问,比如把要写入的数据发给A,由A去写入,把要读的数据要求发给A,由A去读然后返回数据等等。
守护任务的实现不需要什么特性或者机制的支持,是一个协议,设置好任务的代码逻辑后就可以实现,它非常干净利落,把资源私有化后,阻塞等待其它任务的要求
例如穿戴手表中的主机从机之间的数据传输,从机所有传感器的数据都是通过队列发送到守护任务,由守护任务统一发送给主机。
3.全局变量
在嵌入式开发中,难免要使用一些全局变量,如果存在多个任务对同一个全局变量操作,那么将带来共享资源管理问题(上面提到的原子操作问题),即需要对这个全局变量进行保护。
A任务正在使用全局变量S,A任务由于任务切换暂停运行切换到B任务,而B任务也要使用S,这时候B任务修改了S的值。当再次切换到A任务的时候这个变量S就变了,A任务可能就运行出错。
如果存在以上使用场景,可以为全局变量添加互斥保护。即在任务对全局变量进行操作时必须获得互斥量,然后进行读写,读写完后释放信号量。
A任务要使用队列S,先申请,申请成功以后才可以使用。B任务也要使用S的时候也要先申请,当时发现S已经被A任务使用了,所以B任务就没法使用(假设当前的队列长度为1),直到A任务使用完S并且释放掉B任务才申请使用!
这里提到了一种特殊情况:如果一个任务只是对全局变量进行读操作,一个任务只是对全局变量进行写操作,这种情况是是否还要对全局变量添加互斥保护。
- 这种情况最好也添加互斥保护,在程序架构设计时尽量避免这种情况。
4.总结
互斥机制 | 本质 | 优点 | 缺点 |
---|---|---|---|
临界区 | 关闭任务中断和外部中断 | 确保资源访问不可能被打断,资源访问过程简单 | 其余任务停滞、外部中断得不到响应 |
挂起调度器 | 关闭任务中断 | 可以响应外部中断,不能被其它任务打断,资源访问过程简单 | 其余任务停滞 |
互斥量 | 资源需要申请和释放 | 不需要关中断,资源访问过程简单 | 容易出现死锁和优先级反转 |
守护任务 | 资源私有化 | 不再出现以上问题缺点 | 资源访问过程复杂,间接访问可能带来速度和效率问题 |
- 资源一般需要互斥访问,因此需要互斥机制
- FreeRTOS可以实现4种互斥机制,临界区、挂起调度器、互斥量和守护任务
- 4种互斥机制各有优缺点,需要在不同的场合使用