作業系統設計/程序/訊號量
訊號量,在最基本的形式中,是一個受保護的整數變數,它可以在多處理環境中促進和限制對共享資源的訪問。兩種最常見的訊號量型別是計數訊號量和二進位制訊號量。計數訊號量表示多個資源,而二進位制訊號量,顧名思義,表示兩種可能的狀態(通常為 0 或 1;鎖定或解鎖)。訊號量是由已故的艾茲格·迪克斯特拉發明的。
訊號量可以被視為對有限數量資源的一種表示,例如餐館的座位容量。如果一家餐館有 50 個座位,而現在沒有人在那裡,則訊號量將被初始化為 50。隨著每個人到達餐館,他們導致座位容量減少,因此訊號量也會相應減少。當達到最大容量時,訊號量將為零,並且其他人將無法進入餐館。相反,希望的餐廳顧客必須等到有人用完資源,或者在這個比喻中,吃完飯。當一位顧客離開時,訊號量會增加,資源再次可用。
訊號量只能使用以下操作訪問:wait() 和 signal()。wait() 在一個程序想要訪問資源時呼叫。這相當於到達的顧客試圖獲得一張空桌子。如果有一張空桌子,或者訊號量大於零,那麼他就可以使用該資源並坐在桌邊。如果沒有空桌子,並且訊號量為零,則該程序必須等待直到它可用。signal() 在程序使用完資源時呼叫,或者顧客吃完飯時呼叫。以下是此計數訊號量(其中值可以大於 1)的實現。
wait(Semaphore s){
while (s==0); /* wait until s>0 */
s=s-1;
}
signal(Semaphore s){
s=s+1;
}
Init(Semaphore s , Int v){
s=v;
}
從歷史上看,wait() 被稱為 P(代表荷蘭語“Proberen”,意思是嘗試),signal() 被稱為 V(代表荷蘭語“Verhogen”,意思是增加)。標準 Java 庫改用“acquire”代表 P,“release” 代表 V。
當 P 或 V 正在執行時,其他程序不能訪問訊號量。這是透過原子硬體和程式碼實現的。原子操作是不可分割的,也就是說,它可以被認為是一個整體執行的。
如果只有一種資源,則使用二進位制訊號量,它只能具有 0 或 1 的值。它們通常用作互斥鎖。以下是使用二進位制訊號量實現互斥的示例。
do
{
wait(s);
// critical section
signal(s);
// remainder section
} while(1);
在此實現中,想要進入其臨界區的程序必須獲取二進位制訊號量,然後在發出完成訊號之前,它將獲得互斥訪問許可權。
例如,我們有訊號量 s,以及兩個想要同時進入其臨界區的程序 P1 和 P2。P1 首先呼叫 wait(s)。s 的值減為 0,P1 進入其臨界區。當 P1 處於其臨界區時,P2 呼叫 wait(s),但由於 s 的值為零,它必須等到 P1 完成其臨界區並執行 signal(s)。當 P1 呼叫 signal 時,s 的值增加到 1,然後 P2 可以繼續在其臨界區執行(再次減少訊號量)。互斥訪問許可權是透過確保一次只能有一個程序處於其臨界區來實現的。
如上面的示例所示,等待訊號量的程序必須不斷檢查訊號量是否不為零。在真正的多程式設計系統中(其中多個程序通常共享單個 CPU),這種持續迴圈顯然是一個問題。這稱為忙等待,它浪費 CPU 週期。當訊號量執行此操作時,它被稱為自旋鎖。
為了避免忙等待,訊號量可以使用一個與之關聯的程序佇列,這些程序正在等待訊號量,允許訊號量阻塞程序,然後在訊號量增加時喚醒它。作業系統可以提供block() 系統呼叫,它會掛起呼叫它的程序,以及wakeup(P <Process>) 系統呼叫,它會恢復被阻塞程序 P 的執行。如果一個程序對一個值為零的訊號量呼叫wait(),則該程序將被新增到訊號量的佇列中,然後被阻塞。該程序的狀態將切換到等待狀態,並將控制權轉移到 CPU 排程程式,CPU 排程程式將選擇另一個程序來執行。當另一個程序透過呼叫signal() 來增加訊號量時,並且佇列中有任務,則會從佇列中取出一個任務並恢復它。
在一個稍微修改的實現中,訊號量的值可以小於零。當一個程序執行wait() 時,訊號量計數會自動遞減。負值的幅度將決定有多少個程序在等待訊號量。
wait(Semaphore s){
s=s-1;
if (s<0) {
// add process to queue
block();
}
}
signal(Semaphore s){
s=s+1;
if (s<=0) {
// remove process p from queue
wakeup(p);
}
}
Init(Semaphore s , Int v){
s=v;
}