1. 程式人生 > >作業系統學習筆記:程序同步

作業系統學習筆記:程序同步

互相協作的程序之間有共享的資料,於是這裡就有一個併發情況下,如何確保有序操作這些資料、維護一致性的問題,即程序同步。

從底層到高階應用,同步機制依次有臨界區、訊號量、管程、原子事務。

1、臨界區

每個程序有一個程式碼段稱為臨界區,共享資料在此進行操作。沒有兩個程序同時在臨界區執行。

臨界區方案是一種協議,即每個程序進入臨界區操作都需要請求。實現這一請求的程式碼稱為進入區,從臨界區退出的善後工作由退出區,之後是剩餘區。

臨界區方案必須滿足三項要求:

1)互斥

兩個程序不能同時在臨界區操作

2)前進

臨界區空閒,如果有程序需要,且不在剩餘區,則可參加選擇

3)有限等待

程序只要有意願,總有一天會進入臨界區,因為程序進入臨界區的次數有上限。

作業系統內部的臨界區問題中,非搶佔式比較容易,因為程序沒有競爭條件;而搶佔式則困難得多,因為程序可能會執行在不同處理器上。但搶佔式核心更適合實時程式設計。

Peterson演算法是一種臨界區問題演算法。

對於臨界區問題,除了軟體上進行設計,也可以在硬體層面來解決。現代計算機系統提供了一些特殊硬體指令,可以原子地執行。

2、訊號量

臨界區方案比較複雜,可以使用訊號量這個同步工具。

訊號量是一個整數變數,除了初始化,只能通過兩個標準原子操作:wait()和signal()來訪問。

wait(s){
	while(s <= 0)
		;//當s<=0時,迴圈等待,直到S變為正數。如果將這個S看做可用資源,就很好理解了。S<=0,代表沒有資源
	s--;//可用資源減一
}

signal(s){
	s++;//可用資源加一
}


//使用訊號量實現臨界區問題方案
do{
	wait(mutex);
	//臨界區
	signal(mutex);
	//剩餘區
}while(true);

上述例子中,有迴圈等待,又叫忙等待。忙等待浪費了CPU時鐘,這在多道程式系統中,顯然是個問題,因為本可以讓給其他程序執行。

不過,這種依靠忙等待實現的訊號量又稱為自旋鎖(spinlock)。自旋鎖有一定的優越性,因為無須進行上下文切換,有時上下文切換相比之下更浪費時間)。通常,等待時間如果比較短,就適合用自旋鎖。自旋鎖常用在多處理器系統中,因為多執行緒可以用於多處理器,一個執行緒自旋,另一個執行緒可以在另一個處理器上執行。

不過,為了克服忙等的缺點,可以修改wait()和signal()的定義,採用程序堵塞來替代忙等:

typedef struct {
    int value;//記錄了這個訊號量的值 
    struct process *list;//儲存正在等待這個訊號量的程序 
} semaphore;

wait(semaphore *S) {
    S->value--;
    if(S->value < 0) {//沒有資源了
        add this process to S->list;//進入等待佇列
        block();//堵塞
    }
}

signal(semaphore *S) {
    S->value++;
    if(S->value <= 0) {//上面++後,S仍然還<=0,說明資源供不應求,等待者眾,於是喚醒等待佇列中的一個,意思是說,我做完了,你好自為之。至於是否可以獲得資源,看造化。。。就此別過,青山綠水,後會有期,good bye!
        remove a process P from S->list;
        wakeup(P);//切換到就緒狀態
    }
}

3、管程

訊號量比臨界區方便,但如果使用不正確,比如順序不當,仍然會導致一些錯誤。

管程用高階語言封裝了訊號量,方便程式設計師呼叫。

管程結構確保一次只有一個程序能在管程內活動。但是,程序在管程內 應該怎麼理解?難道是程序在管程裡面執行?但看上去,是程序呼叫了管程,依管程的返回訊號而行事?

管程通常是用於管理資源的,因此管程中有程序等待佇列和相應的等待和喚醒操作。在管程入口有一個等待佇列,稱為入口等待佇列。當一個已進入管程的程序等待時,就釋放管程的互斥使用權;當已進入管程的一個程序喚醒另一個程序時,兩者必須有一個退出或停止使用管程。在管程內部,由於執行喚醒操作,可能存在多個等待程序(等待使用管程),稱為緊急等待佇列,它的優先順序高於入口等待佇列。 


因此,一個程序進入管程之前要先申請,一般由管程提供一個enter過程;離開時釋放使用權,如果緊急等待佇列不空,則喚醒第一個等待者,一般也由管程提供外部過程leave。 


管程內部有自己的等待機制。管程可以說明一種特殊的條件型變數:var c:condition;實際上是一個指標,指向一個等待該條件的PCB(程序控制塊)佇列。對條件型變數可執行wait和signal操作


