第7章

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 58

第七章 行程同步

合作行程 (cooperating process)是可以影響系統中其它執行的行程,或被其它行


程影響的行程。合作行程之間可能直接分享一塊邏輯位址空間 (也就是,程式碼和
資料),或是只能經由檔案分享資料。前一種狀況是經由輕量級行程 (lightweight
process)或是執行緒 (thread) 達成,這在第五章討論過。同時存取到共同資料可能造
成資料前後不一致。在本章中,我們將討論不同的方法,以確保分享同一塊邏輯位
址空間的合作行程,可以有秩序地執行,使得資料的前後一致性可以維持。

7.1 背景
在第四章中,我們已發展出一套系統模型,其中包含一些合作的循序行程
(cooperating sequential processes),這些行程都是以非同步的方式執行,並且可能
分享資料。我們已經用有限緩衝區的方法來描述過此模型,它可以用來代表一套作
業系統。

讓我們回到我們在 4.4 節所介紹過的,有限緩衝區這個問題的共用記憶體解


答。就如我們所指出,我們的解答最多只允許 BUTTER_SIZE-1 個緩衝區是填滿的。
假設我們想要修正此演算法以補救這個缺點。一個可能性便是加入一個整數變數
counter,開始時定為 0。每當一個新填滿的緩衝區被加入緩衝器池時,counter 便被
加 1,而每當我們從池中移去一個填滿的緩衝區時便被減 1。生產者行程的程式碼可
被修改如下:

消費者行程之程式碼可修改如下:

1
“Counter++” 和 “counter—“ 敘述的並行執行是相當於循序執行,而上面所列
的較低層次的敘述是以任意的次序 (但是每個高層次的敘述中其次序被維持不變)
來交插的。其中一種交插形式是:

注意,我們已經得到一個不正確的狀態 “counter == 4”,記錄有四個填滿的緩衝區


而實際上卻有五個填滿的緩衝區。如果我們顛倒 T4 和 T5 敘述的次序,我們將得到
一個不正確的狀態 “counter==6”。

我們會得到這個不正確的狀態是因為我們允許兩個行程並行處理這個 counter
變數。像這種數個行程同時存取和處理相同資料的情況,而且執行的結果取決於存
取時的特殊順序,就叫競賽情況 (race condition)。為了避免上述的競爭情況,我們
必須確定一次只能有一個行程來處理變數 counter。這將需要某種形式的同步。這種
狀況在作業系統中的不同部份處理資源時經常發生,而我們希望任何改變不會彼此
影 響 。 本 章 的 主 要 部 份 就 是 在 考 慮 行 程 同 步 (process synchronization) 和 協 調
(coordination)的問題。

7.2 臨界區間問題
若一個系統含有 n 個行程 {P0, P1, ..., Pn-1}。其中每一行程含有一段稱作臨界區
間 (critical section)的碼,在這段程式中,行程可改變共同變數、更新表格、寫一檔
案等工作。這種系統最重要的一個特點為當一個行程在其臨界區間內執行,不允許
其它的行程在它們的臨界區間內執行。因此對每一行程而言,其臨界區間的執行在
時間上來說明是互斥的 (mutually exclusive)。而這臨界區間問題就是設計一套協
定,使得各行程能夠互相合作。每一個行程需要求允許進入其臨界區間執行。而能
完成這種要求的程式碼部份叫作入口區間 (entry section)。又在臨界區間之後必須
緊跟著出口區間 (exit section)。剩餘的碼則是剩餘區間 (remainder section)。
解決臨界區間問題必須滿足下列三項要求:
1. 互斥 (mutual exclusion):如果行程 Pi 正在臨界區間內執行,則其它的行程
不能在其臨界區間內執行。
2. 進行 (progress):如果沒有行程在臨界區間內執行,同時某一行程想要進入
其臨界區間,那麼只有那些不在剩餘區間執行的行程才能加入決定誰將在下

2
一次進入臨界區間,並且這個選擇不得無限期地延遲下去。
3. 限制性的等待 (bounded waiting):在一個行程已經要求進入其臨界區間,而
此要求尚未被答應之前,允許其它的行程進入其臨界區間的次數有一個限制。

在提出一種演算法時,我們僅定義用作同步目的的變數,並且只描述一典型的行程
Pi,其一般結構為圖 7.1 所示。入口區間和出口區間被包含在方塊之中以強調程式碼
的重要段落。

7.2.1 兩個行程之解決方法
7.2.1.1 演算法 1
我們所提出的第一個方法就是令行程在共用一個初值為 0 (或 1)的共同整數變
數 turn。若 turn == i,則允許行程 Pi 在其臨界區間內執行。行程 Pi 的結構如圖 7.2
中所示。

3
這解答可保證一次只有一個行程會位於其臨界區間內。但是,它並未滿足要求,
因為它只是一味的交互改變在臨界區間中執行的行程。舉例來說,若 turn==0 且 P1
欲進入其臨界區間,即使 P0 可能是在它剩餘區間內,P1 仍不能如願。

7.2.1.2 演算法 2
演算法 1 的問題是它不能記憶每一個行程的狀態,而只能記憶被允許進入其臨
界區間的那個行程。為了消除此間題,我們以下述的陣列 (array)來代替變數 turn:

此陣列中的每一個元素之初值都是 false (偽)。若 flag [ i ] = true (真),則行程 Pi 可準


備進入其臨界區間中執行。行程 Pi 的結構如圖 7.3 中所示。

於此解法中,互斥的要求是滿足的。但不幸地,進行的要求則末符合。為說明
此一問題,我們且考慮以下的執行順序。

現在 P0 和 P1 將在它們各自的 while 敘述中永無止境的作迴圈運作。

請注意到設定 flag [ i ] 指令和測試 flag [ i ]之值的順序互換,並不會解決我們的


問題。反而會導致二個行程同時在臨界區間之內的狀況,而違反互斥的要求。

4
7.2.1.3 演算法 3
將演算法 1 和演算法 2 的主要觀念結合一起即可得滿足以上三種要求之臨界區
間問題的正確解答。這些行程有兩個共有的變數:

在啟始時 flag[0] = flag[1] = false,且 turn 的值為 0 或 1。行程 Pi 的結構如圖 7.4 所


示。

現在我們要證明解答是正確的。為了達到這目的,我們需要證明:
1. 互斥性存在,
2. 進行的要求能被滿足,
3. 限制性的等待要求亦能符合。

為了證明性質 1.我們注意到只有在 flag[j] == false 或 turn == i 時每個 Pi 才進


入其臨界區間。同時也注意到,如果兩個行程可同時在它們的臨界區間中執行的話,
那麼 flag[0] == flag[1] == true。這兩個觀察結果表示 P0 和 P1 不能成功地大約在同一
時間執行它們的 while 敘述,因為 turn 的值可能是 0 或 1,但不是二者。所以,其
中一個行程(譬如 Pj)必須已經成功地執行 while 敘述,而 Pi 至少必須執行額外的敘
述 “ turn == j “。然而,由於在這時間點上 flag[j] == true,並且 turn == j,而且只
要 Pj 是在臨界區間中這個條件就能持續,結果是:互斥性存在。

為了證明性質都 2.和 3.,我們注意到一個行程 Pi 只有在它被具有 flag[j] == true


和 turn == j 條件的 while 迴路所限制時它才被防止進入臨界區間;這是唯一的迴
路。如果 Pj 並不熱衷於進入臨界區間,那麼 flag[j] == false 並且 Pi 能夠進入其臨界

5
區間。如果 Pj 已經設定 flag[j] == true 且也在其 while 敘述中執行,則不是 turn ==
i 就是 turn == j。如果 turn == i,那麼 Pi 將進入臨界區間。如果 turn == j,則 Pj 將
進入臨界區間。無論如何,一旦 Pj 跳出其臨界區間,它將重設 flag[j] 為 false,允
許 Pi 進入臨界區間。如果 Pj 重設 flag[j] 為 true,它一定也設定 turn == i。因此,
由於執行 while 敘述時 Pi 並沒有改變 turn 變數的值,在 Pj 最多進入一次之後 (限制
式的等待),Pi 將進入臨界區間(進行)。

7.2.2 多個行程之解決方法
當每一個消費者進入店舖時都可收到一個號碼,而號碼最小者就是下一個將被
服務的顧客。但是 bakery 演算法(圖 7.5)並不能保證兩個行程絕不會取得相同號碼。
在這種號碼相同狀況下,則名稱最小的行程將最先被服務。因此,若 Pi 與 Pj 取得相
同的號碼且 i<j,則 Pi 將先被服務。由於行程的名稱具唯一性及次序性,所以吾人
的演算法是完全可定論的 (completely deterministic)。

共同的資料結構為:

6
7.3 同步之硬體
就如同其它軟體的情況一樣,硬體上的特殊性質可以使寫程式的工作變得比較
容易,並且增進系統的效率。本節裏,我們將提出在許多系統上都有的一些簡單硬
體指令,並指出它們是如何有效地用來解決臨界區間問題。

只要我們能夠在共用變數被更改時不讓中斷發生,則臨界區間的問題可以簡單
地被解決。以這種方法,我們可以確保目前的指令序列可以不被搶先,按照順序地
執行。沒有其它的指令會被執行,所以共用變數不會被意外地改變。

因此,許多機器都提供了一些特殊的硬體指令,允許我們可以在同一記憶體週
期內去測試修改一個字組的內容,或交換兩個字組的內容。我們可以使用這些特殊
的指令輕易地解決臨界區間的問題。除了討論特定機器的特殊的指令之外,讓我們
藉著定義下述的指令來摘要隱藏於這些指令之後的主要概念。

其中 TestAndSet (測試與設定)指令如圖 7.6 所示。這些指令的重要特性就是它


們可以在每一個記憶體週期時間中個別執行;也就是視為一個不可中斷的單位。因
此若兩個 TestAndSet 指令同時被執行(在不同的 CPU 上),則它們將可做任意的順序
執行。

若機器提供 TestAndSet 指令。則可以藉著聲明初值為 false 的布林變數 lock 來


塑造出互斥性。行程 Pi 的結構如圖 7.7 中所示。

Swap 指令的定義如圖 7.8 所示。交換兩個字組之間的內容,如同 TestAndSet


指令,Swap 指令也是自動地執行。

如果機器提供了 Swap 指令,則互斥性可提供如下。全域(global)布林變數 lock


被宣告並設定初值為 false。此外,每個行程也有一個區域布林變數 key。行程 Pi
的結構如圖 7.9 中所示。

7
8
7.4 號誌
在 7.3 節中所提出的臨界區間問題解答,並不易適用至更複雜的問題。為了解
決這困難,吾人將介紹另一種新的同步工具,稱為號誌 (semaphores)。號誌 S 是
一個整數變數,除了初值,它只能經由 wait 和 signal 兩個標準的不可分割的運算
來存取(這些運算最初是被表為 P (對 wait) 和 V (對 signal) 的形式))。這些名稱源
於荷蘭字的 proberen (表測試)和 verhogen (表遞增)。 wait 的傳統定義以假指令表
示為:

signal 的傳統定義以假指令表示為:

wait 與 signal 兩個運算是以不可分之方式來執行修正號誌的整數值。因此,一


個行程修正號誌值時,無其它的行程可同時去修正此號誌。此外,在 wait (S)的情況
下,S 整數值的測試 (S≦0),和它可能的改變 (S--) 也必須不被中斷地執行。在 7.4.2
節中我們可看出這些運算如何被實施;現在,先讓我們看看如何使用 semaphore。

7.4.1 用途
我們可以使用號誌來處理 n 個行程的臨界區間問題。這 n 個行程共用一共同的
號誌 mutex (表示互斥),其初值為 1。其中每一行程 Pi 的組織如圖 7.11 中所示。

號誌又可用來解決各種的同步問題。舉例來說,若有兩個並行執行的行程:P1
具敘述 S1,P2 具敘述 S2。假設吾人令 S2 必須在 S1 完成之後才可被執行。若令 P1
與 P2 共用一共同的號誌 synch,其初值為零,且插入下列敘述

於行程 P1,且插入敘述

9
於 P2,則可建立出一同步體系。由於 Synch 的初值為 0,故 P2 只可在 P1 呼叫 signal
(synch) 之後才可執行 S2。

7.4.2 製作
7.2 節所述的互斥解答與上一節所談論的號誌定義都有一個主要的缺點,那就
是,它們都需要忙碌等待(busy-waiting)。意即當一個行程置於其臨界區間時,其它
欲進入它們的臨界區間之行程必定在入口的程式碼形成迴路。在一個實際的多元程
式規劃系統中,由許多行程共用一個 CPU 的情形下,這顯然是一項問題。忙碌等待
浪 費 了 CPU 可 讓 給 其 它 行 程 使 用 的 循 環 。 此 種 類 型 的 號 誌 也 稱 為 盤 旋 鎖
(spin-lock),因為行程在等待鎖住的同時一直”盤旋”著。盤旋鎖在多元處理器系統中
非常有用。盤旋鎖的優點在於當一行程必須等待一個鎖時毋須做內容轉換 (因內容
轉換可能需要相當的一段時間)。因此,若希望鎖住一段很短的時間,則盤旋鎖很有
用。

