任務(wù)信令和通信機(jī)制
在本章中,將簡要介紹任務(wù)信號和任務(wù)間通信的核心機(jī)制。這些基元是事件驅(qū)動的并行編程的基礎(chǔ),它是基于RTOS的應(yīng)用程序良好實(shí)現(xiàn)的基礎(chǔ)。
與其直接進(jìn)入FreeRTOS的API,不如將每個基元與一些圖形例子和一些關(guān)于每個機(jī)制可被使用的建議一起介紹。不要擔(dān)心:在后面的章節(jié)中,我們將進(jìn)入使用API的細(xì)枝末節(jié)。現(xiàn)在,讓我們把注意力集中在基本原理上。
實(shí)時操作系統(tǒng)隊(duì)列
隊(duì)列的概念相當(dāng)簡單,但它們也非常強(qiáng)大和靈活,特別是如果你傳統(tǒng)上用C語言在裸機(jī)上編程的話。 在其核心,隊(duì)列只是一個循環(huán)緩沖區(qū)。然而,這個緩沖區(qū)包含一些非常特殊的屬性,比如原生的多線程安全,每個隊(duì)列可以靈活地容納任何類型的數(shù)據(jù),以及喚醒正在等待隊(duì)列中出現(xiàn)的項(xiàng)目的其他任務(wù)。默認(rèn)情況下,數(shù)據(jù)存儲在隊(duì)列中使用先進(jìn)先出(FIFO)排序--第一個被放入隊(duì)列的項(xiàng)目就是第一個被從隊(duì)列中移除的項(xiàng)目。
我們將首先看看當(dāng)隊(duì)列處于不同狀態(tài)和以不同方式使用時的一些簡單行為(發(fā)送與接收),然后繼續(xù)討論如何用隊(duì)列在任務(wù)之間傳遞信息。
簡單的隊(duì)列發(fā)送
第一個隊(duì)列例子是簡單地將一個項(xiàng)目添加(也被稱為發(fā)送)到有空位的隊(duì)列中:
當(dāng)項(xiàng)目被添加到有可用空間的隊(duì)列中時,添加立即發(fā)生。因?yàn)殛?duì)列中的空間是可用的,所以將項(xiàng)目發(fā)送到隊(duì)列的任務(wù)繼續(xù)運(yùn)行,除非有另優(yōu)先級更高的任務(wù)在等待隊(duì)列中出現(xiàn)的項(xiàng)目。
盡管與隊(duì)列的交互通常發(fā)生在任務(wù)內(nèi)部,但這并不總是這樣的。在一些特殊情況下,隊(duì)列也可以從ISR中訪問(但這種行為有不同的規(guī)則)。在本章的例子中,我們將假設(shè)任務(wù)從隊(duì)列中發(fā)送和接收項(xiàng)目。
簡單的隊(duì)列接收
在下圖中,任務(wù)被顯從隊(duì)列中接收一個項(xiàng)目:
當(dāng)任務(wù)準(zhǔn)備從隊(duì)列中接收項(xiàng)目時,默認(rèn)情況下,它將獲得最老的項(xiàng)目。在這個例子中,由于隊(duì)列中至少有一個項(xiàng)目,所以接收被立即處理,任務(wù)繼續(xù)運(yùn)行。
滿隊(duì)列發(fā)送
當(dāng)隊(duì)列已滿時,沒有信息被丟棄。相反,試圖將項(xiàng)目發(fā)送到隊(duì)列的任務(wù)將等待隊(duì)列中的可用空間,最長時間為預(yù)先確定的數(shù)量:
當(dāng)隊(duì)列已滿時,試圖向隊(duì)列發(fā)送項(xiàng)目的任務(wù)將等待,直到隊(duì)列中的空間變得可用,但只到指定的超時值。
在這個例子中,如果任務(wù)試圖向滿的隊(duì)列發(fā)送,并且它的超時值是10毫秒--它將只等待10毫秒的隊(duì)列中的空間變得可用。超時結(jié)束后,調(diào)用將返回并通知調(diào)用代碼發(fā)送失敗。如何處理這個失敗是由設(shè)置調(diào)用代碼的程序員決定的,并將根據(jù)使用情況而變化。極大的超時值可以用于真正的非關(guān)鍵性功能。只是要注意,這將導(dǎo)致發(fā)送任務(wù)有效地永遠(yuǎn)等待隊(duì)列中的空位(這顯然不再是實(shí)時的了)
你的代碼通常會被結(jié)構(gòu)化,以便嘗試向隊(duì)列中發(fā)送不會超時。作為程序員,你應(yīng)該根據(jù)具體情況來決定什么是可接受的時間量。你也有責(zé)任確定超時的嚴(yán)重性和糾正措施,如果真的發(fā)生超時。潛在的糾正措施可以從什么都不做(想想視頻通話中的丟幀)到緊急關(guān)機(jī)。
接收空隊(duì)列
訪問隊(duì)列可能導(dǎo)致任務(wù)阻塞的另一種情況是接收空隊(duì)列:
與等待空間的發(fā)送類似,從隊(duì)列中接收的任務(wù)也有可能被延遲。在空隊(duì)列的情況下,試圖從隊(duì)列中接收的任務(wù)將被阻塞,直到隊(duì)列中出現(xiàn)項(xiàng)目。如果在超時之前沒有項(xiàng)目出現(xiàn),調(diào)用代碼將被通知失敗。同樣,要采取的確切行動方案也是不同的。
有時,會使用無限期的等待。你經(jīng)常會遇到一些隊(duì)列的等待時間非常長,這些隊(duì)列正在接收來自外部接口的輸入,如串行端口,它們可能不會不斷地發(fā)送數(shù)據(jù)。如果串口另一端的人類用戶在很長一段時間內(nèi)沒有發(fā)送數(shù)據(jù),那就完全沒有問題。
另一方面,接收超時也可以用來確保你有一個最低可接受的數(shù)據(jù)量來處理。讓我們使用一個旨在以10赫茲(每秒10個讀數(shù))提供新讀數(shù)的傳感器。如果你正在實(shí)現(xiàn)一個依賴于這個傳感器的新鮮讀數(shù)的算法,一個略大于100毫秒的超時可以用來觸發(fā)一個錯誤。這個超時將保證該算法總是在新鮮的傳感器讀數(shù)上行動。在這種情況下,擊中超時可用于觸發(fā)某種類型的糾正措施或通知,說明傳感器沒有按照預(yù)期執(zhí)行。
任務(wù)間通信的隊(duì)列
既然已經(jīng)介紹了隊(duì)列的簡單行為,我們就來看看如何利用它們在任務(wù)之間移動數(shù)據(jù)。隊(duì)列的非常常見的用例是讓一個任務(wù)填充隊(duì)列,而另一個任務(wù)則從同一隊(duì)列中讀取數(shù)據(jù)。這通常是直截了當(dāng)?shù)模赡苡幸恍┘?xì)微的差別,這取決于系統(tǒng)是如何設(shè)置的:
在前面的例子中,任務(wù)1和任務(wù)2都在與同一個隊(duì)列進(jìn)行交互。任務(wù)1將向隊(duì)列發(fā)送一個項(xiàng)目。只要任務(wù)2的優(yōu)先級比任務(wù)1高,它就會立即收到該項(xiàng)目。
讓我們考慮另一個實(shí)例,在實(shí)踐中,當(dāng)多個任務(wù)與隊(duì)列進(jìn)行交互時,經(jīng)常會出現(xiàn)這種情況。由于搶占式調(diào)度器總是運(yùn)行具有最高優(yōu)先級的任務(wù),如果該任務(wù)總是有數(shù)據(jù)要寫入隊(duì)列,那么在另一個任務(wù)有機(jī)會從隊(duì)列中讀取數(shù)據(jù)之前,隊(duì)列就會充滿。下面是一個例子,說明這可能會發(fā)生的情況:
下面的數(shù)字與時間軸上的索引相對應(yīng):
- 任務(wù)2試圖從空隊(duì)列中接收一個項(xiàng)目。沒有項(xiàng)目可用,所以任務(wù)2阻塞。
- 任務(wù)1向隊(duì)列添加項(xiàng)目。由于它是系統(tǒng)中優(yōu)先級最高的任務(wù),任務(wù)1向隊(duì)列中添加項(xiàng)目,直到它沒有更多的項(xiàng)目可以添加,或者直到隊(duì)列已滿。
- 隊(duì)列被填滿了,所以任務(wù)1被阻塞了。
- 任務(wù)2被調(diào)度器賦予上下文,因?yàn)樗F(xiàn)在是可能運(yùn)行的最高優(yōu)先級任務(wù)。
- 一旦有項(xiàng)目從隊(duì)列中移出,任務(wù)1就會再次被賦予上下文(這是系統(tǒng)中優(yōu)先級最高的任務(wù),它現(xiàn)在可以運(yùn)行了,因?yàn)樗诘却?duì)列中的空間時被阻塞了)。在添加一個項(xiàng)目后,隊(duì)列已經(jīng)滿了,任務(wù)1被阻塞了。
- 任務(wù)2被賦予上下文并從隊(duì)列中接收一個項(xiàng)目:
隊(duì)列的另一個極其常見的用例是讓隊(duì)列接受來自許多不同來源的輸入。這對于像調(diào)試串口或日志文件這樣的東西特別有用。許多不同的任務(wù)可以寫入隊(duì)列,由一個任務(wù)負(fù)責(zé)從隊(duì)列中接收數(shù)據(jù)并將其推送到共享資源上。
實(shí)時操作系統(tǒng)的信號
Semaphores是另一種非常直接的,但卻很強(qiáng)大的結(jié)構(gòu)。semaphore這個詞起源于希臘語--近似的英語翻譯是sign-bearer,這是一種非常直觀的思考方式。信號燈被用來表示某些事情已經(jīng)發(fā)生;它們是事件的信號。一些信號燈的使用案例包括以下內(nèi)容:
-
ISR完成了對外圍設(shè)備的服務(wù)。它可以給出信號,為任務(wù)提供信號,表明數(shù)據(jù)已經(jīng)準(zhǔn)備好進(jìn)一步處理。
-
任務(wù)到達(dá)了關(guān)口,它需要等待系統(tǒng)中的其他任務(wù)跟上,然后再繼續(xù)前進(jìn)。在這種情況下,可以用semaphore來同步任務(wù)。
限制受限資源的同時使用者的數(shù)量。
-
使用RTOS的方便之處在于信號燈的預(yù)先存在。它們被包含在每RTOS的實(shí)現(xiàn)中,因?yàn)樗鼈兊墓δ苁侨绱说幕荆ê完P(guān)鍵)。有兩種不同類型的信號燈可供選擇:計數(shù)信號燈和二進(jìn)制信號燈。
計數(shù)信號
Counting semaphores最常被用來管理對同時使用的用戶數(shù)量有限制的共享資源。在創(chuàng)建時,它們可以被配置為持有最大值,稱為上限。通常給出的計算semaphores的例子是數(shù)據(jù)庫中的讀者...... 好吧,我們在這里談?wù)摰氖腔?a target="_blank">MCU的嵌入式系統(tǒng),所以讓我們保持我們的例子的相關(guān)性。如果你對數(shù)據(jù)庫感興趣,你可能最好用通用的操作系統(tǒng)! 對于我們的例子,假設(shè)你正在實(shí)現(xiàn)基于套接字的通信驅(qū)動,而你的系統(tǒng)只有足夠的內(nèi)存來滿足有限數(shù)量的同時套接字連接。
在下圖中,我們有一個共享網(wǎng)絡(luò)資源,可以容納兩個同時進(jìn)行的套接字連接。然而,有三個任務(wù)需要訪問。計數(shù)信號被用來限制同時進(jìn)行的套接字連接的數(shù)量。每當(dāng)任務(wù)使用完共享資源(即它的套接字關(guān)閉),它必須交出它的信號,以便另一任務(wù)能夠獲得對網(wǎng)絡(luò)的訪問。如果任務(wù)碰巧給了已經(jīng)達(dá)到最大計數(shù)的信號燈,這個計數(shù)將保持不變:
前面的圖演繹了一個共享資源只能同時為兩個任務(wù)服務(wù)的例子(盡管系統(tǒng)中有三個任務(wù)需要使用該資源)。如果任務(wù)要使用套接字,而這個套接字受到計數(shù)信號的保護(hù),它必須首先從池中獲取一個信號。如果沒有semaphore,那么該任務(wù)必須等待,直到有semaphore可用:
- 最初,semaphore被創(chuàng)建,最大(上限)為2,初始計數(shù)為0。
- 當(dāng)任務(wù)A和任務(wù)B試圖獲取semaphore時,他們立即成功。這時,他們可以各自打開套接字,通過網(wǎng)絡(luò)進(jìn)行通信。
- TaskC稍后,所以它需要等待,直到semaphores的計數(shù)小于2,這時網(wǎng)絡(luò)套接字就可以自由使用了。
- 在TaskB完成了通過其套接字的通信后,它將返回semaphore。
- 現(xiàn)在有了semaphore,TaskC完成了它的取舍,并被允許訪問網(wǎng)絡(luò)。
- 在TaskC獲得訪問權(quán)后不久,TaskB有另一條消息要發(fā)送,所以它試圖獲取信號燈,但需要等待可用的信號燈,所以它被置于睡眠狀態(tài)。
- 當(dāng)TaskC在網(wǎng)絡(luò)上進(jìn)行通信時,TaskA完成并返回它的semaphore。
- 任務(wù)B被喚醒并完成了它的任務(wù),這使得它能夠開始通過網(wǎng)絡(luò)進(jìn)行通信。
- 在TaskB得到它的信號后,TaskC完成了它的事務(wù)并歸還了它的信號。
等待信號是RTOS與其他大多數(shù)信號實(shí)現(xiàn)不同的地方--任務(wù)在等待信號時可以超時。如果任務(wù)未能及時獲得信號,它就不能訪問共享資源。相反,它必須采取另一種行動。這個替代行動可以是任何數(shù)量的行動,從嚴(yán)重到觸發(fā)緊急關(guān)機(jī)程序的故障,到僅僅在日志文件中提及或推送到調(diào)試串口供以后分析的良性事件。作為一個程序員,應(yīng)該由你來決定什么是適當(dāng)?shù)男袆臃桨福@有時會促使你與其他學(xué)科進(jìn)行一些困難的討論。
二進(jìn)制信號
二進(jìn)制信號燈實(shí)際上就是最大計數(shù)為1的計數(shù)信號燈,它們最常用于同步。當(dāng)任務(wù)需要在事件上進(jìn)行同步時,它將嘗試使用信號鏈,阻塞直到信號鏈變得可用或直到指定的超時時間結(jié)束。系統(tǒng)的另異步部分(無論是任務(wù)還是ISR)將給出信號燈。二進(jìn)制semaphores可以被多次給出,那段代碼沒有必要返回它們。在下面的例子中,任務(wù)A只給出信號,而任務(wù)B只接受信號:
任務(wù)B被設(shè)置為在繼續(xù)履行其職責(zé)之前等待信號(信號):
-
最初,TaskB試圖接受信號,但它并不存在,所以TaskB進(jìn)入了睡眠狀態(tài)。
-
過了一段時間,任務(wù)A發(fā)出了信號。
-
任務(wù)B被喚醒(由調(diào)度器喚醒;這發(fā)生在后臺),現(xiàn)在有了信號燈。它將進(jìn)行它所需要的工作,直到完成。然而,請注意,任務(wù)B不需要?dú)w還二進(jìn)制信號。相反,它只是再次等待它。
-
任務(wù)B再次被阻塞,因?yàn)樾盘枱舨豢捎茫ň拖竦谝淮我粯樱运M(jìn)入睡眠狀態(tài),直到有信號燈可用。
周而復(fù)始。
如果任務(wù)B "交還 "二進(jìn)制信號,它將立即再次運(yùn)行,而不會收到來自任務(wù)A的指令。其結(jié)果只是全速運(yùn)行的循環(huán),而不是在任務(wù)A發(fā)出信號的條件下被提示。
實(shí)時操作系統(tǒng)的互斥
術(shù)語mutex是相互排斥的簡寫。在共享資源和任務(wù)的上下文中,互斥意味著,如果一個任務(wù)正在使用共享資源,那么該任務(wù)是唯一被允許使用該資源的任務(wù)--所有其他任務(wù)都需要等待。
如果這一切聽起來很像二進(jìn)制信號燈,那是因?yàn)樗褪恰H欢€有一個額外的功能,我們很快就會介紹。首先,讓我們來看看使用二進(jìn)制信號燈來提供相互排斥的問題。
優(yōu)先級倒置
讓我們來看看在試圖使用二進(jìn)制信號提供互斥功能時發(fā)生的常見問題。
考慮三個任務(wù),A、B和C,其中A的優(yōu)先級最高,B的優(yōu)先級居中,而C的優(yōu)先級最低。任務(wù)A和C依靠信號燈來訪問它們之間共享的資源。由于任務(wù)A是系統(tǒng)中優(yōu)先級最高的任務(wù),它應(yīng)該總是在其他任務(wù)之前運(yùn)行。然而,由于任務(wù)A和任務(wù)C都依賴于它們之間共享的資源(由二進(jìn)制信號燈守護(hù)),這里有意外的依賴關(guān)系:
讓我們一步一步地通過這個例子來看看這種情況是如何發(fā)生的:
- 任務(wù)C(系統(tǒng)中優(yōu)先級最低的任務(wù))獲得了二進(jìn)制信號,開始做一些工作。
- 在任務(wù)C完成工作之前,任務(wù)A(最高優(yōu)先級的任務(wù))中斷并試圖獲得相同的信號,但由于任務(wù)C已經(jīng)獲得了信號而被迫等待。
- 任務(wù)B也搶占了任務(wù)C,因?yàn)槿蝿?wù)B的優(yōu)先級比任務(wù)C高。
- 任務(wù)C用共享資源完成了剩余的工作,這時它把信號燈還給了任務(wù)C。
- 任務(wù)A終于可以運(yùn)行了。
任務(wù)A最終能夠運(yùn)行,但要等到兩個低優(yōu)先級的任務(wù)都運(yùn)行完了才行。任務(wù)C用共享資源完成它的工作是不可避免的(除非在設(shè)計上做出改變,以防止它與任務(wù)A訪問相同的共享資源)。然而,任務(wù)B也有機(jī)會運(yùn)行到完成,盡管任務(wù)A在旁邊等待,并且有更高的優(yōu)先級!這就是優(yōu)先級倒置--更高的優(yōu)先級是指任務(wù)A在完成工作時,他的任務(wù)也在完成!這就是優(yōu)先級倒置--系統(tǒng)中優(yōu)先級較高的任務(wù)正在等待運(yùn)行,但它被迫等待,而另一個優(yōu)先級較低的任務(wù)正在運(yùn)行--在這種情況下,這兩個任務(wù)的優(yōu)先級實(shí)際上是倒置的。
互斥器使優(yōu)先級倒置最小化
早些時候,我們曾說過,在FreeRTOS中,突變體是二進(jìn)制信號,有一個重要的附加功能。這個重要的特性就是優(yōu)先級繼承--互斥器有能力暫時改變一個任務(wù)的優(yōu)先級,以避免在系統(tǒng)中造成重大延誤。當(dāng)調(diào)度員發(fā)現(xiàn)一個高優(yōu)先級的任務(wù)試圖獲取一個已經(jīng)被低優(yōu)先級的任務(wù)所持有的突變時,就會出現(xiàn)這種情況。在這種特定情況下,調(diào)度器將暫時提高低級任務(wù)的優(yōu)先級,直到它釋放突變。在這一點(diǎn)上,低級任務(wù)的優(yōu)先級將被設(shè)置回它在優(yōu)先級繼承之前的狀態(tài)。讓我們來看看上圖中使用互斥(而不是二進(jìn)制信號)實(shí)現(xiàn)的完全相同的例子:
讓我們一步一步地通過這個例子來看看這種情況是如何發(fā)生的:
- 任務(wù)A仍然在等待任務(wù)C返回互斥。
- 任務(wù)C的優(yōu)先級被提高到與更高優(yōu)先級的任務(wù)A相同。任務(wù)C運(yùn)行到完成,因?yàn)樗钟衜utex,是一個高優(yōu)先級的任務(wù)。
- 任務(wù)C返回突變,它的優(yōu)先級被降到了它持有突變之前的水平,因?yàn)橥蛔兊⒄`了高優(yōu)先級任務(wù)。
- 任務(wù)A拿著mutex并完成了它的工作。
- 任務(wù)B被允許運(yùn)行。
根據(jù)任務(wù)C在共享資源上花費(fèi)的時間,以及任務(wù)A的時間敏感性,這可能是一個主要的問題,也可能不是什么大問題。可以進(jìn)行時間分析,以確保任務(wù)A仍然符合最后期限,但跟蹤所有可能的優(yōu)先級倒置和其他高優(yōu)先級異步事件的原因可能被證明是具有挑戰(zhàn)性的。至少,用戶應(yīng)該利用為獲取突變提供的內(nèi)置超時,并在突變未能及時獲取的情況下執(zhí)行適當(dāng)?shù)奶娲袆印jP(guān)于如何實(shí)現(xiàn)這一目標(biāo)的更多細(xì)節(jié)可以在第9章 "任務(wù)間通信 "中找到。
Mutexes和semaphores是任務(wù)間信號傳遞的相當(dāng)標(biāo)準(zhǔn)的機(jī)制。它們在不同的RTOS之間是非常標(biāo)準(zhǔn)的,并且提供了很好的靈活性。
評論