一些避免競爭條件的例項

2021-06-16 22:47:50 字數 3323 閱讀 1224

人們總是期望核心開發者確定和解決由核心控制路徑的交錯執行所引起的同步問題。但是,避免競爭條件是一項艱鉅的任務,因為這需要對核心的各個成分如何相互作用有乙個清楚的理解。為了直觀地認識核心內部到底是什麼樣子,需要提及前面博文中所定義同步技術的幾種典型應用場景。

1 引用計數器

引用計數器廣泛地用在核心中以避免由於資源的併發分配和釋放而產生的競爭條件。引用計數器(reference counter)只不過是乙個atomic_t計數器,與特定的資源,如記憶體頁、模組或檔案相關。當核心控制路徑開始使用資源時就原子地增加計數器的值,當核心控制路徑使用完資源時就原子地減少計數器。當引用計數器變為0時,說明該資源未被使用,如果必要,就釋放該資源。

2 大核心鎖

在早期的linux核心版本中,大核心鎖(big kernel block,也叫全域性核心鎖或bkl)被廣泛使用。在2.0版本中,這個鎖是相對粗粒度的自旋鎖,確保每次只有乙個程序能執行在核心態。2.2和2.4核心具有極大的靈活性,不再依賴乙個單獨的自旋鎖,而是由許多不同的自旋鎖保護大量的核心資料結構。在linux 2.6版本的核心中,用大核心鎖來裸護舊的**(絕大多數主要是與vfs和幾個檔案系統相關的函式)。

從核心版本2.6.11開始,用乙個叫做kernel_sem的訊號量來實現大核心鎖(在較早的2.6版本中,大核心鎖是通過自旋鎖來實現的)。但是,大核心鎖比簡單的訊號量要複雜一些。

每個程序描述符task_struct都含有lock_depth欄位,其就是乙個整形變數,這個字段允許同乙個程序幾次獲取大核心鎖。因此,對大核心鎖兩次連續的請求不掛起處理器(相對於普通自旋鎖)。如果程序未獲的過鎖,則這個欄位的值為-1;否則,這個欄位的值加1,表示已經請求了多少次鎖。lock_depth欄位對中斷處理程式、異常處理程式及可延遲函式獲取大核心鎖都是至關重要的。如果沒有這個字段,那麼,在當前程序已經擁有大核心鎖的情況下,任何試圖獲得這個鎖的非同步函式都可能產生死鎖。

lock_kernel ()和unlock_kernel()核心函式用來獲得和釋放大核心鎖。前乙個函式等價於:

depth = current->lock_depth + 1;

if (depth == 0)

down(&kernel_sem);

current->lock_depth = depth;

而後者等價於

if (--current->lock_depth < 0)

up(&kernel_sem);

注意,lock_kernel()和unlock_kernel()函式的if語句不需要原子地執行,因為lock_depth不是全域性變數——這是每個cpu在自己當前程序描述符中訪問的乙個字段。在if語句確本地中斷也不會引起競爭條件。即使新核心控制路徑呼叫了lock_kernel(),它在終止前也必須釋放大核心鎖。

足以令人吃驚的是,允許乙個持有大核心鎖的程序呼叫schedule(),從而放棄cpu!不過,schedule()函式檢查被替換程序的lock_depth欄位,如果它的值是0或者正數,就自動釋放kernel_sem訊號量(參見「schedule()函式」博文)。因此,不會有顯式呼叫schedule()的程序在程序切換前後都保持大核心鎖。但是,當schedule()函式再次選擇這個程序來執行的時候,將為該程序重新獲得大核心鎖。

然而,如果乙個持有大核心鎖的程序被另乙個程序搶占,情況就有所不同了。一直到核心版本2.6.10還沒有出現這種情況,因為獲取自旋鎖時會自動禁用核心搶占。但是,現在大核心鎖的實現是基於訊號量的,而且不會由於獲得它而自動禁用核心搶占。實際上,在被大核心鎖保護的臨界區內允許核心搶占是改變大核心鎖實現的主要原因。其次,這對於系統的響應時間會產生有益的影響。

當乙個持有大核心鎖的程序被搶占時,schedule()一定不能釋放訊號量,因為在臨界區內執行**的程序沒有主動觸發程序切換。所以,如果釋放大核心鎖,那麼另外乙個程序就可能獲得它,並破壞由被搶占的程序所訪問的資料結構。