為了解決這種忙碌等待的必要性,我們需修改號誌 wait 與 signal 等運算之定


義。就是當一個行程執行 wait 運算且發現此號誌值不為正,那麼這行程就必須等候。
但是除了忙碌等待之外,這行程也可能自我閉鎖。這閉鎖的運算將使一行程進入等
候狀態中。然後再將控制移交至 CPU 排班程式,以選取其它行程來執行之。

若一行程被閉鎖而等候號誌 S,則其可因其它的行程執行 signal 運算而再啟


始。故這行程可被喚醒(wakeup)運算再啟始,而把這行程的狀態自閉鎖改變成預備,
並將它置於就緒佇列。(CPU 可依據排班演算法而把其正在執行的行程轉換至這新
的處理。)

10
現在可把號誌運算 wait 定義如下:

號誌運算 signal 現在可定義如下:

7.4.3 死結和飢餓
使用等待佇列製做訊號可能導致有二個或以上的行程等待一項僅能由等待行程
所引發事件的情形。此處所謂的事件是指一個 signal 運算的執行;而當上述情形發
生時,我們稱這些行程被打了死結(deadlocked)。

另 一 個 與 死 結 相 關 的 問 題 為 無 限 期 的 阻 滯 (indefinite blocking) 或 是 飢 餓
(starvation);這可能會產生一個行程在號誌間做無限期的等待的情況。如果我們在
一以後進先出(LIFO)次序在號誌中加入或移去行程,上述情況便可能發生。

7.4.4 二元號誌
前面幾個小節所敘述的號誌通常就叫做計數號誌(counting semaphore),因為,
它的數值可以不受限制。一個二元號誌 (binary semaphore)是一個數值可以是 0 或 1
的號誌。二元號誌的製作可能會因為硬體的結構而此計數號誌容易製作。現在我們
將說明如何使用二元號誌來製作一個計數號誌。
令 S 為一計數號誌。使用二元號誌來製作計數號誌時,我們需要以下的資料
結構:

11
一開始時,S1=1,S2=0,而整數 C 的數值則設定為計數號誌 S 的初值。
對於計數號誌 S 的 wait 操作可以製作如下:

對於計數號誌 S 的 signal 操作可以製作如下:

7.5 典型的同步問題
7.5.1 有限緩衝區問題
在 7.1 節中所述的有限緩衝區(bounded-buffer)問題通常只被用來描述同步之基
本運作的功效。在此我們將針對這系統提出一個一般性的結構,而非提出任何的特
定方法。我們假設一台 n 個緩衝區的池(pool),其中每一個緩衝區可保存一項。
mutex 號誌可提供存取此緩衝區群的互斥性且其初值為 1。又 empty 與 full 號誌
可分別計算空的與滿的緩衝區數量,號誌 empty 初值為 n,但 full 之初值為 0。

生產者行程的程式碼如圖 7.12 中所示,而消費者行程程式碼則如圖 7.13 中所


示。注意生產者與消費者的對稱性。吾人可解釋此程式主要是生產者對消費者生產
滿的緩衝區,或消費者對生產者產生空的緩衝區。

12
7.5.2 讀取者/寫入者問題
若資料(如一檔案或一記錄)可被許多並行行程所共用。而這些行程有的只是自
這些共用資料中讀出一些內涵,但有的行程卻想去改變(讀或寫)這共用資料。吾人
即可參照這些行程的工作形態,將之區分為兩種形式,只讀出資料的稱為讀取者
(reader),而其餘的即稱為寫入者(writer)。顯而易見,若有兩個讀取者同時去存取共
用資料,並不會產生不良的結果。然而,如果一個寫入者和某些其它的行程(讀取者
或寫入者)同時存取共用的資料時,紛亂可能因而產生。

為了確保這類錯誤絕不會發生,我們需要這些寫入者一次只有一個可以存取這
共用資料。這同步問題就是所謂的讀取者/寫入者問題。由於它是最初被提出的,因
此已被用來測試幾乎每個新的同步問題。這讀取者/寫入者問題有許多變形,都含有
次序性。最簡單的一種稱為第一種讀取者/寫入者問題,其中除非有一寫入者已獲得
允許去使用這共用資料,否則讀取者不需保持等候狀態。換句話說,讀取者不需等
候其它的讀取者結束,而寫入者需等候。而第二種讀取者/寫入者問題只要一個寫入
者預備好之後,需盡快的使其能撰寫共用資料。換句話說。就是若有一寫入者等候
存取此資料,則沒有新的讀取者可開始去讀取資料。

我們可知這兩個問題的解答都產生飢餓。在第一種狀況下,寫入者可能會飢餓;
而在第二種狀況下,讀取者可能會飢餓。因此,又有其它的解答被提出。在本節中
我們將對第一種讀取者/寫入者提出解答。參考資料中有對第二種讀取者/寫入者問
題的變形提出避免飢餓的解答。

13
針對第一個讀取者/寫入者問題的解答,讀取者行程共用下述資料結構。

其中號誌 mutex 與 wrt 的初值為 1,而 readconut 的初值為 0。又號誌 wrt 為讀


取者與寫入者行程所共用。mutex 號誌在變數 readcount 改變時,被用來保證互斥性
的成立。readcount 被用來記錄現在讀取這共用資料的行程數目。號誌 wrt 被用作寫
入者的互斥號誌,亦可被第一個/最後一個進入/離開臨界間的讀取者使用。但是當
有其它的讀取者在它們的臨界區間時,進入或離開臨界區間的讀取者並不可使用此
號誌。

寫入者行程的一般性結構如圖 7.14 所示;而讀取者行程的一般性結構如圖 7.15


中所示。注意,若有一寫入者位於其臨界區間,且有 n 個讀取者在等候,則其中只
有一個讀取者可在 wrt 中排隊,而其它 n-1 個讀者則在 mutex 中排隊。又當一個撰
寫者執行 signal (wrt),我們即可回復正在等候的讀取者或是正在等候的單一寫入者
之執行。通常可用排班程式來選取下一個行程。

14
7.5.3 哲學家進餐的問題
有五個哲學家將他們的生活全部用於思考與吃飯。這些哲學家共用一張有 5 張
椅子的圓桌,其中每個哲學家擁有一張椅子。又這張桌子除了在其中央有一盒米飯
之外,還擺了 5 支筷子。一個哲學家在思考問題時,並不和他的同僚交換意見。若
某哲學家感覺飢餓時,他就試圖去使用最靠近他的筷子(這些筷子介於他和他的左右
鄰居之間)。又哲學家每次只能使用一雙筷子。顯而易見地,他不能夠使用他的鄰居
正在使用的筷子。一個飢餓的哲學家同時可使用他的兩枝筷子吃飯,而不需放下其
中任何一枝筷子。而等到他吃完飯後,他放下他的兩枝筷子,且重新開始思考。

哲學家吃飯問題被當做一個古典的同步問題來探討並不是因為它在實用上的重
要性,而是因為它是大型系統中並行控制問題的一個範例。它是以免於死結和飢餓
的方式在數個行程間分配數項資源的簡易表示法。

這問題有一個簡單的解決方式就是用一個號誌表示一枝筷子。若一哲學家欲拾
取一枝筷子即執行這號誌的 wait 運算;又若放下此枝筷子時,就執行這號誌的 signal
運算。因此,共用的的資料為:

其中 chopstick 的每一元素之初值為 1。而哲學家 i 的結構如圖 7.17 中所示。

雖然這解答保證沒有兩個相鄰的哲學家可同時吃飯,但是它可能會產生死結,
故必須捨棄此解決方法。假設五個哲學家同時感覺到飢餓,且每個人都拾取他們左
邊的一枝筷子。則此時 chopstick 的每個元素都為 0。而當一哲學家想使用他右邊
的筷子時,將永遠延遲。

15
我們已將恢復此死結問題的可能方法表列於 7.7 節中,我們將提出一個演算法
以確保免於死結。

⚫ 至多允許四個哲學家可同時坐在此桌旁。
⚫ 允許一個哲學家只有在他左右二枝筷子均為可用時,才可拾取,(注意,這
樣做可能會引起臨界區間問題。)
⚫ 使用一不對稱的解決方法。就是座次為奇數的哲學家先使用其左邊的筷
子,然後再使用其右邊的筷子。而座次為偶數的哲學家先使用其右邊的筷
子,再使用其左邊的筷子。

7.6 臨界區域
雖然號誌的方式提供予行程同步的便利及有效率之機能,但若使用不當則仍可
能導致一些難以偵測的時序錯誤,蓋這些錯誤只發生於某些特殊的執行順序時,而
這些順序則不一定產生。

此種類型之錯誤的例子我們在使用計數器 (counter)來解決生產者-消費者問題
時已看到,在該例子中,時序問題極少發生,而即使發生,計數值也是一個合理的
數值,只比正確值少 1。然而,這顯然並非一個可接受的解決方法,正因此理由,
所以最先就介紹號誌。

但不幸地,此種時序錯誤在使用號誌時仍可能發生。為說明它是如何發生,我
們來回顧使用號誌的臨界區間問題之解決。所有的行程都共用一個號誌變數
mutex,它最初是被設定成 1。每個行程在進入臨界區間前必須執行 wait (mutex),
而後為 signal (mutex)。若不遵守此順序,則二個行程可能同時地處於它們臨界區間
中。

為處理以上這些種類的錯誤,一些研究人員已發展出了許多的語言結構。在這
一節中,我們將介紹一種基本的高階同步結構── 臨界區間 (critical region) (有時
候又叫做條件式的臨界區間 (conditional critical region) )。在 7.7 節中,我們將介紹
另外一種基本的同步結構──監督程式 (monitor)。在我們介紹這兩種結構時,我
們假設一個行程是由一些局部資料,以及可以使用這些資料的一個循序行程所組
成。而局部資料則僅可由融入在相同行程內的循序程式來存取。也就是說,一行程
無法直接地存取另一行程的局部資料,但多個行程則可共用全體性資料 (global
data)。

臨界區間的高階語言同步結構需要一個讓許多行程所共用的 T 形態變數 V,它


可被宣告成:

16
而此變數 V 只能被以下的區域取用:

這種結構表示,當指令 S 在執行時,絕對沒有其它的行程會用到變數 V。運算


式 B 是布林運算式,它控制了臨界區間的能否進入。當一個行程想要進入某一個區
間時,必須先測試布林運算式 B 的值。如果是真(true),則執行 S,若是偽(false),
則行程將放棄互斥,並會做某些程度的延緩(delay),直到 B 為真,並且沒有其它的
行程可由變數 V 而進入臨界區域。因此,如果在不同的行程中同時執行以下的敘述:

則結果將等於順序執行 ”S1 後按著 S2”,或者 ”S2 後按著 S1”。

臨界區域的結構可以避免大部份利用號誌來解決臨界區間問題時,因程式設計
師疏忽而引起的錯誤。注意!這並沒有完全剔除了因同步問題而產生的錯誤,而是
減少其數目。如果錯誤發生於程式的邏輯部份,要重新產生一特定的事件串列可就
不是件簡單的事了。

臨界區間的結構可以有效地用來解決某些同步問題。為了說明起見,讓我們把
它用在”有限緩衝區”上。緩衝區空間 (buffer space) 及指標 (pointers) 宣告如下:

生產者 (producer) 行程藉著以下的程式,將一個新元素 nextp,加入共用緩衝區中:

17
而消費者 (consumer) 行程藉著以下程式,將從共用緩衝區中移走一個元素,並將
nextc 加入其中:

現在再來看看編譯程式是如何製作條件式臨界區域 。對共用變數 X 來說 ,
有以下相關的變數

號誌 mutex 一開始時設定成 1;號誌 first_delay 和 second_delay 一開始時則


設定成 0。整數 first_count 和 second_count 的初值則設成 0。

臨界區間的互斥存取則由 mutex 提供 。如果一個行程因為布林條件 B 是假


而不能進入臨界區間,則它會等待號誌 first_delay。等待 first_delay 的行程最終會
在它被允許重新測試布林條件 B 之前,被移到等待號誌 second_delay。我們分別
使用 first_count 和 second_count 來記錄等待 first_delay 和 second_delay 的行程
個數。

當一個行程離開臨界區間時,它可能已經改變某個阻止其它行程進入臨界區間
的布林條件 B 之數值。因此,我們必須追蹤整個在等待 first_delay 和 second_delay
的行程佇列,以允許每個行程都能測試它的布林條件。當一個行程測試它的布林條
件時 (於此追蹤期間),它將發現後者現在已計算出 true 值。此時,這個行程即進入
它的臨界區間。否則,此行程必須再次等待號誌 first_delay 和 second_delay,方法就
如前面所述。

region x when (B) S;

因此,對於一個共用變數 X 而言,以下的敘述可以使用圖 7.18 的程式實現。請


