進程同步、互斥機制


一、進程的並發執行

 

1. 並發是所有問題產生的基礎。

 

2. 進程的特征:

並發:進程執行時間斷性的,執行速度是不可預測的;

共享:進程/線程之間的制約性;

不確定性:進程執行的結果和執行的相對速度有關,所以是不確定的;

 

3. 舉例:

1) 銀行業務系統:進程的關鍵活動出現交叉;

2) get-->copy-->put;

並發環境下執行,時間上,速度上都不確定,執行次序的不同會導致不同的結果。

 

4. 並發環境的制約關系:進程前趨圖

 

 

 

二、進程互斥(MUTUAL EXCLUSIVE)

 

1. 競爭條件:兩個或多個進程在讀寫某些共享數據時,而最后的結果取決於進程運行的精確的時序;

 

2. 進程互斥:由於各個進程要求使用共享資源,而這些資源需要排他性的使用,各個進程之間競爭使用這些資源,這一關系稱為進程互斥;

 

3. 臨界資源:系統中某些資源一次只希望允許一個進程使用,稱這樣的資源為臨界資源或者互斥資源或共享變量;

 

4. 臨界區(互斥區):多個進程中對某個臨界資源實施操作的程序片段;

 

5. 臨界區的使用原則:

1)如果沒有進程在臨界區,想進入臨界區的進程就可以進入;

2)不允許兩個進程同時處於臨界區內;

3)臨界區外運行的進程不得阻塞其他進程進入臨界區;

4)不得使進程無限期等待進入臨界區;

 

6. 實現進程互斥的方法:軟件方案、硬件方案;

 

 

、進程互斥的軟件解法

軟件方法:保護臨界區

 

正確算法:

1. DEKKER算法

2. PETERSON算法(更好)

 

 

四、進程互斥的硬件解法

用特殊指令來達到保護臨界區的目的;

 

1. 開關中斷指令:

1)簡單、高效

2)代價高,限制CPU的並發能力

3)不適用於多處理器

4)適用於操作系統本身,不適用於用戶程序

 

2. 測試並加鎖指令:

 

3. 交換指令: 

 

4. 忙等待:進程在得到臨界區訪問權限之前,持續做測試而不做其他事情;(單CPU不提倡)

    自旋鎖:(多處理器情況),忙等待就比較好,因為切換的開銷是很大的;

 

 

 

五. 進程同步

 

1. 進程同步(synchronization):指系統中多個進程發生的時間存在某種時序關系,需要相互合作,共同完成某一項任務;

進程之間的協作關系;

 

 

2. 生產者消費者問題(又稱為緩沖區問題):

生產者進程-->緩沖區-->消費者進程

只能由一個生產者或者消費者對緩沖區進行操作;

 

避免忙等待:

睡眠與喚醒操作(原語):sleep() 與 wakeup()  操作

 

3. SPOOLing系統:生產者消費者問題

 

 

 

六. 信號量及P、V操作(一種經典的進程同步機制):

1. 1965荷蘭學者Dijkstrat提出;P與V分別是荷蘭語 test (proberen) 和 increment (verhogen);

 信號量:

 一個特殊變量,用於在進程間傳遞信息的一個整數值;

 定義如下:
struct semaphore{
    int count;

    queueType  queue;

}

信號量說明:semaphore a;

對信號量可以實施的操作:初始化、P操作和V操作;

 

2. P(down,semWait)、V(up,semSignal)操作

 

P操作相當於申請資源,而V操作相當於釋放資源。所以要記住以下幾個關鍵字:

P操作----->申請資源

V操作----->釋放資源

 

P操作:信號量值減1;

            然后判斷信號量值是否小於0,如果小於0,則將該進程設置為阻塞狀態;將該進程插入相應的等待隊列s.queue末尾;

           否則實施P操作的進程就繼續執行;

 

