在上篇博文中筆者分析了關於完成量和互斥量的使用以及一些經典的問題,下面筆者將在本篇博文中重點分析有關RCU機制的相關內容以及介紹目前已被淘汰出內核的大內核鎖(BKL)。文章的最后對《大話Linux內核中鎖機制》系列博文進行了總結,並提出關於目前Linux內核中提供的鎖機制的一些基本使用觀點。
十、RCU機制
本節將討論另一種重要鎖機制:RCU鎖機制。首先我們從概念上理解下什么叫RCU,其中讀(Read):讀者不需要獲得任何鎖就可訪問RCU保護的臨界區;拷貝(Copy):寫者在訪問臨界區時,寫者“自己”將先拷貝一個臨界區副本,然后對副本進行修改;更新(Update):RCU機制將在在適當時機使用一個回調函數把指向原來臨界區的指針重新指向新的被修改的臨界區,鎖機制中的垃圾收集器負責回調函數的調用。總結即是讀-拷貝-更新。RCU的結構體定義如圖10.1所示。
圖10.1 RCU的結構體定義
可以看出它的結構體定義是很簡單的。只有一個用於串接鏈表的next指針和一個函數指針,這個函數指針即是上述提及的回調函數,這個需使用RCU機制的用戶向鏈表注冊,即掛接到鏈表下,從而在適當時機下得到調用。
關於RCU機制中的寫者,它是自己負責拷貝的臨界區,而不是由操作系統負責,最后由已注冊的回調函數實現回收。那么所謂的“適當的時機”具體是什么時候呢,這個時機是:所有引用該共享臨界區的CPU都退出對臨界區的操作。即沒有CPU再去操作這段臨界區后,這段臨界區即可回收了,此時回調函數即被調用。
上述討論了如此多RCU內容,可能讀者會問:RCU到底可以做些什么呢。RCU是一種機制,它允許讀寫並發執行,對於讀者來說,讀者間沒有任何同步開銷,因為可隨時讀取臨界區,和其他讀者沒有相互影響;但對於不同的寫者來說,它們之間如果存在同步開銷,則寫者間的同步開銷則取決於寫者間的采用同步機制,和RCU並沒有直接的關系。
正如RCU機制中所說寫者間可並行執行。但RCU並不維護鎖,因此對於不同的寫者來說若要訪問共享數據時,需要寫者和寫者之間“它們相互協商”維護所采用的鎖機制。這樣對RCU機制來說便實現了既允許多個讀者同時訪問被保護的臨界區,又允許多個讀者和多個寫者同時訪問被保護的臨界區。這里需要特別注意的是就是上述提及的是否可以有多個寫者並行訪問臨界區取決於寫者之間所使用的同步鎖機制。因此對於先前筆者討論的讀寫鎖,可發現RCU機制實際上是一種改進的讀寫鎖,但不能替代。因為RCU機制主要是針對指針類型數據的,而讀寫鎖卻非如此。特別的,當寫者的同步開銷比較大,也即寫操作比較多時,對讀者的性能提高不能彌補寫者導致的損失。
下面我們將看到一個例子,但在看這個例子之前,我們須有兩個概念:quiescent state(靜默狀態過程),它表示為CPU發生上下文切換的過程;grace period(即本節內容一開始提及的“適當時機”),它表示為所有CPU都經歷一次quiescent state所需要的等待的時間,也即系統中所有的讀者完成對共享臨界區的訪問。其中當一個進程在執行時,CPU的所有寄存器中的值、進程的狀態以及堆棧中的內容被稱為該進程的上下文。當內核需要切換到另一個進程時,它需要保存當前進程的所有狀態,也就是保存當前進程的上下文,以便再次執行該進程時,能夠得到進程切換時的狀態,從而使該進程能夠執行下去。至此,相信對上述兩個概念有了相對程度的理解了吧。
理解這兩個概念后我們來看下面的這個例子,如圖10.2所示。
圖10.2所展示的示例實現的是寫者要從鏈表中刪除元素B。要達到此目的,寫者首先遍歷該鏈表得到指向元素B的指針,然后修改元素B的前一個元素的next指針指向元素B的next指針指向的元素C,修改元素B的next指針指向的元素C的prep指針指向元素B的prep指針指向的元素A。在此期間可能有讀者訪問該鏈表,由於修改指針指向的操作是原子的,因此這個過程不需要同步,而元素B的指針並沒有去修改,因為讀者可能正在使用B元素來得到鏈表的下一個或前一個元素,即A或C。當寫者完成上述操作后便向系統注冊一個回調函數func以便在 grace period之后能夠刪除元素B,注冊完畢后寫着便可認為它已經完成刪除操作(實際上並未完成)。垃圾收集器在檢測到所有的CPU不在引用該鏈表后,即所有的CPU已經經歷了一次quiescent state(即grace period),當grace period完成后,系統便會去調用先前寫者注冊的回調函數func,從而真正的刪除了元素B。這便是RCU機制的一種使用范例。
經過上述的討論后,相信讀者對於RCU機制有了較好的理解。但是讀者很容易就會發現RCU采用時存在一些約束問題。首當其沖的便是在使用RCU時,對共享資源的訪問在大部分時間應該是只讀的,寫訪問應該相對較少,因為寫訪問多了必然相對於其他鎖機制而已更占系統資源,影響效率。其次是讀者在持有rcu_read_lock(RCU讀鎖定函數)的時候,不能發生進程上下文切換,否則,因為寫者需要等待讀者完成方可進行,則此時寫者進程也會一直被阻塞,影響系統的正常運行。再次寫者執行完畢后需要調用回調函數,此時發生上下文切換,當前進程進入睡眠,則系統將一直不能調用回調函數,更槽糕的是,此時其它進程若再去執行共享的臨界區,必然造成一定的錯誤。最后一點是受RCU機制保護的資源必須是通過指針訪問。因為從RCU機制上看,幾乎所有操作都是針對指針數據的。
下面筆者將討論RCU提供的操作函數以及它的實現實質,包括圖10.3和圖10.4。它們分別在文件中include\linux\rcupdate.h實現。
圖10.3展示的是RCU向讀者提供的函數,包括基本的讀寫鎖函數以及同步函數,其中同步函數最為重要,即synchronize_rcu()。讀者函數的實質其實很簡單:禁止搶占,也就是說在RCU期間不允許發生進程上下文切換,原因上述已提及,即是寫者需要等待讀者完成方可進行,則此時寫者進程也會一直被阻塞,影響系統的正常運行等,故而不允許在RCU期間發生進程上下文切換。下面給出RCU向寫者提供的函數,如圖10.4所示。
圖10.4 RCU機制的函數接口
關於寫者函數,主要就是call_rcu和call_rcu_bh兩個函數。其中call_rcu能實現的功能是它不會使寫者阻塞,因而它可在中斷上下文及軟中斷使用,該函數將函數func掛接到RCU的回調函數鏈表上,然后立即返回,讀者函數中提及的synchronize_rcu()函數在實現時也調用了該函數。而call_rcu_bh函數實現的功能幾乎與call_rcu完全相同,唯一的差別是它將軟中斷的完成當作經歷一個quiescent state(靜默狀態,本節一開始有提及這個概念), 因此若寫者使用了該函數,那么讀者需對應的使用rcu_read_lock_bh() 和rcu_read_unlock_bh()。
為什么這么說呢,這里筆者結合call_rcu_bh的源碼實現給出自己的看法:一個靜默狀態表示一次的進程上下文切換(上述提及),就是當前進程執行完畢並順利切換到下一個進程。將軟中斷的完成當作經歷一個靜默狀態是確保此時系統的軟中斷能夠順利的執行完畢,因為call_rcu_bh可在中斷上下文使用,而中斷上下文能打斷軟中斷的運行,故而當call_rcu_bh在中斷上下文中使用的時候,需確保軟中斷的能夠順利執行完畢。
對應於此時讀者需使用rcu_read_lock_bh() 和rcu_read_unlock_bh()函數的原因是由於call_rcu_bh函數不會使寫者阻塞,可在中斷上下文及軟中斷使用。這表明此時系統中的中斷和軟中斷並沒有被關閉。那么寫者在調用call_rcu_bh函數訪問臨界區時,RCU機制下的讀者也能訪問臨界區。此時對於讀者而言,它若是需要讀取臨界區的內容,它必須把軟中斷關閉,以免讀者在當前的進程上下文過程中被軟中斷打斷(上述內容提過軟中斷可以打斷當前的進程上下文)。而rcu_read_lock_bh() 和rcu_read_unlock_bh()函數的實質是調用local_bh_disable()和local_bh_enable()函數,顯然這是實現了禁止軟中斷和使能軟中斷的功能。
另外在Linux源碼中關於call_rcu_bh函數的注釋中還明確說明了如果當前的進程是在中斷上下文中,則需要執行rcu_read_lock()和rcu_read_unlock(),結合這兩個函數的實現實質表明它實際上禁止或使能內核的搶占調度,原因不言而喻,避免當前進程在執行讀寫過程中被其它進程搶占。同時內核注釋還表明call_rcu_bh這個接口函數的使用條件是在大部分的讀臨界區操作發生在軟中斷上下文中,原因還是需從它實現的功能出發,相信很容易理解,主要是要從執行效率方面考慮。
關於RCU的回調函數實現本質是:它主要是由兩個數據結構維護,包括rcu_data和rcu_bh_data數據結構,實現了掛接回調函數,從而使回調函數組成鏈表。回調函數的原則先注冊到鏈表的先執行。
下面筆者將討論RCU的鏈表操作內容,它在文件include\linux\rculist.h中定義。事實上,對於RCU而言,它的目標不僅是保護一般的指針,而且還保護雙向鏈表。實際上,這就是關於RCU鏈表操作出現的緣由。類似於內核中提供的標准鏈表操作的函數內容,RCU機制提供的函數也是有基於普通鏈表和哈希鏈表的。對於RCU鏈表操作,除了在遍歷鏈表,修改和刪除鏈表元素時,必須調用RCU機制的函數外,其他過程仍能使用標准鏈表函數,而標准的鏈表操作函數於文件include\linux\list.h.定義。事實上,關於RCU的鏈表操作函數,它們的實現機制大部分還是直接調用關於標准鏈表操作函數,少部分還增加了調用針對RCU鏈表操作機制的一些代碼。
下面來給出RCU鏈表操作提供的一些函數,這里只給出部分基本函數,其余函數可以去查具體的內核源碼或相關資料。如圖10.5,10.6所示,普通鏈表函數下依次給出相對應的哈希鏈表函數。
圖10.5 RCU鏈表操作函數
圖10.6 RCU鏈表操作函數
十一、BKL(大內核鎖)
最后來介紹下已經被淘汰出內核的大內核鎖,簡稱BKL,由於它已經被踢出內核,故這里並不打算作過多深入的討論,只是能有個了解,內核中曾經出現這么一個鎖。在較低版本的內核中這個鎖是存在的,只是不再建議使用。它可鎖定整個內核,確保沒有處理器在核心態並行運行。
大內核鎖從Linux 2.6.39 開始正式徹底踢出內核,未踢出前是在include\linux\smp_lock.h 文件中定義。主要提供了兩個函數,lock_kernel可鎖定整個內核,unlock_kernel對應解鎖。大內核鎖的一個特性是它的鎖深度會進行計數。這意味着在內核已經被鎖定時,仍然可以調用lock_kernel。當然對應的解鎖函數unlock_kernel也必須調用同樣的次數,以解鎖內核,使其它處理器能夠進入內核。整個過程簡言之就是能夠實現遞歸獲得鎖。
內核鎖本質上也是自旋鎖,但是它又不同於自旋鎖,不同點在於自旋鎖是不可以遞歸獲得鎖的(會導致死鎖),而大內核鎖則可以遞歸獲得鎖。大內核鎖作用是保護整個內核,而對應自旋鎖則用於保護非常特定的某一共享資源。由於一般情況下使用大內核鎖的時候保持該鎖的時間較長,導致了它嚴重影響系統的性能和可伸縮性,筆者認為這是它被踢出內核的重要原因之一。至此,關於大內核鎖的內容到此介紹完畢,同時,關於Linux內核中的鎖機制也介紹完畢,下面筆者將對上述所介紹的內容作一總結。
總結
最后筆者來總結一下先前討論過的所有內容,討論的內容包括原子操作;自旋鎖,內存屏障;讀寫自旋鎖,順序鎖;信號量,讀寫信號量,完成量;互斥量;RCU機制;BKL(大內核鎖)。
通過上述討論的一些內容,我們可以總結得到以下一些基本觀點:① 原子操作對整數操作,自旋鎖和信號量應用較為廣泛。② 當臨界區小應選擇自旋鎖,反之,則應選擇信號量。③ 關於信號量的選擇問題:信號量是針對進程級的,它在內核中以進程方式運行,故它一般的使用條件是當申請信號量的進程需占用資源較長時間時。④ 讀寫自旋鎖和讀寫信號量條件相對於自旋鎖和信號量來說放寬不少,這一點可從它們的定義得出。⑤ RCU機制的應用目前越來越廣。⑥ 內存屏障函數使用起來較為復雜,而且多數情況下需要和具體的體系結構相關,故而一般不建議使用。
文章的最后給出筆者在分析Linux內核鎖機制過程中參考的一些文獻資料,感興趣的讀者可以看下,不過事實上這些資料講的大部分內容還是較淺的,筆者建議若想搞懂內核中的鎖機制還是要去看、去分析內核源碼。
至此,關於《大話Linux內核中鎖機制》系列博文的內容到此結束。由於筆者水平所限,本系列博文中難免有出錯之處,歡迎讀者指出,大家相互討論,共同進步。
轉載請注明出處:http://blog.sina.com.cn/huangjiadong19880706
參考文獻
[1]、Robert Love,《Linux 內核設計與實現》第三版,機械工業出版社,2011年。
[2]、Wolfgang Mauerer,《深入Linux內核架構》,人民郵電出版社,2011年。
[3]、宋寶華,《Linux設備驅動開發詳解》第二版,人民郵電出版社,2011年。
[4]、李雲華,《獨辟蹊徑品內核:Linux內核源代碼導讀》,人民郵電出版社,2009年。
[5]、http://blog.chinaunix.net/space.php?uid=25845340&do=blog&id=3011577
[6]、http://www.ibm.com/developerworks/cn/linux/l-cn-spinlock/
[7]、http://www.ibm.com/developerworks/cn/linux/l-rcu/