注意!這種製作方式需要花每一次行程離開臨界區域時,重新計算任何等待行程的運
算式 B 之值。如果有數個行程被延遲 (由於等待它們個別的布林運算值為真),則此
種重新計值的花費將導致不具效率的碼。有一些最佳化的方法可用來降低此種花
費,有興趣的讀者可參考本章後面的參考資料。

18
7.7 監督程式
另一種高階之同步架構為監督程式 (monitor) 。監督程式之特色為該程式是由
一組程式設計者定義之運算元所組成。此監督程式之形式之表示是由變數宣告所組
成,該變數之值定義了形式範例的狀態,以及形式運作之程序或函數之實體。監督
程式之語法如圖 7.19 所示。

監督程式的結構可以確保在同一個時間之內,只有一個行程在監督程式內活
動。監督程式本身保證了這一項特性,因此,程式設計師不需要另外設計此同步限
制的程式部份(如圖 7.20)。因此監督程式在定義上和臨界區域有許多相近之處。誠
如我目前面所看到的,臨界區域並不是很有效率的一種作法,因此更進一步的擴展,
便有了所謂的 ”條件式臨界區域”。同樣的。在監督程式的作法上,我們也需要附加
一些處理同步問題的功能。這些功能是建立在條件架構 (condition construct)上。程
式設計師希望自定同步架構時,可以定義一個或多個條件變數:

condition x, y ;

19
20
這種條件變數,只有透過 wait 和 signal 指令才能運作。其中運作:

x . wait ( ) ;

的意義是,執行這運作的行程,必須暫時等待,直到有另一個行程執行底下運作為
止。

x . signal ( ) ;

而 x . signal ( ) ; 運作,每次只能恢復一個等待行程的動作。如果沒有任何等待
行程存在,則此 signal 運作將不產生任何作用,其對應 x 的狀態,就跟沒有執行運
作時一樣 (圖 7.21)。這一點和 signal 運作比較,是有差別的,signal 運作只要一執
行,就必定改變號誌的狀態。

現在假設行程 P 執行 x . signal ( ) ; 運作,而在條件 x 下等候的,有一個行程 Q。


這個時候如果我們允許等候行程 Q 能恢復執行,則發出訊號 (signal) 的行程 P 就必
須等待。否則 P 和 Q 便會同時在監督程式內運作。當然,在原則上這兩個行程任何
一個都可以繼續執行,一般的處理方法有下列兩種:

21
1. P 等候 Q,直到 Q 離開監督程式。或等候其它條件成立為止。
2. Q 等候 P,直到 P 離開監督程式,或等候其它條件成立為止。

現在是我們介紹解決 dinting-philosopher 問題之方法的時候。其中筷子的分配是


由監督程式 dp 所控制,dining-philosopher 是這種監督程式的一個例子,它的定義如
圖 7.22 所示。在開始吃飯之前,每一個哲學家必須有 pickup 的動作,這可能起因於
哲學家行程的懸浮狀態。在動作成功的完成之後,哲學家才可以吃。在此之後,哲
學家會有 putdown 的動作,並且開始思考。因此,第 i 位哲學家必須在下列的執行
順序中有 pickup 與 putdown 這兩個動作。

22
現在我們接著介紹條件變數的實作方法。對每一個條件 x 而言,我們引用一個
號誌 x_sem,和一個整數變數 x_count,初始值都設定為 0。則相對應的 x_wait 運
作可以解析為下列指令:

而 x . signal ( ) ; 運作則可編寫為:

現在讓我們再回到關於行程恢復順序的問題上。這個問題就是說,當許多個行
程在等候著某一個條件 x 成立時,如果有一個 x . signal 運作被執行,到底應該先讓
那一個行程恢復執行?當然最簡單的方法是採用 FCFS 法 (first-come-first-served),
也就是讓等最久的行程最先恢復執行。但是這種單純的處理方法,在許多狀況下是
不適合的。基於這個理由,有人推出所謂的條件式等待 (condition wait) 架構。其格
式如下:

x . wait ( c ) ;

其中 c 為一整數變數,其值在 wait 運作時計算出來,稱之為優先級數 (priority


number)。當一個行程被擱置時,其優先級數和名稱一併儲存起來。當有 x . signal
運作被執行時,其中優先級數最小的一個等候行程首先被恢復執行。

為了說明這項新的處理方法。考慮有一個監督程式如圖 7.23 所示負責把單一資


源分配給相互競爭的行程,而且在每一個行程要求資源的同時,都能夠預先指定使
用此資源的最大可能時間,監督程式就會將資源分配給使用時間最短的行程。

23
任何一個行程存取資源時,都必須遵循下列順序:

其中 R 為資料型態 ResourceAllocation 的實例。

很不幸地,監督程式的概念不能保證,上述的存取順序一定能被遵循。特別是:
⚫ 一個行程可能在沒有先取得某項資源的存取允許權之前就先使用該資源。
⚫ 一個行程在獲得某項資源的存取權之後,就佔住不放。
⚫ 一個行程可能會試圖釋放一項從未取得過的資源。
⚫ 一個行程可能會對相同的資源提出兩次要求 (沒有先釋放出該資源)。

上述問題的一種可能解決方法是,把資源存取的操作包含在資源分配的監督程
式中。但是這種方法會造成,排班將根據監督程式的排班演算法,而不是依照我們

24
撰寫的程式碼來進行。

為確保每一個行程都能遵守正確的執行順序,我們必須查核每一個使用資源分
配的監督程式及其控制下的資源。要確保這套系統的正確性,必須查核底下兩個條
件:第一,使用者行程必須時時依照正確的順序來呼叫監督程式。第二,我們必須
保證某些不合作的行程,不致直接跳過互斥條件的控制,逕自存取共用的資源,而
不經由既定的協定來存取。唯有在這兩個條件都能夠成立的前提下,才有可能保證
與時間有關的錯誤不致發生,且相對應排班法則不致被破壞。

7.9 不可分割的交易
臨界區間的互斥性保證了臨界區間的執行之不可分割。換言之,如果兩個臨界
區間並行地執行,結果相當於它們以某種未知的順序循序地執行。雖然這項性質在
許多應用領域很有用,依然有許多情況下我們希望工作是完整的執行完,或完全不
執行。資金移轉就是一個例子,在這種情況下一個帳戶扣除金額,另一帳戶則加入
金額。很明顯地,扣除和加入的一起發生或都不發生以使資料一致是很重要的。

本節的剩餘部份是關於資料庫系統的領域。資料庫 (databases) 是關於資料的


儲存和取出,以及資料的一致性。最近,興起應用資料庫系統的技術在作業系統的
興趣。作業系統可以視為資料的處理者;因此,可以從資料庫研究的進階技術和模
型得到一些幫助。例如,許多使用在作業系統中管理檔案的各種技巧可以在正規資
料庫方法使用上時,變得更有彈性和功能強大。在 7.9.2 到 7.9.4 節中,我們描述這
些資料庫的技術,以及如何應用在作業系統。

7.9.1 系統模型
一組執行單一邏輯函數的指令(操作)稱為交易(transaction)。處理交易的一
項主要事項是不管電腦系統中的各種失效可能依然維持其不可分割。我們首先考慮
一次只能執行一項交易的環境。然後,再考慮可同時有數個交易動作的情況。交易
是一個存取或可能更新在磁碟上一些檔案之不同資料項的程式單元。從我們的觀點
來看,交易只是一系列的讀和寫的動作,最後以 commit (交付)或 abort (中止)操作
結束。交付操作指出交易已成功地結束其執行,而中止操作則指示交易由於一些邏
輯錯誤而停止其正常執行。一個成功地完成其執行的終止交易是被交付
(committed);否則,它就被中止 (aborted)。一個已交付的交易不能由中止此交易而
撤消。

交易也可能由於系統失效而停止其正常執行。在早期的例子,因為一個中止的
交易可能已經修改它所存取的各種資料,這些資料的狀態可能和此交易完整執行完

25
畢時不一樣。為了能確保不可分割的性質,被中止的交易對於已經修改的資料必須
不影響其狀態。因此,被一個中止交易存取過的資料狀態必須恢復到交易開始執行
前的情況。我們稱此交易被撤回 (rolled back)。這是系統的部份責任以確保此性質。

為了決定系統該如何確保不可分割的性質,我們首先需要識別用來儲存被交易
存取各項資料之裝置的特性。不同型態的儲存媒體是由它們的速度、容量和失效彈
性來區分。

⚫ 揮發性儲存體 (volatile storage):儲存在揮發性儲存體的資訊在系統當掉


時通常不存在。這種儲存體的例子有主記憶體和快取記憶體。對揮發性儲
存體的存取非常快,這是因為記憶體存取本身的速度,和因為可以直接在
揮發性儲存體存取任何資料項。
⚫ 非揮發性儲存體 (nonvolatile storage):儲存在非揮發性儲存體的資訊在系
統當掉之後通常還存在。這種儲存體的例子有磁碟和磁帶。但是磁碟和磁
帶也會失效,這就造成資訊的遺失。目前非揮發性儲存體的速度比揮發性
儲存體的速度慢好幾個級次。因為磁碟和磁帶裝置是電子機械式,並且需
要實體移動以存取資料。
⚫ 穩定儲存體 (stable storage):在穩定儲存體的資訊絕不會遺失 (理論上無
法保證絕對)。為了製作近似這種的儲存體,我們需要複製資訊到數個非揮
發性的儲存體快取 (通常是磁碟),每一個有各自的失效模式,並且以控制
的方式更新資訊(14.7 節)。

7.9.2 以記載簿為基礎的復原
確保不可分割的方法之一是在穩定儲存體上記錄,描述所有由交易對各項資料
所做修改的資訊。達成這種記錄方式最廣泛使用的方法是預寫式登錄 (write-ahead
logging)。系統在穩定的儲存體上維持一份叫做登錄簿(log)的資料結構。每一筆登 錄
記載著一筆寫入交易的操作,並有以下各欄:

⚫ 交易名稱 (transaction name):執行 write 操作之交易的唯一名稱。


⚫ 資料項名稱 (data item name):寫入資料項的唯一名稱。
⚫ 舊值 (old value):寫入動作前的資料項數值。
⚫ 新值 (new value):寫入後的資料項數值。

7.9.3 檢查點
當一個系統失效發生時,我們必須參考記錄簿以決定那些交易需要被重做,而
那些交易需要復原。原則上,我們需要搜尋整個記錄簿以做此決定。這種做法有兩

26
項缺點:

1. 搜尋過程耗時。
2. 根據我們的方法,大部份需要被重做的交易已經實際地更新過記錄簿說它
們需要修改的資料。雖然重做此資料修改不會造成任何傷害,但卻造成復
原時間變得更久。

為了降低這種型態的額外負擔,我們引入檢查點(checkpoint)的觀念。在執行期
間,系統維護預寫記錄簿。除此之外,系統週期性地執行檢查點,這將要求以下的
系列動作發生:

1. 將所有記錄簿中目前在揮發性儲存體(通常是主記憶體)的記錄輸出到穩定
儲存體。
2. 將所有在揮發性儲存體的已修改資料輸出到穩定儲存體。
3. 輸出一筆記錄簿記錄《checkpoint》到穩定儲存體。

在記錄簿中出現的 《checkpoint》 允許系統加快其復原步驟。考慮一筆交易 Ti


在檢查點之前交付執行。 《Ti commits》的記錄出現在《checkpoint》記錄前。任何所
做的修改在檢查點之前或同一時間已經被寫到穩定儲存體。因此,在復原的時候就
不需要對 Ti 執行 redo 操作。

7.9.4 並行的不可分割交易
因為每一筆交易都是不可分割,交易的並行執行就相當於以某種任意順序依序
地執行這些交易。這項性質(叫做可串列化,serializability)可以藉著在一個臨界區間
只執行一項交易來維持。換言之,所有的交易都共享一個共用號誌 mutex,此號誌
被設定成 1 的初值。當一項交易開始執行時,它的第一個動作時執行 wait (mutex)。
在此交易交付或中止時,它就執行 signal (mutex)。

雖然這種技巧可確保所有並行執行之交易不被分割,但是卻限制太大。我們將
看到,有許多狀況可以允許交易重疊執行,而依然維持可串列化。有許多不同的並
行控制 (concurrency-control) 法則可確保可串列化。以下將一一敘述。

7.9.4.1 可串列化
考慮一個有兩資料項 A 和 B 的系統,A 和 B 同時被交易 T0 和 T1 讀寫。假設這
些交易是以 T0 ,接下去 T1 的順序做不可分割地執行。此執行順序 (稱為排班,
schedule) 以圖 7.24 表示。在圖 7.24 的排班 1 中,指令順序是以規則的順序由上往
下排,其中 T0 的指令出現在左欄,而 T1 的指令出現在右欄。

27
每一交易皆按照不可分割的順序執行之排班叫做串列排班 (serial schedule)。串
列排班是由從不同交易的一系列指令所組成,其中屬於同一交易的指令全部一起出
現在排班中。因此對於一組 n 筆交易,就存在 n!種不同的有效串列排班。每一種串
列排班都是正確的,因為這相當於以某種任意順序,不可分割地執行不同的參與交
易。

