信號量
信號量的英文叫做 semaphore,是一個用於在進程間傳遞的特殊便量,通常定義成一個結構體,其中包含一個整形變量,以及一個隊列,如下:
struct semaphore {
int count;
Queue queue; // 存放進程
}
-
semaphore.count
信號量的值 (count) 與相應資源的使用情況有關。當count > 0
時,表示當前可用資源的數量,當count < 0
時,其絕對值就表示當前等待使用資源的進程數。這邊要注意,semaphore.count
只能由 PV 操作來改變! -
semaphore.queue
semaphore.queue
的作用在於,存放那些被阻塞的進程(等待被喚醒的進程)。就是說,假設當前沒有可用資源了(semaphore.count <= 0)
,那麼這時如果又有進程想要使用資源,這時該進程應該進入阻塞狀態,我們就將該進程加入到semaphore.count
中。
而當今天某些資源被釋放了(semaphore.count > 0)
,那這時我們就要喚醒最早開始等待的那個進程,也就是semaphore.count
的隊列開頭。因此這邊採用先進先出的隊列是比較合適的。
PV 操作
一般來說,S >= 0,S 表示可用資源數量。執行一次 P 操作意味著請求分配一個單位資源,因此 S–,當 S <= 0 表示已經沒有可用資源了,這時請求者就必須等待別的進程釋放該資源,它才能繼續運行,否則就進入阻塞狀態。
而執行一次 V 操作意味著釋放一個單位資源,因此 S++,而當執行完一次 V 操作後,如果此時 S < 0,就表示說當前還有 |S| 個正在等待的阻塞態進程,因此要喚醒一個進程,使之運行下去。
在往下看 PV 操作的實現之前,我們先假設,當前調用 PV 操作的進程為 p_proc_ready
,然後對一個進程而言,有兩種狀態(pv_state
),分別為阻塞態 PV_PROCESS_BLOCK
以及就緒態 PV_PROCESS_READY
。
並且,我們還要加上一個 schedule()
方法,目的是在執行完 P 操作後,因為當前進程進入了阻塞態,因此我們要找到下一個要執行的進程並把控制權交給它,簡單來說,schedule()
就是負責重新調度。
P 操作
P 操作意味請求分配一個資源,也稱為 wait() 操作,使 S–,若 S < 0,進程進入阻塞態,放入信號量的等待隊列。
P(semaphore S) {
S.count--;
if(S.count < 0) {
p_proc_ready->pv_state = PV_PROCESS_BLOCK;
S.queue.add(p_proc_ready);
schedule();
}
}
V 操作
V 操作意味釋放一個單位資源,也可以稱為 signal() 操作,使 S++,若 S <= 0,要喚醒一個進程,使之繼續執行。
V(semaphore S) {
S.count++;
if(S.count <= 0) {
// 喚醒隊列中第一個阻塞進程
// get() 操作返回出隊元素
S.queue.get()->pv_state = PV_PROCESS_READY;
}
}
讀者-寫者問題
讀者-寫者問題可以分為兩種策略,以下就來分別做介紹以及代碼實現。不過在介紹這兩種策略之前,先要有一些基礎知識。
在讀者-寫者問題中,有一區是進程之間共享的數據區,而進程分為讀者進程和寫者進程。讀者進程當然是負責從這個共享區讀取數據,而寫者進程則是向該共享區寫入數據。並且,有一些規則無論是在哪種策略下,都必須遵守:
- 同時可以有多個讀者讀共享區
- 同時只能有至多一個寫者寫共享區
- 如果當前有寫者在寫共享區,那麼就不能有任何讀者在讀共享區
優先的概念
無論是哪種策略,在沒有進程占用臨界區時,讀者與寫者的競爭都是公都是公平的,所謂的不公平(優先)是在讀者優先和寫者優先中,優先方只要佔有臨界區,那麼之後所有優先方的進程就都有了臨界區的主導權。
除非沒有優先方進程提出要求,否則始終是優先方進程佔有臨界區,反觀,即使非優先方在某次佔有了臨界區,那麼釋放過後,回到沒有進程佔有臨界區的情況時,非優先方又要和優先方公平競爭。所謂「優先」的概念其實可以理解為優先方在佔有臨界區後便可以對臨界區進行 “壟斷”。
讀者優先
讀者優先策略的規則如下:
即使寫者發出了請求寫的信號,但是只要還有讀者在讀取內容,就還允許其他讀者繼續讀取內容(體現了優先方始終擁有進入臨界區的主導權),直到所有讀者結束讀取,才真正開始寫。
- 有讀者在讀後面來的讀者可以直接進入臨界區,而已經在等待的寫者繼續等待直到沒有任何一個讀者時。
- 讀者之間不互斥,寫者之間互斥,只能一個寫,可以多個讀。
- 讀者寫者之間互斥,有寫者寫則不能有讀者讀。
讀者優先解決方案:
wrt
為互斥信號量,其count == 1
表示可用資源只有一個,相當於只有一個共享的文件。readCount
相當於一個計數器,紀錄讀者數,初值為 0。當每個讀者進程來,就要readCount++
,而當readCount == 1
就表示第一個讀者來了,需要搶佔共享的資源,否則表示已經有其他讀者進程在讀數據了。當每走一個讀者進程,就要readCount--
,當readCount == 0
,就表示走的已經是最後一個讀者進程了,所以需要釋放共享的資源,否則表示還有其他讀者進程還在讀。readCount
是多個讀者進程共享的變量,應該要在臨界區中,用互斥信號量mutex1
控制,mutex1.count
初值為 1。
int readCount = 0;
semaphore mutex1, wrt;
mutex1.count = 1;
wrt.count = 1;
// 讀者進程結構
reader_first_reader {
P(mutex1);
readCount++;
if(readCount == 1) {
P(wrt);
}
V(mutex1);
/* reading operation */
P(mutex1);
readCount--;
if(readCount == 0) {
V(wrt);
}
V(mutex1);
}
// 寫者進程結構
reader_first_writer {
P(wrt);
/* writing operation */
V(wrt);
}
寫者優先
寫者優先策略的規則如下:
如果有寫者申請寫數據,在申請之前已經開始讀取數據的可以繼續讀取,但是如果再有讀者申請讀取數據,則被拒絕,只有在所有的寫者寫完之後才可以讀取。
- 寫者執行緒的優先順序高於讀者執行緒。
- 當有寫者到來時應該阻塞讀者執行緒的隊列。
- 當有一個寫者正在寫時或在阻塞隊列時應當阻塞讀者進程的讀操作,直到所有寫者進程完成寫操作時放開讀者進程。
寫者優先解決方案:
在讀者優先的基礎上增加:
- 信號量
rd
,rd.count
初值為 1,用於在至少有一個寫者準備訪問共享數據區時,禁止所有讀進程。 - 計數器
writeCount
,紀錄寫者數,初值為 0。writeCount
是多個寫者進程共享的變量,應該要在臨界區中,因此還要增加一個信號量mutex2
,用互斥信號量mutex2
控制,mutex2.count
初值為 1。 - 信號量
mutex3
,初值為 1。因為在rd
上不允許建立長隊列,否則寫者進程將無法跳過這個隊列,因此只允許一個讀進程在rd
上排隊,其餘都先在mutex3
上排隊。
int readCount, writeCount = 0;
semaphore mutex1, mutex2, mutex3, wrt, rd;
mutex1.count = 1;
mutex2.count = 1;
mutex3.count = 1;
wrt.count = 1;
rd.count = 1;
// 讀者進程結構
writer_first_reader {
P(mutex3);
P(rd);
P(mutex1);
readCount++;
if(readCount == 1) {
P(wrt);
}
V(mutex1);
V(rd);
V(mutex3);
/* reading operation */
P(mutex1);
readCount--;
if(readCount == 0) {
V(wrt);
}
V(mutex1);
}
// 寫者進程結構
writer_first_writer {
P(mutex2);
writeCount++;
if(writeCount == 1) {
P(rd);
}
V(mutex2);
P(wrt);
/* writing operation */
V(wrt);
P(mutex2);
writeCount--;
if(writeCount == 0) {
V(rd);
}
V(mutex2);
}