V操作:信號量值加1;

            如果信號量值<=0,則說明原來信號量上有進程在等待,所以喚醒s.queue中的第一個等待進程;改變其為就緒態,並將其插入就緒隊列;

           否則,實施v操作的進程就繼續執行;

 

3. 說明:

  • P操作和V操作是原語操作;
  • 信號量上定義了三個操作:初始化(非負數)、P操作和V操作;
  • 最初提出的是二元信號量(解決互斥),最后推廣到一半信號量(多值)或計數信號量解決同步;

 

 

4. 在理解了PV操作的的含義后,就必須講解利用PV操作可以實現進程的兩種情況:互斥和同步。 

1)一個生產者,一個消費者,公用一個緩沖區。

可以作以下比喻:將一個生產者比喻為一個生產廠家,如伊利牛奶廠家,而一個消費者,比喻是學生小明,而一個緩沖區則比喻成一間好又多。

第一種情況,可以理解成伊利牛奶生產廠家生產一盒牛奶,把它放在好又多一分店進行銷售,而小明則可以從那里買到這盒牛奶。只有當廠家把牛奶放在商店里面后,小明才可以從商店里買到牛奶。所以很明顯這是最簡單的同步問題。

 

解題如下:

定義兩個同步信號量:

empty——表示緩沖區是否為空,初值為1。

full——表示緩沖區中是否為滿,初值為0。

 

生產者進程

while(TRUE){

    生產一個產品;

    P(empty);

     產品送往Buffer;

     V(full);

}

 

消費者進程

while(TRUE){

   P(full);

   從Buffer取出一個產品;

   V(empty);

   消費該產品;

}

 

2)一個生產者,一個消費者,公用n個環形緩沖區。

 第二種情況可以理解為伊利牛奶生產廠家可以生產好多牛奶,並將它們放在多個好又多分店進行銷售,而小明可以從任一間好又多分店中購買到牛奶。同樣,只有當廠家把牛奶放在某一分店里,小明才可以從這間分店中買到牛奶。
不同於第一種情況的是,第二種情況有N個分店(即N個緩沖區形成一個環形緩沖區),所以要利用指針,要求廠家必須按一定的順序將商品依次放到每一個分店中。緩沖區的指向則通過模運算得到。

 

解題如下:

  定義兩個同步信號量:

  empty——表示緩沖區是否為空,初值為n。

  full——表示緩沖區中是否為滿,初值為0。

  設緩沖區的編號為1~n-1,定義兩個指針in和out,分別是生產者進程和消費者進程使用的指針,指向下一個可用的緩沖區。

 

生產者進程

while(TRUE){

     生產一個產品;

     P(empty);

     產品送往buffer(in);

     in=(in+1)mod n;

     V(full);

}

 

消費者進程

while(TRUE){

   P(full);

   從buffer(out)中取出產品;

   out=(out+1)mod n;

   V(empty);

   消費該產品;

}

 

3)一組生產者,一組消費者,公用n個環形緩沖區

第三種情況,可以理解成有多間牛奶生產廠家,如蒙牛,達能,光明等,消費者也不只小明一人,有許許多多消費者。不同的牛奶生產廠家生產的商品可以放在不同的好又多分店中銷售,而不同的消費者可以去不同的分店中購買。當某一分店已放滿某個廠家的商品時,下一個廠家只能把商品放在下一間分店。所以在這種情況中,生產者與消費者存在同步關系,而且各個生產者之間、各個消費者之間存在互斥關系,他們必須互斥地訪問緩沖區。

 

解題如下:

定義四個信號量:

empty——表示緩沖區是否為空,初值為n。

full——表示緩沖區中是否為滿,初值為0。

mutex1——生產者之間的互斥信號量,初值為1。

mutex2——消費者之間的互斥信號量,初值為1。

設緩沖區的編號為1~n-1,定義兩個指針in和out,分別是生產者進程和消費者進程使用的指針,指向下一個可用的緩沖區。

 

生產者進程

