RCU補丁稽核清單¶
本文件包含用於生成和稽核使用RCU的補丁的清單。違反下面列出的任何規則都將導致與省略鎖定原語相同的問題。此列表基於相當長一段時間內稽核此類補丁的經驗,但始終歡迎改進!
RCU是否應用於以讀取為主的情況?如果資料結構的更新頻率超過大約10%,那麼您應該強烈考慮其他方法,除非詳細的效能測量表明RCU仍然是正確的工具。是的,RCU確實透過增加寫入端開銷來降低讀取端開銷,這正是RCU的正常使用會進行比更新更多的讀取的原因。
另一個例外是效能不是問題,並且RCU提供更簡單的實現。Linux 2.6核心中的動態NMI程式碼就是這種情況的一個例子,至少在NMI很少見的架構上是這樣。
還有一個例外是RCU讀取端原語的低即時延遲至關重要。
最後一個例外是RCU讀取器用於防止無鎖更新的ABA問題(https://en.wikipedia.org/wiki/ABA_problem)。這確實導致了一種輕微的反直覺的情況,即
rcu_read_lock()和rcu_read_unlock()用於保護更新,但是,這種方法可以為某些型別的無鎖演算法提供與垃圾收集器相同的簡化。更新程式碼是否具有適當的互斥?
RCU允許讀取器(幾乎)裸奔,但寫入器仍然必須使用某種互斥,例如
鎖定,
原子操作,或
將更新限制為單個任務。
如果您選擇#b,請準備好描述您如何在弱序機器上處理了記憶體屏障(幾乎所有機器--甚至x86允許稍後的載入重新排序以先於較早的儲存),並準備好解釋為什麼這種增加的複雜性是值得的。 如果您選擇#c,請準備好解釋此單個任務如何不會成為大型系統上的主要瓶頸(例如,如果任務正在更新與其他任務可以讀取的自身相關的資訊,則根據定義可能沒有瓶頸)。 請注意,“大型”的定義已發生重大變化:八個CPU在2000年是“大型”,但一百個CPU在2017年並不出色。
RCU讀取端臨界區是否正確使用
rcu_read_lock()及其朋友?需要這些原語來防止寬限期過早結束,這可能導致資料從您的讀取端程式碼中被不體面地釋放出來,這會大大增加您的核心的精算風險。作為粗略的經驗法則,對受RCU保護的指標的任何解引用都必須由
rcu_read_lock()、rcu_read_lock_bh()、rcu_read_lock_sched()或相應的更新端鎖覆蓋。顯式停用搶佔(例如,preempt_disable())可以充當rcu_read_lock_sched(),但可讀性較差,並且會阻止lockdep檢測鎖定問題。獲取自旋鎖也會進入RCU讀取端臨界區。請注意,您不能依賴於已知僅在非搶佔核心中構建的程式碼。這樣的程式碼會並且將會崩潰,尤其是在使用CONFIG_PREEMPT_COUNT=y構建的核心中。
讓受RCU保護的指標“洩漏”出RCU讀取端臨界區與讓他們從鎖下洩漏出來一樣糟糕。當然,除非您在讓他們離開RCU讀取端臨界區之前安排了一些其他的保護手段,例如鎖或引用計數。
更新程式碼是否容忍併發訪問?
RCU的全部意義在於允許讀者在沒有任何鎖或原子操作的情況下執行。這意味著讀者將在更新進行時執行。根據情況,有許多方法可以處理這種併發
使用列表和hlist更新原語的RCU變體,以在受RCU保護的列表上新增、刪除和替換元素。或者,使用已新增到Linux核心的其他受RCU保護的資料結構。
這幾乎總是最好的方法。
按照上面的(a)進行,但也維護每個元素的鎖(由讀者和寫入器獲取),以保護每個元素的狀態。如果需要,讀者避免訪問的欄位可以由僅由更新程式獲取的其他鎖保護。
這也運作得很好。
使更新對讀者來說顯得是原子的。例如,對正確對齊的欄位的指標更新將顯得是原子的,單個原子原語也是如此。在鎖下執行的操作序列對RCU讀者來說不會顯得是原子的,多個原子原語的序列也不會。一種替代方法是將多個單獨的欄位移動到單獨的結構中,從而透過施加額外的間接級別來解決多欄位問題。
這可以工作,但開始變得有點棘手。
仔細訂購更新和讀取,以便讀者在更新的所有階段都能看到有效資料。這通常比聽起來更困難,尤其是考慮到現代CPU重新排序記憶體引用的趨勢。通常必須在程式碼中大量新增記憶體排序操作,使其難以理解和測試。如果它有效,最好使用諸如smp_store_release()和smp_load_acquire()之類的東西,但在某些情況下,需要smp_mb()完全記憶體屏障。
如前所述,通常最好將更改的資料分組到單獨的結構中,以便可以透過更新指標以引用包含更新值的新結構來使更改顯得是原子的。
弱序CPU提出了特殊的挑戰。幾乎所有CPU都是弱序的--甚至x86 CPU也允許稍後的載入重新排序以先於較早的儲存。RCU程式碼必須採取以下所有措施來防止記憶體損壞問題
讀者必須保持其記憶體訪問的正確順序。
rcu_dereference()原語確保CPU在獲取指標指向的資料之前先獲取指標。這在Alpha CPU上確實是必要的。rcu_dereference()原語也是一個極好的文件輔助工具,讓閱讀程式碼的人確切地知道哪些指標受到RCU的保護。請注意,編譯器也可以重新排序程式碼,並且它們越來越積極地這樣做。rcu_dereference()原語因此也防止了破壞性的編譯器最佳化。但是,透過一些狡猾的創造力,有可能錯誤地處理rcu_dereference()的返回值。有關更多資訊,請參見正確處理來自 rcu_dereference() 的返回值。rcu_dereference()原語被各種“_rcu()”列表遍歷原語使用,例如list_for_each_entry_rcu()。請注意,更新端程式碼使用rcu_dereference()和“_rcu()”列表遍歷原語是完全合法的(如果冗餘)。這在讀者和更新者通用的程式碼中特別有用。但是,如果您在RCU讀取端臨界區之外訪問rcu_dereference(),lockdep會抱怨。請參閱RCU和lockdep檢查,以瞭解如何處理此問題。當然,無論是
rcu_dereference()還是“_rcu()”列表遍歷原語都不能替代協調多個更新程式的良好併發設計。如果正在使用列表宏,則必須使用
list_add_tail_rcu()和list_add_rcu()原語,以防止弱序機器錯誤地排序結構初始化和指標植入。類似地,如果正在使用hlist宏,則需要hlist_add_head_rcu()原語。如果正在使用列表宏,則必須使用
list_del_rcu()原語,以防止list_del()的指標中毒對併發讀者造成毒性影響。類似地,如果正在使用hlist宏,則需要hlist_del_rcu()原語。可以使用
list_replace_rcu()和hlist_replace_rcu()原語,以在其各自型別的受RCU保護的列表中用新結構替換舊結構。類似於(4b)和(4c)的規則適用於“hlist_nulls”型別的受RCU保護的連結列表。
更新必須確保在釋出指向給定結構的指標之前,發生給定結構的初始化。在釋出指向可以透過RCU讀取端臨界區遍歷的結構的指標時,請使用
rcu_assign_pointer()原語。
如果使用了
call_rcu()、call_srcu()、call_rcu_tasks()或call_rcu_tasks_trace()中的任何一個,則可以從softirq上下文呼叫回撥函式,並且在任何情況下都會停用下半部。特別是,此回撥函式不能阻塞。如果需要回調阻塞,請在該回調中排程的workqueue處理程式中執行該程式碼。在call_rcu()的情況下,queue_rcu_work()函式為您執行此操作。由於
synchronize_rcu()可以阻塞,因此不能從任何型別的irq上下文中呼叫它。相同的規則適用於synchronize_srcu()、synchronize_rcu_expedited()、synchronize_srcu_expedited()、synchronize_rcu_tasks()、synchronize_rcu_tasks_rude()和synchronize_rcu_tasks_trace()。這些原語的加速形式與非加速形式具有相同的語義,但加速形式的CPU密集度更高。加速原語的使用應限制為罕見的配置更改操作,這些操作通常不會在即時工作負載執行時進行。請注意,對IPI敏感的即時工作負載可以使用rcupdate.rcu_normal核心啟動引數來完全停用加速寬限期,但這可能會產生效能影響。
特別是,如果您發現自己在迴圈中重複呼叫其中一個加速原語,請幫大家一個忙:重構您的程式碼,使其批次更新,從而允許單個非加速原語覆蓋整個批次。這很可能比包含加速原語的迴圈更快,並且對系統的其餘部分(尤其是對系統其餘部分上執行的即時工作負載)更容易。或者,改為使用非同步原語,例如
call_rcu()。從v4.20開始,給定的核心僅實現一種RCU風格,對於PREEMPTION=n,該風格為RCU-sched,對於PREEMPTION=y,該風格為RCU-preempt。如果更新程式使用
call_rcu()或synchronize_rcu(),則相應的讀者可以使用:(1)rcu_read_lock()和rcu_read_unlock(),(2)任何停用和重新啟用softirq的原語對,例如,rcu_read_lock_bh()和rcu_read_unlock_bh(),或者(3)任何停用和重新啟用搶佔的原語對,例如,rcu_read_lock_sched()和rcu_read_unlock_sched()。如果更新程式使用synchronize_srcu()或call_srcu(),則相應的讀者必須使用srcu_read_lock()和srcu_read_unlock(),並且使用相同的srcu_struct。加速RCU寬限期等待原語的規則與其非加速原語相同。類似地,正確使用RCU Tasks風格是必要的
如果更新程式使用
synchronize_rcu_tasks()或call_rcu_tasks(),則讀者必須避免執行自願上下文切換,即避免阻塞。如果更新程式使用
call_rcu_tasks_trace()或synchronize_rcu_tasks_trace(),則相應的讀者必須使用rcu_read_lock_trace()和rcu_read_unlock_trace()。如果更新程式使用
synchronize_rcu_tasks_rude(),則相應的讀者必須使用任何停用搶佔的工具,例如,preempt_disable()和preempt_enable()。
將各種內容混合在一起會導致混亂和損壞的核心,甚至會導致可利用的安全問題。因此,當使用不明顯的原語對時,當然必須進行註釋。一個不明顯的配對的例子是網路中的XDP功能,該功能從網路驅動程式NAPI(softirq)上下文呼叫BPF程式。BPF嚴重依賴RCU保護其資料結構,但是由於BPF程式呼叫完全發生在NAPI輪詢週期中的單個local_bh_disable()部分中,因此此用法是安全的。此用法安全的原因是,當更新程式使用
call_rcu()或synchronize_rcu()時,讀者可以使用任何停用BH的工具。雖然
synchronize_rcu()比call_rcu()慢,但通常會導致更簡單的程式碼。因此,除非更新效能至關重要,否則更新程式無法阻塞,或者從使用者空間可見synchronize_rcu()的延遲,否則應優先使用synchronize_rcu()而不是call_rcu()。此外,kfree_rcu()和kvfree_rcu()通常比不使用synchronize_rcu()的synchronize_rcu()導致更簡單的程式碼,而沒有synchronize_rcu()的多毫秒延遲。因此,請在適用的情況下利用kfree_rcu()和kvfree_rcu()的“發射後不管”的記憶體釋放功能。synchronize_rcu()原語的一個尤其重要的屬性是它能自動限制自身:如果寬限期由於任何原因被延遲,synchronize_rcu()原語會相應地延遲更新。相反,使用call_rcu()的程式碼應該顯式地限制更新速率,以防寬限期被延遲,因為不這樣做會導致過多的即時延遲,甚至 OOM(記憶體不足)情況。使用
call_rcu()、kfree_rcu()或 kvfree_rcu() 時,獲得這種自我限制屬性的方法包括:保持由 RCU 保護的資料結構所使用的資料結構元素數量的計數,包括那些等待寬限期結束的元素。強制限制此數量,根據需要暫停更新,以允許先前延遲的釋放完成。或者,僅限制等待延遲釋放的數量,而不是元素的總數。
暫停更新的一種方法是獲取更新端的互斥鎖。(不要嘗試使用自旋鎖 -- 其他 CPU 在該鎖上自旋可能會阻止寬限期結束。)另一種暫停更新的方法是讓更新使用記憶體分配器的包裝函式,以便此包裝函式在等待 RCU 寬限期的記憶體過多時模擬 OOM。當然,還有許多其他變體。
限制更新速率。例如,如果更新每小時僅發生一次,則不需要顯式速率限制,除非您的系統已經嚴重損壞。dcache 子系統的舊版本採用此方法,使用全域性鎖保護更新,限制其速率。
可信更新 -- 如果更新只能由超級使用者或其他可信使用者手動完成,則可能不需要自動限制它們。這裡的理論是,超級使用者已經有很多方法來使機器崩潰。
定期呼叫
rcu_barrier(),允許每個寬限期進行有限數量的更新。
同樣的注意事項適用於
call_srcu()、call_rcu_tasks()和call_rcu_tasks_trace()。這就是為什麼分別存在srcu_barrier()、rcu_barrier_tasks()和rcu_barrier_tasks_trace()的原因。請注意,儘管這些原語確實採取措施避免在任何給定 CPU 具有過多回調時耗盡記憶體,但有決心的使用者或管理員仍然可以耗盡記憶體。如果具有大量 CPU 的系統配置為將其所有 RCU 回撥解除安裝到單個 CPU 上,或者系統擁有的空閒記憶體相對較少,則尤其如此。
所有 RCU 列表遍歷原語,包括
rcu_dereference()、list_for_each_entry_rcu()和 list_for_each_safe_rcu(),必須位於 RCU 讀取側臨界區內,或者必須受到適當的更新側鎖的保護。RCU 讀取側臨界區由rcu_read_lock()和rcu_read_unlock()或類似的基元(如rcu_read_lock_bh()和rcu_read_unlock_bh())分隔,在這種情況下,必須使用匹配的rcu_dereference()原語才能使 lockdep 正常工作,在這種情況下,即rcu_dereference_bh()。當持有更新側鎖時允許使用 RCU 列表遍歷原語的原因是,這樣做對於減少讀者和更新者之間共享公共程式碼時的程式碼膨脹非常有幫助。為此情況提供了額外的原語,如 RCU 和 lockdep 檢查 中所述。
此規則的一個例外是,當資料僅新增到連結的資料結構中,並且在讀者可能正在訪問該結構的任何時間段內從未刪除時。在這種情況下,可以使用 READ_ONCE() 代替
rcu_dereference(),並且可以省略讀取側標記(例如,rcu_read_lock()和rcu_read_unlock())。相反,如果您位於 RCU 讀取側臨界區中,並且您沒有持有適當的更新側鎖,則必須使用列表宏的“_rcu()”變體。否則會破壞 Alpha,導致激進的編譯器生成錯誤的程式碼,並使試圖理解您的程式碼的人感到困惑。
RCU 回撥獲取的任何鎖都必須在其他地方停用軟中斷的情況下獲取,例如,透過 spin_lock_bh()。如果在給定獲取該鎖時未停用軟中斷,則一旦 RCU 軟中斷處理程式在中斷該獲取的臨界區時恰好執行您的 RCU 回撥,就會導致死鎖。
RCU 回撥可以並行執行,並且確實如此。在許多情況下,回撥程式碼只是
kfree()的包裝器,因此這不是問題(或者,更準確地說,在某種程度上這是個問題,記憶體分配器鎖定可以處理它)。但是,如果回撥確實操作共享資料結構,則它們必須使用所需的任何鎖定或其他同步來安全地訪問和/或修改該資料結構。不要假設 RCU 回撥將在執行相應
call_rcu()、call_srcu()、call_rcu_tasks()或call_rcu_tasks_trace()的同一 CPU 上執行。例如,如果給定 CPU 在具有掛起的 RCU 回撥時離線,則該 RCU 回撥將在某些倖存的 CPU 上執行。(如果不是這種情況,自我生成的 RCU 回撥將阻止受害 CPU 永遠離線。)此外,由 rcu_nocbs= 指定的 CPU 很可能始終在其他 CPU 上執行其 RCU 回撥,事實上,對於某些即時工作負載,這是使用 rcu_nocbs= 核心啟動引數的全部意義。此外,不要假設以給定順序排隊的回撥將按該順序呼叫,即使它們都在同一 CPU 上排隊。此外,不要假設同一 CPU 的回撥將按順序呼叫。例如,在最近的核心中,可以在解除安裝和取消解除安裝的回撥呼叫之間切換 CPU,並且當給定 CPU 正在進行此類切換時,其回撥可能會由該 CPU 的軟中斷處理程式和該 CPU 的 rcuo kthread 併發呼叫。在這些時候,該 CPU 的回撥可能會併發和亂序執行。
與大多數 RCU 風格不同,允許在 SRCU 讀取側臨界區(由
srcu_read_lock()和srcu_read_unlock()標示)中阻塞,因此得名“SRCU”:“可睡眠 RCU”。請注意,如果您不需要在讀取側臨界區中睡眠,則應使用 RCU 而不是 SRCU,因為 RCU 幾乎總是比 SRCU 更快且更易於使用。此外,與其他形式的 RCU 不同,需要透過 DEFINE_SRCU() 或 DEFINE_STATIC_SRCU() 在構建時或透過
init_srcu_struct()和cleanup_srcu_struct()在執行時進行顯式初始化和清理。後兩個函式傳遞一個“struct srcu_struct”,它定義給定 SRCU 域的範圍。初始化後,srcu_struct 會傳遞給srcu_read_lock()、srcu_read_unlock()、synchronize_srcu()、synchronize_srcu_expedited()和call_srcu()。給定的synchronize_srcu()僅等待受傳遞了相同 srcu_struct 的srcu_read_lock()和srcu_read_unlock()呼叫所控制的 SRCU 讀取側臨界區。此屬性使可睡眠的讀取側臨界區可以容忍 -- 給定的子系統僅延遲其自身的更新,而不是延遲使用 SRCU 的其他子系統的更新。因此,如果允許 RCU 的讀取側臨界區睡眠,則 SRCU 不像 RCU 那樣容易使系統 OOM。在讀取側臨界區中睡眠的能力不是免費的。首先,對應的
srcu_read_lock()和srcu_read_unlock()呼叫必須傳遞相同的 srcu_struct。其次,寬限期檢測開銷僅在共享給定 srcu_struct 的那些更新中攤銷,而不是像其他形式的 RCU 那樣全域性攤銷。因此,只有在極度讀取密集的情況下,或者在需要 SRCU 的讀取側死鎖免疫或低讀取側即時延遲的情況下,才應使用 SRCU 而不是 rw_semaphore。當您需要輕量級讀取器時,還應考慮 percpu_rw_semaphore。SRCU 的加速原語 (
synchronize_srcu_expedited()) 永遠不會將 IPI 傳送到其他 CPU,因此它比synchronize_rcu_expedited()對即時工作負載更友好。允許在 RCU Tasks Trace 讀取側臨界區中睡眠,該臨界區由
rcu_read_lock_trace()和rcu_read_unlock_trace()分隔。但是,這是一種特殊的 RCU 風格,您應該在使用它之前與當前的使用者確認。在大多數情況下,您應該改用 SRCU。請注意,
rcu_assign_pointer()與 SRCU 的關係與其他形式的 RCU 相同,但您應該使用srcu_dereference()而不是rcu_dereference(),以避免 lockdep splats。call_rcu()、synchronize_rcu()及其相關函式存在的全部意義是等待所有預先存在的讀取者完成,然後再執行某些破壞性操作。因此,至關重要的是首先刪除讀取者可以遵循的任何可能受到破壞性操作影響的路徑,然後呼叫call_rcu()、synchronize_rcu()或相關函式。因為這些原語僅等待預先存在的讀取者,所以呼叫者有責任保證任何後續讀取者都會安全地執行。
各種 RCU 讀取側原語不一定包含記憶體屏障。因此,您應該計劃 CPU 和編譯器自由地將程式碼重新排序到 RCU 讀取側臨界區內和外。 RCU 更新側原語負責處理此問題。
對於 SRCU 讀取者,您可以在
srcu_read_unlock()之後立即使用smp_mb__after_srcu_read_unlock()來獲取完整的屏障。使用 CONFIG_PROVE_LOCKING、CONFIG_DEBUG_OBJECTS_RCU_HEAD 和 __rcu sparse 檢查來驗證您的 RCU 程式碼。這些可以幫助您發現以下問題:
- CONFIG_PROVE_LOCKING
檢查對 RCU 保護的資料結構的訪問是否在適當的 RCU 讀取側臨界區下執行,同時持有正確的鎖組合,或者滿足任何其他適當的條件。
- CONFIG_DEBUG_OBJECTS_RCU_HEAD
檢查您是否在自從上次將同一物件傳遞給
call_rcu()(或相關函式) 以來,RCU 寬限期尚未結束之前,將同一物件傳遞給call_rcu()(或相關函式)。- CONFIG_RCU_STRICT_GRACE_PERIOD
與 KASAN 結合使用,以檢查從 RCU 讀取側臨界區洩露的指標。此 Kconfig 選項對效能和可伸縮性都有影響,因此僅限於四 CPU 系統。
- __rcu sparse 檢查
使用 __rcu 標記指向 RCU 保護的資料結構的指標,如果您在沒有使用
rcu_dereference()的某個變體的情況下訪問該指標,則 sparse 將發出警告。
這些除錯輔助工具可以幫助您發現原本極難發現的問題。
如果您將模組中定義的回撥函式傳遞給
call_rcu()、call_srcu()、call_rcu_tasks()或call_rcu_tasks_trace()之一,則必須等待所有掛起的回撥被呼叫,然後才能解除安裝該模組。請注意,僅僅等待寬限期絕對不夠!例如,不能保證synchronize_rcu()實現會等待透過call_rcu()在其他 CPU 上註冊的回撥。或者甚至在當前 CPU 上,如果該 CPU 最近離線又重新聯機。相反,您需要使用以下屏障函式之一:
但是,這些屏障函式絕對不保證等待寬限期。例如,如果系統中沒有任何地方排隊
call_rcu()回撥,rcu_barrier()可以並且將會立即返回。因此,如果您需要等待寬限期和所有預先存在的回撥,則需要呼叫這兩個函式,該對取決於 RCU 的風格:
無論是
synchronize_rcu()還是synchronize_rcu_expedited(),以及rcu_barrier()無論是
synchronize_srcu()還是synchronize_srcu_expedited(),以及srcu_barrier()synchronize_tasks_trace() 和
rcu_barrier_tasks_trace()
如有必要,您可以使用類似於工作佇列的內容來併發執行所需的一對函式。
有關更多資訊,請參閱RCU和可解除安裝模組。