信號量及其使用和實現(超詳細)(z)


轉自http://c.biancheng.net/view/1232.html

pv操作是一對原子操作,p操作的作用是申請資源,即將資源數減1,然后判斷資源數是否小於0,若小於0,則自我阻塞在當前資源鏈表中。
v操作的作用是歸還資源,將所申請的資源數加一,然后判斷資源數是否小於等於0,若小於等於0說明有進程阻塞在當前資源上,喚醒一個當前資源鏈表中的進程。

互斥鎖,我們剛剛討論過了,通常認為是最簡單的同步工具。本節將會討論一個更棒的工具,它的功能類似於互斥鎖,但是它能提供更為高級的方法,以便進程能夠同步活動。

一個信號量 S 是個整型變量,它除了初始化外只能通過兩個標准原子操作:wait () 和 signal() 來訪問:

  • 操作 wait() 最初稱為 P(荷蘭語proberen,測試);
  • 操作 signal() 最初稱為 V(荷蘭語verhogen,增加);


可按如下來定義wait ():

  1. wait(S){
  2. while (S <= 0)
  3. ;// busy wait
  4. S--;
  5. }

可按如下來定義signal():

  1. signal(S) {
  2. S++;
  3. }

在 wait() 和 signal() 操作中,信號量整數值的修改應不可分割地執行。也就是說,當一個進程修改信號量值時,沒有其他進程能夠同時修改同一信號量的值。另外,對於 wait(S),S 整數值的測試(S < 0)和修改(S--)也不能被中斷。

首先,我們看看如何使用信號量。

信號量的使用

操作系統通常區分計數信號量與二進制信號量。計數信號量的值不受限制,而二進制信號量的值只能為 0 或 1。因此,二進制信號量類似於互斥鎖。事實上,在沒有提供互斥鎖的系統上,可以使用二進制信號量來提供互斥。

計數信號量可以用於控制訪問具有多個實例的某種資源。信號量的初值為可用資源數量。當進程需要使用資源時,需要對該信號量執行 wait() 操作(減少信號量的計數)。當進程釋放資源時,需要對該信號量執行 signal() 操作(增加信號量的計數)。當信號量的計數為 0 時,所有資源都在使用中。之后,需要使用資源的進程將會阻塞,直到計數大於 0。

我們也可以使用信號量來解決各種同步問題。例如,現有兩個並發運行的進程:P1 有語句 S1 而 P2 有語句 S2。假設要求只有在 S1 執行后才能執行 S2。我們可以輕松實現這一要求:讓 P1 和 P2 共享同一信號量 synch,並且初始化為 0。

在進程 P1 中,插入語句:

S1;
signal (synch);

在進程 P2 中,插入語句:

wait (synch);
S2;

因為 synch 初始化為 0,只有在 P1 調用 signal(synch) ,即 S1 語句執行之后,P2 才會執行 S2。

信號量的實現

回想一下,互斥鎖實現具有忙等待。剛才描述的信號量操作 wait() 和 signal(),也有同樣問題。

為了克服忙等待需要,可以這樣修改信號量操作 wait() 和 signal() 的定義:當一個進程執行操作 wait() 並且發現信號量值不為正時,它必須等待。然而,該進程不是忙等待而是阻塞自己。阻塞操作將一個進程放到與信號量相關的等待隊列中,並且將該進程狀態切換成等待狀態。然后,控制轉到 CPU 調度程序,以便選擇執行另一個進程。

等待信號量 S 而阻塞的進程,在其他進程執行操作 signal() 后,應被重新執行。進程的重新執行是通過操作 wakeup() 來進行的,它將進程從等待狀態改為就緒狀態。然而,進程被添加到就緒隊列。(取決於 CPU 調度算法,CPU 可能會也可能不會從正在運行的進程切換到新的就緒進程。)

為了實現這樣定義的信號量,我們按如下定義信號量:

  1. typedef struct {
  2. int value;
  3. struct process *list;
  4. } semaphore;

每個信號量都有一個整數 value 和一個進程鏈表 list。當一個進程必須等待信號量時,就被添加到進程鏈表。操作 signal() 從等待、進程鏈表上取走一個進程,並加以喚醒。

現在,信號量操作 wait() 可以定義如下:

  1. wait(semaphore *S) {
  2. S->value--;
  3. if (S->value < 0) {
  4. add this process to S->list;
  5. block();
  6. }
  7. }

而信號量操作 signal() 可定義如下:

  1. signal(semaphore *S) {
  2. S->value++;
  3. if (S->value <= 0) {
  4. remove a process P from S->list;
  5. wakeup(P);
  6. }
  7. }

操作 block() 掛起調用它的進程。操作 wakeup(P) 重新啟動阻塞進程 P 的執行。這兩個操作都是由操作系統作為基本系統調用來提供的。

注意,這樣實現的信號量的值可以是負數,而在具有忙等待的信號量經典定義下,信號量的值不能為負。如果信號量的值為負,那么它的絕對值就是等待它的進程數。出現這種情況源於,在實現操作 wait() 時互換了遞減和測試的順序。

