本篇应该算是这一阶段FreeRTOS源码阅读的最后一篇了,虽然还有一些想细看的,比如任务通知,PLUS里的命令交互还有TCP的协议栈甚至LWIP到FreeRTOS的移植,但是原计划是5月份开始重新整理系统学习linux,所以不得不收一收,也给点时间沉淀一下这部分,人生漫漫,学海茫茫,不可能所有感兴趣的东西都能有时间有精力去看,找准方向,一深多广才行。
这段时间重新找回了一些学习的感觉,就是觉得上班之后的心,变的浮躁了,当初在学校学的东西很多也还给老师之后,再拿回来有点慢。还是要理一理,静一静,做技术,总是要不断学习不断进步才行。保持不动其实就是落后。
最近组里新来了一个94年的小伙子,很强,让我有些汗颜,自己这么多年好像都浪费了,长江后浪推前浪,新人只会越来越优秀越年轻,如果我们没有什么沉淀的东西,恐怕是连想平平淡淡过一生就做不到,会被社会抛弃掉。
好了,又扯远了。
最近受疫情影响,想的比较多,还是收收。
进入正题。
在FreeRTOS中,当遇到一些并发的需求时,不可避免的要用到临界区或者信号量来保证资源的正常使用。
先给结论
互斥量、信号量、临界区,都是什么情况下使用?
个人看法:
下面具体来说。
临界资源指会被多个任务(或中断)访问到的公共资源。包含软件资源和硬件资源。
临界区指会访问临界资源的代码片段。
FreeRTOS临界区使用起来比较简单,主要是用来保护临界段的代码运行不被中断。
进出的接口是:
taskENTER_CRITICAL()
taskEXIT_CRITICAL()
中断函数中的接口
taskENTER_CRITICAL_FROM_ISR()
taskEXIT_CRITICAL_FROM_ISR()
临界区的实现非常简单,就是记录层数,开启/关闭中断。
以taskENTER_CRITICAL为例,taskENTER_CRITICAL就是宏portENTER_CRITICAL,也就是宏定义vPortEnterCritical。
相关的代码如下:
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
/* This is not the interrupt safe version of the enter critical function so
assert() if it is being called from an interrupt context. Only API
functions that end in "FromISR" can be used in an interrupt. Only assert if
the critical nesting count is 1 to protect against recursive calls if the
assert function also uses a critical section. */
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
进出临界区的操作:
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
临界区要注意的就是,临界区不是屏蔽了所有的中断。
见源码,vPortEnterCritical 调用 portDISABLE_INTERRUPTS(也就是vPortRaiseBASEPRI)来实现关闭中断,我们来看看做了什么。
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
static portFORCE_INLINE void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
msr basepri, ulNewBASEPRI
dsb
isb
}
}
刚才看到临界区进出函数都有uxCriticalNesting来记录层数,但是中断函数中的临界区进出却没有这个记录,为什么?
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
/* Barrier instructions are not used as this function is only used to
lower the BASEPRI value. */
msr basepri, ulBASEPRI
}
}
这里没有使用uxCriticalNesting 来记录层数,而是通过保存和恢复寄存器 basepri 的数值来实现嵌套使用。
看官方给的一个demo。
Example usage:
/* A function called from an ISR. */
void vDemoFunction( void )
{
UBaseType_t uxSavedInterruptStatus;
/* Enter the critical section. In this example, this function is itself called from
within a critical section, so entering this critical section will result in a nesting
depth of 2. Save the value returned by taskENTER_CRITICAL_FROM_ISR() into a local
stack variable so it can be passed into taskEXIT_CRITICAL_FROM_ISR(). */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* Perform the action that is being protected by the critical section here. */
/* Exit the critical section. In this example, this function is itself called from a
critical section, so interrupts will have already been disabled before a value was
stored in uxSavedInterruptStatus, and therefore passing uxSavedInterruptStatus into
taskEXIT_CRITICAL_FROM_ISR() will not result in interrupts being re-enabled. */
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
}
/* A task that calls vDemoFunction() from within an interrupt service routine. */
void vDemoISR( void )
{
UBaseType_t uxSavedInterruptStatus;
/* Call taskENTER_CRITICAL_FROM_ISR() to create a critical section, saving the
returned value into a local stack variable. */
uxSavedInterruptStatus = taskENTER_CRITICAL_FROM_ISR();
/* Execute the code that requires the critical section here. */
/* Calls to taskENTER_CRITICAL_FROM_ISR() can be nested so it is safe to call a
function that includes its own calls to taskENTER_CRITICAL_FROM_ISR() and
taskEXIT_CRITICAL_FROM_ISR(). */
vDemoFunction();
/* The operation that required the critical section is complete so exit the
critical section. Assuming interrupts were enabled on entry to this ISR, the value
saved in uxSavedInterruptStatus will result in interrupts being re-enabled.*/
taskEXIT_CRITICAL_FROM_ISR( uxSavedInterruptStatus );
}
在中断进入临界区时会保存当前屏蔽中断等级,退出时用这个值恢复basepri,这样保证了系统不会一退出就打开了所有中断。
举个小例子(里面的数值都是我编的,只是说一下流程),
信号量主要都定义在 ==“semphr.h”==文件中。
FreeRTOS中,信号量有二值化信号量 Binary、计数信号量 Counting、互斥信号量Mutex,用到的主要接口函数如下:
拿建立函数来看一下:
类别 | 建立函数 | define | 调用 |
---|---|---|---|
二值信号量 | xSemaphoreCreateBinary | xQueueGenericCreate | |
计数信号量 | xSemaphoreCreateCounting | xQueueCreateCountingSemaphore | xQueueGenericCreate |
互斥信号量 | xSemaphoreCreateMutex | xQueueCreateMutex | xQueueGenericCreate |
可见各种信号量实际建立的都是队列queue。
接口比较统一,不分是哪一种信号量,只分场合——中断内或者中断外。
类别 | 给出信号量函数 | define | 调用 |
---|---|---|---|
非中断 | xSemaphoreGive | xQueueGenericSend | |
中断内 | xSemaphoreGiveFromISR | xQueueGiveFromISR |
实现都是用队列的Give接口。
接口依然只分场合——中断内或者中断外。
类别 | 给出信号量函数 | define | 调用 |
---|---|---|---|
非中断 | xSemaphoreTake | xQueueSemaphoreTake | |
中断内 | xSemaphoreTakeFromISR | xQueueReceiveFromISR |
实现都是用队列的Give接口。
信号量的实现都是队列,所以这部分内容很少,就是一个头文件里实现了一些宏定义,复用了队列的接口。
基本的API都列在下面。
vSemaphoreCreateBinary | 创建二值信号量 | xQueueGenericCreate |
xSemaphoreTake | 获取信号量 | xQueueSemaphoreTake |
xSemaphoreTakeRecursive | 获取递归信号量 | xQueueTakeMutexRecursive |
xSemaphoreGive | 给出信号量 | xQueueGenericSend |
xSemaphoreGiveRecursive | xQueueGiveMutexRecursive | |
xSemaphoreGiveFromISR | xQueueGiveFromISR | |
xSemaphoreTakeFromISR | xQueueReceiveFromISR | |
xSemaphoreCreateMutex | 创建互斥信号量 | xQueueCreateMutex |
xSemaphoreCreateMutexStatic | xQueueCreateMutexStatic | |
xSemaphoreCreateRecursiveMutex | xQueueCreateMutex | |
xSemaphoreCreateCounting | 创建计数信号量 | xQueueCreateCountingSemaphore |
vSemaphoreDelete | vQueueDelete | |
xSemaphoreGetMutexHolder | xQueueGetMutexHolder | |
xSemaphoreGetMutexHolderFromISR | xQueueGetMutexHolderFromISR | |
uxSemaphoreGetCount | uxQueueMessagesWaiting |
复用了队列之前分析过的源码的,就不再讨论了,
单独把信号量相关的几个函数拿出来看一下。比如xQueueSemaphoreTake。
xQueueSemaphoreTake 和 队列取出API xQueueReceive 基本是一样的。对比了一下,感觉唯一区别就是xQueueSemaphoreTake 加入了对configUSE_MUTEXES的宏的支持。这部分源码基本和队列的实现是一样。
其实很多时候,为了达到共用资源独享的效果,使用互斥量、二值信号量或者临界区都可以。那几者有什么区别?
1.
一个task进入临界区后,其他任务都会被挂起,
一个task获取互斥量后,只有同样需要这个互斥量的task会被阻塞。
2.
互斥信号量可以防止优先级翻转,而二值信号量不支持。
二值信号会造成优先级翻转,所以在优先级有严格要求的场合,请使用互斥信号。