為了避免被搶占的程序失去大核心鎖,preempt_schedule_irq()臨時把程序的lock_depth欄位設定為-1。觀察這個欄位的值,schedule()假定被替換的程序不擁有kernel_sem訊號量,也就不釋放它。結果,被搶占的程序就一直擁有kernel_seen訊號量。一旦這個程序再次被排程程式選中,preempt_schedule_irq()函式就恢復lock_depth欄位原來的值,並讓程序在被大核心鎖保護的臨界區中繼續執行。

3 記憶體描述符讀/寫訊號量

mm_struct型別的每個記憶體描述符在mmap_sem欄位中都包含了自己的訊號量。由於幾個輕量級程序之間可以共享乙個記憶體描述符,因此,訊號量保護這個描述符以避免可能產生的競爭條件。

例如,讓我們假設核心必須為某個程序建立或擴充套件乙個記憶體區。為了做到這一點,核心呼叫do_mmap()函式分配乙個新的vm_area_struct資料結構。在分配的過程中,如果沒有可用的空閒記憶體,而共享同一記憶體描述符的另外乙個程序可能在執行,那麼當前程序可能被掛起。如果沒有訊號量,那麼需要訪問記憶體描述符的第二個程序的任何操作(例如,由於寫時複製而產生的缺頁)都可能會導致嚴重的資料崩潰。

這種訊號量是作為讀/寫訊號量來實現的,因為一些核心函式,如缺頁異常處理程式只需要掃瞄記憶體描述符。

4 slab快取記憶體鍊錶的訊號量

slab快取記憶體描述符鍊錶是通過cache_chain_sem訊號量保護的,這個訊號量允許互斥地訪問和修改該鍊錶。

當kmem_cache_create()在鍊錶中增加乙個新元素,而kmem_cache_shrink()和kmem_cache_reap()順序地掃瞄整個鍊錶時,可能產生競爭條件。然而,在處理中斷時,這些函式從不被呼叫,在訪問鍊錶時它們也從不阻塞。由於核心是支援搶占的,因此這種訊號量在多處理器系統和單處理器系統中都會起作用。

5 索引節點的訊號量

我們以後將會提到,linux把磁碟檔案的資訊存放在一種叫做索引節點(inode)的記憶體物件中。相應的資料結構也包括有自己的訊號量,存放在i_sem欄位中。

在檔案系統的處理過程中會出現很多競爭條件。實際上,磁碟上的每個檔案都是所有使用者共有的一種資源,因為所有程序都(可能)會訪問檔案的內容、修改檔名或檔案位置、刪除或複製檔案等等。例如,讓我們假設乙個程序在顯示某個目錄所包含的檔案。由於每個磁碟操作都可能會阻塞,因此即使在單處理器系統中,當第乙個程序正在執行顯示操作的過程中,其他程序也可能訪問同一目錄並修改它的內容。或者,兩個不同的程序可能同時修改同一目錄。所有這些競爭條件都可以通過用索引節點訊號量保護目錄檔案來避免。

只要乙個程式使用了兩個或多個訊號量,就存在死鎖的可能,因為兩個不同的控制路徑可能互相死等著釋放訊號量。一般來說,linux在訊號量請求上很少會發生死鎖問題,因為每個核心控制路徑通常一次只需要獲得乙個訊號量。然而,在有些情況下,核心必須獲得兩個或更多的訊號量鎖。索引節點訊號量傾向於這種情況,例如,在rename()系統呼叫的服務例程中就會發生這種情況。在這種情況下,操作涉及兩個不同的索引節點,因此,必須採用兩個訊號量。為了避免這樣的死鎖,

訊號量的請求按預先確定的位址順序進行。

linux中避免競爭條件的途徑

在linux中提供了一些機制用來避免競爭條件,當乙個臨界區的資料在多個函式之間被呼叫時,為了保護資料不被破壞,可以採用一定的機制來保護臨界區的資料,主要有自旋鎖spinlock 訊號量,互斥鎖。第一種 首先說自旋鎖spinlock 在linux中定義spinlock的方法很簡單,與普通的結構體定義方...

優化的一些例項

優化使用的工具,使用loadrunner做為壓力測試工具,使用jprobe進行 剖析。1 第乙個例項。原狀況 呼叫乙個api,發現執行的時間很高,用jprobe分析,發現消耗時間最長的是把快取中的乙個樹從第三個節點進行扁平化,就是把第二個節點的子樹構造為乙個列表,不知道為什麼構造這個資料的耗時比直接...

一些jQuery 例項

設定內容 text html 以及 val changehtml click function 新增新的 html 內容 向 html 元素追加內容 在 html 元素之後追加內容。before after html click function jquery 操作 css 改變 html 元素的 ...