通過每個進程控制塊 PCB 的一個鏈接字段,等待進程的鏈表可以輕松實現。每個信號量包括一個整數和一個 PCB 鏈表指針。向鏈表中增加和刪除進程以便確保有限等待的一種方法采用 FIFO 隊列,這里的信號量包括隊列的首指針和尾指針。然而,一般來說,鏈表可以使用任何排隊策略。信號量的正確使用不依賴於信號量鏈表的特定排隊策略。

關鍵的是,信號量操作應原子執行。我們應保證:對同一信號量,沒有兩個進程可以同時執行操作 wait() 和 signal()。這是一個臨界區問題。

對於單處理器環境,在執行操作 wait() 和 signal() 時,可以簡單禁止中斷。這種方案在單處理器環境下能工作,這是因為一旦中斷被禁用,不同進程指令不會交織在一起。只有當前運行進程一直執行,直到中斷 被重新啟用並且調度程序重新獲得控制。

對於多處理器環境,每個處理器的中斷都應被禁止;否則,在不同處理器上不同的運行進程可能會以任意不同方式一起交織執行。每個處理器中斷的禁止會很困難,也會嚴重影響性能。因此,SMP 系統應提供其他加鎖技術,如 compare_and__swap() 或自旋鎖,以確保 wait() 與 signal() 原子執行。

重要的是,對於這里定義的操作 wait() 和 signal(),我們並沒有完全取消忙等待。我們只是將忙等待從進入區移到臨界區。此外,我們將忙等待限制在操作 wait() 和 signal() 的臨界區內,這些區比較短(如經合理編碼,它們不會超過 10 條指令)。因此,臨界區幾乎不被占用,忙等待很少發生,而且所需時間很短。對於應用程序,存在一種完全不同的情況,即臨界區可能很長(數分鍾或數小時)或幾乎總是被占用。在這種情況下,忙等待極為低效。

死鎖與飢餓

具有等待隊列的信號量實現可能導致這樣的情況:兩個或多個進程無限等待一個事件,而該事件只能由這些等待進程之一來產生。當出現這樣的狀態時,這些進程就為死鎖。

為了說明起見,假設有一個系統,它有兩個進程 P0 和 P1,每個訪問共享信號量 S 和 Q,這兩個信號量的初值均為 1:

P0 P1
wait(S);  wait(Q);
wait(Q);   wait(S);
signal(S); signal(Q);
signal(Q); signal(S);


假設 P0 執行 wait(S),接着 P1 執行 wait(Q)。當 P0 執行 wait(Q) 時,它必須等待直到 P1 執行 signal(Q)。類似地,當 P1 執行 wait(S) 時,它必須等待直到 P0 執行 signal(S)。由於這兩個操作 signal() 都不能執行,這樣 P0 和 P1 就死鎖了。

我們說一組進程處於死鎖狀態:組內的每個進程都等待一個事件,而該事件只可能由組內的另一個進程產生。這里主要關心的事件是資源的獲取和釋放。然而,其他類型的事件也能導致死鎖。

與死鎖相關的另一個問題是無限阻塞或飢餓,即進程無限等待信號量。如果對與信號量有關的鏈表按 LIFO 順序來增加和刪除進程,那么可能發生無限阻塞。

優先級的反轉

如果一個較高優先級的進程需要讀取或修改內核數據,而且這個內核數據當前正被較低優先級的進程訪問(這種串聯方式可涉及更多進程),那么就會出現一個調度挑戰。由於內核數據通常是用鎖保護的,較高優先級的進程將不得不等待較低優先級的進程用完資源。如果較低優先級的進程被較高優先級的進程搶占,那么情況變得更加復雜。

比如,假設有三個進程 L、M 和 H,它們的優先級順序為 L<M<H。假定進程 H 需要資源 R,而 R 目前正在被進程 L 訪問。通常,進程 H 將等待 L 用完資源 R。但是,現在假設進程 M 進入可運行狀態,從而搶占進程 L。間接地,具有較低優先級的進程 M,影響了進程 H 應等待多久,才會使得進程 L 釋放資源 R。

這個問題稱為優先級反轉。它只出現在具有兩個以上優先級的系統中,因此一個解決方案是只有兩個優先級。然而,這對於大多數通用操作系統是不夠的。通常,這些系統在解決問題時采用優先級繼承協議。

根據這個協議,所有正在訪問資源的進程獲得需要訪問它的更高優先級進程的優先級,直到它們用完了有關資源為止。當它們用完時,它們的優先級恢復到原始值。在上面的示例中,優先級繼承協議將允許進程 L 臨時繼承進程 H 的優先級,從而防止進程 M 搶占執行。當進程 L 用完資源 R 時,它將放棄繼承的進程 H 的優先級,以采用原來的優先級。因為資源 R 現在可用,進程 H(而不是進程 M)會接下來運行。


免責聲明!

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



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