FreeRTOS-使用计数信号量
1 二值信号量的无法记录事件次数的问题:
文章FreeRTOS-使用二值信号量的最后,描述了一个使用二值信号量,在中断处理函数和任务间进行“事件”同步的例子。
我们使用按键作为“中断源”,每当按键按下时,设置信号量。任务button_pressed_handler
等待信号量(等待按键事件发生),一旦信号量可用,表示等待的“事件”(按键按下)发生了,则做一些相关的工作。
但这个例子存在一个问题:
第一次按键事件发生后(中断处理函数中设置了信号量,信号量变为可用
状态),任务button_pressed_handler
获得信号量(信号量被获取后,由可用状态再次变为不可用状态
),恢复运行,并开始执行一些相关处理动作。
如果任务button_pressed_handler
在执行相关处理过程中,又连续发生了第二次、第三次按键动作(又触发了两次事件)。
由于二值信号量,只有可用
和不可用
状态,第二次按键事件发生时,信号量由不可用
变为可用
状态。仅接着的第三次按键事件发生后,中断处理函数中同样也是设置信号量,由于当前信号量已经是可用
状态,所以设置操作并未起作用,信号量依旧保持可用状态
。
当任务button_pressed_handler
相关处理操作执行完成后,再次获取信号量,由于当前信号量为可用
状态,因此可以立刻获取到信号量,并再次执行相关处理工作。但是,此时对于任务button_pressed_handler
来说,它只感知到 “事件”发生了一次,但实际上在处理之前已经发生了两次“事件”。上述过程的时间线如下图所示:
下面我们用代码复现上述问题,我们使用 vTaskDelay(pdMS_TO_TICKS(2000))
让任务button_pressed_handler
在获取到信号量后等待2 秒,来模拟任务button_pressed_handler
在处理相关工作。
任务代码为:
1 | void button_pressed_handler( void *pvParameters ) { |
按键中断和main函数与文章FreeRTOS-使用二值信号量中的一致:
1 |
|
运行代码后,我们在2秒内,连续按下三次按键,可以发现任务只处理了两次事件:
1 | start FreeRTOS |
上述这种情况,就是二值信号量存在的“弊端”,即无法记录“事件”发生的次数。如果上面例子中“事件”发生的“次数”也是一个重要的数据(例如,每次发生一次“事件”,任务button_pressed_handler
都必须有一次对应的处理),那么二值信号量就不再适用。
因此,二值信号量适用于事件发生频率“较低”的场景中,开发者能确定事件发生后,在后续的处理过程中,事件不会在此期间内,再发生超过一次。
更简单的情形,就是应用场景不关心事件发生的次数,只关心有没有发送过。这种场景下也可以使用二值信号量。
2 计数信号量
由于二值信号量
的局限性,一般提供信号量功能的系统同时会提供计数信号量
功能,其功能和二值信号量一致,但是多了计数功能。上文的问题,使用计数信号量
就可以记录下按键事件发生的次数。
计数信号量的设置和获取API 和二值信号量是一样的,区别只是在创建API:
1 | SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, |
- uxMaxCount:设置计数信号量在被获取前,可以保留的的最大计数值(以上文的例子解释,就是在任务
button_pressed_handler
执行处理工作期间,按键只要按下的次数小于uxMaxCount
,信号量都可以记录下来)。 - 指定创建后的初始值(例如,如果指定为0,信号量创建后就是不可用状态,调用
xSemaphoreTake
获取信号量时,就会让任务阻塞。如果指定为2,创建完成后,立刻连续调用2 次xSemaphoreTake
,都能成功获取到信号量)。
二值信号量一文中,我们解释了FreeRTOS中的二值信号量,实际是用一个长度为1 的消息队列来实现的。 同理,FreeRTOS的计数信号量
也是使用消息队列来实现的,只是消息队列的大小为xSemaphoreCreateCounting
API 创建计数信号量
时指定的uxMaxCount
值。消息队列的大小,即表示信号量被获取前最大可以记录的“事件”发生次数。(同样,如文章二值信号量中所述,这里创建的大小为uxMaxCount
的消息队列,实际上除了消息队列的控制块占用内存,消息本身是不占内存的,因为内部指定的每个消息大小为0,所以消息存储所占内存为大小为uxMaxCount * 0 = 0
)
对于计数信号量的设置与获取,原理也是和二值信号量一样:
- 使用信号量设置API
xSemaphoreGive
,就是向计数信号量
表示的消息队列中放入一个消息(即信号量计数值加1。如果消息队列已经满了,即已经达到计数信号量能记录的最大值uxMaxCount
,则设置不生效,信号量仍旧保持最大计数值状态)。 - 使用信号量获取API
xSemaphoreTake
,就是从计数信号量
表示的消息队列中提取一个消息(只要消息队列中存在消息,就能立刻成功获取到信号量,成功获取后,计数信号量的计数值会减1。如果消息队列为空,则表示信号量不可用,会阻塞当前任务,直到信号量可用,或到达设置的等待超时时间)。
我们使用计数信号量
来改造上文的按键事件同步的例子,需要改动的代码很少,使用计数信号量
首先需要在工程配置文件FreeRTOSConfig.h
中添加使用计数信号量
的宏定义:
1 |
之后,修改上文中的main 函数,将创建信号量API,替换成xSemaphoreCreateCounting
即可:
1 |
|
程序运行后,在2秒内连续按键三次,可以发现按键按下了三次,任务button_pressed_handler
也执行了三次处理。如下所示:
1 | start FreeRTOS |
程序的执行时间线如下图所示:
3 使用计数信号量管理有限数量的资源
目前为止,我们使用的二值信号量
以及计数信号量
,都是用来作为任务和任务之间(或中断处理程序和任务)同步的工具。这种使用方式有一个明显的特征:任务a
中使用xSemaphoreTake
来等待信号量(等待某事件A
发生),另一个任务b
(或中断处理程序)中在事件A
发生时使用xSemaphoreGive
设置信号量(告诉任务a
事件发生了)。
但信号量
不仅可以用来在多个任务间“同步事件”,还可以用来管理资源。
假设,我们有一个系统,该系统会收到外部访问请求,每次请求都会访问系统内部的某个资源Z
。
假设我们有2 个资源Z
,每次收到请求时,可以访问任意一个资源Z
,假设访问资源Z
需要1秒
时间,资源Z 在被访问期间内是不允许再被另一个请求同时访问的。
当外部访问请求频率很低时,如1s一次,每次外部请求都能立刻访问到某个资源Z
(2个中的任意一个)。
但某个时刻(1秒钟内),突然有3个外部请求,由于系统只有2个资源Z
,因此,只能有2个请求能立刻访问到资源Z
,而第三个请求必须等前两个请求中的任意一个访问结束,才能去访问资源Z
。
诸如上述情况,对有限资源的互斥访问,就可以使用计数信号量
来管理。
在创建计数信号量
的API 中:
1 | SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount, |
第二个值指示了信号量创建完成后,它的初始计数值,即表示我们有几个资源Z
可用。
任何请求,每次访问资源Z
前都要使用xSemaphoreTake
先获得信号量,能成功获取到信号量则表示当前还有资源Z
可供访问,如果获取不到,就表明没有资源Z
可以访问了。
当获取到信号量后,开始访问资源Z
,访问结束后,需要使用xSemaphoreGive
设置信号量,可以理解为将获取的资源Z
还回去。这样其它请求到来时,才能获取到信号量(只有成功获取到信号量后,才能访问资源)。
对于前面访问资源Z
的例子,我们就可以将xSemaphoreCreateCounting
第二个参数uxInitialCount
设置为2
。这样,信号量创建完成后的计数状态就是2,表示有2 个资源Z
可用。
某个时刻,同时来了3个对资源Z
的访问请求:req1
、req2
、req3
。
对于每个请求,访问资源前,先使用xSemaphoreTake
获取信号量,由于当前信号量的计数状态为2,req1
和req2
两个请求都可以立刻获取到信号量,从而开始访问资源Z
。但当req3
请求获取信号量时,此时信号量的计数状态为0,则获取不到信号量,任务就会阻塞。直到req1
和req2
中的任意一个访问资源Z
完成后,重新设置了信号量,req3
才能成功获取到信号量,恢复运行并访问资源Z
。
该过程时间线大致如下图所示:
需要再次强调的一点是:对资源Z的访问,要求是互斥访问。即这里虽然有两个全局共享的资源Z
,但对任意一个资源Z
的访问都需要是互斥的,不允许两个任务同时访问同一个资源Z,所以才需要使用信号量对访问进行管理,如果资源本身允许多个任务同时访问,那就不需要对访问进行管理了。
最后,我们使用代码来模拟上述过程:
首先创建一个信号量来管理对共享资源Z
的访问:
1 |
|
将对资源的访问进行封装,访问前需要先“获取”信号量,访问完成后需要再重新“设置”信号量,即返还资源。
1 |
|
再创建三个任务,来访问资源Z
。简单起见,这三个任务的代码都是一致的。可以只实现一个,创建三次,这里为了更清晰,写了三份任务代码:
1 |
|
最后main函数实现:
1 |
|
程序结果如下所示,task_c由于最后运行,当其请求访问资源时,由于仅有的两个资源正在被task_a和task_b访问中,task_c这时是无法获得到信号量的,将被阻塞在请求信号量函数xSemaphoreTake
内部。当前面两个任务中任意一个访问结束后,返还了信号量,这时任务task_c就可以访问资源了:
1 | start FreeRTOS |
ps:需要注意文章代码中的日志输出函数,产品代码中如果需要使用的话,需要考虑线程安全性(多任务安全性),因为中断/任务切换可能发生在另一个任务正在输出日志但还未输出完的时候,这就可能造成日志错乱
FreeRTOS交流QQ群-663806972