在使用共享記憶體的應用程式中,必須特別留意保護共享資源,防止共享資源併發訪問。核心也不例外。共享資源要防止併發訪問,是因為如果多個執行執行緒同時訪問和運算元據,有可能發生各執行緒之間相互覆蓋共享資料的情況,造成被訪問資料處於不一致狀態。併發訪問共享資料是造成系統不穩定的一類隱患,而且這種錯誤一般難以跟蹤和除錯——所以首先應該認識到這個問題的重要性。
要做到對共享資源的恰當保護很困難。在linux未支援smp時,避免併發訪問資料的方法相對來說簡單。在單一處理器時,只有在中斷發生時,或在核心**請求重新排程、執行另乙個任務時,資料才可能被併發訪問。
從2.0版本開始,核心開始支援smp,而且對它的支援不斷地加強和完善。支援多處理器意味著核心**可以同時執行在兩個或更多的處理器上。如果不加保護,執行在兩個不同的處理器上的核心**完全可能在同乙個時刻併發訪問共享資料。2.6版本核心的出現,linux核心已發展成為搶占式核心,這意味著排程程式可以在任何時刻搶占正在執行的核心**,重新排程其他的程序執行。現在,核心**中有不少部分都能夠同步執行,而且它們都必須保護起來。
9.1 臨界區和競爭條件
1、為什麼需要保護
為了認清同步的必要性,首先要明白臨界區無處不在。
2、單個變數
9.2 加鎖
假設需要處理乙個佇列上的所有請求。假定該佇列是通過鍊錶實現,鍊錶中的每個結點代表乙個請求。有兩個函式可以用來操作此佇列:乙個函式將新請求新增到佇列尾部,另乙個函式從佇列頭刪除請求,然後處理它。核心各個部分都會呼叫這兩個函式,所以核心會不斷地在佇列中加入請求,從佇列中刪除和處理請求。對請求佇列的操作要用到多條指令。如果乙個執行緒試圖讀取佇列,而這時正好另乙個執行緒正在處理該佇列,那麼讀取執行緒會發現佇列此刻正處於不一致狀態。如果允許併發訪問佇列,就會產生危害。當共享資源是乙個複雜的資料結構時,競爭條件會使該資料結構遭到破壞。
需要一種方法確保一次有且只有乙個執行緒對資料結構進行操作,或者當另乙個執行緒在對臨界區標記時,就禁止其他訪問。鎖提供這種機制。
如上請求佇列,可使用乙個單獨的鎖進行保護。每當有乙個新請求要加入佇列,執行緒會首先站住鎖,然後可以安全地將請求加入到佇列中,結束操作後再釋放該鎖;同樣當乙個執行緒從請求佇列中刪除乙個請求時,也需要先佔住鎖,然後才能從佇列中讀取和刪除請求,操作完成必須釋放鎖。要訪問佇列的任何其他執行緒,必須獲得鎖後才能進行操作。因為乙個時刻只能有乙個執行緒持有鎖,所以在乙個時刻只有乙個執行緒可以操作佇列。如果乙個執行緒正在更新佇列時,出現另乙個執行緒,那麼第二個執行緒必須等待第乙個執行緒釋放鎖,它才能繼續進行。由此可見,鎖機制可防止併發執行,且保護佇列不受競爭條件影響。
任何要訪問佇列的**首先都需要獲得相應的鎖,這樣該鎖就能阻止別的執行執行緒的併發訪問:
備註:鎖的使用是自願的、非強制的,它屬於一種程式設計者自選的程式設計手段。沒有什麼可以強制程式設計者在操作虛構的佇列時必須使用鎖。如果不這麼做,會造成競爭條件而破壞佇列。
鎖的形式多種多樣,加鎖的粒度範圍各不相同——linux自身實現了幾種不同的鎖機制。各種鎖機制之間的區別主要在於:當鎖被其它執行緒持有,不可用時的行為表現——一些鎖被爭用時會簡單地執行忙等待,而另外一些鎖會使當前任務睡眠直到鎖變得可用為止。
鎖是把臨界區縮小到加鎖和解鎖之間的**,但仍然有潛在的競爭!鎖是採用原子操作實現的,而原子操作不存在競爭。
1、造成併發執行的原因
使用者空間同步,是因為使用者程式會被排程程式搶占和重新排程。使用者程序可能在任何時刻被搶占,而排程程式完全可能選擇另乙個高優先順序的程序到處理器上執行,所以會使得乙個程式正處在臨界區時,被非自願搶占了。如果新排程的程序隨後也進入同乙個臨界區,前後兩個程序之間會產生競爭。另外,因為訊號處理是非同步發生的,所以,即使是單執行緒的多個程序共享檔案,或者在乙個程式內部處理訊號,也可能產生競爭條件。這種型別的併發操作——其實兩者並不真是同時發生,但它們相互交叉進行,也可稱作偽併發執行。
如果有支援smp的機器,那麼兩個程序可以真正地在臨界區中同時執行了,這是真併發。雖然真併發和偽併發的原因和含有不同,但都會造成競爭條件,而且也需要同樣的保護。
核心中有類似可能造成併發執行的原因:
中斷——中斷幾乎可以在任何時刻非同步發生,也就可能隨時打斷當前正在執行的**。
軟中斷和tasklet——核心能在任何時刻喚醒或排程軟中斷和tasklet,打斷當前正在執行的**。
核心搶占——因為核心具有搶占性,所以核心中的任務可能會被另一任務搶占。
睡眠及與使用者空間的同步——在核心中執行的程序可能會睡眠,會喚醒排程程式,導致排程乙個新的使用者程序執行。
smp——兩個或多個處理器可以同時執行**。
核心開發者必須理解併發執行的原因,並且事先做足準備工作。如果在一段核心**操作某資源時系統產生乙個中斷,而且該中斷的處理程式還要訪問這一資源,這是乙個bug;如果一段核心**在訪問乙個共享資源期間可被搶占,這也是乙個bug。注意:兩個處理器絕對不能同時訪問同一共享資料。當清楚什麼樣的資料需要被保護時,提供鎖來保護系統穩定也就不難做到。真正的困難是發現上述的潛在併發執行的可能,並有意識地採取某些措施來防止併發執行。
在編寫**的開始階段就要設計恰當的鎖。
在中斷處理程式中能避免併發訪問的安全**稱作中斷安全**,在smp的機器中能避免併發訪問的安全**稱為smp安全**,在核心搶占時能避免併發訪問的安全**稱為搶占安全**。
2、了解要保護些什麼
找出哪些資料需要保護是關鍵。什麼資料需要加鎖?如果有其他執行執行緒可以訪問這些資料,那麼就給這些資料加上某種形式的鎖;如果任何其他什麼東西都能看到它,那麼就要鎖住它。注意:要給資料而不是**加鎖。
編寫核心**時,要問自己如下這些問題:
這個資料是不是全域性的?除了當前執行緒外,其他執行緒能不能訪問它?
這個資料會不會在程序上下文和中斷上下文中共享?是不是要在兩個不同的中斷處理程式中共享?
程序在訪問資料時可不可能被搶占?被排程的新程式會不會訪問同一資料?
當前程序是不是會睡眠在某些資源上,如果是,它會讓共享資料處於何種狀態?
怎樣防止資料失控?
如果這個函式又在另乙個處理器上被排程將會發生什麼?
如何確保**遠離併發威脅?
總結:幾乎訪問所有的核心全域性變數和共享資料都需要某種形式的同步方法。
9.3 死鎖
產生死鎖需要一定條件:要有乙個或多個執行執行緒和乙個或多個資源,每個執行緒都在等待其中的乙個資源,但所有的資源都已經被占用了。所有執行緒都在相互等待,但它們永遠不會釋放已經占有的資源。於是任何執行緒都無法繼續,這就意味著產生死鎖。
最簡單的死鎖例子是自死鎖:如果乙個執行執行緒試圖去獲得乙個自己已經持有的鎖,它將不得不等待鎖被釋放,但因為它正在忙著等待這個鎖,所以自己永遠不會有機會釋放鎖,最終結果就是死鎖。
同樣道理,有n個執行緒和n個鎖,如果每個執行緒都持有一把其他程序需要得到的鎖,那麼所有的執行緒都將阻塞地等待它們希望得到的鎖重新可用。例如有兩個執行緒和兩把鎖,通常被叫做abba死鎖。
每個執行緒都在等待其他執行緒持有的鎖,但是沒有乙個執行緒會釋放一開始持有的鎖,所以沒有任何鎖會在釋放後被其他執行緒使用。
預防死鎖的發生非常重要,雖很難證明**不會發生死鎖,但可以寫出避免死鎖的**,一些簡單的規則對避免死鎖有幫助:
按順序加鎖。使用巢狀的鎖時必須保證以相同的順序獲取鎖,這樣可以阻止致命擁抱型別的死鎖。
防止發生飢餓。
不要重複請求同乙個鎖。
設計應力求簡單——越複雜的加鎖方案越有可能造成死鎖。
如果有兩個或多個鎖曾在同一時間裡被請求,那麼以後其他函式請求它們也必須按照前次的加鎖順序進行。
備註:儘管釋放鎖的順序和死鎖無關,但最好還是以獲得鎖的相反順序來釋放鎖。
核心同步介紹
1.臨界區和競爭條件 所謂臨界區 critical region 就是訪問和操作共享資料的 段。為了避免在臨界區中併發訪問,必須保證這些 是原子地執行,即 在執行結束前不可被打斷,就如同整個臨界區是乙個不可分割的指令一樣。如果兩個執行執行緒有可能處於同乙個臨界區中,稱為競爭條件 race condi...
作業系統筆記 第9章 同步
合作的執行緒 執行緒之間對共享資源協同合作,程序 執行緒 計算機 裝置需要合作。程式可以呼叫函式fork 建立乙個新程序 假設兩個程序併發執行 同步互斥產生的背景 不確定性要求並行程式的正確性 race condition 競態條件 atomic operation 原子操作 實際上作業系統的操作往...
第九章 核心同步介紹
1.隨著2.6版核心的出現,linux核心已經發展成搶占式核心,如果不加保護,排程程式可以在任何時刻搶占正在執行的核心程式碼,重新排程其他的程序執行 2.臨界區或者臨界段 訪問和操作共享資料的程式碼段 3.如果兩個執行執行緒 指代的是任何正在執行的程式碼,如乙個在核心執行程序 乙個中斷處理程式或者核...