本文聊聊临界区,以及RT-Thread对临界区的处理,
通过源码分析一下 RT-Thread 对临界区保护的实现以及与 FreeRTOS 处理的不同。
为什么要聊临界区?
因为在 RT-Thread 中临界区关系到线程的顺序执行,也就是线程同步的问题。
在使用RTOS的时候,多个运行的线程往往都需要访问临界资源,比如一些全局变量,那么如果不进行一定的保护措施,程序运行就可能出现意想不到的结果。
RT-Thread 提供了多种途径来保护临界区,本文主要说明的是:关闭系统调度和禁止中断的方式 。
经常会听到临界区,临界资源之类的名词,那么什么叫临界区,临界资源?
对于我们的多任务的RTOS而言,除了外部中断,自身的多线程和系统调度机制,多个线程可能会对共享资源进行访问,为了保证数据的可靠性和完整性,那么就需要对临界区进行保护,共享资源要互斥的访问(比如全局变量)。
首先是最基础的示例,外部中断!这个不仅在RTOS存在,前后台系统也存在:
上面的例子中,如果在线程函数中,加入临界区保护,使得线程对临界资源 a 的操作没有结束以前不响应中断,就不会发生问题。
再来看一个线程间对临界资源访问的例子:
在上图的示例中 (可能delay(1)
和时钟节拍一样可能有点问题,可能需要多一点延时,这里意思到了就行,不纠结了= =!),我已经分析了如果没有临界区保护会出现的问题(有问题请指出),实际程序结果可能不会是程序本来想要的结果,这种错误是需要避免的!
本小结以下内容包括后面临界区的保护源码分析是扩展说明,懂与不懂不影响学会使用 RT-Thread 临界区保护,因为涉及的 RTOS的调度原理,PendSV异常等知识,需要一定的基础,这里建议想学习RTOS的小伙伴务必好好看看《Cortex-M3与Cortex-M4权威指南》这个文档。
理解上面示例关系到RTOS的调度原理,上面解释中用到的中断打断线程后现场保存,现场恢复,线程调度。得对RTOS的调度原理有一定的理解,在RTOS中除了外部中断会打断线程的执行,还有Systick中断和一个重要的 PendSV 异常。
PendSV 也称为可悬起的系统调用,它是一种异常,可以像普通的中断一样被挂起,它是专门用来辅助操作系统进行上下文切换的。PendSV 异常会被初始化为最低优先级的异常。每次需要进行上下文切换的时候,会手动触发 PendSV 异常,在 PendSV 异常处理函数中进行上下文切换。
详细理解请参考我另一篇博文:
FreeRTOS记录(三、RTOS任务调度原理解析_Systick、PendSV、SVC)
总之,对于RTOS而言,在访问临界资源的时候,需要特别注意,做好临界区的保护。
为了避免出现上面我们所说的问题,RTOS对临界区采取了一些对应的保护方法,一般来说有:
关闭系统调度,关中断,利用信号量,互斥量。
RT-Thread 信号量,互斥量我们会在下篇博文来说明,本文主要来了解下关闭中断和系统调度的操作。
RT-Thread 调度器上锁 和 调度器解锁的函数如下:
void rt_enter_critical(void);//调度器上锁,进入调度临界区,不再切换线程
void rt_exit_critical(void);//调度器解锁,退出调度临界区
注意,调度锁不会阻止系统的响应中断,只不过是中断处理完成退出后,继续执行被锁住的线程。如果中断中有访问临界资源的情况,此方式不适用!!
调度器上锁和调度器解锁函数,是成对使用的,切记!
我们找到rt_enter_critical
函数,看看是如何实现的:
但是上面的函数只对rt_scheduler_lock_nest
变量进行了自增,并没有别的操作,那么这个变量是如何影响调度器的呢?
我们查到使用到变量rt_scheduler_lock_nest
的地方,找到如下代码:
那么同样的,在rt_exit_critical
函数中,当然就是变量自减了:
仔细看了这段代码还能发现一个细节,就是这个关闭调度和打开调度是支持嵌套的! 调度器上锁一次,就要解锁一次,上锁2次,就得解锁2次。
通过这个也告诉我们,有些时候多看看源码,会比直接看说明对逻辑的理解更直观!
RTOS所有的线程调度都是建立在中断基础上的,关闭中断,不仅可以屏蔽,外部中断,也可以禁止调度,他比上面的禁止调度“更能够保护”临界区。
RT-Thread 屏蔽中断 和 使能中断的函数如下:
/*
返回值:
中断状态 rt_hw_interrupt_disable 函数运行前的中断状态
*/
rt_base_t rt_hw_interrupt_disable(void);//屏蔽中断
/*
参数:
level 前一次 rt_hw_interrupt_disable 返回的中断状态
*/
void rt_hw_interrupt_enable(rt_base_t level);//中断使能
注意,上面的终端所中断锁是最强大的和最高效的同步方法,这个方法最主要的问题在于,中断响应延时会拉长,对于实时性特别极端的场合需要注意,所以实际使用要根据应用场合,合理的使用。
中断屏蔽和中断使能函数也是是成对使用的,切记!
上面的函数找到申明,但是跳转不到函数原型:
那么函数的实现在什么地方呢?如下图:
因为使用的是 gcc 编译器,所以context_gcc.S
文件中的函数体前后语句会与 MDK下有一定的区别,但函数实现的汇编语言都是一样的:
/*
* rt_base_t rt_hw_interrupt_disable();
*/
/*
.global关键字用来让一个符号对链接器可见,可以供其他链接对象模块使用
前面两句意思就类似于定义了一个全局可调用的函数rt_hw_interrupt_disable
*/
.global rt_hw_interrupt_disable //告诉编译器rt_hw_interrupt_disable 是一个全局可见的
.type rt_hw_interrupt_disable, %function//告诉编译器rt_hw_interrupt_disable是一个函数
rt_hw_interrupt_disable:
MRS R0, PRIMASK //读取PRIMASK寄存器的值到r0寄存器
CPSID I //关闭全局中断,具体原因见博文后续说明
BX LR //函数返回,通过LR 连接寄存器 返回
/*
* void rt_hw_interrupt_enable(rt_base_t level);
*/
.global rt_hw_interrupt_enable //与上面类似
.type rt_hw_interrupt_enable, %function
rt_hw_interrupt_enable:
MSR PRIMASK, R0 //将 r0 的值寄存器写入到 PRIMASK 寄存器
BX LR //函数返回,通过LR 连接寄存器 返回
即便上面的代码我写了注释,告诉了意思,但是还是会有问题,为什么 CPSID I
就是关闭全局中断?
如果好好看了《Cortex-M3与Cortex-M4权威指南》这个文档,所有东西都能明白了。
PRIMSK:中断屏蔽特殊寄存器。利用 PRIMSK,可以禁止除HardFault 和 NMI外的所有异常。在上面推荐文档中有说明:
CPSID I
就是禁止中断,CPSIE I
就是使能中断。
一个细节,为什么 rt_hw_interrupt_enable
函数,不用 CPSIE I
恢复中断?
答案就是,如果使用CPSIE I
使能中断,那么中断锁就无法嵌套。使用R0
寄存器将当前的PRIMASK
的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。
另外值得一说的是, 在上面的示例中R0
寄存器中保存的值,就是 rt_base_t level
这个变量!
通过上述分析,我们应该完全明白了,RT-Thread 的中断锁是如何实现的,那么其他的RTOS是不是都是这个样子呢? 我们来看看 FreeRTOS 对于中断锁是如何实现的。
FreeRTOS的临界区,在我的博文介绍过:
FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)
这里我们就只看一下他的实现代码来和 RT-Thread 比较一下(同样是以M3为例,M0与M3又是不同的):
这里我们分析就用在任务中屏蔽中断的函数来分析,在中断中屏蔽分析类似,只不过稍微复杂一点。
屏蔽中断:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
/*----------------------------------------------*/
/*只需要注意操作的寄存器为 basepri*/
/*----------------------------------------------*/
portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI;
__asm volatile
(
" mov %0, %1 \n" \
" msr basepri, %0 \n" \
" isb \n" \
" dsb \n" \
:"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY ) : "memory"
);
}
使能中断:
//...
#define portENABLE_INTERRUPTS() vPortSetBASEPRI(0)
/*只需要注意操作的寄存器为 basepri*/
portFORCE_INLINE static void vPortSetBASEPRI( uint32_t ulNewMaskValue )
{
__asm volatile
(
" msr basepri, %0 " :: "r" ( ulNewMaskValue ) : "memory"
);
}
这里我们通过 FreeRTOS 中断锁的代码可以看出,它操作的是basepri
寄存器,而不是PRIMSK
寄存器,那么basepri
寄存器又是什么呢? 答案还是从《Cortex-M3与Cortex-M4权威指南》文档中可以找到:
FreeRTOS 在中断锁的操作上面,是利用 basepri
寄存器屏蔽特定优先级的中断。 这个优先级的设置是用户可以自行设置的。这给非常紧急的中断留了一条后路。
但是不管怎样,在任何时候,临界区处理的代码当然是时间越短越好!!
简单总结一下,临界区的保护实际应用中可能需要的场合:
在一般的场合,普通临界区的保护使用禁止调度的方式就可以满足需求了,除非你中断中有对临界资源的访问。
当然事无绝对,有些时候中断的发生对某些普通任务(比如ADC采样)也可能产品影响,所以还是需要根据实际情况,合理的使用 临界区保护。
本文的内容从学会 RT-Thread 临界区保护的使用来说是比较简单,只需要掌握几个函数的调用就可以。但对于了解实现原理来说相对复杂些,需要对内核,对操作系统基本原理有一定的理解。
我们通过对这几个函数源码的简单分析,让我们对其原理的实现有了更直观的理解,养成看源码是对我们学习有帮助的一个好习惯!
下一篇 RT-Thread 记录,就要来学习 RT-Thread 的线程间同步相关的信号量,互斥量,这也是 RT-Thread 对临界区的另一种保护方式。
谢谢!