嵌入式實時作業系統15——優先級反轉和死鎖

程序猿李巍 發佈 2022-02-13T00:51:03+00:00

1.信號量和互斥量的使用中的兩個問題信號量在作業系統中用於實現任務同步,通過同步機制可以實現多個任務合作,讓多任務之間按照先後順序執行。互斥量在作業系統中用於協調多任務使用共享共享資源。

1.信號量和互斥量的使用中的兩個問題

信號量在作業系統中用於實現任務同步,通過同步機制可以實現多個任務合作,讓多任務之間按照先後順序執行。

互斥量在作業系統中用於協調多任務使用共享共享資源。當一些共享資源被正在一個任務使用時,其它準備使用這些資源的任務,只能等待資源使用者放棄使用權後才能使用該資源。


信號量和互斥量廣泛應用於作業系統中,正是由於這些機制使得我們可以構建功能豐富的,龐大的,移植性強的軟體系統。但是如果使用不正確就會產生如下兩個常見的問題:
1、優先級反轉。
2、死鎖。

2.優先級反轉
優先級反轉是作業系統中使用信號量時,出現的一種優先級不合理的現象:
高優先級任務被低優先級任務阻塞,使得中等優先級的任務優先運行,而高優先級任務得不到調度。從運行現象上看,像是
中優先級任務高優先級任務具有更高的優先權。

導致優先級反轉的原因是:當高優先級的任務正等待信號量A,該信號量A正被一個低優先級任務占有,此時一個介於這兩個任務優先之間的中優先級任務搶占了低優先級任務的CPU使用權而運行。使得高優先級任務等待一個低優先級任務,而低優先級任務卻被中等優先級任務搶占無法執行。
優先級反轉運行圖如下:


優先級反轉後導致高優先級任務得不到調度,會影響系統的實時性,嚴重時甚至會產生錯誤結果。
舉例一個生活中的例子說明優先級反轉:
現在有一個公司,公司內部有3個員工:公司老闆,銷售經理,銷售員。這3個員工的等級是:
公司老闆 > 銷售經理 > 銷售員。
一天有一個重要的客戶來到公司,此時老闆需要列印一份公司的專利文件展示給客戶看,於是
老闆安排銷售員去列印專利文件,銷售員來到列印室開始列印專利文件,銷售員正在列印文件,此時銷售經理來到列印室列印一份很多頁數銷售合同,由於銷售經理是銷售員的上級,因此銷售員停止列印並將印表機讓給銷售經理列印合同。等待銷售經理完成列印合同後,銷售員繼續列印專利文件,完成列印後銷售員將專利文件交給老闆。由於等待的時間較久,導致客戶和老闆都很不愉快。

3.優先級繼承

優先級反轉後導致高優先級任務得不到調度,會影響系統的實時性,嚴重時甚至會產生錯誤結果。因此我們應該避免這種情況,如何解決優先級反轉?

優先級繼承就是解決優先級反轉問題的一種方法,優先級繼承的工作原理是:
低優先級任務獲得信號時候,此時如果有高優先級任務正在等待該信號時,作業系統臨時將低優先級任務的等級提高為和高優先級任務一樣的優先級,使得低優先級任務能更快的執行(不被中優先級任務搶占),釋放信號後低優先級任務恢復其原來的優先級

開啟優先級繼承後的運行圖如下:


回到上文中提到的例子:一個重要的客戶來到公司,老闆安排銷售員去列印專利文件,銷售員來到列印室開始列印專利文件,此時銷售經理來到列印室列印一份頁數很多銷售合同,此時銷售員說「
老闆正在等我列印文件,你必須等我列印完成」。由於等待的時間很短暫,使得客戶和老闆能很愉快溝通列印文件中的內容。


這就是優先級繼承的工作原理,優先級繼承保證了多任務系統中的實時性,保證多任務系統以正確合理的方式進行調度

4.死鎖

死鎖是指多個任務在運行過程中,由於競爭共享資源或者通信而造成的一種阻塞的現象。如果沒有外力作用,它們都將處於互相等待的狀態,這些任務永遠無法得到調度
產生死鎖的原因是:多個線程同時占用多個資源,但又在彼此等待對方釋放資源,
導致這些任務都將處於無限等待中

