前言
為什么要聊臨界區?
因為在 RT-Thread 中臨界區關系到線程的順序執行,也就是線程同步的問題。
在使用RTOS的時候,多個運行的線程往往都需要訪問臨界資源,比如一些全局變量,那么如果不進行一定的保護措施,程序運行就可能出現意想不到的結果。
RT-Thread 提供了多種途徑來保護臨界區,本文主要說明的是:關閉系統調度和禁止中斷的方式 。
本 RT-Thread 專欄記錄的開發環境:
RT-Thread記錄(一、RT-Thread 版本、RT-Thread Studio開發環境 及 配合CubeMX開發快速上手)
RT-Thread記錄(二、RT-Thread內核啟動流程 — 啟動文件和源碼分析
RT-Thread 內核篇系列博文鏈接:
RT-Thread記錄(三、RT-Thread 線程操作函數及線程管理與FreeRTOS的比較)
RT-Thread記錄(四、RT-Thread 時鐘節拍和軟件定時器)
一、臨界區
經常會聽到臨界區,臨界資源之類的名詞,那么什么叫臨界區,臨界資源?
1.1 什么是臨界區

簡單的概括就是圖中兩句話:
-
臨界資源
一次僅允許一個進程使用的共享資源 -
臨界區
每個進程中訪問臨界資源的那段代碼稱為 臨界區
1.2 RTOS中的臨界區
對于我們的多任務的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臨界區保護
2.1 禁止調度
RT-Thread 調度器上鎖 和 調度器解鎖的函數如下:
void rt_enter_critical(void);//調度器上鎖,進入調度臨界區,不再切換線程
void rt_exit_critical(void);//調度器解鎖,退出調度臨界區
注意,調度鎖不會阻止系統的響應中斷,只不過是中斷處理完成退出后,繼續執行被鎖住的線程。如果中斷中有訪問臨界資源的情況,此方式不適用!!
調度器上鎖和調度器解鎖函數,是成對使用的,切記!
使用示例:

禁止調度源碼簡析

但是上面的函數只對rt_scheduler_lock_nest
變量進行了自增,并沒有別的操作,那么這個變量是如何影響調度器的呢?
我們查到使用到變量rt_scheduler_lock_nest
的地方,找到如下代碼:

那么同樣的,在rt_exit_critical
函數中,當然就是變量自減了:

仔細看了這段代碼還能發現一個細節,就是這個關閉調度和打開調度是支持嵌套的! 調度器上鎖一次,就要解鎖一次,上鎖2次,就得解鎖2次。
通過這個也告訴我們,有些時候多看看源碼,會比直接看說明對邏輯的理解更直觀!
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記錄(四、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"
);
}
————————————————
版權聲明:本文為CSDN博主「矜辰所致」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/weixin_42328389/article/details/123593592
這里我們通過 FreeRTOS 中斷鎖的代碼可以看出,它操作的是basepri
寄存器,而不是PRIMSK
寄存器,那么basepri
寄存器又是什么呢? 答案還是從《Cortex-M3與Cortex-M4權威指南》文檔中可以找到:

FreeRTOS 在中斷鎖的操作上面,是利用 basepri
寄存器屏蔽特定優先級的中斷。 這個優先級的設置是用戶可以自行設置的。這給非常緊急的中斷留了一條后路。
但是不管怎樣,在任何時候,臨界區處理的代碼當然是時間越短越好!!
2.3 實際應用場合
簡單總結一下,臨界區的保護實際應用中可能需要的場合:
調用公共函數的代碼(不可重入函數)
讀取或者修改變量(全局變量)
使用硬件資源(在操作內存或者flash的時候)
對時序有精準要求的操作(I2C通訊,但是得注意在通訊中不能使用利用了systick的延時函數,用干等的延時)
某些用戶不想被打斷的代碼(比如 printf 打印)
在一般的場合,普通臨界區的保護使用禁止調度的方式就可以滿足需求了,除非你中斷中有對臨界資源的訪問。
當然事無絕對,有些時候中斷的發生對某些普通任務(比如ADC采樣)也可能產品影響,所以還是需要根據實際情況,合理的使用 臨界區保護。
結語
本文的內容從學會 RT-Thread 臨界區保護的使用來說是比較簡單,只需要掌握幾個函數的調用就可以。但對于了解實現原理來說相對復雜些,需要對內核,對操作系統基本原理有一定的理解。
我們通過對這幾個函數源碼的簡單分析,讓我們對其原理的實現有了更直觀的理解,養成看源碼是對我們學習有幫助的一個好習慣!
下一篇 RT-Thread 記錄,就要來學習 RT-Thread 的線程間同步相關的信號量,互斥量,這也是 RT-Thread 對臨界區的另一種保護方式。
謝謝!
-
FreeRTOS
+關注
關注
12文章
488瀏覽量
63738 -
Studio
+關注
關注
2文章
204瀏覽量
29506 -
線程
+關注
關注
0文章
507瀏覽量
20073 -
RT-Thread
+關注
關注
32文章
1369瀏覽量
41507
發布評論請先 登錄
RT-Thread編程指南
RT-Thread用戶手冊
RT-Thread開發,如何有效學習RT-Thread的五個步驟

RT-Thread全球技術大會:Kconfig在RT-Thread中的工作機制

RT-Thread學習筆記 RT-Thread的架構概述

RT-Thread文檔_RT-Thread 潘多拉 STM32L475 上手指南

評論