Linux核心是一個複雜的系統,它需要處理多種多樣的並發問題,如進程調度、記憶體管理、裝置驅動、網路協定等。為了確保資料的一致性和正確性,Linux核心提供了多種同步機制,如自旋鎖、信號量、讀寫鎖等。但是,這些同步機制都有一些缺點,例如:
那麼,有沒有一種更好的同步機制呢?答案是有的,那就是RCU(Read Copy Update)。 RCU是一種基於發布-訂閱模式的同步機制,它可以實現高效率的讀取操作和低延遲的更新操作。 RCU的基本思想是這樣的:
RCU有什麼優點呢?它有以下幾個特點:
那麼,RCU是如何運作的呢?它又有哪些應用場景呢?本文將從原理與應用兩個面向來介紹RCU這個高效率的同步機制。 RCU的設計想法比較明確,透過新舊指針替換的方式來實現免鎖方式的共享保護。但是具體到程式碼的層面,要理解多少還是會有些困難。在《深入Linux裝置驅動程式核心機制》第4章中,已經非常明確地敘述了RCU背後所遵循的規則,這些規則是從一個比較高的視角來看,因為我覺得過多的程式碼分析反而容易讓讀者在細節上迷失方向。最近拿到書後,我又重頭仔細看了RCU部分的文字,覺得還應該補充一點點內容,因為有些東西不一定適合寫在書裡。
RCU讀取側進入臨界區的標誌是呼叫rcu_read_lock,這個函數的程式碼是:
1. 2. static inline void rcu_read_lock(void) 3. { 4. __rcu_read_lock(); 5. __acquire(RCU); 6. rcu_read_acquire(); 7. }
此實作裡面貌似有三個函數調用,但實質性的工作由第一個函數__rcu_read_lock()來完成,__rcu_read_lock()透過呼叫 preempt_disable()關閉核心可搶佔性。但是中斷是允許的,假設讀取者正處於rcu臨界區中且剛讀取了一個共享資料區的指標p(但是還沒有存取p中的資料成員),發生了一個中斷,而該中斷處理例程ISR恰好需要修改p所指向的資料區,依照RCU的設計原則,ISR會新分配一個同樣大小的資料區new_p,再把舊資料區p中的資料拷貝到新資料區,接著是在new_p的基礎上做資料修改的工作(因為是在new_p空間中修改,所以不存在對p的並發訪問,因此說RCU是一種免鎖機制,原因就在這裡),ISR在把資料更新的工作完成後,將new_p賦值給p(p=new_p),最後它會再註冊一個回呼函數用以在適當的時候釋放老指標p。因此,只要對舊指標p上的所有引用都結束了,釋放p就不會有問題。當中斷處理例程做完這些工作返回後,被中斷的進程將依然存取到p空間上的數據,也就是舊數據,這樣的結果是RCU機制所允許的。 RCU規則對讀取者與寫入者之間因指標切換所造成的短暫的資源視圖不一致問題是允許的。
接下來關於RCU一個有趣的問題是:何時才能釋放舊指標。我看過很多書中對此的回答是:當系統中所有處理器上都發生了一次進程切換。這種程式化的回答常常讓剛接觸RCU機制的讀者感到一頭霧水,為什麼非要等所有處理器上都發生一次進程切換才可以調用回調函數釋放老指針呢?這其實是RCU的設計規則決定的: 所有對舊指標的引用只可能發生在rcu_read_lock與rcu_read_unlock所包含的臨界區中,而在這個臨界區中不可能發生行程切換,而一旦出了該臨界區就不應該再有任何形式的對老指標p的引用。很明顯,這個規則要求讀取者在臨界區中不能發生進程切換,因為一旦有進程切換,釋放老指針的回調函數就有可能被調用,從而導致老指針被釋放掉,當被切換掉的進程被重新調度運行時它就有可能引用到一個被釋放掉的記憶體空間。
現在我們看到為什麼rcu_read_lock只需要關閉核心可搶佔性就可以了,因為它使得即便在臨界區中發生了中斷,當前進程也不可能被切換除去。 核心開發者,確切地說,RCU的設計者所能做的只能到這個程度。接下來就是使用者的責任了,如果在rcu的臨界區中呼叫了一個函數,該函數可能睡眠,那麼RCU的設計規則就遭到了破壞,系統就會進入一種不穩定的狀態。
這再次說明,如果想使用一個東西,一定要搞清楚其內在的機制,像上面剛提到的那個例子,即便現在程序不出現問題,但是系統中留下的隱患如同一個定時炸彈,隨時可能被引爆,尤其是過了很久問題才突然爆發出來。絕大多數情形下,找到問題所花費的時間可能遠大於靜下心來仔細搞懂RCU的原理要多得多。
RCU中的讀取者相對rwlock的讀取者而言,自由度較高。因為RCU的讀取者在存取一個共享資源時,不需要考慮寫入者的感受,這不同於rwlock的寫入者,rwlock reader在讀取共享資源時需要確保沒有寫入者在操作該資源。 兩者之間的差異化源自RCU對共享資源在讀取者與寫入者之間進行了分離,而rwlock的讀取者和寫入者則至始至終只使用共享資源的一份拷貝。這也意味著RCU中的寫入者要承擔更多的責任,而且對同一共享資源進行更新的多個寫入者之間必須引入某種互斥機制,所以RCU屬於一種”免鎖機制”的說法僅限於讀取者與寫入者之間。所以我們看到:RCU機制應該用在有大量的讀取操作,而更新操作相對較少的情況。此時RCU可以大幅提升系統係能,因為RCU的讀取操作相對其他一些有鎖機製而言,在鎖上的開銷幾乎沒有。
實際使用中,共享的資源常常以鍊錶的形式存在,核心為RCU模式下的鍊錶操作實作了幾個介面函數,讀取者和使用者應該使用這些核心函數,例如list_add_tail_rcu, list_add_rcu,hlist_replace_rcu等等,具體的使用可以參考某些內核程式設計或裝置驅動程式方面的資料。
在釋放舊指標方面,Linux核心提供兩種方法供使用者使用,一個是呼叫call_rcu,另一個是呼叫synchronize_rcu。前者是一種非同步方式,call_rcu會將釋放舊指標的回呼函數放入結點中,然後將該結點加入到目前正在執行call_rcu的處理器的本機鍊錶中,在時脈中斷的softirq部分(RCU_SOFTIRQ ), rcu軟中斷處理函數rcu_process_callbacks會檢查當前處理器是否經歷了一個休眠期(quiescent,此處涉及內核進程調度等方面的內容),rcu的核心程式碼實現在確定係統中所有的處理器都經歷過了一個休眠期之後(意味著所有處理器上都發生了一次進程切換,因此老指針此時可以被安全釋放掉了),將調用call_rcu提供的回調函數。
synchronize_rcu的實作則利用了等待佇列,在它的實作過程中也會在call_rcu到目前處理器的本機鍊錶中加入一個結點,與call_rcu不同之處在於該結點中的回呼函數是wakeme_after_rcu,然後synchronize_rcu將在一個等待佇列中睡眠,直到系統中所有處理器都發生了一次進程切換,因而wakeme_after_rcu被rcu_process_callbacks所呼叫以喚醒睡眠的synchronize_rcu,被喚醒之後,synchronize_rcu知道它現在可以釋放老指針了。
所以我們看到,call_rcu返回後其註冊的回調函數可能還沒被調用,因而也就意味著舊指針還未被釋放,而synchronize_rcu返回後老指針肯定被釋放了。所以,是呼叫call_rcu還是synchronize_rcu,要視特定需求與當前上下文而定,例如中斷處理的上下文絕對不能使用 synchronize_rcu函數了。
本文介紹了RCU這個Linux核心中的高效同步機制,它是一種基於發布-訂閱模式的同步機制。我們從原理方面分析了RCU的基本想法、關鍵介面和實作細節,並且給出了相應的程式碼範例。我們也從應用方面介紹了RCU在鍊錶操作、定時器管理、軟中斷處理等場景中的使用方法,並且給出了對應的程式碼範例。透過本文的學習,我們可以掌握RCU的基本用法,並且能夠在實際開發中靈活地運用RCU來實現高效的同步需求。希望本文對你有幫助!
以上是詳解Linux核心中的RCU機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!