不同OS的处理临界段的区别
FreeRTOS:
FreeRTOS对中断的开和关是通过操作 BASEPRI 寄存器来实现的,即大于等于 BASEPRI 的值的中断会被屏蔽,小于 BASEPRI 的值的中断则不会被屏蔽。这样子的好处就是用户可以设置 BASEPRI 的值来选择性的给一些非常紧急的中断留一条后路。比如飞控的防撞处理。代码在portmacro.h 中实现:
屏蔽中断:
1static portFORCE_INLINE voidvPortRaiseBASEPRI( void )
2{
3uint32_t ulNewBASEPRI =configMAX_SYSCALL_INTERRUPT_PRIORITY;
4
5 __asm
6 {
7 msr basepri, ulNewBASEPRI
8 d***
9 i***
10 }
11}
打开中断:
1static portFORCE_INLINE voidvPortSetBASEPRI( uint32_t ulBASEPRI )
2{
3 __asm
4 {
5 msr basepri, ulBASEPRI
6 }
7}
RT-Thread:
与FreeRTOS不同的是,RT-Thread 对临界段的保护处理的很干脆,不管三七二十一直接把中断全部关了(直接操作PRIMASK内核寄存器), 只有NMI FAULT 和硬 FAULT能被相应。 这种方法简单粗暴,是很不错的选择。一般我们临界段的处理时间是比较短的,关了再开其实并没有太大的影响。
现在要看看RT-Thread的关中断的代码实现:
1rt_hw_interrupt_disable PROC
2 EXPORT rt_hw_interrupt_disable
3 MRS r0, PRIMASK
4 CPSID I
5 BX LR
6 ENDP
开中断:
1rt_hw_interrupt_enable PROC
2 EXPORT rt_hw_interrupt_enable
3 MSR PRIMASK, r0
4 BX LR
5 ENDP
这短短的几句代码其实还是很有意思的,我就引用火哥的话来解释一下这些处理操作(我个人是不会汇编的,但是跟着书来解读这些代码还是很轻而易举的)
可能有人懂汇编的话,就会看出来,关中断,不就是直接使用 CPSID I 指令就行了嘛~开中断,不就是使用 CPSIE I 指令就行了嘛,为啥跟我等凡人想的不一样?
RT-Thread的处理好像是多此一举了,实则不然,“所有东西的存在必然有其存在的意义”这句话应该没人反驳吧~~因为RT-Thread要防止用户错误地退出了中断临界段,因为这样子可能会产生巨大的危害,所以RT-Thread将当前的PRIMASK的状态保存起来,这样子就必须要关多少次中断就得开多少次中断。
怎么说呢,用例子来证明吧:
1/* 临界段 1 开始 */
2rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */
3{
4 /* 临界段 2 */
5 rt_hw_interrupt_disable(); /* 关中断,PRIMASK = 1 */
6 {
7 }
8 rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */(注意)
9}
10/* 临界段 1 结束 */
11rt_hw_interrupt_enable(); /* 开中断,PRIMASK = 0 */
如果直接操作PRIMASK,而不保存PRIMASK的状态,这样子当临界段2结束后调用一次打开中断,那么连临界段1的后半部分就无效了。而RT-Thread的实现就能很好避免这种问题,也用代码来说明吧:
1/* 临界段 1 开始 */
2level1 = rt_hw_interrupt_disable(); /* 关中断,level1=0,PRIMASK=1 */
3{
4 /* 临界段 2 */
5 level2 = rt_hw_interrupt_disable(); /* 关中断,level2=1,PRIMASK=1*/
6 {
7 }
8 rt_hw_interrupt_enable(level2); /* 开中断,level2=1,PRIMASK=1*/
9}
10/* 临界段 1 结束 */
11rt_hw_interrupt_enable(level1); /* 开中断,level1=0,PRIMASK=0 */
这样子就完全避免了对吧!
有人又会问了,FreeRTOS的临界段能允许嵌套吗,答案是肯定的,FreeRTOS中早已给我们想好调用的函数了,并且全部使用宏定义实现了:
1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
3#define portENTER_CRITICAL() vPortEnterCritical()
4#define portEXIT_CRITICAL() vPortExitCritical()
5#defineportSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
6#defineportCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
其实原理都是差不多的,通过保存和恢复寄存器basepri的数值就可以实现嵌套使用。
1UBaseType_t uxSavedInterruptStatus;
2
3uxSavedInterruptStatus =portSET_INTERRUPT_MASK_FROM_ISR();
4{
5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
6 {
7 //临界区代码
8 }
9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
10}
11portCLEAR_INTERRUPT_MASK_FROM_ISR(uxSavedInterruptStatus );
进入中断源码的实现:
1static portFORCE_INLINE uint32_tulPortRaiseBASEPRI( void )
2{
3uint32_t ulReturn, ulNewBASEPRI =configMAX_SYSCALL_INTERRUPT_PRIORITY;
4
5 __asm
6 {
7 mrs ulReturn, basepri
8 msr basepri, ulNewBASEPRI
9 d***
10 i***
11 }
12 return ulReturn;
13}
退出中断源码实现:(跟前面的函数一样)
1static portFORCE_INLINE voidvPortSetBASEPRI( uint32_t ulBASEPRI )
2{
3 __asm
4 {
5 msr basepri, ulBASEPRI
6 }
7}
总结
对于时间关键的任务而言,恰如其分地使用 PRIMASK 和 BASEPRI 来暂时关闭一些中断是非常重要的。
FreeRTOS源码中就有多处临界段的处理,除了FreeRTOS操作系统源码所带的临界段以外,用户写应用的时候也有临界段的问题,比如以下两种:
读取或者修改变量(特别是用于任务间通信的全局变量)的代码,一般来说这是最常见的临界代码。
调用公共函数的代码,特别是不可重入的函数,如果多个任务都访问这个函数,结果是可想而知的。
总之,对于临界段要做到执行时间越短越好,否则会影响系统的实时性。
那假如我有一个线程,处理的时间较长,但是我又不想被其他线程打断,关中断可能影响系统的正常运行,怎么办呢?其实很简单,在OS中一般可以直接挂起调度器,系统正常运行,但是不会切换线程,当我处理完再把调度器解除即可。
RTOS使用得好,开发起来比裸机更简单,使用得不好,那将是噩梦——杰杰