如果我們允許兩筆交易重疊它們的執行,則產生的排班就不再是串列的。一個
非串列的排班 (nonserial schedule) 並不表示所產生的執行不正確。為了瞭解這種
情況,我們需要定義衝突操作 (conflicting operation) 這個名詞。考慮一個排班 S,
其中有兩項連續操作 Qi 和 Qj 分屬交易 Ti 和 Tj。我們說,如果 Qi 和 Qj 存取到相同
的資料項,而且其中至少有一項操作是 write 操作,則 Qi 和 Qj 就衝突 (conflict)。為
了說明衝突操作的觀念,考慮圖 7.25 的非串列排班 2。T0 的 write(A)操作和 T1 的
read(A)操作衝突。然而,T1 的 write(A)就沒有和 T0 的 read(B)衝突,因為這兩項操
作存取不同的資料項。

28
令 Qi 和 Qj 是排班 S 的連續操作。如果 Qi 和 Qj 分屬不同的交易,而且 Qi 和 Qj
不衝突,則我們可以交換 Qi 和 Qj 的順序以產生一個新的排班 S'。我們期待 S 和 S'
相等,因為在兩個排班中的所有操作都以相同的順序出現,除了 Qi 和 Qj 之外,而
Qi 和 Qj 的順序並不重要。

讓我們再次用圖 7.25 說明交換的觀念。因為 T1 的 write(A)操作不會和 T0 的


read(B)操作衝突,我們可以交換這兩項操作以產生一個相等的排班。無論系統的初
始狀態是什麼,兩個排班都會產生相同的最終系統狀態。繼續此交換非衝突操作的
步驟,我們會得到:

⚫ 交換 T0 的 read (B) 操作和 T1 的 read (A) 操作。


⚫ 交換 T0 的 write (B) 操作和 T1 的 write (A) 操作。
⚫ 交換 T0 的 write (B) 操作和 T1 的 read (A) 操作。

這些交換的結果是圖 7.24 的排班 1,這正是一個串列排班。因為我們證明了排班 2


等於串列排班。這項結果表示,無論最初系統狀態是什麼,排班 2 將產生和某種串
列排班相同的最後狀態。

如果排班 S 可以經由一連串的交換非衝突操作轉換成一個串列排班,我們就稱
排班 S 是衝突可串列化 (conflict serializable)。因排班 2 是衝突可串列化,因為它可
以轉換成串列排班 1。

7.9.4.2 上鎖規約
確保可串列化的一種方法是,為每一資料項加上一個鎖,並要求每一筆交易遵
循上鎖規約 (locking protocol ),此規約管理鎖如何取得和釋放。資料可以用不同的
模式被鎖上。在這一節中,我們將注意力限制在兩種模式:

