什么是臨界段
代碼的臨界段也稱為臨界區(qū),指處理時不可分割的代碼區(qū)域,一旦這部分代碼開始執(zhí)行,則不允許任何中斷打斷。為確保臨界段代碼的執(zhí)行不被中斷,在進入臨界段之前須關(guān)中斷,而臨界段代碼執(zhí)行完畢后,要立即打開中斷。
臨界段的作用
其實在RTOS中,使用最多的臨界段是OS本身的調(diào)用,但是我們用戶也是需要對臨界資源進行保護的(臨界資源是一次僅允許一個線程使用的共享資源),特別是一些全局變量,當(dāng)線程正在使用的時候不希望有人來打斷我的操作,就行很多時候我們寫代碼時,需要集中精力,不希望別人打斷我們的思路一樣。這樣子使得系統(tǒng)的運行更加穩(wěn)定健壯。
什么時候會打斷代碼的執(zhí)行?
顧名思義,代碼正在正常運行的時候,基本不會被打斷,能被打斷的都是系統(tǒng)發(fā)生了異常(中斷也是異常),在OS中,除了外部中斷能將正在運行的代碼打斷,還有線程的調(diào)度——PendSV,系統(tǒng)產(chǎn)生 PendSV中斷,在 PendSV Handler 里面實現(xiàn)線程的切換。我們要將這項東西屏蔽掉,保證當(dāng)前只有一個線程在使用臨界資源。
如何關(guān)閉中斷?
其實,在我們常用的MCU中,一般為Cortex-M內(nèi)核的,M內(nèi)核是有一些指令能快速關(guān)閉中斷,一起來看看Cortex-M權(quán)威指南吧(以Cortex-M3為例)。
簡單來說,快速屏蔽中斷就是處理這些內(nèi)核寄存器,在Cortex-M中有相應(yīng)的操作指令,一般我們無需關(guān)注,因為OS已經(jīng)給我們寫好了這些底層的東西。不過如果你是想自己寫一個OS的話,可以了解一下,要訪問 PRIMASK, FAULTMASK 以及 BASEPRI,同樣要使用 MRS/MSR 指令,如:
1MRS R0, BASEPRI ;讀取 BASEPRI 到 R0 中
2MRS R0, FAULTMASK ;似上
3MRS R0, PRIMASK ;似上
4MSR BASEPRI, R0 ;寫入 R0 到 BASEPRI 中
5MSR FAULTMASK, R0 ;似上
6MSR PRIMASK, R0 ;似上
只有在特權(quán)級下,才允許訪問這 3 個寄存器。
其實,為了快速地開關(guān)中斷, CM3 還專門設(shè)置了一條 CPS 指令,有 4 種用法:
1CPSID I ;PRIMASK=1, ;關(guān)中斷
2CPSIE I ;PRIMASK=0, ;開中斷
3CPSID F ;FAULTMASK=1, ;關(guān)異常
4CPSIE F ;FAULTMASK=0 ;開異常
上面的代碼中的PRIMASK和 FAULTMAST 是 Cortex-M 內(nèi)核 里面三個中斷屏蔽寄存器中的兩個,還有一個是 BASEPRI,這些寄存器都用于屏蔽中斷。
不同OS的處理臨界段的區(qū)別
FreeRTOS對中斷的開和關(guān)是通過操作 BASEPRI 寄存器來實現(xiàn)的,即大于等于 BASEPRI 的值的中斷會被屏蔽,小于 BASEPRI 的值的中斷則不會被屏蔽。這樣子的好處就是用戶可以設(shè)置 BASEPRI 的值來選擇性的給一些非常緊急的中斷留一條后路。比如飛控的防撞處理。代碼在portmacro.h 中實現(xiàn):
屏蔽中斷:
1static portFORCE_INLINE void vPortRaiseBASEPRI( void )
2{
3uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
4
5 __asm
6 {
7 msr basepri, ulNewBASEPRI
8 dsb
9 isb
10 }
11}
打開中斷:
1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
2{
3 __asm
4 {
5 msr basepri, ulBASEPRI
6 }
7}
與FreeRTOS不同的是,RT-Thread 對臨界段的保護處理的很干脆,不管三七二十一直接把中斷全部關(guān)了(直接操作PRIMASK內(nèi)核寄存器), 只有NMI FAULT 和硬 FAULT能被相應(yīng)。 這種方法簡單粗暴,是很不錯的選擇。一般我們臨界段的處理時間是比較短的,關(guān)了再開其實并沒有太大的影響。
現(xiàn)在要看看RT-Thread的關(guān)中斷的代碼實現(xiàn):
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
這短短的幾句代碼其實還是很有意思的,我就引用火哥的話來解釋一下這些處理操作(我個人是不會匯編的,但是跟著書來解讀這些代碼還是很輕而易舉的)
可能有人懂匯編的話,就會看出來,關(guān)中斷,不就是直接使用 CPSID I 指令就行了嘛~開中斷,不就是使用 CPSIE I 指令就行了嘛,為啥跟我等凡人想的不一樣?
RT-Thread的處理好像是多此一舉了,實則不然,“所有東西的存在必然有其存在的意義”這句話應(yīng)該沒人反駁吧~~因為RT-Thread要防止用戶錯誤地退出了中斷臨界段,因為這樣子可能會產(chǎn)生巨大的危害,所以RT-Thread將當(dāng)前的PRIMASK的狀態(tài)保存起來,這樣子就必須要關(guān)多少次中斷就得開多少次中斷。
怎么說呢,用例子來證明吧:
1/* 臨界段 1 開始 */
2rt_hw_interrupt_disable(); /* 關(guān)中斷,PRIMASK = 1 */
3{
4 /* 臨界段 2 */
5 rt_hw_interrupt_disable(); /* 關(guān)中斷,PRIMASK = 1 */
6 {
7 }
8 rt_hw_interrupt_enable(); /* 開中斷,PRIMASK = 0 */ (注意)
9}
10/* 臨界段 1 結(jié)束 */
11rt_hw_interrupt_enable(); /* 開中斷,PRIMASK = 0 */
如果直接操作PRIMASK,而不保存PRIMASK的狀態(tài),這樣子當(dāng)臨界段2結(jié)束后調(diào)用一次打開中斷,那么連臨界段1的后半部分就無效了。而RT-Thread的實現(xiàn)就能很好避免這種問題,也用代碼來說明吧:
1/* 臨界段 1 開始 */
2level1 = rt_hw_interrupt_disable(); /* 關(guān)中斷,level1=0,PRIMASK=1 */
3{
4 /* 臨界段 2 */
5 level2 = rt_hw_interrupt_disable(); /* 關(guān)中斷,level2=1,PRIMASK=1 */
6 {
7 }
8 rt_hw_interrupt_enable(level2); /* 開中斷,level2=1,PRIMASK=1 */
9}
10/* 臨界段 1 結(jié)束 */
11rt_hw_interrupt_enable(level1); /* 開中斷,level1=0,PRIMASK=0 */
這樣子就完全避免了對吧!
有人又會問了,F(xiàn)reeRTOS的臨界段能允許嵌套嗎,答案是肯定的,F(xiàn)reeRTOS中早已給我們想好調(diào)用的函數(shù)了,并且全部使用宏定義實現(xiàn)了:
1#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
2#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
3#define portENTER_CRITICAL() vPortEnterCritical()
4#define portEXIT_CRITICAL() vPortExitCritical()
5#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
6#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
其實原理都是差不多的,通過保存和恢復(fù)寄存器basepri的數(shù)值就可以實現(xiàn)嵌套使用。
1UBaseType_t uxSavedInterruptStatus;
2
3uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
4{
5 uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR();
6 {
7 //臨界區(qū)代碼
8 }
9 portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
10}
11portCLEAR_INTERRUPT_MASK_FROM_ISR( uxSavedInterruptStatus );
進入中斷源碼的實現(xiàn):
1static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
2{
3uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
4
5 __asm
6 {
7 mrs ulReturn, basepri
8 msr basepri, ulNewBASEPRI
9 dsb
10 isb
11 }
12 return ulReturn;
13}
退出中斷源碼實現(xiàn):(跟前面的函數(shù)一樣)
1static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
2{
3 __asm
4 {
5 msr basepri, ulBASEPRI
6 }
7}
總結(jié)
對于時間關(guān)鍵的任務(wù)而言,恰如其分地使用 PRIMASK 和 BASEPRI 來暫時關(guān)閉一些中斷是非常重要的。
FreeRTOS源碼中就有多處臨界段的處理,除了FreeRTOS操作系統(tǒng)源碼所帶的臨界段以外,用戶寫應(yīng)用的時候也有臨界段的問題,比如以下兩種:
讀取或者修改變量(特別是用于任務(wù)間通信的全局變量)的代碼,一般來說這是最常見的臨界代碼。
調(diào)用公共函數(shù)的代碼,特別是不可重入的函數(shù),如果多個任務(wù)都訪問這個函數(shù),結(jié)果是可想而知的。
總之,對于臨界段要做到執(zhí)行時間越短越好,否則會影響系統(tǒng)的實時性。
那假如我有一個線程,處理的時間較長,但是我又不想被其他線程打斷,關(guān)中斷可能影響系統(tǒng)的正常運行,怎么辦呢?其實很簡單,在OS中一般可以直接掛起調(diào)度器,系統(tǒng)正常運行,但是不會切換線程,當(dāng)我處理完再把調(diào)度器解除即可。
-
寄存器
+關(guān)注
關(guān)注
31文章
5336瀏覽量
120230 -
RTOS
+關(guān)注
關(guān)注
22文章
811瀏覽量
119595 -
MRS
+關(guān)注
關(guān)注
0文章
7瀏覽量
7625 -
Cortex-M
+關(guān)注
關(guān)注
2文章
229瀏覽量
29752 -
MCU控制
+關(guān)注
關(guān)注
0文章
48瀏覽量
6732
發(fā)布評論請先 登錄
相關(guān)推薦
評論