多个任务都想获取某一个变量的状态,这时应该怎么办?如何实现互斥访问?
全局变量
我们可以在这个c文件下定义一个全局变量,多个函数都可以获取这个变量的状态,从而完成自己的功能逻辑。但是这种多个任务同时操作共享资源的操作是不安全的!比如:从汇编的角度来讲(num++分成三步:读取到寄存器R0,R0进行自加运算,内存再读取回去),一旦在这三步中间被打断,这就是不安全的数据传输。
环形缓冲区
因此,我们引入环形缓冲区(RingBuffer),它的核心是使用双指针w和r表示下一个读/写位置,生产者读操作时只对r指针进行修改,消费者写操作只对w指针进行修改,这样就是安全的。
同时,环形缓冲区还有另外的优点。
解耦生产者和消费者:环形缓冲区作为生产者(producer)和消费者(consumer)之间的一个中间层,解耦了它们之间的直接依赖。生产者不需要等待消费者处理完数据后再继续工作,同样,消费者也不需要等待生产者产生数据。
但是,环形缓冲区还是不够好。
- 如果多个任务都想来操作环形缓冲区的读、写指针怎么办?
- 如果任务A完成某些实际操作,而任务B只是等待某个状态,那么每次任务B都获得与任务A相同的运行时间,是否对CPU资源造成一种浪费?
队列
FreeRTOS的队列机制相比于全局变量和环形缓冲区,拥有互斥措施和阻塞-唤醒机制,效率更高。
- 互斥措施
- 互斥锁(Mutex)保护:FreeRTOS的队列操作通过互斥量(Mutex)来实现对队列的互斥访问。当一个任务在操作队列时,它可以通过获取互斥锁来确保在其操作完成前,其他任务不能同时访问此队列。这种机制避免了多个任务同时读写队列可能带来的数据不一致问题,确保了队列操作的原子性。
- 读写操作的完全性:每个读写操作都有明确的开始和结束点。在操作进行中,该任务拥有对队列的独占访问权,这直接体现了互斥原则。
- 消息传递的一致性:由于队列操作是互斥的,无论何时一个任务向队列中写入数据或从队列中读出数据,这个操作都是不可被打断的。这样一来,数据的一致性得到了保证,不会像环形缓冲区那样需要额外的机制来确保数据在读写时的一致性。
- 阻塞-唤醒机制
- 当一个任务试图从队列中读取数据,但队列为空时,该任务可以选择进入阻塞状态直到有数据可用。类似地,如果一个任务试图向已满的队列中写入数据,它也可以选择阻塞直到队列中有空间可用。
- 假如有任务A和任务B,任务B要读取队列,如果此时队列为空,则任务B将从ReadyList链表里移除,放入队列的recvlist和DelayedList,进入阻塞状态;
接下来就分为两种情况:- (1)任务A唤醒任务B。任务A运行,写队列,并且唤醒队列的recvlist里的第一个任务(也就是任务B),把任务B从队列的recvlist和DelayedList这两个链表里删除,重新放入ReadyList链表。等任务B可以运行的时候,就可以读队列。
- (2)超时唤醒。Tick中断每一个Tick都来判断一下是否有超时任务,等到任务B的超时时间到了,同样将任务B从那两个链表里删除,唤醒它,放入到ReadyList链表。任务B运行的时候就会返回一个错误表示没有读到队列。
- 假如有任务A和任务B,任务B要读取队列,如果此时队列为空,则任务B将从ReadyList链表里移除,放入队列的recvlist和DelayedList,进入阻塞状态;
- 系统资源管理与优化
在队列创建时,FreeRTOS动态分配一块连续内存来存储队列控制块及消息空间,有效管理内存资源。这种管理方式比静态分配的环形缓冲区更加灵活高效,有助于系统资源的整体优化。
总之,队列也是为了实现多任务互斥访问共享资源的一个机制。