⚫ 共用 (shared):如果交易 Ti 取得資料 Q 的一個共用模式鎖 (表示成 S,則


Ti 可以讀取這項資料,但不能寫入 Q。
⚫ 互斥 (exclusive):如果交易 Ti 取得資料項 Q 的一個互斥模式鎖 (表示成
X),則 Ti 可以讀或寫 Q。

有一種規約可以保證可串列化,那就是雙相上鎖規約 (two-phase locking


protocol)。此規約要求每一筆交易以兩種相位發出上鎖和解鎖要求。

⚫ 成長相位 (growing phase):一筆交易可以獲得鎖,但不能釋放任何鎖。


⚫ 收縮相位 (shrinking phase):一筆交易可以釋放鎖,但不能獲得任何新鎖。

29
一開始,一筆交易是在成長相位。此交易依照需要取得鎖。一旦此交易釋放出
一個鎖,它就進入收縮相位,並且不能再發出任何上鎖要求。

雙相上鎖規約可確保衝突可串列化。然而它並不保證能免於死結。我們要注意
到,對一組交易而言,某些衝突可串列化的排班無法經由雙相上鎖規約得到。然而,
為了增進雙相上鎖規約的性能,我們需要有關於交易的額外資訊,或者是在資料庫
上加上某些結構或順序。

7.9.4.3 時間戳記為基礎的規約
在上面描述過的上鎖規約中,每一組衝突交易間的順序是由執行時它們兩者要
求的第一個鎖,以及第一個鎖牽涉到的不相容模式而決定。另外一種決定串列順序
的方法是在交易間預先選好一種順序。這種做法的最常用方法是使用時間戳記排序
(timestamp-ordering) 技巧。

對於系統中的每一筆交易 Ti,我們配上一個獨一無二的固定時間戳記,表示為
TS(Ti)。此時間戳記是由系統在交易 Ti 開始前設定。如果一筆交易 Ti 已經被設定好
時間戳記 TS(Ti),而另一筆新的交易 Tj 進入系統,則 TS(Ti) < TS(Tj)。

有兩種簡單的方法可以製作此技巧。
⚫ 使用系統時鐘的數值做為時間戳記;換言之,一筆交易的時間戳記等於此
交易進入系統時的時鐘數值。這種方法對於發生在分隔系統間的交易,或
沒有共用同一時鐘的處理器之交易無效。
⚫ 使用邏輯計數器做為時間戳記;換言之,交易的時間戳記等於此交易進入
系統時的計數器數值。當設定一個新的時間戳記之後,計數器就加 1。

交易的時間戳記決定了串列的順序。因此,如果 TS(Ti) < TS(Tj),則系統必須


保證所產生的排班相當於交易 Ti 出現在交易 Tj 前的串列排班。

為了製作這項技巧,我們將每一資料項 Q 配上兩個時間戳記數值:

⚫ W-timestamp (Q) 表示任何執行 write (Q) 成功的最大時間戳記。


⚫ R-timestamp (Q) 表示任何執行 read (Q) 成功的最大時間戳記。

每當一個新的 read (Q)或 write (Q) 指令執行時,這兩項時間戳記就更新。

由於發出 read 或 write 操作而被並行控制技巧撤回的交易 Ti,將被設定一個新


的時間戳記,並且重新開始。

30
為了說明此規約,我們來看圖 7.26 中包含交易 T2 和 T3 的排班 3。我們假設一
筆交易在它的第一個指令之前就被設定一個時間戳記。因此,在排班 3 中 TS(T2) <
TS(T3),而且在時間戳記規約下,此排班是可能的。

時間戳記排序規約確保衝突可串列化。這項能力是從以下事實得到,衝突操作
是以時間戳記的順序去處理。此規約確保能免於死結,因為沒有交易要等待。

31
重點補充:

補-1 生產者與消費者問題(Producer-Consumer Problem)


試看生產者與消費者(producer-consumer)問題,也稱作有限緩衝器(bounded
buffer)問題。二行程共用一個固定大小的緩衝器。其中一行程是生產者,將資訊放
於緩衝器中。另一行程是消費者,將資訊由緩衝器中取出。

這種方法看似簡單,但是也會引起若干競賽條件。為了確定緩衝器中資訊項的
數目,需要變數 count 來記錄。如果緩衝器的容量為 N,生產者程式應先測試 count
是否為 N。若為 N,生產者應去安睡。否則生產者便加入一資訊項並使 count 遞增
1。消費者程式也頗類似:首先測試 count 看它是否為 0。若為 0 則睡去。否則移去
此項並遞減計數器 count。

補-1-1 號誌(Semaphores)
E.W.Dijkstra(1995)建議使用整數變數,計算所存放以供未來使用的喚醒訊號。
在他所提的方法裡,介紹了一種新的變數型式,稱作號誌(semaphore)。號誌之值為
0 時表示無喚醒訊號被存放。若為其他正值,則表示有一個或多個訊號被擱置。

Dijkstra 提出訊號的兩種操作,DOWN 和 UP(此即 SLEEP 和 WAKEUP 的一般


化操作)。DOWN 是用以測得號誌是否大於 0。若大於 0,便遞減其值(意指使用一
個存放的喚醒訊號)並繼續該行程。若號誌之值為 0,此行程便需沈睡。以上所提測
試號誌之值、變更其值或是令程式安睡皆是一氣喝成,不予分開的原子性處理
(atomic action)。也就是保證號誌的操作一旦開始,在其結束前沒有任何其他行程能
觸及此號誌。

至於 UP 操作是用以遞增號誌之值。如果一個或多個行程沈睡於某號誌無法完
成 DOWN 操作,在 UP 操作發生於該號誌後,隨機選出上述行程之一去完成它的
DOWN 操作。但是只要有行程仍安睡於某號誌上,該號誌之值便仍為 0。其中遞增
號誌之值與喚醒行程的動作也是不可分的。沒有行程會在執行 UP 時被暫停,如同
沒有行程在執行 WAKEUP 時會停滯一般。

其實在 Dijkstra 的原始論文裡,他使用 P 和 V 代替 DOWN 和 UP,但是對不


熟荷蘭語文的人而言,原用符號缺乏意義,故我們採用 DOWN 和 UP 的術語。

使用號誌解決生產者和消費者問題(Solving the Producer-Consumer Problem


using Semaphore)
圖 2-11 的解法使用了三個號誌。 full 用以計算填滿的格子數,empty 用以計算
拿空的格子數,mutex 是用以確保生產者與消費者不會同時存取緩衝器。full 之初值
為 0,empty 之初值等於緩衝器的格子數,mutex 之初值為 1。使用初值為 1 的號誌

32
以確保諸行程無法同時進入臨界區的這類號誌,稱作雙號誌(binary semaphore)。若
是每一行程在進入臨界區時先進行 DOWN 操作,離開後進行 UP 操作,離開後進
行 UP 操作,便能達成互斥。
#include "prototypes.h"
#define N 100 /* number of slots in the buffer*/
typedef int semaphore; /* semaphores are a special kind of int */
semaphore mutex = 1; /* controls access to critical region */
semaphore empty = N; /* counts empty buffer slots */
semaphore full = 0; /* counts full buffer slots */

void producer(void)
{
int item;
while (TRUE) { /* TRUE is the constant 1 */
produce_item(&item); /* generate something to put in buffer */
down(&empty); /* decrement empty count */
down(&mutex); /* enter critical region */
enter_item(item); /* put new item in buffer */
up(&mutex); /* leave critical region */
up(&full); /* increment count of full slots */
}
}

void consumer(void)
{
int item;
while (TRUE) { /* infinite loop */
down(&full); /* decrement full count */
down(&mutex); /* enter critical region */
remove_item(&item); /* take item from buffer */
up(&mutex); /* leave critical region */
up(&empty); /* increment count of empty slots */
consume_item(item); /* do something with the item */
}
}
圖2-11 使用號誌解決生產者和消費者問題

在圖 2-11 的例子中,我們真正使用兩種不同方法的號誌而足夠說明此差別。
mutex 號誌是使用互斥,此保證一次只能有一個行程被讀或寫到緩衝器和相關的變

33
數。此互斥必須預防混亂。

其它使用號誌是為了同步問題(synchronization)。空的及滿的號誌須要被保證某
些事件的順序是發生或不發生。在此例中須確定當緩衝器是滿的時,生產者必須停
且執行,而當緩衝器是空的時,消費者必須停且執行。此使用不同的互斥原理。

補-1-2 事件計數器(Event Counters)


前一小節所敘述的方法是利用號誌達成互斥,以避免生產者-消費者問題發生競
賽條件。事實上,也可採用無需互斥的方法來解決問題。首先介紹一種特殊的變數
稱作事件計數器(event counter)。

有三種定義於事件計數器 E 的操作:

1.read(E):傳回 E 的現值。

2.advance(E):自動以 1 遞增 E 值。

3.await(E,v):等候直到 E 之值大於或等於 v。
#include "prototypes.h"
#define N 100 /* number of slots in the buffer */
typedef int event_counter; /* event_counters are a special kind of int*/
event_counter in = 0; /* counts items inserted into buffer */
event counter out = 0; /* counts items renioved from buffer */

void producer(void)
{
int item, sequence = 0;
while (TRUE) { /* infinite loop */
produce_item(&item); /* generate something to put in buffer */
sequence = sequence + 1; /* count items produced so far */
await(out, sequence - N); /* wait until there is room in buffer */
enter_item(item); /* put item in slot (sequence-i) % N */
advance(&in); /* let consumer know about another item */
}
}

void consumer(void)
{
int item, sequence = 0;
while (TRUE) { /* infinite loop */
sequence = sequence + 1; /* number of item to remove from buffer */

34
await(in, sequence); /* wait until required item is present */
remove_item(&item); /* take item from slot (sequence-i, % N */
advance(&out); /* let producer know that item is gone */
consume_item(item); /* do something with the item */
}
}
圖 2-12 使用事件計數器的生產者-消費者問題

當生產者計算出一項新資料,它會查看緩衝器是否還有空間,其方法是使用
AWAIT 系統呼叫。起初,out 等於 sequence-N 會為負值,因此生產者不會暫停。如
果在消費者執行之前,生產者已生產 N+1 項資料,AWAIT 敘述便會令其等待,直
到 out 之值為 1 時,也就是在消費者移走一項之前,任何事件都不再發生。

消費者程式的邏輯更簡單,當它想取用第 K 項資料時,必須等候 in 之值到達 K。


也就是等到生產者已放入 K 項資料於緩衝器時。

補-1-3 監督程式(Monitors)
使用號誌與事件計數器的方法,是否可輕易解決行程間聯繫的問題。答案足否
定的,仔細思考圖 2-11 中 SOWN 的操作順序。如果生產者程式的兩項 DOWN 操
作次序顛倒,mutex 在 empty 之前即被遞減。試想緩衝器填滿時,生產者會停滯,
mutex 被設為 0。然在消費者下次想要取用資料時,因操作 DOWN(mutex)而亦停滯。
兩個行程皆停滯使得工作無法進行,這種現象稱作死結(deadlock),其細節在第六章
討論之。

為了易於編寫正確的程式,Hoare(1974)和 Brinch Hansen(1975)提出一種高層次


的同步基元(synchronization primitive)稱作監督程式(monitor)。

所謂監督程式是一些程序、變數和資料結構的集合,它們都聚集成特殊的模組
或套裝(module or package)。行程可以呼叫監督程式內的程序,但是無法由監督程式
外宣告的程序,直接存取監督程式內部的資料結構。圖 2-13 的監督程式是以類似
PASCAL 的虛擬語言所表示。
monitor example
integer i;
condition c;

procedure producer(x);
.
.
.
end;

35
procedure consumer(x);
.
.
.
end;
end monitor;
圖 2-13 監督程式

監督程式具有一種重要特質,可利於互斥的達成:在任何時刻只能有一個行程
在監督程式運作。監督程式是一種程式語言架構,因此編譯程式對於呼叫監督程式
內程序的處理不同於一般的程序呼叫。行程呼叫此類程序(簡稱監督程序)時,程序
的前幾行指令會檢視監督程式內,是否有其他行程在運作。如果已有行程運作,便
將現行呼叫行程予以擱置,直到監督程式裡的另一運作行程離去。如果沒有其他行
程,則此呼叫行程可以進入。

對於監督程式內的互斥實作乃決定於編譯器,而一般作法是使用雙號誌。因為
編譯器已為互斥作安排,不需使用者經手故不易出錯。程式員只需將所有臨界區間
寫入監督程式,便不必擔心同時有二行程進入臨界區。

雖然監督程式提供簡易的方式以達成互斥,但依然不夠。我們尚需一種方法使
得不能進行的行程便停滯。在生產者_消費者的問題中,把對於緩衝器填滿或拿空
的測試放入監督程式是件容易事,但是在發現緩衝器填滿時,此行程當如何停滯呢?

解決之道是使用條件變數(conditin variable)與其兩項操作 WAIT 和 SIGNAL。當


一監督程序發現本身不能繼續時(例如緩衝器滿了),它便操作 WAlT 於某一條件變
數,譬如說是 full。這項運作使得發出呼叫的行程停滯。它也可讓早先被禁止的行
程,現在可以進入監督程式。

而另一個消費者行程可操作SlGNAL於該項條件變數,而喚醒他的夥伴。為了
避免同時有二行程運作於監督程式,我們必須在SIGNAL之後設定規則。

Hoare 提議讓剛醒來的行程運作,而暫停原運作行程。 Brinch Hansen 則主張


操作 SlGNAL 的行程需立刻離開監督程式。換言之,SIGNAL 敘述只能出現於一監
督程序的最後一行。我們採用 Brinch Hansen 的主張,因為在觀念上較簡單,也易
於實作。如果 SlGNAL 操作於一個許多行程都在等待的條件變數,只有其中之一可
復甦,且由排班程式決定之。

有條件變數不能用做計數計,亦不能做為號誌的累加記號。因此,若沒有一個
等待記號在條件變數上時,則記號將消失。所以在 SlGNAL 之前 WAIT 必須先來。
此規則使得實作更簡單。在練習中根本沒有困難,因為很容易掌握每一行程的變
數,若是需要的話。可以用別的方法做 SlGNAL 的行程亦可明白,而不用注視著所
有變數。

36
圖 2-14 是以監督程式的方法解決生產者-消費者的問題。

WAIT 和 SIGNAL 非常類似於早先提過的 SLEEP 和 WAKEUP。但是卻有一


項關鍵性的差異: SLEEP 和 WAKEUP 方式的缺陷發生於一行程將要安睡,而另
一行程正要叫醒它。但在監督程式裡,這是不可能發生的。因為在監督程式裡的一
程序若發現緩衝器已滿而進行 WAIT 操作時,毋需擔心排班程式會在 WAlT 操作完
成前,將 CPU 轉給消費者。因為在生產者停滯前,消費者根本無法進入監督程式。

由於監督程式具有自動達成臨界區互斥的特性,因此比號誌的作法不易出錯,
然而仍有一些缺點。如前所述,監督程式是一種程式語言的觀念。編譯程式必須確
認它,並以某種方式安排互斥。但是 C、Pascal 以及其他大多數語言皆不提供監督
程式的用法,故無法要求編譯程式強制互斥的規則。

這些語言也未提供號誌,不過加入號誌倒是不難。只需在程序庫中加上兩個簡
短的組合語言常式(assembly language routine)以發出 UP 與 DOWN 之系統呼叫。編
譯程式並不知道它們的存在,但作業系統必須知道。至於監督程式,需要語言本身
提供之。有些語言如 Concurrent Euclid(Holt 1983)則具有此項架構,但是這些語言
不太常用。

監督程式的另一項問題也發生於號誌。由於它們解決互斥的問題,皆限於一個
或多個 CPU 存取共用的記憶空間。因此在面臨分散式系統時(每個 CPU 皆有本身
的記憶體而以網路連結起來),這些基元便發揮不了作用。結論是號誌方式層次太
低,監督程式又受限於程式語言。對於不同機器間的資訊傳遞皆無法用前述基元達
成。可見還需構想其他方式。
monitor ProducerConsurner
condition full, empty;
integer count;

procedure enter;
begin
if count = N then wait(full);
enter_item ;
count := count + 1;
if count = 1 then signal(empry);
end;

procedure remove;
begin
if count = O then wait(empty);
remove_item;
count := count - 1;

37
if count = N - 1 then signal(full);
end;

count := O;
end monitor;

procedure producer;
begin
while true do
begin
produce_item ;
ProducerConsumer.enter;
end
end;

procedure consumer;
begin
while true do
begin
ProducerConsumer.remove;
consume_item;
end
end;
圖 2-14 帶有監督程式的生產者-消費者問題,其中緩衝器有 N 格

補-1-4 訊息傳遞(Message Passing)


上一小節提到"其他方式" 即是訊息傳遞(message passing)。這種行程間聯繫的
方法使用兩種基本操作 SEND 和 RECEIVE。它們類似號誌而不像監督程式,是一
種系統呼叫而非語言架構。因此它們可以很方便的置於程序庫。譬如:

send(destination, &message); 和

receive(source, &message);
前者發出訊息給指定的目地,後者由給定的來源接收訊息(也可用 ANY 型式表
示接收者不在意來源所在)。如果沒有可用的訊息,接收者(receiver)便暫停以待。

訊息傳遞系統的設計課題
訊息傳遞系統具有許多不曾發生於號誌或監督程式的問題與設計課題。尤其針

38
對網路上不同機器間行程的聯繫。例如,訊息會在網路上流失。為了避免訊息的流
失,發訊者發出一訊息後,接收者若收到此訊息便發回感謝(acknowledgement)訊息
以告知發訊者。如果發訊者在某一定時內未收到回訊,便再將原訊息傳送一次。

試想如果訊息本身被接收無誤,而回訊卻流失時會如何?發訊者將再次傳送,
使得接收者得到兩次相同的訊息,因此接收者必須具有分辨新舊訊息的能力。通常
解決的方式是在每一原定訊息上加一循序編號。如果接收者獲得與前一訊息相同編
號的訊昔,便予以忽略。

訊息系統需考慮行程命名的問題,因為必須在 SEND 或 RECEIVE 呼叫中指定


行程名稱以避免含混。常用的命名方式是 Process @ machine 或 machine: process。
如果網路中的機器很多,又無統一的命名規則,便可能發生機器同名的困擾。為了
減少命名的衝突,可將機器分組為若干定義域(domains),並將行程予以定址為
Process @ machine.domain。在這種方法下,只要同一定義域內不發生同名狀況即
可,當然定義域也各有不同的名稱。

另外確認(authentication)也是訊息傳遞的重要課題:客戶如何確知與其聯繫者確
實是檔案服務程式,而非其他別有用心的冒牌者?檔案服務程式又如何得知是那一
位客戶提出了檔案要求?此時,可以將訊息予以編碼,而其解碼的鑰匙(key)則握於
合法的使用者手中。

至於同在一機器上的發訊者和接收者又有其他問題。其中之一是效能的考慮。
自一行程將訊息拷貝至另一行程,總是較號誌操作或進入監督程式為慢。因此如何
使訊息傳遞有效迅速成為眾多研究者的題材。 Cheritor(1984)曾提出限制訊息的規
格須適用於機器的暫存器,並使用此暫存器完成訊息傳遞。生產者-消費者問題之訊
息傳遞

現在以訊息傳遞方式(不用共用記憶空間)來解決生產者-消費者問題,如圖2-15
所示。
#include "prototypes.h"
#define N 100 /*number of slots in the buffer */
#define MSIZE 4 /* message size */

typedef int message[MSIZE];

void producer(void)
{
int item;
message m; /* message buffer */
while(TRUE) {
produce_item(&item); /* generate something to put in buffer */
receive(consumer, &m); /* wait for an empty to arrive */

39
build_message(&m, item); /* comstruct a message to send */
send(consumer, &m); /* send item to consumer */
}
}
void consumer(void)
{
int item, i;
message m;
for (i = O; i < N; i++) send(producer, &m); /*send N empties */
while (TRUE) {
receive(producer, &m); /* get message containing item */
extract_item(&m, &item); /* take item out of message */
send(producer, &m); /* send back emptry reply */
consumer_item(item) ; /* do·.something with item */
}
}
圖 2-15 帶有七項訊息的生產者-消費者問題

至於訊息的定址方法有多種。其中一種是對每一行程指派唯一位址,將訊息以
此定址傳給行程。另一種方法是新加一資料結構,稱作郵箱(mailbox)。郵箱是用來
暫存固定數目訊息的緩衝空間。當使用郵箱時,SEND 和 RECEIVE 系統呼叫的位
址參數是指郵箱而非行程。若有一行程欲送訊息至裝滿的郵箱中,此行程會停滯直
到郵箱中訊息被移出。

對於生產者-消費者問題,兩者需建立足以放入 N 項訊息的郵箱。生產者將含
有資料的訊息傳至消費者的郵箱,消費者也將空白訊息傳回給生產者郵箱。使用郵
箱時,緩衝機能是很明確的:目的郵箱(destination mailbox)是用來存放目的行程尚
未接收的訊息。

另一種極端作法是令郵箱除去所有的緩衝作用,方法描述如下。試想 SEND 在
RECEIVE 之前完成,發訊行程停滯直到 RECEIVE 發生,屆時訊息直接由發訊者拷
貝至接受者,不作中間的緩衝。同樣,如果 RECEIVE 先完成,接收者便停滯以待
SEND 發生。這種策略即是集結(rendezvous)。這比緩衝訊息方法易於實行,但是卻
因發訊者與接收者需強制鎖定步伐而缺乏彈性。

UNIX 中使用者行程間聯繫是採用管道(pipes),可視為一種有效率的郵箱。而
在訊息傳遞系統使用郵箱與管道的差別,僅在於管道不限定一次只傳出一項訊息。
舉例來看,如有一行程寫入 10 項訊息(每項 1O0 位元組)至管道中,而另一行程由
管道讀取 1000 位元組,此時可一次得 10 項訊息。然而在真實的訊息傳遞系統,
每一次 READ 應當只能傳回一項訊息。因此行程間可依規定讀寫固定格式大小的訊
息,或是在每一訊息後端加一特殊字元(如 Line feed),便可解決問題。

40
補-2 基元間相等運作(Equivalence of Primitives)

補-2-1 用號誌構建監督程式和訊息

如果作業系統已提供號誌特性,任何編譯程式的設計者皆可將監督程式納入語
言。首先將一小小管理監督執行時間聚集的程序建造且放入程式庫,列在圖2-16,
只要有包括監督程式的產生碼的呼叫便產生適合的執行時間程序。
#include "prototypes.h"
typedef int semaphore;
semaphore mutex = 1; /* to control access to the monitor*/

void enter_monitor(void) /* code to execute upon entry to monitor */


{
down(mutex); /* only one at a time inside please */
}

void leave_normally(void) /* leave monitor without signaling */


{
up(mutex); /* allow other processes to enter ·/*/
}

void leave_with_signal(c) /* signal on c and leave monitor */


semaphore c; /* which condition variable to signal */
{
up(c); /* release one process waiting on c */
}
void wait(c) /* go to sleep on a condition */
semaphore c; /* which condition */
{
up(mutex); /* allow another process to enter */
down(c); /* go to sleep on the condition */
}
圖2-16 在號誌實施監督督程式時,所需程式庫程序

以監督程式完成訊息的作法,基本上與前述以號誌完成訊息相似,只是將每一
行程所設的號誌以條件變數代替之。而兩種實作中,郵箱的結構皆相同。

41
補-2-2 用訊息完成號誌與監督程式
如果有可用的訊息系統,只要一點小技術便能完成號誌與監督程式。此技術便
是引進一同步行程(synchronization process)。首先讓我們來看號誌的完成。此同步行
程為每一號誌準備一計數器和一連結串列。為了進行 UP 或 DOWN 操作,行程會
呼叫程序庫中已備妥的程序 up 或 down,它們會發出訊息給同步行程指明需要的操
作和使用的號誌。此程序再以 RECEIVE 操作由同步行程中接收回覆訊號。

當同步行程發現有訊息到達時,首先檢視計數器決定被要求的操作是否可行。
UP 操作說是可以完成,而 DOWN 操作在相對號誌為 0 時便會停滯。當操作獲准
時,同步行程會回覆空白訊息,並將呼叫者喚醒。若是碰上號誌為 0 的 DOWN 操
作,同步行程會將呼叫者置於佇列,且不發出回訊。而此執行‧DOWN 操作的行
程便停滯。稍後當 UP 操作完成時,同步行程會選取因該項號誌而停滯的一行程,
令其執行並傳給它回訊。至於選擇的方法可依先進先出或是優先順位。由於同步行
程每次只處理一項要求,故而可避免競賽條件。

用訊息完成監督程式的方法也引用上述同步行程的技巧。早先我們己知如何以
號誌完成監督程式,現在又己知如何用訊息完成號誌。故而合併兩者,我們可以由
訊息方式模擬監督程式。其中一種方法是讓編譯程式針對 mutex 和每一行程的號
誌,呼叫程序庫中的 up 與 down。這些程序的實作則可經由發訊給同步行程而完
成。

補-3 典型行程聯繫問題(CLASSICAL IPC PROBLEMS)


作業系統的文獻裡探討過許多有趣的例子。以下舉出三個特別有名的問題。

補-3-1 哲學家用餐問題(The Dining Philosophers Problem)


其中使用了一陣列 state來記錄每一哲學家正在用餐、思考或是飢餓(嘗試拿取
叉子)。哲學家只有在鄰座的兩位皆不用餐時才能進入用餐狀態。哲學家i的兩鄰座
以微常式LEFT和 RlGHT定義之。譬如i是2,其LEFT便為1,RIGHT 則為3。
#include "prototypes.h"
#define N 5 /* number of philosophers */
#define LEFT (i-1)%N /* number of i's left neighbor *I
#define RIGHT (i+l)%N /* number of i's right neighbor */
#define THINKING O /* philosopher is thinking */
#define HUNGRY 1 /* philosopher is trying to get forks */
#define EATING 2 /* philosopher is eating */

42
typedef int semaphore; /* semaphores are a special kind of int */
int state[N]; /* array to keep track of everyone's state */
semaphore mutex 1; /* mutual exclusion for critical regions *I
semaphore s[N]; /* one semaphore per philosopher */
void philosopher(int i) /*· i: which philosopher (O to N-1) */
{
while (TRUE) { /* repeat forever */
think( ); /* philosopher is thinking */
take_forks(i); /* acquire two forks or block */
eat( ); /* yum-yum, spaghetti ·!*/
put_forks(i); /* put both forks back on table */
}
}
void take_forks(int i) /* i: which philosopher (O to N-l) */
{
down(&mutex); /* enter critical region */
state[i] = HUNGRY; /* record fact that philosopher i is hungry */
test(i); /* try to acquire 2 forks */
up(&mutex); /* exit critical region */
down(&s[i]); /* block if forks were not acquired */
}
void put_forks(int i) /* i: which philosopher (0 to N-1) */
{
down(&mutex); /* enter critical region */
state[i] = THINKING; /* philosopher has finished eating */
test(LEFT); /* see if left neighbor can now eat */
test(RIGHT); /* see if right neighbor can now eat */
up(Bmutex); /* exit critical region */
}
void test(int i) /* i: which philosopher (O to N-l) */
{
if (state[i] == HUNGRY && state[LEFT] != EATING &&
state[RIGHT] != EATING) {
state[i] = EATING;
up(&s[i]);
}
}
圖 2-20 哲學家用餐問題的最佳解

43
此程式對每一哲學家安排了一號誌,因此哲學家在餓了又拿不到所需叉子時,
便會停滯。

補-3-2 讀寫協調問題(The Readers and Writers Problem)


哲學家用餐問題在建立行程競爭有限資源的模式時相當有用。所謂有限資
源是指磁碟等輸出入裝置。另一個有名的問題是 1971 年 Courtois 提出的讀寫協調
問題,可用來建立資料庫存取的模式。試想一個大型資料庫(例如航空定位系統),
有許多爭的行程欲讀取和寫入它。多數行程同時讀取資料庫是可行的。但是如有一
行程正在改寫資料庫,則不允許任何行程取用此資料庫。Courtois 等對此問題提出
的解法列於圖 2-21。
#include "prototypes.h"
typedef int semaphore; /* use your imagination */
semaphore mutex = 1; /* controls access to 'rc' */
semaphore db = 1; /* controls access to the data base */
int rc = 0; /* # of processes reading or wanting to */
void reader(void)
{ while (TRUE) { /* repeat forever */
down(&mutex); /* get exclusive access to 'rc' */
rc = rc + 1; /* one reader more now */
if (rc == 1) down(&db); /* if this is the first reader ... */
up(&mutex); /* release exclusive access to 'rc' */
read_data_base( ); /* access the data */
down(&mutex); /* get exclusive access to 'rc' */
rc = rc - 1; /*· one reader fewer now */
if (rc == 0) up(&db); /* if this is the last reader ... */
up(&mutex); /* release exclusive access to 'rc' */
use_data_read( ); /* noncritical section */
}
}
void writer(void)
{ while (TRUE) { /* repeat forever */
think_up_data( ); /* noncritical section */
down(&db); /* get exclusive access */
write_data_base( ); /* update the data */
up(&db); /* release exclusive access */
}
}
圖2-21 讀寫協調問題之解法

44
補-3-3 沈睡的理髮師問題(The Sleeping Barber Problem)
另一個典型的內部行程溝通問題,理髮店裏有一個理髮師、理髮椅和幾個等待
的椅子。若沒有顧客則理髮師坐在理髮椅上而且沈睡,當顧客到達時,他就必須起
來。若是有多的顧客則必須坐在椅子上等待(若有空的椅子)或離去(若椅子是滿的),
此問題是理髮師和顧客之間的程式而沒有競賽條件。我們的解決方法使用了三個號
誌:顧客(包括坐在理髮椅上的顧客)、理髮師(閒滯和等待顧客(0 或 1)和 mutex(使用
互斥),亦使用 waiting 變數(計算等待顧客計算器)及 customers 變數,因為 waiting
沒有辦法去讀目前號誌的值,且在此解中顧客進入理髮店時必須紀錄正等待顧客的
人數,若比椅子少則可以留下來等待,否則必須離開。我們的解在圖 2‧23 中。
#include "prototypes.h"
#define CHAIRS 5 /* # chairs for waiting customers */
typedef int semaphore; /* use your imagination */
semaphore customers = 0; /* # of customers waiting for service */
semaphore barbers = 0; /* # of barbers waiting for customers */
semaphore mutex = 1; /* for mutual exclusion */
int waiting = 0; /* customers are waiting (not being cut) */

void Barber (void)


{
while (TRUE) { /* go to sleep if # of customers is 0 */
down(customers); /* acquire access to waiting‘ */
down(mutex); /* decrement count of waiting customers */
waiting = waiting + 1; /* one barber is now ready to cut hair */
up(barbers); /* release ‘waiting’ */
cut_hair( ); /* cut hair (outside critical region) */
}
}

void Customer(void)
{
down(mutex); /* enter critical region */
if (waiting < CHAIRS) { /* if there are no free chairs, leave */
waiting = waiting + 1; /* increment count of waiting customers */
up(customers); /* wake up barber if necessary */
up(mutex); /* release access to ‘waiting’ */
down(barbers); /* go to sleep if # of free barbers is 0 */
get_haircut( ); /* he seated and be serviced */

45
} else {
up(mutex); /* shop is full, do not wait */
}
}
圖 2-23 沈睡理髮師問題之解

補-4 原子交易法(ATOMIC TRANSACTIONS)


我們已經討論的所有同步技巧在本質上都是像號誌同類,屬於低層次的,他們
需要程式設計師很小心的考慮所有關於互斥及臨界區域管理,以避免死結的發生以
及故障的修復等細節問題,而我們希望討論的較高層次,在技術問題之外,能夠讓
程式設計師集中注意力於它的演算法,以及所有的形成如何平行處理的共同工作,
這種較抽象但實際上廣泛地應用在分散式系統中,我們將它稱為原子交易或是簡單
的稱它為交易,原子動作(atomic action)這樣的名稱也是時常被使用到,在這一段我
們將檢驗原子交易如何使用,設計以及如何的實際運作。

補-4-1 原子交易的簡介(Introduction to Atomic Transactions)


原子交易的原始模型來自商業的世界,假設有一家貿易公司需要一批布料,他
們接洽到一家布料供應商,這家公司訂購十萬美元 10 公分寬紫色的布料需要在六月
交貨,而這家供應商能提供十萬美元 4 英吋寬淡紫色的布在十二月交貨,雙方都同
意價格部份,但買方不同意顏色及交貨日期並且堅持 10 公分的規格,經過多次的協
商之後,雙方終於達成協議,同意紫羅蘭色 3 又 959/1024 英吋寬的布料於八月十五
日交貨。

此時,雙方同意終止討論,在此一協議下,雙方簽定合約,在合約書的規定下,
進行這項買賣,因此在簽定合約之前,任何一方都可放棄協議,就像沒有發生任何
事情一樣,沒有任何的責任需要承擔,但是一旦簽定了合約,就必須照合約書所定
履行合約,來完成這項交易。

電腦的模式也是非常類似,當一個行程宣告它要開始與一或是多個行程進行交
易,他們可以做多項的討論,譬如誰進行製造或刪除目的檔,誰負責執行等等,然
後由起始的行程宣佈各行程所負責的工作,如果所有的行程都同意,那麼就照這樣
的宣告來實施,如果有一或多個行程不同意(或是在同意之前故障)
,那又回到了交
易開始的狀況,重新協定各項資源的分配與管理,這種所有或是完全沒有的特性使
得程式設計的工作變得較單純。

在電腦系統中這種交易的觀念,完全超出了六十年代電腦工作的方法,在磁碟
與線上資料庫系統出現之前,所有的檔案都是記錄在磁帶上,你可以想像超級市場

46
的自動存貨與交易管理系統,每天結束營業之後,電腦以兩個輸入磁帶來作業,第
一個包括今日開門前所有的物品的清單,第二個包含今日所有交易的變更記錄:售
出貨品與廠商今日供應之貨品,電腦讀入這兩個磁帶的資料,更新後產生一個新的
主檔包括所有物品最新清單的磁帶,如圖 11-15。

這種作法最好的部份就是當任何情況下發生錯誤,我們可將磁帶迴轉重新再做
一次而沒有任何影響,這種方式正好符合了原子交易法全部一次完成或是完全不做
的特性(all-or-nothing)。

前一日物品清單
新的物品清單

輸入磁帶 電腦 輸出磁帶

今日的更新

圖11-15 更新主檔是可容錯的

現在我們來看現代銀行在線上作業的資料庫應用,顧客利用個人電腦與模變器
與銀行電腦連線,並且從一個帳戶中提款,然後存入另外一個帳戶(轉帳作業)
,整
個作業程序分成兩個步驟:

1.提款(金額數量,帳戶 1)。

2.存款(金額數量,帳戶 2)。

如果電話連線恰巧在第一步驟完成而第二步驟尚未完成時中斷,第一帳戶已完
成提款,而第二個帳戶尚未存入,此時這筆金額將消失不見了。

如果將這兩個步驟以原子交易法合在一起,將可解決這個問題,那就是兩者都
完成,或是兩者都不進行,這秘訣是如果整個交易有一部份未完成,就會回到整個
交易開始的最初狀態,我們所需要做的是將資料庫已更改的資料迴轉,如同我們前
面所提到將磁帶迴轉一樣,這種能力正是原子交易法所提供的一種。

補-4-2 交易模式(The Transaction Model)

穩定的儲存體(Stable Storage)
儲存體有三種不同的方式,第一種我們使用隨機存取(RAM)記憶體,但是當電
源斷掉或是機器故障時,所有的資料就失去了,第二種我們用磁碟儲存,這種可解
決 CPU 罷工時的問題,但是當磁頭壞了時,所有的資料也將失去。
最後我們談到穩定的儲存體,這種設計是為了解決所有上述資料可能失去的問
題,除非是碰到洪水、地震、戰爭等天災人禍,穩定儲存體可使用一對普通的磁碟

47
來達成,如圖 11-16(a)所示,在磁碟 2 上的每一個區間是完全對應磁碟 1 相同區間
的資料,當一個區間的資料被更新,首先磁碟 1 上的此區間被修改,接著驗證,然
後在磁碟 2 上以同樣的方法處理。

假設系統故障發生在磁碟 1 已經被更新,而磁碟 2 尚未被更新,如圖 11-16(b)


所示,在修復之後,磁碟可將每一個區間一一做比較,如果有兩個相對應的區間資
料不同,我們可以假設磁碟 1 的資料是正確的(因為磁碟 1 總是較磁碟 2 先更新),
所以新的區間資料就從磁碟 1 複製到磁碟 2 上,當這個復原的工作完成後,兩個磁
碟的資料都需要被確認。

另一個問題是整個區間一種自發性的損壞,灰塵粒子、衣服上的纖維,甚至眼
淚都能使原本有效的區間,突然間發生檢查總和的錯誤(checksum error),如圖
11-16(c)所顯示的,當這類的錯誤被偵測到時,壞的區間可以藉著在其他驅動磁碟上
相同的區間重新複製產生。

穩定儲存體 穩定儲存體 穩定儲存體

磁碟1 s a s a' s a'


o h o h o h
t f t f t f
b w b w b w

壞的區間
磁碟2 s a s a s a'
o h o h o

t f t f t f
b w b w b w

(a) (b) (c)

圖11-16 (a)穩定儲存體(b)磁碟1更新後發生故障(c)壞的狀況

如同我們前面所看到的實際操作方式,穩定儲存體通常應用在需要高度容錯
性,如我們前面所提到的原子交易法中,當資料被寫到穩定儲存體後,接著它再讀
回另一區間來檢查確認資料的正確性,因此資料發生錯誤的機會,相對的就變成非
常小了。

交易的基元(Transaction Primitives)
使用交易的觀念來寫程式,通常有些特別的基元必須由作業系統或是語言的執
行系統來支援,舉例而言:
1.BEGIN_TRANSACTION 此指令之後接著形成交易。

48
2.END_TRANSACTION 終結交易,嘗試交付委託。

3.ABORT_TRANSACTION 廢除交易,還原交易前的變動值。

4.READ 從檔案中讀取資料(或其他物件)。

5.WRITE 將資料寫入檔案中(或其他物件)。

一個完全的基元項目,必須看交易的種類而定,在一個郵遞系統中,它的基元
可能是傳送、接收,以及向前郵遞,然而在會計系統中,可能稍有不同,無論如何,
READ 與 WRITE 可能是一種典型的例子,通常敘述,程序呼叫甚至註解等都允許
被包括在交易內。

BEGIN_TRANSACTION 與 END_TRANSACTION 被用來界定交易的範圍,在


他們之間的操作,形成了交易的主體,他們或是完全被執行,或是完全不被執行,
我們可能使用系統呼叫、程式庫程序、或是成為語言中同類敘述的一部份,來應用
它,這一切都要看我們實際的運作方式為何。

交易的特性(Proporties of Transactions)
交易有三個基本的特性:

1.序列性--同時進行的交易,不會彼此互相干擾。

2.原子性--在外在的世界中,交易的發生是具有不可分割性。

3.永久性--當交易一旦正式進行,這種改變是具有永久性的。

第一個特性,序列性(serializability),保證如果在同一時間有兩個或更多個交易
同時進行,那麼他們彼此之間或其它的行程中,最後的結果看來如同所有的交易是
在一個固定的次序下(依系統而定)進行。

在圖 11-18(a)~(c)中,我們有三個交易在三個不同的行程中同時進行,如果他
們是按著次序執行的,x 的最後值將可能是 1、2 或 3,這完全要看哪一個交易是最
後執行而定,(x 可能是一個共通的變數、一個檔案或是其他的物件) ,在圖 11-18(b)
中,我們看到幾種不同的次序,叫做時間表,在這裡他們可能互相插入,時間表 1
是一個實際的序列性,換句話說,這個交易是在完全的次序性下執行,因此它符合
序列性所定義的狀況,時間表 2 不是序列性,但是它顯然是合法性的,因為它的 x
最後的值能夠達到如同序列性交易的結果值一樣,第三種情形是不合法的,因為他
產生出 x=5,這是在序列性的交易中所無法產生出來的值,他已經超出了系統所保
證在個別的操作下而能得到正確的插入,為了允許系統能夠自由地選擇他們所要的
操作次序--並提供他們得到正確的答案--,我們消除了個別程式設計師擁有各自的互
斥區,因此而簡化了程式設計。

第二種關鍵性的特性,在所有的交易中都會顯示出來,原子性,保證每一次的
交易都會完全的進行,或是完全不進行,並且當它進行時,是完全不可分割的發生,

49
即時性的行動,當一個交易在進行的過程中,其他的行程(不論他們是否包括在這
次的交易中)不能看見任何進行中的中間狀態。

BEGIN_TRANSACTION BEGIN_TRANSACTION BEGIN_TRANSACTION

X=0; X=0; X=0;

X=X+1; X=X+2; X=X+3;

END_TRANSACTION END_TRANSACTION END_TRANSACTION

(a) (b) (c)

時間

時間表 1
x=0; x=x+1; x=0; x=x+2; x=0; x=x+3; Legal

時間表 2
x=0; x=0; x=x+1; x=x+2; x=0; x=x+3; Legal

時間表 3
x=0; x=0; x=x+1; x=0; x=x+2; x=x+3; Illegal

(d)

圖 11-18 (a)--(c)三種交易;(d)可能的時間表

假設,我們舉一個例子,一些檔案長度為 10 個位元組,當一個交易要進行附
加長度到這個檔案,如果其他的行程讀取這個檔案,在交易正在進行的過程當中,
他們僅能讀取原始的 10 個位元組,而不論這個交易已經附加了多少個位元組到這個
檔案上,如果交易完全成功的完成了,這個檔案立刻在交易完成的同時,變成了新
的檔案長度,在這個中間並沒有中間狀態的產生,不論要達到這個完成狀態需要經
過多少次的操作,都不會產生中間狀態。

巢狀交易(Nested Transactions)
交易可能包含子交易,這種情況我們稱做巢狀交易,在最上層的交易通常會將
它的交易分叉開來,使他們在一種平行處理的情況下執行,並使他們在不同的處理
器上,以獲得最大的執行效率以及簡化程式,這些子交易也可能還有他自己的子交
易,他也是以同樣分叉平行處理的方式來對待這些子交易。

子交易產生出一個複雜的但是很重要的問題,想像一個交易平行的開始進行數
個子交易,其中之一完成了它的交易,並將它的結果顯示給父交易,經過了多次的
計算之後,父交易放棄了,並且重新存回所有在最上層交易發生之前的系統開始數
值,必然的,前面子交易所完成的工作都必須重新回復原來狀況,因此,上述例子
的執行效率僅能適用於最上層的交易。

50
因為巢狀交易可以有任意多層的深度,我們就必須有相當程度的管理措施,使
得所有事情都能保持正確性,無論如何,這種語意是相當明顯的,當任何交易或子
交易開始的時候,我們就給予一塊私人的拷貝區域,使得這些交易能獲得整個系統
中,任何他們所需要的目標物,按照他們的方式加以處理,如果最後整個交易放棄
了,它的私人區域就消失了,如同它根本就沒有存在過一樣,如果整個交易成功了,
這個私人的工作區域就取代了原來的那些資料,因此,如果第一個子交易完成了,
接著開始了另一個新的子交易,第二個子交易所看到的將是第一個子交易所產生出
來的結果。

補-4-3 實作(Implementation)
交易的觀念似乎是很好的想法,但是我們要如何使它能實際運用呢?這一段我
們就是要來處理這問題,現在我們應該很清楚,如果一個行程執行一個交易僅僅更
新它所使用到的目標物(如檔案,資料庫記錄等)
,很顯然這個交易不具有原子性,
並且當這個交易放棄時,它所做過的改變並不會如魔術般的消失掉,更深一層的當
多個交易進行時,也不具有序列性,很明顯的我們需要一些其他的實作方法來解決
這個問題,在下面我們將會討論到兩種最常被使用到的方法。

私人的工作區(Private Workspace)
觀念上來說,當一個行程開始交易,就得到一個私人工作區,裏面包含所有它
所使用到的檔案(或是其他的目標)
,直到它的交易完成或放棄之前,它所有的讀寫
動作都是在它的私人工作區內進行,以此來區別真實的檔案系統,這種方式指出了
第一種實作的解決方法:在每一個行程開始交易的時刻,立即給予一個私人的工作
區域。

這種技巧的問題是複製所有需要的東西到私人工作區內,成本上是非常昂貴
的,但是幾種較樂觀的方法使它具有可行性,第一個樂觀性是根據一項事實,那就
是當一個行程僅讀取檔案,而不修正它時,我們就不需要一個私人的複製,我們可
以使用這真實的一個(除非在交易開始時,它已經被改變了)
,必然的,當行程開始
交易時,它能夠開啟一塊私人的工作區,裏面僅儲存一些指標指向它的父工作區,
當這個交易是在最上階層時,父工作區就是真實的檔案系統,

當形成開啟一個用來讀取資料的檔案,指標就往回指一直到這個檔案是位於他
的父工作區內(或是更前面的工作區中)。

當一個檔案開啟的目的是寫入,他能夠用上面讀取的方式來找到他的所在位
置,除非他是第一次被複製到私人的工作區,而第二個樂觀的方法是將大部份複製
的東西拿掉,用僅僅複製檔案索引到私人工作區的方式來取代複製整個的檔案,這
索引是一個區塊的資料,裡面包含了每一個檔案在磁碟中的區塊位址,在 UNIX 系
統中,這個索引就是 i-node,使用私人的索引,檔案仍然能按照正常的方式讀取,

51
因為磁碟位址所包含的是最原始的磁碟區塊,因此當一個檔案區塊第一次被修正
時,這個區塊將被複製,而他被複製的位址將會插入到索引之內,如圖 11-19 所顯
示的,這個區塊接著會被更新而不影響到原始的狀況,附加區塊也是用同樣的方法
進行,這個新產生出來的區出來的區塊有時我們叫他影子區塊(shadow klocks)。

私人工作區
索引 原始索引 0' 0
0 0 1 1
1 1 2 2
2 2 3' 3
磁碟

1 2 0 1 2 0 1 2

0' 3' 0 3

未使用的自由區塊
(a) (b) (c)

圖 11-19 (a) 三個檔案區塊的檔案索引及磁碟區塊。

(b) 當交易更新區塊 0 及附加區塊 3 時所產生的狀況。

(c) 當整個交易完成時。

如同圖 11-19(b)我們所看到的,執行交易的形成看到了檔案的修改,但是其他
行程所看見的仍然是原始的檔案,在更複雜的交易中,私人工作區可能包含了許多
的檔案,而不僅僅是一個檔案,如果整個交易被放棄,這個私人工作區就被刪除掉,
並且所有被這個私人工作區所指向的私人區塊都被釋回,放入自由的串列中,如果
這個交易完成了,私人工作區內的指標將被移到他的父工作區內,如同圖 11-19(c)
所顯示的,這個原來的私人區塊將不在存在而被放入自由串列中。

先行記錄的日誌(Writeahead Log)
另外一個常被使用處理交易的方法是先行記錄的日誌,有時我們稱他為目的串
列,這種方法是檔案資料確實的被修改了,但是在任何區塊改變之前,在先行記錄
的日誌裡已經記載了是什麼交易促成的改變,有哪些檔案及區塊被改變以及改變前
後的值為何,將這些都記錄在穩定的儲存體內,只有當這個日誌的記錄都完成以後,
才真正去改變檔案。
如果這個交易成功的完成了,一個完成的記錄被寫入日誌,但是資料結構不需

52
要被改變,因為他的資料已經被更新過了,如果這個交易被放棄了,這個日誌的資
料可以用來恢復到最初原始的狀態,每一個日誌的記錄從最後面開始往回讀取資
料,並且改變回原來未執行的狀態,這種作法被稱做迴轉(rollback)。

兩相委託的協定(Two-Phase Commit Protocol)


就如同我們前面一再提到的,交易的完成委託行為必須是具有原子性的,也就
是說具有即時與不可分割性,在分散式系統中,這個完成委託的工作,可能是由數
個不同機器上的多個行程合作完成的,可能一些行程負責變數、檔案、或是資料庫
的處理,而另外一些物件藉著這個交易被改變,在這一段中,我們將學習如何在分
散式系統中達到具有原子性委託的協定。

我們所需要研究的協定叫做兩相委託的協定(Gray,1978),雖然他不是唯一的,
但卻可能是這類處理被使用的最廣泛的一種通訊協定,圖 11-21 就是他基本觀念的
介紹,其中一個行程扮演一個協調者在日誌的開始處寫入開始委託的協定,接著就
送出訊息給其他的行程(委託參與者)
,叫他們準備委託工作,這就是整個委託的協
定開始了。

協調者 附屬者
將 "準 備 開 始 "寫 入 日 誌
送 出 "準 備 開 始 "訊 息 將 "準 備 妥 當 "寫 入 日 誌

第一相
收集所有的回應 送 出 "準 備 妥 當 "訊 息

寫下日誌記錄 將 "委 託 "寫 入 日 誌


送 出 "委 託 "訊 息 (進 行 ) 進行委託工作
第二相
送 出 "完 成 "訊 息

圖 11-21 兩相委託協定成功完成的過程

當附屬者得到訊息之後,他就檢查自己是否準備進行委託,如果是,他就做日
誌記錄,並且送回他的決定,當協調者收到所有的回應之後,他就知道是否要進行
委託或是放棄,如果所有的行程都準備要進行委託,那麼這個交易將進行委託,如
果其中有一個或多個行程不能進行委託,或是完全沒有反應,那麼這個交易將被放
棄,另一個方法是協調者寫入日誌,然後送出訊息通知所有的附屬者他的決定,這
種寫入日誌是實際進行交易的委託,並且不論以後發生任何狀況,他都讓交易繼續
進行。

由於使用穩定儲存體來記錄日誌,這種通訊協定對於系統故障有很強的適應能
力,如果協調者在寫入日誌記錄後發生故障,在修復之後他只需要從他故障之處開
始繼續工作即可,如果有需要亦可重新送出訊息,如果故障是發生在他已經將所有
附屬者送回的結果寫入日誌,那麼修復之後他只需要重新整理這些附屬者送回的結

53
果即可,如果附屬者在回應第一個訊息之前就發生故障,那麼協調者會繼續送出訊
息直到他放棄為止,如果故障是發生在送出訊息之後,我們可從日誌上看出故障產
生之處,並且可根據進行的狀況加以處理。

補-4-4 同時進行的控制(Concurreny Control)


當多重交易在不同的行程(不同的處理器)上,同時執行時,我們必須有一些
策略使得這些交易不會互相干擾,這種策略我們稱做同時進行控制的演算法,在這
一段我們將討論三種不同的處理方式。

鎖定(Locking)
最古老並且使用最廣泛的同步控制演算法就是鎖定,在最簡單的情況是當一個
行程需要讀或寫一個檔案(或其他的處理物件)
,而這是整個交易處理的一部份,那
麼他首先鎖定這個檔案,我們可以使用中央鎖定管理者來鎖定檔案,或是使用區域
鎖定管理者在個別的機器上來管理區域的檔案,這兩個例子都是鎖定管理者維護一
個已鎖定檔案的記錄,並且拒絕接受任何行程企圖鎖定已經遭受到鎖定的檔案,因
為一個正常的行程,不會去嘗試要求接受一個已經遭受到鎖定的檔案,設定鎖定一
個檔案,將可避免其他的行程要求使用這個檔案,因此可以保證在整個交易期間,
這個檔案將不會被其他的行程更改,正常情況下,鎖定的取得或釋改是由交易系統
來管理,而不需要程式設計師採取任何的行動。

這個基本的計畫是限制太嚴格了,我們可以用區分讀取鎖定與寫入鎖定的方式
來改善他,如果一個讀取鎖定被設定在一個檔案上,那麼我們允許其他的讀取鎖定
使用這的檔案,讀取鎖定的目的是為了避免這個檔案被更改(除去所有企圖寫入的
行動)
,但是我們沒有理由禁止其他的交易讀取這個檔案,另一種情形是當一個檔案
被鎖定的目的是寫入,那麼就不允許任何的交易使用這個檔案,因此讀取鎖定是可
共用的,而寫入鎖定是獨佔的。

為了簡單名瞭的緣故,我們是假設鎖定是針對整個檔案,但實際上,我們所需要的
資料可能只是很小的項目,比如說一個單獨的記錄,或是一頁資料,有時我們要參
考的項目會大到一整個的資料庫,這個到底鎖定項目需要多大的問題,我們稱做顆
粒的鎖定(granularity of locking),較好的顆粒劃分,就能產生較精確的鎖定範圍,並
且能達到較多個的平行處理, (例如一些行程在使用檔案的前端,而我們不鎖定檔案
的尾端以提供其他需要的行程使用。)另外一方面,較精確的劃分鎖定區間,需要
較多次的鎖定動作,因此成本較昂貴,並且較容易導致死結問題。

54
鎖定點
(成 長 相 ) (收 縮 相 )
鎖 成長區間 收縮區間



時間
圖 11-22 兩相的鎖定

取得和釋放鎖定精確地在他們需要或不在需要時進行,有時會產生不一致或死
結的情形,一般而言,大多數的交易會使用兩相的鎖定這種方式來做為鎖定的實際
運用,兩相的鎖定我們在圖 11-22 中,可看到他的運用方法,行程首先在成長相中,
得到所有他需要鎖定的物件,接著在收縮相中釋放他們,如果行程在到達收縮相時,
仍然未能更改任何的檔案,那麼他就必須釋放先前所有他鎖定的檔案,等待一段時
間之後,再重新開始鎖定的工作,更進一步的,已經有人證明(Eswaran et al, 1976) 如
果所有的交易都是使用兩相鎖定的方法,那麼他們以插入方式白出來的形式將是具
有序列性的,這就是為何兩相鎖定如此廣泛被使用的原因。

實際上,兩相鎖定的方法稍微有一些超出我們的需要,如果一個檔案僅是為了
讀取一次,那麼在他讀完之後,立即釋放給其他的行程使用,將不會產生任何的問
題,另一種情況是,如果一個檔案是寫入一次,那麼在到達收縮相之前,他可能不
能夠釋放這個鎖定,假設一個檔案被寫入之後立即釋放鎖定,一些其他的交易又來
鎖定他,並使用他來完成所有的工作,後來前一個交易被放棄了,那麼第二個交易
(已經使用檔案完成了他的工作)必須被恢復原狀後重做,因為他的結果是依據不
正確的檔案資料產生的,這種因為別的交易被放棄,而導致必須重做某些已經完成
託付的交易的情況我們稱做串極異常結束(cas- caded abort),這種情形應該避免發
生。

即使是兩相鎖定,仍然可能導致死結,如果兩個行程以相反的次序,要取得相
同的鎖定物件,那麼可能就會導致死結,在這裡我們所使用的一個技巧就是以一種
標準的次序,來排定取得鎖定的方法,以避免佔據一一等待(hold and wait)的情況發
生,另一種可能的方法是使用死結偵測法,藉著明白的圖形指出哪些行程在何處可
能產生死結,以及循環的檢查圖形上可能發生死結之處預先加以處理,最後一種是
使用時間的控制方法,預設所用鎖定的時間都不會超過 T 秒,如果有一個鎖定在同
一個使用者的使用下持續超過 T 秒,那麼就必定是產生死結了。

55
最佳化同步控制(Optimisic Concurrency Control)
第 二 種 達 到 同 一 時 間 進 行 多 重 交 易 的 方 法 是 最 佳 化 同 步 控 制 (Kung and
Robinson, 1981. ),這種方法的技巧是非常的簡單,那就是去做你想要做的事,不需
要去管別人怎麼做,如果發生問題,那等到發生問題時,再去想辦法解決, (許多的
政客也是使用這樣的演算法) ,實際上,產生衝突的情況非常少,因此大多數的時間
這種方法非常有效。

雖然衝突的情況很少,但並非不可能發生,因此必須有一些方法來解決衝突時
的情況,在最佳化同步控制方法中是運用追蹤記錄每一次有哪些檔案被讀取或寫
入,當一個交易要交付委託時,他先檢查其他的交易看看在他交易開始之後是否有
他的檔案被其他交易更改過了,如果有,這個交易就必須放棄,如果沒有,他就可
以交付委託(committed)。

最佳化同步控制法最適合使用在私人的工作區域中,這種情況下,每一個交易
改變他的檔案在個人的區域中,而不會影響到其他的交易,在最後這些新的檔案或
者被交付委託或者是被釋放掉。

使用最佳化同步控制法最大的好處是沒有死結問題,並且允許最大數量的平行
處理,因為沒有任何一個行程需要為了鎖定的問題而等待,而他的最大不利之處在
於他可能會發生失敗的情況,而此時所有的交易必須重新回復到開始的狀態,在一
個頻繁使用的情況下,失敗的機率相對的昇高,因此最佳化同步控制法將是最差的
選擇。

時間戳記(Timestamps)
另一種達到同步控制完全不同的方法,是在每一個交易開始的時刻給予一個時
間戳記(Reed, 1983),在藍波演算法中,我們可以保證每一個時間戳記都是獨一的,
這一點在這裡非常重要,在系統中的每一個檔案都有一個讀取時間戳記一個寫入時
間戳記,讓我們知道是哪一個交易最後讀取或寫入這個這個案,如果交易是簡短而
廣泛的分佈與使用,那麼在正常的情況下,當一個行程嘗試接受一個檔案時,這個
檔案的讀與寫的時間戳記將比現在交易的時間戳記為低(較早) ,這個次序的意義就
是交易是在一個正確的次序下進行,所以一切正常。

當這個次序不正確時,那就表示有一個交易較現在進行的交易晚發生,但是卻
較早得到這個檔案,並且已完成委託,這種情況就是說現在進行的交易太遲了,所
以就放棄這個交易,從某一方面來看,這個方法有點類似樂觀的同步控制,但在細
節上卻也有所不同,在前面的方法中,我們希望同步交易不要同時使用相同的檔案,
卻無法控制,但在時間戳記的方法中,我們不需要擔心會發生這種情形,因為較低
號碼的交易總是先得到檔案。
時間戳記的方法最容易使用舉例來說明,假設現在有三個交易甲、乙、丙,甲

56
最早開始,並且使用到所有乙和丙所需要使用的檔案,所以所有他們使用到的檔案
都有了甲的時間戳記,乙和丙同時開始,乙擁有較丙為低的時間戳記(當然兩者都
較甲為高)。

讓我們先考慮乙要寫入檔案,我們稱他的時間印記為 T,而檔案個別的讀取與
寫入時間戳記分別為 Trd 與 Twr,除非丙已經先行使用了這個檔案並且完成委託,
否則這個檔案的 Trd 和 Twr 將仍然是甲的時間戳記,因此將比 T 為小,在圖 11-23(a)
和(b)中我們看到 T 比 Trd 與 Twr 都大(表示丙尚未使用此檔案),所以寫入是可被
接受的(暫時性的) ,一直到乙完成委託,這個寫入才是永久性的,乙的時間戳記到
此時才暫時性的寫入這個檔案內(取代甲的時間戳記)。
寫入 讀出

(a) Trd Twr T (e)


Twr T
(甲 ) (甲 ) (乙 ) (甲 )(乙 )
執行
(b)
Twr Trd T 暫 時 性 (f) Twr Ttent T
(甲 ) (甲 ) (乙 ) 的寫入 (甲 ) (丙 ) (乙 )
T Trd T Twr
(c) (g)
(乙 ) (丙 ) (乙 ) (丙 )
放棄 T Ttent
T Twr 放棄
(d) (失 敗 ) (h)
(乙 ) (丙 ) (乙 ) (丙 )
時間 時間
圖 11-23 使用時間戳記的同步控制

在圖 11-23(c)和(d)中,乙失去了鎖定,丙即讀取了又寫入了檔案,並且完成委
託,因此乙必須放棄交易,當然他可以重新開始並且取得一個新的時間戳記,重做
他的工作。

現在我們來看讀取,在圖 11-23(e)中,沒有任何的衝突,因此我們能夠立刻讀
取,在圖 11-23(f)中有一個好事者闖入並且嘗試寫入檔案,這個好事者的時間戳記
比乙為低,所以乙只能等待一直到這個好事者完成委託,乙才可繼續讀取這個新檔
案。

在圖 11-23(g)中,丙已經改變了檔案並且完成委託,因此乙必須要放棄,在圖
11-23(h)中,丙正在進行改變檔案,雖然他還沒有交付委託,但是乙仍然是太遲了,
必須要放棄。

時間戳記的方法是與鎖定法有不同的特性,當一個交易遇到一個較大(較晚)
的時間戳記,他就必須放棄,而在鎖定的情形下他可能是等待,或是立刻的進行交
易,但在另一方面,時間戳記沒有死結的問題,這是最大的優點。

總而言之,交易法提供了許多的優點使得穩定的分散式系統可以達到,而他主
要的問題是若要實際運用卻是相當複雜,因此也降低了他的效率,這些問題我們仍
然在繼續研究中,也許有一天我們能有效的解決這些問題。

57
本講義內容主要摘自
Operating System Concepts 6th Edition
部份補充則節錄自
Modern Operating System 2nd Edition
因講義內容為片段選錄,無法代表全部內容,詳細完整之敘述仍須以原書為準。

58

You might also like