RCU是linux系統的一種讀寫同步機制,說到底他也是一種內核同步的手段,本問就RCU概率和實現機制,給出筆者的理解。
【RCU概率】
我們先看下內核文檔中對RCU的定義:
RCU is a synchronization mechanism that was added to the Linux kernel during the 2.5 development effort that is optimized for read-mostly situations.
翻譯:RCU是在2.5版本內核引入的一種同步機制,目的在於優化數據讀取較多之場景下的效率。
說道讀讀多寫少的場景,我們能自然聯想到讀寫鎖,不錯,RCU正是和讀寫鎖相似的一種提高讀多寫少場景下代碼執行效率的機制,它的核心思想就是“訂閱發布”機制。
實際上,我們使用鎖來保護互斥資源,無非就是防止這兩種情況:
1)讀者在讀取數據時,寫者對數據同時進行改寫,導致讀者讀到不完整的數據
2)寫者在寫數據時,有另一寫者同時寫數據,導致數據被寫臟
由此我們很早久已經使用了各種鎖機制來保護互斥資源,而且針對讀多寫少的情況,我們還專門優化出讀寫鎖,使得在沒有寫者的情況下,多個讀者可以並行持鎖,從而可以並行讀取數據,提高效率。那么有沒有一種去鎖的辦法實現對互斥資源的保護呢?所以這里RCU機制就登場了。它的核心思想是:互斥數據采用指針來訪問,當寫者想要更新數據時,先將數據復制一份,對復制的數據進行修改,這樣可以不干擾同一時間正在讀取數據的讀者。當修改完畢后,通過指針賦值,將舊數據指針更新指向到新的數據。最后再完成對舊數據的釋放,釋放時需要等待正在使用之前舊數據的讀者退出臨界區,而等待的這段時間在RCU機制中被稱作“寬限期”。這里幾個重要的概念就是“寫時復制”、“指針賦值”、以及“寬限期”。它就像雜志訂閱和發布,讀者讀取數據就好比訂閱雜志,寫者
復制並修改數據好比雜志的編輯,最后通過指針賦值更新數據久好比雜志的發布,而寬限期等待就好比期刊的發布周期,所以這是一個形象的比喻。通過這種機制,我們可以實現讀者的去鎖,它有如下幾個特點:
1)讀者讀取數據不需要枷鎖,因為數據時通過指針賦值更新的,而現代CPU處理器基本都可以保證指針賦值的原子性,另外寫者保證在指針賦值前數據已經修改好,所以讀者讀到的數據始終是完整的,無需加鎖
2)寫者必須通過“寫時復制”和“指針賦值”的方式更新數據,而對舊數據釋放前需要等待數據更新前已經讀取了舊數據的讀者完成對舊數據的使用。
3)寫者和寫者直接仍然需要鎖來互斥同步,但由於RCU的使用場景時多讀寫少,所以開銷是可以接受的。
內核文檔明確指出了一個RCU數據更新的典型步驟:
a. Remove pointers to a data structure, so that subsequent
readers cannot gain a reference to it.
b. Wait for all previous readers to complete their RCU read-side
critical sections.
c. At this point, there cannot be any readers who hold references
to the data structure, so it now may safely be reclaimed
(e.g., kfree()d).
翻譯:
a. (通常是從鏈表中)移除指向數據結構(通常是鏈表節點)的指針, 使得后續讀者無法再(通過鏈表)引用這個數據
b. 等待移除數據之前已經讀取並正在使用該數據的讀者退出臨界區
c. 此時,已經沒有讀者在使用這個數據結構了,因此它可以被安全的回收
舉個例子,比如有如下這樣一個鏈表:
____ ____ ____
-->|__A_|-->|__B_|-->|__C_|-->...
現需要將B鏈表回收,那么:
a. 先將B節點從鏈表中移除,此后則不會再有讀者能訪問到B節點了,移除后情況如下:
____ ____ ____
-->|__A_|-->|__C_|-->... N-->|__C_|
其中“N”表示此時正在使用C節點的N個讀者,雖然C已經不在鏈表當中,但仍有讀者持有指向C的指針,所以暫時C的內存還不能回收
b. 等待所以正在使用C節點的讀者使用完畢,即退出臨界區,此時情況如下:
____ ____ ____
-->|__A_|-->|__C_|-->... 0-->|__C_|
“0”表示已經沒有讀者使用C節點了,因此可以安全回收
c. 銷毀C節點,回收內存:
____ ____
-->|__A_|-->|__C_|-->...
d. 如果不想刪除B,而只是想更新B的內容,那么此時便以安全的修改,修改完畢后果再將B節點以原子的方式插回隊列中,如下:
____ ____ ____
-->|__A_|-->|__B_|-->|__C_|-->...
那么,這里有幾個關鍵點沒有講清楚:
1. 如何知道當前有那些讀者進程正在使用C節點呢?
2. 讀者全部退出臨界區的時候,如果通知出來呢?
所以,內核要給我們提供API去完成這些事情,請繼續往下看。
【RCU的核心API】
內核文檔列出了如下幾個核心API函數:
a. rcu_read_lock()
b. rcu_read_unlock()
c. synchronize_rcu() / call_rcu()
d. rcu_assign_pointer()
e. rcu_dereference()
就是說這5個API時最基本的,還有其他一些API,但是都可以通過這5個API的組合來實現,下面一一講解:
a. void rcu_read_lock(void);
翻譯:用於通知回收者當前讀者已進入臨界區,在讀者的臨界區里時不允許阻塞的。
b. void rcu_read_unlock(void);
用於通知回收者當前讀者已經退出臨界區。
c. void synchronize_rcu(void);
synchronize_rcu用於等待在synchronize_rcu調用之前通過rcu_read_lock進入臨界區的讀者(在synchronize_rcu調用之后進入臨界區的並不關心),在此之前函數會一直阻塞,當返回時,舊數據可以被安全的釋放。
內核文檔還給了一個例子,自己體會:
CPU 0 CPU 1 CPU 2
----------------- ------------------------- ---------------
1. rcu_read_lock()
2. enters synchronize_rcu()
3. rcu_read_lock()
4. rcu_read_unlock()
5. exits synchronize_rcu()
6. rcu_read_unlock()
d.typeof(p) rcu_assign_pointer(p, typeof(p) v);
這是一個宏實現,也只能是宏,自己體會下(提示:typeof。。。)
引用一段內核文檔原話:The updater uses this function to assign a new value to an RCU-protected pointer, in order to safely communicate the change in value from the updater to the reader. This function returns the new value, and also executes any memory-barrier instructions required for a given CPU architecture.
這個函數就是用來完成前面提到的“指針賦值”的動作的,它會處理一些內存屏障的情況,否則我們直接賦值就是了,何必用這個宏呢?
e. typeof(p) rcu_dereference(p);
同樣時通過宏實現的, 內核文檔的解釋:
The reader uses rcu_dereference() to fetch an RCU-protected pointer, which returns a value that may then be safely dereferenced. Note that rcu_deference() does not actually dereference the pointer, instead, it protects the pointer for later dereferencing. It also executes any needed memory-barrier instructions for a given CPU architecture.
這段話比較難懂,但說白了就是,當你想獲取一個指向某個RCU數據時,rcu_dereference能返回一個安全的引用。 這里dereference是個很有意思的詞,大家可以查下reference和dereference的區別,很好玩。
【總結】
理解RCU機制的關鍵點就是如何去理解“訂閱發布”,確實如此,我們在APP商店購買應用的時候,用戶得到的都是一個完整可用的APK,即最終產品的樣子,而應用的開發過程是不會讓用戶看到的。作者要更新軟件時,會線下修改,改好之后推送更新,即發布。同理,RCU機制在更新數據時,先將數據從鏈表中移除(類似商品下架),然后等待正在使用該數據的讀者使用完畢,這段時間我們叫“寬限期”(類似以下架應用仍然繼續提供客服,但會有一個期限),等寬限期過后,便修改跟新,然后重新插回鏈表中(類似應用重新上架)。這是一個非常巧妙的設計,需要花些時間去理解,但是一旦理解, 就很容易掌握這些概念了,甚至不需要任何記憶。