while(TRUE){

     生產一個產品;

     P(empty);

     P(mutex1);

     產品送往buffer(in);

     in=(in+1)mod n;

     V(mutex1);

     V(full);

}

 

消費者進程

while(TRUE){

   P(full);

   P(mutex2);

   從buffer(out)中取出產品;

   out=(out+1)mod n;

   V(mutex2);

   V(empty);

}

 

 

5. 用P、V操作解決進程間互斥問題:

  • 分析並發進程的關鍵活動,划定臨界區;
  • 設置信號量mutex,初值為1;
  • 在臨界區之前實施P(mutex);
  • 在臨界區之后實施V(mutex);

 

七. 生產者消費者問題

生產者、緩沖區、消費者

 

生產者-消費者(producer-consumer)問題,也稱作有界緩沖區(bounded-buffer)問題,兩個進程共享一個公共的固定大小的緩沖區。

其中一個是生產者,用於將消息放入緩沖區;另外一個是消費者,用於從緩沖區中取出消息。

問題出現在當緩沖區已經滿了,而此時生產者還想向其中放入一個新的數據項的情形,其解決方法是讓生產者此時進行休眠,等待消費者從緩沖區中取走了一個或者多個數據后再去喚醒它。

同樣地,當緩沖區已經空了,而消費者還想去取消息,此時也可以讓消費者進行休眠,等待生產者放入一個或者多個數據時再喚醒它。

 

聽起來好像蠻對的,無懈可擊似的,但其實在實現時會有一個競爭條件存在的。為了跟蹤緩沖區中的消息數目,需要一個變量 count。

如果緩沖區最多存放 N 個消息,則生產者的代碼會首先檢查 count 是否達到 N,如果是,則生產者休眠;否則,生產者向緩沖區中放入一個消息,並增加 count 的值。

消費者的代碼也與此類似,首先檢測 count 是否為 0,如果是,則休眠;否則,從緩沖區中取出消息並遞減 count 的值。同時,每個進程也需要檢查是否需要喚醒另一個進程。

 

代碼可能如下:

 

// 緩沖區大小
#define N 100               
int count = 0;                // 跟蹤緩沖區的記錄數

/* 生產者進程 */
void procedure(void)
{
        int item;                // 緩沖區中的數據項
        
        while(true)                // 無限循環
        {               
                item = produce_item();                // 產生下一個數據項
                if (count == N)                                // 如果緩沖區滿了,進行休眠
                {
                        sleep();
                }
                insert_item(item);                        // 將新數據項放入緩沖區
                count = count + 1;                        // 計數器加 1
                if (count == 1)                         // 表明插入之前為空,
                {                                                        // 消費者等待
                        wakeup(consumer);                // 喚醒消費者
                }
        }
}

/* 消費者進程 */
void consumer(void)
{
        int item;                // 緩沖區中的數據項
        
        while(true)                // 無限循環
        {
                if (count == 0)                                // 如果緩沖區為空,進入休眠
                {
                        sleep();
                }
                item = remove_item();                // 從緩沖區中取出一個數據項
                count = count - 1;                        // 計數器減 1
                if (count == N -1)                        // 緩沖區有空槽
                {                                                        // 喚醒生產者
                        wakeup(producer);
                }
                consume_item(item);                        // 打印出數據項
        }
}

 

 看上去很美,哪里出了問題,這里對 count 的訪問是有可能出現競爭條件的:緩沖區為空,消費者剛剛讀取 count 的值為 0,而此時調度程序決定暫停消費者並啟動執行生產者。生產者向緩沖區中加入一個數據項,count 加 1。

現在 count 的值變成了 1,它推斷剛才 count 為 0,所以此時消費者一定在休眠,於是生產者開始調用 wakeup(consumer) 來喚醒消費者。但是,此時消費者在邏輯上並沒有休眠,所以 wakeup 信號就丟失了。

