FreeRTOS五种方式传递信号(队列,信号量,互斥量,事件组,任务通知)
FreeRTOS提供了五种通信方式来传递信号,包括队列、信号量、互斥量、事件组和任务通知。队列适用于数据传输,信号量用于状态传递和资源保护,互斥量针对临界资源访问控制,事件组能组合多个事件标志,任务通知则是一种快速但单向的通知机制。这些方法提高了多任务环境下的效率和安全性。
在裸机编程中,一般信号传递都是全局变量,数组或者是返回值函数等等其他方式,在freertos中,我们不再使用裸机编程方式,而是使用FreeRTOS 内核通信的资源功能,有五种选择方式传递信号量,更加规范的,安全,传递(通知)信号,提高效率。
1、队列(queue)
在实际的应用中,常常会遇到一个任务或者中断服务需要和另外一个任务进行“沟通交流”,这个“沟通交流”的过程其实就是消息传递的过程。在没有操作系统的时候两个应用程序进行消息传递一般使用全局变量的方式,但是如果在使用操作系统的应用中用全局变量来传递消息就会涉及到“资源管理”的问题。 FreeRTOS 对此提供了一个叫做“队列”的机制来完成任务与任务、任务与中断之间的消息传递。
队列是为了任务与任务、任务与中断之间的通信而准备的,可以在任务到任务、任务到中断、中断到任务之间传递消息,队列中可以存储有限的、大小固定的数据项目。队列的常规操作(简化):
队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
每个数据大小固定
创建队列时就要指定长度、数据大小
数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读,也可以使用 LIFO 的存储缓冲,也就是后进先出
也可以强制写队列头部:覆盖头部数据
2、信号量(semaphore)
信号量只需要维护一个数值,使用信号量效率更高、更节省内存
信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步, FreeRTOS中信号量又分为二值信号量、 计数型信号量、互斥信号量和递归互斥信号量。不同的信号量其应用场景不同,但有些应用场景是可以互换着使用的。
信号量常常用于控制对共享资源的访问和任务同步。举一个很常见的例子,某个停车场有100 个停车位,这 100 个停车位大家都可以用,对于大家来说这 100 个停车位就是共享资源。假设现在这个停车场正常运行,你要把车停到这个这个停车场肯定要先看一下现在停了多少车了?还有没有停车位?当前停车数量就是一个信号量,具体的停车数量就是这个信号量值,当这个值到 100 的时候说明停车场满了。停车场满的时你可以等一会看看有没有其他的车开出停车场,当有车开出停车场的时候停车数量就会减一,也就是说信号量减一,此时你就可以把车停进去了,你把车停进去以后停车数量就会加一,也就是信号量加一。 这就是一个典型的使用信号量进行共享资源管理的案例,在这个案例中使用的就是计数型信号量。再看另外一个案例:使用公共电话,我们知道一次只能一个人使用电话,这个时候公共电话就只可能有两个状态:使用或未使用,如果用电话的这两个状态作为信号量的话,那么这个就是二值信号量。信号量用于控制共享资源访问的场景相当于一个上锁机制, 代码只有获得了这个锁的钥匙才能够执行。
信号量的另一个重要的应用场合就是任务同步,用于任务与任务或中断与任务之间的同步。 在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了, 当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函数里面不能放太多的代码,否则的话会影响的中断的实时性。 裸机编写中断服务函数的时候一般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在使用 RTOS 系统的时候我们就可以借助信号量完成此功能, 当中断发生的时候就释放信号量,中断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间非常短。 这个例子就是中断与任务之间使用信号量来完成同步,当然了, 任务与任务之间也可以使用信号量来完成同步。
2.1、二值信号量
二值信号量通常用于互斥访问或同步, 二值信号量和互斥信号量非常类似,但是还是有一些细微的差别, 互斥信号量拥有优先级继承机制, 二值信号量没有优先级继承。 因此二值信号另更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。
在实际应用中通常会使用一个任务来处理 MCU 的某个外设,比如网络应用中,一般最简单的方法就是使用一个任务去轮询的查询 MCU 的 ETH(网络相关外设,如 STM32 的以太网MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费CPU 资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任务就进入阻塞态,把 CPU 让给其他的任务,当有数据的时候网络任务才去执行。现在使用二值信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入阻塞态,而网络中断服务函数(大多数的网络外设都有中断功能,比如 STM32 的 MAC 专用 DMA中断,通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络数据,网络任务可以去提取处理了。 网络任务只是在一直的获取二值信号量,它不会释放信号量,而中断服务函数是一直在释放信号量,它不会获取信号量。
2.2、计数型信号量
有些资料中也将计数型信号量叫做数值信号量, 二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列。 同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可。 计数型信号量通常用于如下两个场合:
(1)事件计数
在这个场合中, 每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一,信号量值就是队列结构体成员变量uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为 0。
(2)资源管理
在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100。
2.3、互斥信号量
(1)优先级翻转
在使用二值信号量的时候会遇到很常见的一个问题——优先级翻转,优先级翻转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果,图 14.6.1 就是一个优先级翻转的例子。

(1) 任务 H 和任务 M 处于挂起状态,等待某一事件的发生,任务 L 正在运行。
(2) 某一时刻任务 L 想要访问共享资源,在此之前它必须先获得对应该资源的信号量。
(3) 任务 L 获得信号量并开始使用该共享资源。
(4) 由于任务 H 优先级高,它等待的事件发生后便剥夺了任务 L 的 CPU 使用权。
(5) 任务 H 开始运行。
(6) 任务 H 运行过程中也要使用任务 L 正在使用着的资源,由于该资源的信号量还被任务L 占用着,任务 H 只能进入挂起状态,等待任务 L 释放该信号量。
(7) 任务 L 继续运行。
(8) 由于任务 M 的优先级高于任务 L,当任务 M 等待的事件发生后,任务 M 剥夺了任务L 的 CPU 使用权。
(9) 任务 M 处理该处理的事。
(10) 任务 M 执行完毕后,将 CPU 使用权归还给任务 L。
(11) 任务 L 继续运行。
(12) 最终任务 L 完成所有的工作并释放了信号量,到此为止,由于实时内核知道有个高优先级的任务在等待这个信号量,故内核做任务切换。
(13) 任务 H 得到该信号量并接着运行。
在这种情况下,任务 H 的优先级实际上降到了任务 L 的优先级水平。因为任务 H 要一直等待直到任务 L 释放其占用的那个共享资源。由于任务 M 剥夺了任务 L 的 CPU 使用权,使得任务 H 的情况更加恶化,这样就相当于任务 M 的优先级高于任务 H,导致优先级翻转。
(2)互斥量
互斥信号量其实就是一个拥有优先级继承的二值信号量, 在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。 互斥信号量适合用于那些需要互斥访问的应用中。 在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的特性。 当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级, 这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。
优先级继承并不能完全的消除优先级翻转, 它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,原因如下:
● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
用于临界资源(比如:串口就是临界资源)的保护一般建议使用互斥量。
用于互锁的互斥量可以充当保护资源的令牌,当一个任务希望访问某个资源时,它必须先获取令牌。当任务使用完资源后,必须还回令牌,以便其它任务可以访问该资源。是不是很熟悉,在我们的二值信号量里面也是一样的,用于保护临界资源,保证多任务的访问井然有序。当任务获取到信号量的时候才能开始使用被保护的资源,使用完就释放信号量,下一个任务才能获取到信号量从而可用使用被保护的资源。但是信号量会导致的另一个潜在问题,那就是任务优先级翻转(具体会在下文讲解)。而 FreeRTOS 提供的互斥量可以通过优先级继承算法,可以降低优先级翻转问题产生的影响,
3、事件组(event group)
前面学习了使用信号量来完成同步,但是使用信号量来同步的话任务只能与单个的事件或任务进行同步。有时候某个任务可能会需要与多个事件或任务进行同步,此时信号量就无能为力了。 FreeRTOS 为此提供了一个可选的解决方法,那就是事件标志组。
一个 32 位的事件集合(EventBits_t 类型的变量,实际可用与表示事件的只有 24 位),其中的每一位都可以表示一个事件,每一位事件的含义由程序员决定,比如:Bit0 表示用来串口是否就绪,Bit1 表示按键是否被按下。
(1)事件位(事件标志)
事件标志是一个用于指示事件是否发生的布尔值,一个事件标志只有 0 或 1 两种状态,事件位用来表明某个事件是否发生, 事件位通常用作事件标志, 比如下面的几个例子:
当收到一条消息并且把这条消息处理掉以后就可以将某个位(标志)置 1,当队列中没有消息需要处理的时候就可以将这个位(标志)置 0。
当把队列中的消息通过网络发送输出以后就可以将某个位(标志)置 1,当没有数据需要从网络发送出去的话就将这个位(标志)置 0。
现在需要向网络中发送一个心跳信息,将某个位(标志)置 1。现在不需要向网络中发送心跳信息,这个位(标志)置 0。
(2)事件组
FreeRTOS 将多个事件标志储存在一个变量类型为 EventBits_t 变量中,这个变量就是事件组。事件组是一组事件标志的集合,一个事件组就包含了一个 EventBites_t 数据类型的变量。
一个事件组就是一组的事件位, 事件组中的事件位通过位编号来访问,同样,以上面列出的三个例子为例:
事件标志组的 bit0 表示队列中的消息是否处理掉。
事件标志组的 bit1 表示是否有消息需要从网络中发送出去。
事件标志组的 bit2 表示现在是否需要向网络发送心跳信息。
(3)事件标志组和事件位的数据类型
事件标志组的数据类型为 EventGroupHandle_t, 当 configUSE_16_BIT_TICKS 为 1 的时候事件标志组可以存储 8 个事件位,当 configUSE_16_BIT_TICKS 为 0 的时候事件标志组存储 24个事件位。
事件标志组中的所有事件位都存储在一个无符号的 EventBits_t 类型的变量中, EventBits_t在 event_groups.h 中有如下定义:
typedef TickType_t EventBits_t;
数据类型 TickType_t 在文件 portmacro.h 中有如下定义:
#if( configUSE_16_BIT_TICKS == 1 ) typedef uint16_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffff #else typedef uint32_t TickType_t; #define portMAX_DELAY ( TickType_t ) 0xffffffffUL #define portTICK_TYPE_IS_ATOMIC 1 #endif
可以看出当 configUSE_16_BIT_TICKS 为 0 的时候 TickType_t 是个 32 位的数据类型, 因此 EventBits_t 也是个 32 位的数据类型。 EventBits_t 类型的变量可以存储 24 个事件位,另外的那高 8 位有其他用。 事件位 0 存放在这个变量的 bit0 上,变量的 bit1 就是事件位 1,以此类推。对于 STM32 来说一个事件标志组最多可以存储 24 个事件位,如图 16.1.1 所示:

FreeRTOS 将这个 EventBits_t 数据类型的变量拆分成两部分,其中低 24 位[23:0](configUSE_16_BIT_TICKS 配置位 1 时,是低 8 位[7:0])用于存储事件标志。而高 8 位[31:24](configUSE_16_BIT_TICKS 配置位 1 时,依然是高 8 位[15:8])用作存储事件标志组的一些控制信息。一个事件组最多可以存储 24 个事件标志。
4、任务通知(Task Notifications)
在 FreeRTOS 中,每一个任务都有两个用于任务通知功能的数组,分别为 " 任务通知数组 " 和 " 任务通知状态数组 " 。
优点:消息通知虽然处理更快,RAM 开销更小。
缺点:只能有一个任务接收通知消息,因为必须指定接收通知的任务;只有等待通知的任务可以被阻塞,发送通知的任务,在任何情况下都不会因为发送失败而进入阻塞态

浙公网安备 33010602011771号