wait(c):若緊急等待佇列不空,喚醒第一個等待者,否則釋放管程使用權。執行本操作的程序進入C佇列尾部; 


signal(c):若C佇列為空,繼續原程序,否則喚醒佇列第一個等待者,自己進入緊急等待佇列尾部。

(額,從上述描述看,管程可以控制程序等待、喚醒等,從這點來說,程序在管程內是說得過去的)

生產者-消費者問題(有buffer)

問題描述:(一個倉庫可以存放K件物品。生產者每生產一件產品,將產品放入倉庫,倉庫滿了就停止生產。消費者每次從倉庫中去一件物品,然後進行消費,倉庫空時就停止消費。 
解答: 
管程:buffer=MODULE; 
(假設已實現一基本管程monitor,提供enter,leave,signal,wait等操作)

notfull,notempty:condition; // notfull控制緩衝區不滿,notempty控制緩衝區不空; 
count,in,out: integer;     // count記錄共有幾件物品,in記錄第一個空緩衝區,out記錄第一個不空的緩衝區 
buf:array [0..k-1] of item_type; 
define deposit,fetch; 
use monitor.enter,monitor.leave,monitor.wait,monitor.signal;
 
procedure deposit(item); 
{ 
  if(count=k) monitor.wait(notfull); 
  buf[in]=item; 
  in:=(in+1) mod k; 
  count++; 
  monitor.signal(notempty); 
} 
procedure fetch:Item_type; 
{ 
  if(count=0) monitor.wait(notempty); 
  item=buf[out]; 
  in:=(in+1) mod k; 
  count--; 
  monitor.signal(notfull); 
  return(item); 
} 
{ 
count=0; 
in=0; 
out=0; 
} 

程序:producer,consumer; 
producer(生產者程序): 
Item_Type item; 
{ 
  while (true) 
  { 
    produce(&item); 
    buffer.enter(); 
    buffer.deposit(item); 
    buffer.leave(); 
  } 
} 

consumer(消費者程序): 
Item_Type item; 
{ 
  while (true) 
  { 
    buffer.enter(); 
    item=buffer.fetch(); 
    buffer.leave(); 
    consume(&item); 
  } 
}

4、原子事務

有一些操作裡面的步驟必須一口氣全部執行完,不可分割,結果是要麼全部成功,要麼就失敗。

這點在資料庫技術上體現得淋漓盡致:事務。近來(什麼時候的事了?)有將資料庫技術應用於作業系統的熱潮。

1)日誌

資料庫的資料為什麼能儲存得那麼好?很大程度上是歸功於日誌。

最常用的方法是操作資料的時候,先記錄日誌,再操作資料。

每條日誌記錄:

(1)事務名稱

(2)資料項名稱

(3)舊值

(4)新值

事務開始前,記錄<t_start>記入日誌;

當事務提交時,記錄<t_commit>記入日誌;

如果事務失敗,或者系統故障,系統就會檢查日誌(這一步也許在系統重啟之時),凡有<t_start>記錄而無<t_commit>的,系統做回滾操作;兩條記錄都有的,系統則將資料重新寫一遍。(有些重寫可能是不必要的,但也不會引起錯誤)

但這種做法很浪費,因為絕大多數的事務都是成功的。於是引入檢查點(checkpoint):

當系統將資料從記憶體寫入硬碟或穩定儲存裝置時,記錄一個<checkpoint>。以後系統重啟時只處理這個checkpoint之後的日誌記錄。

2)鎖及時間戳

在併發的情況下,多個事務同時執行,由於事務是原子性的,所以事務併發,其實相當於讓一個個事務序列化執行。這裡就牽扯到序列排程和非序列排程。

非序列排程不一定會引起錯誤,因為事務之間,裡面的步驟不一定會相關。將這些步驟打散、組合,可能效率會更高。

序列處理可以依靠:

(1)鎖

(2)時間戳

方案是資料讀寫時記錄時間值:

W-timestamp(Q)

R-timestamp(Q)

Q是資料項,只要操作Q,即記錄時間。

在一個事務中,如果發出read(Q)

(1)事務開始時間 < W-timestamp(Q),表明值正在被改寫,read被拒絕,事務回滾;

(2)事務開始時間 >= W-timestamp(Q),read,R-timestamp(Q) = MAX(R-timestamp(Q),事務時間);

如果事務發出write(Q)

(1)事務開始時間 < R-timestamp(Q),表明值正在被讀取,write被拒絕,事務回滾;

(2)事務開始時間 < W-timestamp(Q),表明值正在被修改,write被拒絕,事務回滾;;

(3)否則,write

參考文章:

http://www.cnblogs.com/sonic4x/archive/2011/07/05/2098036.html