標籤:code 競爭 html 調度 使用 調用 lin 規則 喚醒
核心同步
同步介紹
同步的概念
臨界區:也稱為臨界段,就是訪問和操作共用資料的程式碼片段。
競爭條件: 2個或2個以上線程在臨界區裡同一時候啟動並執行時候,就構成了競爭條件。
所謂同步。事實上防止在臨界區中形成競爭條件。
假設臨界區裡是原子操作(即整個操作完畢前不會被打斷),那麼自然就不會出競爭條件。但在實際應用中。臨界區中的代碼往往不會那麼簡單,所以為了保持同步,引入了鎖機制。但又會產生一些關於鎖的問題。
死結產生的條件:要有一個或多個運行線程和一個或多個資源,每一個線程都在等待當中的一個資源。但全部資源都已被佔用。
所以線程相互等待。但它們永遠不會釋放已經佔有的資源。於是不論什麼線程都無法繼續,死結發生。
自死結:假設一個運行線程試圖去獲得一個自己已經持有的鎖。它不得不等待鎖被釋放。但由於它正在忙著等待這個鎖。所以自己永遠也不會有機會釋放鎖,死結產生。
饑餓(starvation) 是一個線程長時間得不到須要的資源而不能啟動並執行現象。
造成並發的原因
中斷——中斷差點兒能夠在不論什麼時刻非同步發生。也就是可能隨時打斷當前正在執行的代碼。
非強制中斷和tasklet ——核心能在不論什麼時刻喚醒或調度中斷和tasklet。打斷當前正在啟動並執行代碼。
核心搶佔——由於核心具有搶佔性。所以核心中的任務可能會被還有一任務搶佔。
睡眠及使用者空間的同步——在核心啟動並執行進程可能會睡眠,這就會喚醒發送器從而導致調度一個新的使用者進程運行。
對稱式多處理——兩個或多個處理器能夠同一時候運行代碼。
避免死結的簡單規則
加鎖的順序是關鍵。
使用嵌套的鎖時必須保證以同樣的順序擷取鎖,這樣能夠阻止致命擁抱類型的死結。最好能記錄下鎖的順序,以便其它人能照此順序使用。
防止發生饑餓。推斷這個代碼的運行是否會結束。假設A不發生,B要一直等待下去嗎?
不要反覆請求同一個鎖。
越複雜的加鎖方案越可能造成死結。---設計應力求簡單。
鎖的粒度
加鎖的粒度用來描寫敘述加鎖保護的資料規模。一個過粗的鎖保護大塊資料,比方一個子系統的全部資料結構。一個過細的鎖保護小塊資料。比方一個大資料結構中的一個元素。
在加鎖的時候,不僅要避免死結,還須要考慮加鎖的粒度。
鎖的粒度對系統的可擴充性有非常大影響,在加鎖的時候,要考慮一下這個鎖是否會被多個線程頻繁的爭用。
假設鎖有可能會被頻繁爭用。就須要將鎖的粒度細化。
細化後的鎖在多處理器的情況下。效能會有所提升。
同步方法
原子操作
原子操作指的是在運行過程中不會被別的代碼路徑所中斷的操作。核心代碼能夠安全的調用它們而不被打斷。
原子操作分為整型原子操作和位原子操作。
spinlock自旋鎖
自旋鎖的特點就是當一個線程擷取了鎖之後,其它試圖擷取這個鎖的線程一直在迴圈等待擷取這個鎖。直至鎖又一次可用。
因為線程實在一直迴圈的擷取這個鎖,所以會造成CPU處理時間的浪費,因此最好將自旋鎖用於能非常快處理完的臨界區。
自旋鎖使用時有2點須要注意:
1.自旋鎖是不可遞迴的,遞迴的請求同一個自旋鎖會自己鎖死自己。
2.線程擷取自旋鎖之前。要禁止當前處理器上的中斷。(防止擷取鎖的線程和中斷形成競爭條件)比方:當前線程擷取自旋鎖後。在臨界區中被中斷處理常式打斷,中斷處理常式正好也要擷取這個鎖,於是中斷處理常式會等待當前線程釋放鎖,而當前線程也在等待中斷運行完後再運行臨界區和釋放鎖的代碼。
中斷處理下半部的操作中使用自旋鎖尤其須要小心:
1. 下半部處理和進程上下文共用資料時,因為下半部的處理能夠搶佔進程內容相關的代碼,所以進程上下文在對共用資料加鎖前要禁止下半部的運行,解鎖時再同意下半部的運行。
2. 中斷處理常式(上半部)和下半部處理共用資料時,因為中斷處理(上半部)能夠搶佔下半部的運行。所下面半部在對共用資料加鎖前要禁止中斷處理(上半部),解鎖時再同意中斷的運行。
3. 同一種tasklet不能同一時候執行。所以同類tasklet中的共用資料不須要保護。
4. 不同類tasklet中共用資料時,當中一個tasklet獲得鎖後。不用禁止其它tasklet的運行,由於同一個處理器上不會有tasklet相互搶佔的情況
5. 同類型或者非同類型的非強制中斷在共用資料時,也不用禁止下半部,由於同一個處理器上不會有非強制中斷互相搶佔的情況
讀-寫自旋鎖
假設臨界區保護的資料是可讀可寫的,那麼僅僅要沒有寫操作,對於讀是能夠支援並行作業的。
對於這樣的僅僅要求寫操作是相互排斥的需求,假設還是使用自旋鎖顯然是無法滿足這個要求(對於讀操作實在是太浪費了)。為此核心提供了還有一種鎖-讀寫自旋鎖,讀自旋鎖也叫共用自旋鎖,寫自旋鎖也叫排他自旋鎖。
讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了“自旋”的概念,可是在寫操作方面。僅僅能最多有一個寫進程。在讀操作方面,同一時候能夠有多個讀運行單元,當然,讀和寫也不能同一時候進行。
自旋鎖提供了一種高速簡單的所得實現方法。假設加鎖時間不長而且代碼不會睡眠,利用自旋鎖是最佳選擇。假設加鎖時間可能非常長或者代碼在持有鎖時有可能睡眠,那麼最好使用訊號量來完畢加鎖功能。
訊號量
Linux中的訊號量是一種睡眠鎖。假設有一個任務試圖獲得一個已經被佔用的訊號量時,訊號量會將其推進一個等待隊列,然後讓其睡眠,這時處理器能重獲自由,從而去運行其他代碼,當持有訊號量的進程將訊號量釋放後。處於等待隊列中的哪個任務被喚醒。並獲得該訊號量。
1)由於爭用訊號量的過程在等待鎖又一次變為可用時會睡眠。所以訊號量適用於鎖會被長時間持有的情況;相反,鎖被短時間持有時,使用訊號量就不太適宜了。
由於睡眠、維護等待隊列以及喚醒所花費的開銷可能比鎖被佔用的所有時間還要長。
2)由於運行線程在鎖被爭用時會睡眠。所以僅僅能在進程上下文中才幹擷取訊號量鎖,由於中斷上下文中是不能進行調度的。
3)你能夠在持有訊號量時去睡眠。由於當其它進程試圖獲得同一訊號量時不會因此而死結(由於該進程也僅僅是去睡眠而已,終於會繼續啟動並執行)。
4)在你佔用訊號量的同一時候不能佔用自旋鎖。由於在你等待訊號量時可能會睡眠,而在持有自旋鎖時是不同意睡眠的。
5)訊號量同一時候同意隨意數量的鎖持有人,而自旋鎖在一個時刻最多同意一個任務持有它。
原因是訊號量有個計數值,比方計數值為5,表示同一時候能夠有5個線程訪問臨界區。
假設訊號量的初始值始1,這訊號量就是相互排斥訊號量(MUTEX)。對於大於1的非0值訊號量,也可稱為計數訊號量(counting semaphore)。
對於一般的驅動程式使用的訊號量都是相互排斥訊號量。
訊號量支援兩個原子操作:P/V原語操作(也有叫做down操作和up操作的):
P:假設訊號量值大於0,則遞減訊號量的值,程式繼續運行。否則。睡眠等待訊號量大於0。
V:遞增訊號量的值,假設遞增的訊號量的值大於0,則喚醒等待的進程。
down操作有兩個版本號碼,分別對於睡眠可中斷和睡眠不可中斷。
讀-寫訊號量
讀寫訊號量和訊號量之間的關係 與 讀寫自旋鎖和普通自旋鎖之間的關係 差點兒相同。
讀寫訊號量都是二值訊號量,即計數值最大為1,添加讀者時。計數器不變,添加寫者,計數器才減一。也就是說讀寫訊號量保護的臨界區,最多僅僅有一個寫者,但能夠有多個讀者。
全部讀-寫鎖的睡眠都不會被訊號打斷,所以它僅僅有一個版本號碼的down操作。
瞭解何時使用自旋鎖和訊號量對編寫優良代碼非常重要,可是多數情況下,並不須要太多考慮。由於在中斷上下文僅僅能使用自旋鎖,而在任務睡眠時僅僅能使用訊號量。
完畢變數 |
建議的加鎖方法 |
低開銷加鎖 |
優先使用自旋鎖 |
短期加鎖 |
優先使用自旋鎖 |
長期加鎖 |
優先使用訊號量 |
中斷上下文加鎖 |
使用自旋鎖 |
持有鎖須要睡眠 |
使用訊號量 |
完畢變數
假設在核心中一個任務須要發出訊號通知還有一任務發生了某個特定事件,利用完畢變數(completion variable)是使兩個任務得以同步的簡單方法。假設一個任務要運行一些工作時,還有一個任務就會在完畢變數上等待。
當這個任務完畢工作後,會使用完畢變數去喚醒在等待的任務。比如。當子進程運行或者退出時,vfork()系統調用使用完畢變數喚醒父進程。
Seq鎖(順序鎖)
這樣的鎖提供了一種非常easy的機制,用於讀寫共用資料。
實現這樣的鎖主要依靠一個序列計數器。當有疑義的資料被寫入時,會得到一個鎖。而且序列值會添加。
在讀取資料之前和之後,序號都被讀取。假設讀取的序號值同樣,說明在讀操作進行的過程中沒有被寫操作打斷過。
此外,假設讀取的值是偶數。那麼就表明寫操作沒有發生(要明確由於鎖的初值是0。所以寫鎖會使值成奇數,釋放的時候變成偶數)。
在多個讀者和少數寫者共用一把鎖的時候,seq鎖有助於提供一種很輕量級和具有可擴充性的外觀。可是 seq 鎖對寫者更有利,僅僅要沒有其它寫者,寫鎖總是可以被成功獲得。掛起的寫者會不斷地使得讀操作迴圈(前一個範例),直到不再有不論什麼寫者持有鎖為止。
禁止搶佔
因為核心是搶佔性的,核心中的進程在不論什麼時刻都可能停下來以便還有一個具有更高優先權的進程執行。這意味著一個任務與被搶佔的任務可能會在同一個臨界區內執行。
為了避免這樣的情況,核心搶佔代碼使用自旋鎖作(能夠防止多處理器機器上的真並發和核心搶佔)為非搶佔地區的標記。假設一個自旋鎖被持有,核心便不能進行搶佔。
實際中,某些情況(不須要模擬多處理器機器上的真並發。但須要防止核心搶佔)並不須要自旋鎖,可是仍然須要關閉核心搶佔。
為瞭解決問題。能夠通過 preempt_disable 禁止核心搶佔。這是一個能夠嵌套調用的函數。能夠調用隨意次。每次調用都必須有一個對應的 preempt_enable 調用。當最後一次 preempt_enable 被調用後,核心搶佔才又一次佔用。
順序和屏障
對於一段代碼。編譯器或者處理器在編譯和運行時可能會對運行順序進行一些最佳化。從而使得代碼的運行順序和我們寫的代碼有些差別。
普通情況下,這沒有什麼問題。可是在並發條件下,可能會出現取得的值與預期不一致的情況,比方以下的代碼:
/* * 線程A和線程B共用的變數 a和b * 初始值 a=1, b=2 */int a = 1, b = 2;/* * 如果線程A 中對 a和b的操作 */void Thread_A(){ a = 5; b = 4;}/* * 如果線程B 中對 a和b的操作 */void Thread_B(){ if (b == 4) printf("a = %d\n", a);}
因為編譯器或者處理器的最佳化。線程A中的賦值順序可能是b先賦值後,a才被賦值。
所以假設線程A中 b=4; 運行完,a=5; 還沒有啟動並執行時候,線程B開始運行,那麼線程B列印的是a的初始值1。
這就與我們預期的不一致了,我們預期的是a在b之前賦值,所以線程B要麼不列印內容,假設列印的話,a的值應該是5。
在某些並發情況下,為了保證代碼的運行順序。引入了一系列屏障方法來阻止編譯器和處理器的最佳化。
方法 |
描寫敘述 |
rmb() |
阻止跨越屏障的載入動作發生重排序 |
read_barrier_depends() |
阻止跨越屏障的具有資料依賴關係的載入動作重排序 |
wmb() |
阻止跨越屏障的儲存動作發生重排序 |
mb() |
阻止跨越屏障的載入和儲存動作又一次排序 |
smp_rmb() |
在SMP上提供rmb()功能,在UP上提供barrier()功能 |
smp_read_barrier_depends() |
在SMP上提供read_barrier_depends()功能。在UP上提供barrier()功能 |
smp_wmb() |
在SMP上提供wmb()功能,在UP上提供barrier()功能 |
smp_mb() |
在SMP上提供mb()功能,在UP上提供barrier()功能 |
barrier() |
阻止編譯器跨越屏障對載入或儲存操作進行最佳化 |
為了使得上面的小範例能正確運行,用上表中的函數改動線程A的函數就可以:
/* * 如果線程A 中對 a和b的操作 */void Thread_A(){ a = 5; mb(); /* * mb()保證在對b進行載入和儲存值(值就是4)的操作之前 * mb()代碼之前的所有載入和儲存值的操作所有完畢(即 a = 5;已經完畢) * 僅僅要保證a的賦值在b的賦值之前進行,那麼線程B的運行結果就和預期一樣 */ b = 4;}
總結:
來自http://www.cnblogs.com/wang_yb/archive/2013/05/01/3052865.html
參考:
http://www.cnblogs.com/wang_yb/archive/2013/05/01/3052865.html
http://www.cnblogs.com/pennant/archive/2012/12/28/2833383.html
Linux核心設計與實現
Linux核心設計與實現——核心同步