《太空旅客》電影講述了未來世界一艘搭載著五千餘名乘客的移民飛船,飛向殖民星球的故事。電影中有一個叫休眠倉的設備,人類進入休眠倉後可以進入休眠狀態。假設電影中的男主角進入了休眠倉等待女主角來喚醒,之後女主角也進入休眠倉等待男主角來喚醒,最終的結果是男女主角將進入無盡的等待,永遠無法喚醒


現有兩任務:每個任務都需要等待信號A和B,得到信號後執行相關操作,任務完成操作後就釋放信號。任務的代碼如下:


假設兩個任務的運行過程為:
task1開始執行,task1獲取信號A執行handle1()操作,此時切換到task2任務獲取信號B執行handle3()操作,隨後切換到task1,任務執行完成handle1()操作等待信號B(此時信號B為task2占有)進入休眠,task2任務完成handle3()操作等待等待信號A(此時信號A為task1占有)進入休眠。從此task1和task2進入死鎖狀態兩個任務都永遠得不到執行。


由此可見:在無外界干預的情況下,
處於死鎖狀態的任務將永遠無法運行。由於任務死鎖無法運行將導致軟體將失去相應的功能,因此我們必須避免在軟體設計中出現死鎖情況。

5.死鎖的解決方法

死鎖將對軟體系統帶來嚴重的影響,如何才能避免死鎖?

我們可以使用以下3個方法避免死鎖:
1、每個任務增加信號等待超時限制
2、每個任務等待獲取全部信號資源後再執行其它操作
3、每個任務避免嵌套使用信號資源


超時限制
死鎖任務在等待信號時可以設定一個超時時間,這個功能可以緩解死鎖問題,避免任務出現無限等待的情況。當出現死鎖情況時,任務在設定時間內沒有得到信號資源,任務則會放棄等待繼續執行。
回到《太空旅客》的例子,假設電影中的
男主角設定一個休眠時間進入了休眠倉並等待女主角來喚醒,之後不管女主角是否來喚醒,最終休眠倉也會自動將男主角喚醒


獲取全部信號資源後再執行其它操作
每個任務等待獲取全部信號資源後再執行其它操作,這種方法可以避免出現鎖死的情況,因為任務都獲得了所有信號資源才開始進行執行,在執行過程中不會因為信號資源進入休眠,當任務執行完成後釋放所有信號資源。優化後的代碼如下:

/* github: liyinuo2017        author:liwei */
void task1(void *pv)
{
        for(;;)
        {
                /* 等待信號A */
                wait_signal(A);                         
                /* 等待信號B */
                wait_signal(B);
                /* 執行操作 */
                handle1();                      
                /* 執行操作 */
                handle2(); 
                /* 釋放信號A */
                release_signal(A);                      
                /* 釋放信號B */
                release_signal(B);
        }
}

void task2(void *pv)
{
        for(;;)
        {
                /* 等待信號B */
                wait_signal(B);
                /* 等待信號A */
                wait_signal(A);                 
                /* 執行操作 */
                handle3();                      
                /* 執行操作 */
                handle4(); 
                /* 釋放信號B */
                release_signal(B);                      
                /* 釋放信號A */
                release_signal(A);

        }
}

避免嵌套使用信號資源
每個任務避免嵌套使用信號資源,當任務使用完信號後立即釋放信號,保證每個任務在一個時刻儘量少的占有信號資源,做到「即用即放」。優化後的代碼如下:

/* github: liyinuo2017        author:liwei */
void task1(void *pv)
{
        for(;;)
        {
                /* 等待信號A */
                wait_signal(A);
                /* 執行操作 */
                handle(); 
                /* 釋放信號A */
                release_signal(A);                      
                /* 等待信號B */
                wait_signal(B);
                /* 執行操作 */
                handle(); 
                /* 釋放信號B */
                release_signal(B);

        }
}

void task2(void *pv)
{
        for(;;)
        {
                /* 等待信號B */
                wait_signal(B);
                /* 執行操作 */
                handle(); 
                /* 釋放信號B */
                release_signal(B);                      
                /* 等待信號A */
                wait_signal(A);
                /* 執行操作 */
                handle(); 
                /* 釋放信號A */
                release_signal(A);

        }
}


未完待續…
實時作業系統系列將持續更新
創作不易希望朋友們點讚,轉發,評論,關注。
您的點讚,轉發,評論,關注將是我持續更新的動力
作者:李巍
Github:liyinuoman2017
CSDN:liyinuo2017
今日頭條:程序猿李巍

關鍵字: