1. 程式人生 > >作業系統中鎖的實現

作業系統中鎖的實現

在多執行緒程式設計中,為了保證資料操作的一致性,作業系統引入了鎖機制,用於保證臨界區程式碼的安全。通過鎖機制,能夠保證在多核多執行緒環境中,在某一個時間點上,只能有一個執行緒進入臨界區程式碼,從而保證臨界區中操作資料的一致性。

鎖機制的一個特點是它的同步原語都是原子操作。那麼作業系統是如何保證這些同步原語的原子性呢?

作業系統之所以能構建鎖之類的同步原語,是因為硬體已經為我們提供了一些原子操作,比如:中斷禁止和啟用(interrupt enable/disable),記憶體載入和存入(load/store)測試與設定(test and set)指令。禁止中斷這個操作是一個硬體步驟,中間無法插入別的操作。同樣,中斷啟用,測試與設定均為一個硬體步驟的指令。在這些硬體原子操作之上,我們便可以構建軟體原子操作:鎖,睡覺與叫醒,訊號量等。

1.以中斷啟用和禁止來實現鎖

要防止一段程式碼在執行過程中被別的程序插入,就要考慮在一個單處理器上,一個執行緒在執行途中被切換的途徑。我們知道,要切換程序,必須要發生上下文切換,上下文切換隻有兩種可能:

①一個執行緒自願放棄CPU而將控制權交給作業系統排程器(通過yield之類的作業系統呼叫來實現)

②一個執行緒被強制放棄CPU而失去控制權(通過中斷來實現)

原語執行過程中,我們不會自動放棄CPU控制權,因此要防止程序切換,就要在原語執行過程中不能發生中斷。所以採用禁止中斷,且不自動呼叫讓出CPU的系統呼叫,就可以防止程序切換,將一組操作變為原子操作。

lock之中斷啟用與禁止:

lock()

{

disable interrupt

while(value!=FREE)

{

enable interrupt //使其他執行緒可以搶佔,從而改變value的值

disable interrunpt //只有在這兩行語句之間,別的程序才擁有搶佔時機

}

value=BUSY

enable interrupt

}

unlock之中斷啟用與禁止:

unlock()

{

disable interrupts

value=FREE

enable interrupts

}

2.以測試與設定指令來實現鎖

原子操作:(設定操作)將1寫入到指定記憶體單元,(讀取操作)返回指定記憶體單元裡原來的值,也即寫入新值1之前的內容。

測試與設定指令:

test_and_set(x)

{

tmp=x

x=1

return (tmp)

}

test_and_set(x)的操作是將1寫入到變數x裡,並將寫1之前x的值返回。

使用測試與設定指令實現lock:

(value初始值為0,代表鎖是開啟的。)

lock()

{

while(test_and_lock(value)==1) {} //每次執行完原子操作後都有可能會被搶佔

}

如果鎖是開啟的,即value是0的話,則返回值是0,該指令將value設定為1,獲得鎖並退出迴圈。

如果鎖是閉合的,即value是1的話,則返回值是1,迴圈繼續。直至成功獲得鎖為止。

使用測試與設定指令實現unlock:

unlock()

{

value=0 //因為是賦值0,可以直接在總線上產生,不用中斷包裹著也沒有問題

}

3.以非繁忙等待,中斷啟用與禁止來實現鎖

前面兩種鎖的實現方式都較為簡單,但都有一個問題,就是存在繁忙等待。而繁忙等待浪費資源,我們想到對前兩種方法進行改善。改善思路:不進行繁忙等待,在拿不到鎖的時候去睡覺,等待別人的叫醒。

先看一種鎖操作實現方式:

lock()

{

disable interrupt

if(value==free)

{

value=busy

}

else

{

新增到鎖的等待佇列

切換到下一個執行緒

}

enable interrupt

}

使用非繁忙等待中斷禁止與啟用來實現釋放鎖操作:

unlock()

{

disable interrupt

value=free

if(有執行緒在等待鎖)

{

移到就緒佇列

value=busy

}

enable interrupt

}

但是這種方式存在著問題:是因為切換到別的程序之後,該程式無法再執行,那麼後面的中斷啟用指令就不能執行了。而我們是在中斷處於禁止狀態下切換到別的程序的,如果別的程序沒有執行中斷啟用或者自動放棄CPU給另一個執行緒,系統將進入死鎖狀態。

解決辦法是閉鎖操作不啟用中斷,而是留給別的執行緒去啟用中斷。

也就是說,我們要求所有執行緒遵守下列約定:

①所有執行緒承諾在呼叫執行緒切換呼叫時將中斷留在禁止狀態。

②所有執行緒承諾在從切換返回時將中斷重新啟用。

因此,使用非繁忙等待中斷禁止與啟用來實現鎖操作的正確方式如下:

這裡注意,switch表示切換執行緒;中斷的啟用與禁止是在系統呼叫(lock和yield)裡面實現的,即由作業系統實現。

4.以最少繁忙等待,測試與設定來實現鎖

使用測試與設定來實現鎖不可能完全避免繁忙等待,我們的目的就是儘可能降低等待的時間。

我們的中心思想就是:我們只用繁忙等待來執行閉鎖的操作,如果不能這樣做就放棄CPU。

使用一個額外的變數guard用來保證每次只有一個執行緒獲得value並對其操作。

lock()

{

while(test_and_set(guard)){}

if(value==free)

{

value=busy

guard=0

}

else

{

新增到鎖的等待佇列

guard=0

切換執行緒

}

}

unlock()

{

while(test_and_set(guard)){}

value=free

if(有其他執行緒在等待鎖)

{

移到就緒佇列

value=busy

}

guard=0

}

我們瞭解瞭如何使用中斷禁止,測試與設定兩種硬體原語來實現軟體的鎖原語。這兩種方式比較起來,顯然測試與設定更加簡單,也因此使用的更為普遍。此外,test and set還有一個優點,就是可以在多CPU環境下工作,而中斷啟用和禁止則不能。