核心中的同步
摘自《Linux核心之旅》
核心只要存在任務交錯執行,就必然會存在對共用資料的並發問題,也就必然存在對資料的保護。而核心中任務交錯執行的原因歸根結底還是由於核心任務調度造成的。我們下面歸納一下核心中同步的原因。
同步原因
l 中斷——中斷幾乎可以在任何時刻非同步發生,也就可能隨時打斷當前正在執行的代碼。
l 睡眠及與使用者空間的同步——在核心執行的進程可能會睡眠,這就會喚醒發送器,從而導致調度一個新的使用者進程執行。
l 對稱式多處理——兩個或多個處理器可以同時執行代碼。
l 核心搶佔——因為核心具有搶佔性,所以核心中的任務可能會被另一任務搶佔(在2.6核心引進的新能力)。
後兩種情況大大增加了核心任務並發執行的可能性,使得並發隨時隨刻都有可能發生,而且不可清晰預見,規律難尋。
核心同步措施
為了避免並發,防止競爭。核心提供了一組同步方法來提供對共用資料的保護。 我們的重點不是介紹這些方法的詳細用法,而是強調為什麼使用這些方法和它們之間的差別。
Linux使用的同步機制可以說從2.0到2.6以來不斷髮展完善。從最初的原子操作,到後來的訊號量,從大核心鎖到今天的自旋鎖。這些同步機制的發展伴隨 Linux從單一處理器到對稱式多處理器的過度;伴隨著從非搶佔核心到搶佔核心的過度。鎖機制越來越有效,也越來越複雜。
目前來說核心中原子操作多用來做計數使用,其它情況最常用的是兩重鎖以及它們的變種,一個是自旋鎖,另一個是訊號量。我們下面就來著重介紹一下這兩種鎖機制。
自旋鎖
自旋鎖是專為防止多處理器並發而引入的一種鎖,它在核心中大量應用於中斷處理等部分(對於單一處理器來說,防止中斷處理中的並發可簡單採用關閉中斷的方式,不需要自旋鎖)。
自旋鎖最多隻能被一個核心任務持有,如果一個核心任務試圖請求一個已被爭用(已經被持有)的自旋鎖,那麼這個任務就會一直進行忙迴圈——旋轉——等待鎖重新可用。要是鎖未被爭用,請求它的核心任務便能立刻得到它並且繼續進行。自旋鎖可以在任何時刻防止多於一個的核心任務同時進入臨界區,因此這種鎖可有效地避免多處理器上並發啟動並執行核心任務競爭共用資源。
事實上,自旋鎖的初衷就是:在短期間內進行輕量級的鎖定。一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用的期間進行自旋(特別浪費處理器時間),所以自旋鎖不應該被持有時間過長。如果需要長時間鎖定的話, 最好使用訊號量。
自旋鎖的基本形式如下:
spin_lock(&mr_lock);
/*臨界區*/
spin_unlock(&mr_lock);
因為自旋鎖在同一時刻只能被最多一個核心任務持有,所以一個時刻只有一個線程允許存在於臨界區中。這點很好地滿足了對稱式多處理機器需要的鎖定服務。在單一處理器上,自旋鎖僅僅當作一個設定核心搶佔的開關。如果核心搶佔也不存在,那麼自旋鎖會在編譯時間被完全剔除出核心。
自旋鎖在核心中有許多變種,如對bottom half 而言,可以使用spin_lock_bh()用來獲得特定鎖並且關閉半底執行。相反的操作由spin_unlock_bh()來執行;如果臨界區的訪問邏輯可以被清晰的分為讀和寫這種模式,那麼可以使用讀者/寫者自旋鎖,調用形式為:
讀者的代碼路徑:
read_lock(&mr_rwlock);
/*唯讀臨界區*/
read_unlock(&mr_rwlock);
寫者的代碼路徑:
write_lock(&mr_rwlock);
/*讀寫臨界區*/
write_unlock(&mr_rwlock);
簡單的說,自旋鎖在核心中主要用來防止多處理器中並發訪問臨界區,防止核心搶佔造成的競爭。另外自旋鎖不允許任務睡眠(持有自旋鎖的任務睡眠會造成自死結——因為睡眠有可能造成持有鎖的核心任務被重新調度,而再次申請自己已持有的鎖),它能夠在中斷上下文中使用。
死結:假設有一個或多個核心任務和一個或多個資源,每個核心都在等待其中的一個資源,但所有的資源都已經被佔用了。這便會發生所有核心任務都在相互等待,但它們永遠不會釋放已經佔有的資源,於是任何核心任務都無法獲得所需要的資源,無法繼續運行,這便意味著死結發生了。自死瑣是說自己佔有了某個資源,然後自己又申請自己已佔有的資源,顯然不可能再獲得該資源,因此就自縛手腳了。
訊號量
Linux中的訊號量是一種睡眠鎖。如果有一個任務試圖獲得一個已被持有的訊號量時,訊號量會將其推入等待隊列,然後讓其睡眠。這時處理器獲得自由去執行其它代碼。當持有訊號量的進程將訊號量釋放後,在等待隊列中的一個任務將被喚醒,從而便可以獲得這個訊號量。
訊號量的睡眠特性,使得訊號量適用於鎖會被長時間持有的情況;只能在進程上下文中使用,因為中斷上下文中是不能被調度的;另外當代碼持有訊號量時,不可以再持有自旋鎖。
訊號量基本使用形式為:
staticDECLARE_MUTEX(mr_sem);//聲明互斥訊號量
…
if(down_interruptible(&mr_sem))
/*可被中斷的睡眠,當訊號來到,睡眠的任務被喚醒 */
/*臨界區…*/
up(&mr_sem);
同自旋鎖一樣,訊號量在核心中也有許多變種,比如讀者-寫者訊號量等,這裡不再做介紹了。
訊號量和自旋鎖區別
雖然聽起來兩者之間的使用條件複雜,其實在實際使用中訊號量和自旋鎖並不易混淆。注意以下原則。
如果代碼需要睡眠——這往往是發生在和使用者空間同步時——使用訊號量是唯一的選擇。由於不受睡眠的限制,使用訊號量通常來說更加簡單一些。如果需要在自旋鎖和訊號量中作選擇,應該取決於鎖被持有的時間長短。理想情況是所有的鎖都應該儘可能短的被持有,但是如果鎖的持有時間較長的話,使用訊號量是更好的選擇。另外,訊號量不同於自旋鎖,它不會關閉核心搶佔,所以持有訊號量的代碼可以被搶佔。這意味者訊號量不會對影響調度反應時間帶來負面影響。
自旋鎖對訊號量
―――――――――――――――――――――――――――――――
需求 建議的加鎖方法
低開銷加鎖 優先使用自旋鎖
短期鎖定 優先使用自旋鎖
長期加鎖 優先使用訊號量
中斷上下文中加鎖 使用自旋鎖
持有鎖是需要睡眠、調度 使用訊號量
―――――――――――――――――――――――――――――――
引自 《Linux核心開發》
防止並發的方式除了上面提到的外還有很多,我們不詳細介紹了。說了這麼多,希望大家認識到,並發控制在核心編程中是個特別難纏的問題,要駕禦它必須清楚地認識到核心中各種任務的調度時機與特點,並且在開發初期就應特別小心保護共用資料(一切共用資料、一切能被別人看到的資料都要注意保護),別等到開發完成才去亡羊補牢。