當消費者下次運行時,它將測試先前讀到的 count 值,發現為 0(注意,其實這個時刻 count 已經為 1 了),於是開始休眠(邏輯上)。而生產者下次運行的時候,count 會繼續遞增,並且不會喚醒 consumer 了,所以遲早會填滿緩沖區的,

然后生產者也休眠,這樣兩個進程就都永遠的休眠下去了。

 

使用信號量解決生產者-消費者問題

首先了解一下信號量吧,信號量是 E.W.Dijkstra 在 1965 年提出的一種方法,它是使用一個整型變量來累計喚醒的次數,供以后使用。在他的建議中,引入了一個新的變量類型,稱為信號量(semaphore).

一個信號量的取值可以為 0(表示沒有保存下來的喚醒操作)或者為正值(表示有一個或多個喚醒操作)。

並且設立了兩種操作:down 和 up(分別為一般化后的 sleep 和 wakeup,其實也是一般教科書上說的 P/V 向量)。對一個信號量執行 down 操作,表示檢查其值是否大於 0,如果該值大於 0,則將其值減 1(即用掉一個保存的喚醒信號)並繼續;

如果為 0,則進程休眠,而且此時 down 操作並未結束。另外,就是檢查數值,修改變量值以及可能發生的休眠操作都作為單一的,不可分割的 原子操作 來完成。

下面開始考慮用信號量來解決生產者-消費者問題了,不過在此之前,再次分析一下這個問題的本質會更清晰點:問題的實質在於發給一個(尚)未休眠進程(如上的消費者進程在只判斷了 count == 0 后即被調度出來,還未休眠)的 wakeup 信號丟失

(如上的生產者進程在判斷了 count == 1 后以為消費者進程休眠,而喚醒它)了。如果它沒有丟失,則一切都會很好。

#define N 100                                // 緩沖區中的槽數目
typedef int semaphore;                // 信號量一般被定義為特殊的整型數據
semaphore mutex = 1;                // 控制對臨界區的訪問
semaphore empty = N;                // 計數緩沖區中的空槽數目
semaphore full         = 0;                // 計數緩沖區中的滿槽數目

/* 生產者進程 */
void proceducer(void)
{
        int item;
        
        while(1)
        {
                item = procedure_item();        // 生成數據
                down(&empty);                                // 將空槽數目減 1
                down(&mutex);                                // 進入臨界區
                insert_item(item);                        // 將新數據放入緩沖區
                up(&mutex);                                        // 離開臨界區
                up(&full);                                        // 將滿槽的數目加 1
        }
}

/* 消費者進程 */
void consumer(voi)
{
        int item;
        
        while(1)
        {
                down(&full);                                // 將滿槽數目減 1
                down(&mutex);                                // 進入臨界區
                item = remove_item();                // 從緩沖區中取出數據項
                up(&mutex);                                        // 離開臨界區
                up(&empty);                                        // 將空槽數目加 1
                consumer_item(item);                // 處理數據項
        }
}

 

該解決方案使用了三個信號量:一個為 full,用來記錄充滿的緩沖槽的數目,一個為 empty,記錄空的緩沖槽總數,一個為 mutex,用來確保生產者和消費者不會同時訪問緩沖區。

mutex 的初始值為 1,供兩個或者多個進程使用的信號量,保證同一個時刻只有一個進程可以進入臨界區,稱為二元信號量(binary semaphore)。

如果每一個進程在進入臨界區前都執行一個 down(...),在剛剛退出臨界區時執行一個 up(...),就能夠實現互斥。

另外,通常是將 down 和 up 操作作為系統調用來實現,而且 OS 只需要在執行以下操作時暫時禁止全部中斷:測試信號量,更新信號量以及在需要時使某個進程休眠。

這里使用了三個信號量,但是它們的目的卻不相同,其中 full 和 empty 用來同步(synchronization),而 mutex 用來實現互斥。

 

 

七. 用信號量解決讀者/寫者問題:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM