作業系統中鎖的實現
在多執行緒程式設計中,為了保證資料操作的一致性,作業系統引入了鎖機制,用於保證臨界區程式碼的安全。通過鎖機制,能夠保證在多核多執行緒環境中,在某一個時間點上,只能有一個執行緒進入臨界區程式碼,從而保證臨界區中操作資料的一致性。
鎖機制的一個特點是它的同步原語都是原子操作。那麼作業系統是如何保證這些同步原語的原子性呢?
作業系統之所以能構建鎖之類的同步原語,是因為硬體已經為我們提供了一些原子操作,比如:中斷禁止和啟用(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環境下工作,而中斷啟用和禁止則不能。