文章目录
1. 引言
本篇应该算是这一阶段FreeRTOS源码阅读的最后一篇了,虽然还有一些想细看的,比如任务通知,PLUS里的命令交互还有TCP的协议栈甚至LWIP到FreeRTOS的移植,但是原计划是5月份开始重新整理系统学习linux,所以不得不收一收,也给点时间沉淀一下这部分,人生漫漫,学海茫茫,不可能所有感兴趣的东西都能有时间有精力去看,找准方向,一深多广才行。
这段时间重新找回了一些学习的感觉,就是觉得上班之后的心,变的浮躁了,当初在学校学的东西很多也还给老师之后,再拿回来有点慢。还是要理一理,静一静,做技术,总是要不断学习不断进步才行。保持不动其实就是落后。
最近组里新来了一个94年的小伙子,很强,让我有些汗颜,自己这么多年好像都浪费了,长江后浪推前浪,新人只会越来越优秀越年轻,如果我们没有什么沉淀的东西,恐怕是连想平平淡淡过一生就做不到,会被社会抛弃掉。
好了,又扯远了。
最近受疫情影响,想的比较多,还是收收。
进入正题。
在FreeRTOS中,当遇到一些并发的需求时,不可避免的要用到临界区或者信号量来保证资源的正常使用。
- 临界区是
- FreeRTOS给出了信号量机制。需要包含semphr.h头文件,内部的实现其实都是队列。
先给结论
互斥量、信号量、临界区,都是什么情况下使用?
个人看法:
- 不是所有的共享资源都必须使用信号量或者临界区,比如单生产单消费的场景,一个环形无锁队列就可以满足。比如linux的kfifo
- 信号量用于“同步”,用于传递一个信号。比如:TaskA每周期依赖于TaskB的计算结果才能运行,TaskA平时就处于阻塞态,等TaskB计算完给出这个信号量,TaskA再运行。
- 互斥量用于那些“共享资源”。这个没什么说的,在一个任务获取资源前,必须先获取锁,拿到锁(互斥量)才能操作,同时把资源锁上,其他任务获取不到就会阻塞在这里。(与互斥量相关的还有一个自旋锁,FreeRTOS里没有,暂时不说)
- 临界区主要用于那些“原子操作”,是不可被打断的操作。比如模拟了一段时序图,中间的时序(延时之类的)是固定的,一旦发生了任务切换,即使别的任务没有访问共享资源,依然会导致延时异常,这种情况下,要用临界区来保护这段代码。
下面具体来说。
2. 临界区
临界资源指会被多个任务(或中断)访问到的公共资源。包含软件资源和硬件资源。
临界区指会访问临界资源的代码片段。
2.1 API接口
FreeRTOS临界区使用起来比较简单,主要是用来保护临界段的代码运行不被中断。
进出的接口是:
taskENTER_CRITICAL()
taskEXIT_CRITICAL()
中断函数中的接口
taskENTER_CRITICAL_FROM_ISR()
taskEXIT_CRITICAL_FROM_ISR()
2.2 源码分析
临界区的实现非常简单,就是记录层数,开启/关闭中断。
以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 );
}
}
进出临界区的操作:
- 前面两个宏定义,说明 portENTER_CRITICAL 和 taskENTER_CRITICAL 还有vPortEnterCritical是等价的。
- 然后我们来看vPortEnterCritical的实现。
先调用 portDISABLE_INTERRUPTS 关闭中断。
然后uxCriticalNesting 来记录进入临界段的层数。这个是为了避免退出时误把中断打开。
比如两层临界区嵌套,
在第一层的时候中断就会关闭。
在第二层的时候中断依然关闭,uxCriticalNesting =2。
退出临界区时,
退出第二层,uxCriticalNesting =1,这时候不能立刻把中断打开,因为外面还有一层临界区。
一定要等退完第一层的临界区,uxCriticalNesting =0.这时候才能开启中断。
看
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
2.3 临界区注意点
2.3.1 不是屏蔽所有中断
临界区要注意的就是,临界区不是屏蔽了所有的中断。
见源码,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
}
}
- 先ulNewBASEPRI 获取配置文件中的configMAX_SYSCALL_INTERRUPT_PRIORITY配置的优先级,
- 然后操作进入临界段前操作寄存器 basepri 关闭了所有小于等于宏定义 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
所定义的中断优先级,这样临界段代码就不会被中断干扰到。优先级小于等于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY,即优先级的值大于等于 configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY。(Cotex-m中,优先级值越小,优先级越高,FreeRTOS相反)
2.3.2 为什么中断临界区不用加层数计数
刚才看到临界区进出函数都有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,这样保证了系统不会一退出就打开了所有中断。
举个小例子(里面的数值都是我编的,只是说一下流程),
- 刚开始,basepri 为 255,不屏蔽任何中断,
我们代码在优先级为100的中断里,这种情况下我们会被90中断打断,因为他的优先级更高。 - 但是我们进入临界区,保存basepri的255到变量1,同时把basepri设置为50(configMAX_SYSCALL_INTERRUPT_PRIORITY),这时候我们不会再被90打断,临界区生效。
- 然后我们第二次进入临界区,保存basepri的50到变量2,同时依然把basepri设置为50(configMAX_SYSCALL_INTERRUPT_PRIORITY),这时候我们还是不会被90打断,临界区生效。
- 之后我们退出第二层临界区,会把basepri设置为之前保存的变量2,这样依然临界区有效。
- 最后我们退出第一层临界区,把basepri设置为之前保存的变量1,也就是255,不再屏蔽中断,彻底退出屏蔽区。
3. 信号量
信号量主要都定义在 ==“semphr.h”==文件中。
3.1 API
FreeRTOS中,信号量有二值化信号量 Binary、计数信号量 Counting、互斥信号量Mutex,用到的主要接口函数如下:
- 建立函数
- 给信号量
- 取信号量
3.1.1 建立
拿建立函数来看一下:
类别 | 建立函数 | define | 调用 |
---|---|---|---|
二值信号量 | xSemaphoreCreateBinary | xQueueGenericCreate | |
计数信号量 | xSemaphoreCreateCounting | xQueueCreateCountingSemaphore | xQueueGenericCreate |
互斥信号量 | xSemaphoreCreateMutex | xQueueCreateMutex | xQueueGenericCreate |
可见各种信号量实际建立的都是队列queue。
3.1.2 给出信号量
接口比较统一,不分是哪一种信号量,只分场合——中断内或者中断外。
类别 | 给出信号量函数 | define | 调用 |
---|---|---|---|
非中断 | xSemaphoreGive | xQueueGenericSend | |
中断内 | xSemaphoreGiveFromISR | xQueueGiveFromISR |
实现都是用队列的Give接口。
3.1.3 获取信号量
接口依然只分场合——中断内或者中断外。
类别 | 给出信号量函数 | define | 调用 |
---|---|---|---|
非中断 | xSemaphoreTake | xQueueSemaphoreTake | |
中断内 | xSemaphoreTakeFromISR | xQueueReceiveFromISR |
实现都是用队列的Give接口。
3.2 源码分析
信号量的实现都是队列,所以这部分内容很少,就是一个头文件里实现了一些宏定义,复用了队列的接口。
基本的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的宏的支持。这部分源码基本和队列的实现是一样。
4. 一些思考
4.1 二值量、互斥量和临界区的区别
其实很多时候,为了达到共用资源独享的效果,使用互斥量、二值信号量或者临界区都可以。那几者有什么区别?
1.
一个task进入临界区后,其他任务都会被挂起,
一个task获取互斥量后,只有同样需要这个互斥量的task会被阻塞。
2.
互斥信号量可以防止优先级翻转,而二值信号量不支持。
二值信号会造成优先级翻转,所以在优先级有严格要求的场合,请使用互斥信号。