前言
為什么要聊臨界區(qū)?
因?yàn)樵?RT-Thread 中臨界區(qū)關(guān)系到線程的順序執(zhí)行,也就是線程同步的問題。
在使用RTOS的時候,多個運(yùn)行的線程往往都需要訪問臨界資源,比如一些全局變量,那么如果不進(jìn)行一定的保護(hù)措施,程序運(yùn)行就可能出現(xiàn)意想不到的結(jié)果。
RT-Thread 提供了多種途徑來保護(hù)臨界區(qū),本文主要說明的是:關(guān)閉系統(tǒng)調(diào)度和禁止中斷的方式 。
本 RT-Thread 專欄記錄的開發(fā)環(huán)境:
RT-Thread記錄(一、RT-Thread 版本、RT-Thread Studio開發(fā)環(huán)境 及 配合CubeMX開發(fā)快速上手)
RT-Thread記錄(二、RT-Thread內(nèi)核啟動流程 — 啟動文件和源碼分析
RT-Thread 內(nèi)核篇系列博文鏈接:
RT-Thread記錄(三、RT-Thread 線程操作函數(shù)及線程管理與FreeRTOS的比較)
RT-Thread記錄(四、RT-Thread 時鐘節(jié)拍和軟件定時器)
一、臨界區(qū)
經(jīng)常會聽到臨界區(qū),臨界資源之類的名詞,那么什么叫臨界區(qū),臨界資源?
1.1 什么是臨界區(qū)
簡單的概括就是圖中兩句話:
-
臨界資源
一次僅允許一個進(jìn)程使用的共享資源 -
臨界區(qū)
每個進(jìn)程中訪問臨界資源的那段代碼稱為 臨界區(qū)
1.2 RTOS中的臨界區(qū)
對于我們的多任務(wù)的RTOS而言,除了外部中斷,自身的多線程和系統(tǒng)調(diào)度機(jī)制,多個線程可能會對共享資源進(jìn)行訪問,為了保證數(shù)據(jù)的可靠性和完整性,那么就需要對臨界區(qū)進(jìn)行保護(hù),共享資源要互斥的訪問(比如全局變量)。
首先是最基礎(chǔ)的示例,外部中斷!這個不僅在RTOS存在,前后臺系統(tǒng)也存在:
上面的例子中,如果在線程函數(shù)中,加入臨界區(qū)保護(hù),使得線程對臨界資源 a 的操作沒有結(jié)束以前不響應(yīng)中斷,就不會發(fā)生問題。
再來看一個線程間對臨界資源訪問的例子:
在上圖的示例中 (可能delay(1)和時鐘節(jié)拍一樣可能有點(diǎn)問題,可能需要多一點(diǎn)延時,這里意思到了就行,不糾結(jié)了= =?。乙呀?jīng)分析了如果沒有臨界區(qū)保護(hù)會出現(xiàn)的問題(有問題請指出),實(shí)際程序結(jié)果可能不會是程序本來想要的結(jié)果,這種錯誤是需要避免的!
本小結(jié)以下內(nèi)容包括后面臨界區(qū)的保護(hù)源碼分析是擴(kuò)展說明,懂與不懂不影響學(xué)會使用 RT-Thread 臨界區(qū)保護(hù),因?yàn)樯婕暗?RTOS的調(diào)度原理,PendSV異常等知識,需要一定的基礎(chǔ),這里建議想學(xué)習(xí)RTOS的小伙伴務(wù)必好好看看《Cortex-M3與Cortex-M4權(quán)威指南》這個文檔。
理解上面示例關(guān)系到RTOS的調(diào)度原理,上面解釋中用到的中斷打斷線程后現(xiàn)場保存,現(xiàn)場恢復(fù),線程調(diào)度。得對RTOS的調(diào)度原理有一定的理解,在RTOS中除了外部中斷會打斷線程的執(zhí)行,還有Systick中斷和一個重要的 PendSV 異常。
PendSV 也稱為可懸起的系統(tǒng)調(diào)用,它是一種異常,可以像普通的中斷一樣被掛起,它是專門用來輔助操作系統(tǒng)進(jìn)行上下文切換的。PendSV 異常會被初始化為最低優(yōu)先級的異常。每次需要進(jìn)行上下文切換的時候,會手動觸發(fā) PendSV 異常,在 PendSV 異常處理函數(shù)中進(jìn)行上下文切換。
詳細(xì)理解請參考我另一篇博文:
FreeRTOS記錄(三、RTOS任務(wù)調(diào)度原理解析_Systick、PendSV、SVC)
這里用文中截圖稍微解釋一下:
總之,對于RTOS而言,在訪問臨界資源的時候,需要特別注意,做好臨界區(qū)的保護(hù)。
為了避免出現(xiàn)上面我們所說的問題,RTOS對臨界區(qū)采取了一些對應(yīng)的保護(hù)方法,一般來說有:
關(guān)閉系統(tǒng)調(diào)度,關(guān)中斷,利用信號量,互斥量。
RT-Thread 信號量,互斥量我們會在下篇博文來說明,本文主要來了解下關(guān)閉中斷和系統(tǒng)調(diào)度的操作。
二、RT-Thread臨界區(qū)保護(hù)
2.1 禁止調(diào)度
RT-Thread 調(diào)度器上鎖 和 調(diào)度器解鎖的函數(shù)如下:
void rt_enter_critical(void);//調(diào)度器上鎖,進(jìn)入調(diào)度臨界區(qū),不再切換線程
void rt_exit_critical(void);//調(diào)度器解鎖,退出調(diào)度臨界區(qū)
注意,調(diào)度鎖不會阻止系統(tǒng)的響應(yīng)中斷,只不過是中斷處理完成退出后,繼續(xù)執(zhí)行被鎖住的線程。如果中斷中有訪問臨界資源的情況,此方式不適用??!
調(diào)度器上鎖和調(diào)度器解鎖函數(shù),是成對使用的,切記!
使用示例:
禁止調(diào)度源碼簡析
但是上面的函數(shù)只對rt_scheduler_lock_nest
變量進(jìn)行了自增,并沒有別的操作,那么這個變量是如何影響調(diào)度器的呢?
我們查到使用到變量rt_scheduler_lock_nest
的地方,找到如下代碼:
那么同樣的,在rt_exit_critical
函數(shù)中,當(dāng)然就是變量自減了:
仔細(xì)看了這段代碼還能發(fā)現(xiàn)一個細(xì)節(jié),就是這個關(guān)閉調(diào)度和打開調(diào)度是支持嵌套的! 調(diào)度器上鎖一次,就要解鎖一次,上鎖2次,就得解鎖2次。
通過這個也告訴我們,有些時候多看看源碼,會比直接看說明對邏輯的理解更直觀!
2.2 屏蔽中斷
RTOS所有的線程調(diào)度都是建立在中斷基礎(chǔ)上的,關(guān)閉中斷,不僅可以屏蔽,外部中斷,也可以禁止調(diào)度,他比上面的禁止調(diào)度“更能夠保護(hù)”臨界區(qū)。
RT-Thread 屏蔽中斷 和 使能中斷的函數(shù)如下:
/*
返回值:
中斷狀態(tài) rt_hw_interrupt_disable 函數(shù)運(yùn)行前的中斷狀態(tài)
*/
rt_base_t rt_hw_interrupt_disable(void);//屏蔽中斷
/*
參數(shù):
level 前一次 rt_hw_interrupt_disable 返回的中斷狀態(tài)
*/
void rt_hw_interrupt_enable(rt_base_t level);//中斷使能
注意,上面的終端所中斷鎖是最強(qiáng)大的和最高效的同步方法,這個方法最主要的問題在于,中斷響應(yīng)延時會拉長,對于實(shí)時性特別極端的場合需要注意,所以實(shí)際使用要根據(jù)應(yīng)用場合,合理的使用。
中斷屏蔽和中斷使能函數(shù)也是是成對使用的,切記!
使用示例:
中斷鎖源碼簡析
上面的函數(shù)找到申明,但是跳轉(zhuǎn)不到函數(shù)原型:
那么函數(shù)的實(shí)現(xiàn)在什么地方呢?如下圖:
因?yàn)槭褂玫氖?gcc 編譯器,所以context_gcc.S
文件中的函數(shù)體前后語句會與 MDK下有一定的區(qū)別,但函數(shù)實(shí)現(xiàn)的匯編語言都是一樣的:
/*
* rt_base_t rt_hw_interrupt_disable();
*/
/*
.global關(guān)鍵字用來讓一個符號對鏈接器可見,可以供其他鏈接對象模塊使用
前面兩句意思就類似于定義了一個全局可調(diào)用的函數(shù)rt_hw_interrupt_disable
*/
.global rt_hw_interrupt_disable //告訴編譯器rt_hw_interrupt_disable 是一個全局可見的
.type rt_hw_interrupt_disable, %function//告訴編譯器rt_hw_interrupt_disable是一個函數(shù)
rt_hw_interrupt_disable:
MRS R0, PRIMASK //讀取PRIMASK寄存器的值到r0寄存器
CPSID I //關(guān)閉全局中斷,具體原因見博文后續(xù)說明
BX LR //函數(shù)返回,通過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 //函數(shù)返回,通過LR 連接寄存器 返回
即便上面的代碼我寫了注釋,告訴了意思,但是還是會有問題,為什么 CPSID I
就是關(guān)閉全局中斷?
如果好好看了《Cortex-M3與Cortex-M4權(quán)威指南》這個文檔,所有東西都能明白了。
PRIMSK:中斷屏蔽特殊寄存器。利用 PRIMSK,可以禁止除HardFault 和 NMI外的所有異常。在上面推薦文檔中有說明:
CPSID I
就是禁止中斷,CPSIE I
就是使能中斷。
一個細(xì)節(jié),為什么 rt_hw_interrupt_enable
函數(shù),不用 CPSIE I
恢復(fù)中斷?
答案就是,如果使用CPSIE I
使能中斷,那么中斷鎖就無法嵌套。使用R0
寄存器將當(dāng)前的PRIMASK
的狀態(tài)保存起來,這樣子就必須要關(guān)多少次中斷就得開多少次中斷。
另外值得一說的是, 在上面的示例中R0
寄存器中保存的值,就是 rt_base_t level
這個變量!
通過上述分析,我們應(yīng)該完全明白了,RT-Thread 的中斷鎖是如何實(shí)現(xiàn)的,那么其他的RTOS是不是都是這個樣子呢? 我們來看看 FreeRTOS 對于中斷鎖是如何實(shí)現(xiàn)的。
與FreeRTOS區(qū)別
FreeRTOS的臨界區(qū),在我的博文介紹過:FreeRTOS記錄(四、FreeRTOS任務(wù)堆棧溢出問題和臨界區(qū))
這里我們就只看一下他的實(shí)現(xiàn)代碼來和 RT-Thread 比較一下(同樣是以M3為例,M0與M3又是不同的):
這里我們分析就用在任務(wù)中屏蔽中斷的函數(shù)來分析,在中斷中屏蔽分析類似,只不過稍微復(fù)雜一點(diǎn)。
屏蔽中斷:
#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"
);
}
————————————————
版權(quán)聲明:本文為CSDN博主「矜辰所致」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_42328389/article/details/123593592
這里我們通過 FreeRTOS 中斷鎖的代碼可以看出,它操作的是basepri
寄存器,而不是PRIMSK
寄存器,那么basepri
寄存器又是什么呢? 答案還是從《Cortex-M3與Cortex-M4權(quán)威指南》文檔中可以找到:
FreeRTOS 在中斷鎖的操作上面,是利用 basepri
寄存器屏蔽特定優(yōu)先級的中斷。 這個優(yōu)先級的設(shè)置是用戶可以自行設(shè)置的。這給非常緊急的中斷留了一條后路。
但是不管怎樣,在任何時候,臨界區(qū)處理的代碼當(dāng)然是時間越短越好??!
2.3 實(shí)際應(yīng)用場合
簡單總結(jié)一下,臨界區(qū)的保護(hù)實(shí)際應(yīng)用中可能需要的場合:
調(diào)用公共函數(shù)的代碼(不可重入函數(shù))
讀取或者修改變量(全局變量)
使用硬件資源(在操作內(nèi)存或者flash的時候)
對時序有精準(zhǔn)要求的操作(I2C通訊,但是得注意在通訊中不能使用利用了systick的延時函數(shù),用干等的延時)
某些用戶不想被打斷的代碼(比如 printf 打?。?/p>
在一般的場合,普通臨界區(qū)的保護(hù)使用禁止調(diào)度的方式就可以滿足需求了,除非你中斷中有對臨界資源的訪問。
當(dāng)然事無絕對,有些時候中斷的發(fā)生對某些普通任務(wù)(比如ADC采樣)也可能產(chǎn)品影響,所以還是需要根據(jù)實(shí)際情況,合理的使用 臨界區(qū)保護(hù)。
結(jié)語
本文的內(nèi)容從學(xué)會 RT-Thread 臨界區(qū)保護(hù)的使用來說是比較簡單,只需要掌握幾個函數(shù)的調(diào)用就可以。但對于了解實(shí)現(xiàn)原理來說相對復(fù)雜些,需要對內(nèi)核,對操作系統(tǒng)基本原理有一定的理解。
我們通過對這幾個函數(shù)源碼的簡單分析,讓我們對其原理的實(shí)現(xiàn)有了更直觀的理解,養(yǎng)成看源碼是對我們學(xué)習(xí)有幫助的一個好習(xí)慣!
下一篇 RT-Thread 記錄,就要來學(xué)習(xí) RT-Thread 的線程間同步相關(guān)的信號量,互斥量,這也是 RT-Thread 對臨界區(qū)的另一種保護(hù)方式。
謝謝!
-
FreeRTOS
+關(guān)注
關(guān)注
12文章
484瀏覽量
62178 -
Studio
+關(guān)注
關(guān)注
2文章
190瀏覽量
28693 -
線程
+關(guān)注
關(guān)注
0文章
504瀏覽量
19683 -
RT-Thread
+關(guān)注
關(guān)注
31文章
1289瀏覽量
40129
發(fā)布評論請先 登錄
相關(guān)推薦
評論