什么是RCU?是Read,Copy-Update的縮寫,意指讀-復制更新。是一種同步機制。其將同步開銷的非對稱分布發揮到邏輯極限,
RCU 基本概念
-
讀側臨界區 (read-side critical sections): RCU讀者執行的區域,每一個臨界區開始於
rcu_read_lock()
,結束於rcu_read_unlock()
,可能包含rcu_dereference()
等訪問RCU保護的數據結構的函數。這些指針函數實現了依賴順序加載的概念,稱為memory_order_consume
加載。 -
寫側臨界區:為適應讀側臨界區,寫側推遲銷毀並維護多個版本的數據結構,有大量的同步開銷。此外,編寫者必須使用某種同步機制(例如鎖定)來提供有序的更新。
-
靜默態(quiescent state): 當一個線程沒有運行在讀側臨界區時,其就處在靜默狀態。持續相當長一段時間的靜默狀態稱之為延長的靜默態(extended quiescent state)。
-
寬限期(Grace period): 寬限期是指所有線程都至少A一次進入靜默態的時間。寬限期前所有在讀側臨界區的讀者在寬限區后都會結束。不同的寬限期可能有部分或全部重疊。
讀者在讀臨界區遍歷RCU數據。如果寫者從此數據中移除一個元素,需要等待一個寬限期后才能執行回收內存操作。上述操作的示意圖如下圖所示,其中,標有read
的框框為一個讀臨界區。
上圖中,每一個讀者、更新者表示一個獨立的線程,總共4個讀線程,一個寫線程。
RCU更新操作分為兩個階段:移除階段和回收階段。兩個階段通過寬限期隔開。更新者在移除元素后,通過synchronize_rcu()
原語,初始化一個寬限期,並等待寬限期結束后,回收移除的元素。
- 移除階段:RCU更新通過
rcu_assign_pointer()
等函數移除或插入元素。現代CPU的指針操作都是原子的,
rcu_assign_pointer()
原語在大多數系統上編譯為一個簡單的指針賦值操作。移除的元素僅可被移除階段(以灰色顯示)前的讀者訪問。 - 回收階段:一個寬限期后, 寬限期開始前的原有讀者都完成讀操作,因此,此階段可安全釋放由刪除階段刪除的元素。
一個寬限期可以用於多個刪除階段,即可由多個更新程序執行更新。
此外,跟蹤RCU寬限期的開銷可能會均攤到現有流程調度上,因此開銷較小。對於某些常見的工作負載,寬限期跟蹤開銷可以被多個RCU更新操作均攤,從而使每個RCU更新的平均開銷接近零。
臨界區指的是一個訪問共用資源(例如:共用設備或是共用存儲器)的程序片段,而這些共用資源又無法同時被多個線程訪問的特性。當有線程進入臨界區段時,其他線程或是進程必須等待(例如:bounded waiting 等待法),有一些同步的機制必須在臨界區段的進入點與離開點實現,以確保這些共用資源是被互斥獲得使用,例如:semaphore。只能被單一線程訪問的設備,例如:打印機。
讀者提供一個信號告訴寫者什么時候可以安全執行銷毀操作,但是這個信號可能是被延時的,允許多個讀側臨界區使用一個信號。RCU通常通過一個非原子地增加本地計數器告訴寫者,這種操作開銷非常小。另外,RCU中沒有指定寫者是否並行。
RCU 的關鍵思想有兩個:1)復制后更新;2)延遲回收內存。典型的RCU更新時序如下:
- 復制:將需要更新的數據復制到新內存地址;
- 更新:更新復制數據,這時候操作的新的內存地址;
- 替換:使用新內存地址指針替換舊數據內存地址指針,此后舊數據將無法被后續讀者訪問;
- 等待,所有訪問舊數據的讀者進入靜默期,即訪問舊數據完成;
- 回收:當沒有任何持有舊數據結構引用的讀者后,安全地回收舊數據內存。
可見,RCU 首先將需要修改的內容復制出一份副本,然后在副本上進行修改操作。在寫者進行修改操作的過程中,舊數據沒有做任何更新,不會產生讀寫競爭,因此依然可以被讀者並行訪問。當寫者修改完成后,寫者直接將新數據內存地址替換掉舊數據的內存地址,由於內存地址替換操作是原子的,因此可以保證讀寫不會產生沖突。內存地址替換后,原有讀者訪問舊數據,新的讀者將訪問新數據。當原有讀者訪問完舊數據,進入靜默期后,舊數據將被寫者刪除回收。當然,通常寫者只進行更新、刪除指針操作,舊數據內存的回收由另一個線程完成。
下面,以雙向鏈表為例,說明使用 RCU 更新鏈表中的數據的過程,如下圖所示:
- 復制更新:復制舊數據到新數據,並在新數據進行修改操作;
- 替換,延時回收:將新數據替換掉鏈表中的舊數據,當無讀者訪問舊數據時,就行內存回收。
使用RCU注意如下事項:
- RCU適用多讀少寫場景。RCU和讀寫鎖相似.但RCU的讀者占鎖沒有任何的系統開銷。寫者與寫者之間必須要保持同步,且寫者必須要等它之前的讀者全部都退出之后才能釋放之前的資源。
- RCU保護的是指針.這一點尤其重要.因為指針賦值是一條單指令.也就是說是一個原子操作.因它更改指針指向沒必要考慮它的同步.只需要考慮cache的影響;
- 讀者是嵌套。也就是說rcu_read_lock()可以嵌套調用;
- 讀者在持有rcu_read_lock()的時候,不能發生進程上下文切換.否則,因為寫者需要要等待讀者完成,寫者進程也會一直被阻塞.
- 因為在非搶占場景中上下文切換不能發生在RCU的讀側臨界區,所以已阻塞的任何線程必須在RCU讀側臨界區之前完成。
- 任何沒有跑在RCU讀側臨界區的線程不能持有任何RCU受保護的引用
- 阻塞的線程不能持有受保護的RCU數據結構
- 線程不能引用已經刪除的的受保護的RCU數據結構
- 阻塞的線程在受保護的RCU指針被移除后,不能再引用
- 從RCU保護的數據結構中刪除給定元素后,一旦觀察到所有線程處於阻塞狀態,則RCU讀側臨界區中的任何線程都無法持有該元素的引用
核心API
核心的RCU API非常的少,如下:
rcu_read_lock()
rcu_read_unlock()
synchronize_rcu()/call_rcu()
rcu_assign_pointer()
rcu_dereference()
RCU API還有許多其他成員,但是其余的可以用這五種來表示。
rcu_read_lock()
void rcu_read_lock(void);
讀者讀取受RCU保護的數據結構時使用,通知回收者讀者進入了RCU的讀端臨界區。在RCU讀端臨界區訪問的任何受RCU保護的數據結構都會保證在臨界區期間保持未回收狀態。另外,引用計數可以與RCU一起使用,以維護對數據結構的長期引用。在RCU讀側臨界區阻塞是非法的。在Linux普通的TREE RCU實現中,rcu_read_lock的實現非常簡單,是關閉搶占:
static inline void __rcu_read_lock(void)
{
preempt_disable();
}
rcu_read_unlock()
void rcu_read_unlock(void);
讀者結束讀取后使用,用於通知回收者其退出了讀端臨界區。RCU的讀端臨界區可能被嵌套或重疊。Linux普通的TREE RCU實現中,rcu_read_unlock 的實現是開發搶占。
static inline void __rcu_read_unlock(void)
{
preempt_enable();
}
synchronize_rcu()
void synchronize_rcu(void);
synchronize_rcu 函數的關鍵思想是等待。確保讀者完成對舊結構體的操作后釋放舊結構體。synchronize_rcu 的調用點標志着“更新者代碼的結束”和“回收者代碼的開始”。它通過阻塞來做到這一點,直到所有cpu上所有預先存在的RCU讀端臨界區都完成。
需要注意的是,synchronize_rcu()
只需要等待調用它之前的讀端臨界區完成,不需要等待調用它之后開始的讀取者完成。另外,synchronize_rcu()不一定在最后一個預先存在的RCU讀端臨界區完成之后立即返回。具體實現中可能會有延時調度。同時,為了提高效率,許多RCU實現請求批量處理,這可能會進一步延遲 synchronize_rcu() 的返回。
call_rcu()
call_rcu()
API是syncnize_rcu()
的回調形式,它注冊而不是阻塞,而是注冊一個函數和自變量,這些函數和自變量在所有正在進行的RCU讀取側關鍵部分均已完成之后被調用。 在禁止非法訪問或更新端性能要求比較高時,此回調變體特別有用。
但是,不應輕易使用call_rcu()
API,因為對syncnize_rcu()
API的使用通常會使代碼更簡單。
此外,synchronize_rcu()
API具有不錯的屬性,可以在寬限期被延遲時自動限制更新速率。
面對拒絕服務攻擊,此屬性導致系統具有彈性。 使用call_rcu()的代碼應限制更新速率,以獲得相同的彈性。 有關限制更新速率的一些方法,請參見checklist.txt。
在上面的例子中,foo_update_a()阻塞直到一個寬限期結束。這很簡單,但在某些情況下,人們不能等這么久——可能還有其他高優先級的工作要做。
在這種情況下,使用call_rcu()
而不是synchronize_rcu()
。call_rcu()
API如下:
void call_rcu(struct rcu_head * head, void (*func)(struct rcu_head *head));
此函數在寬限期過后調用func(heda)
。此調用可能發生在softirq或進程上下文中,因此不允許阻止該函數。foo結構需要添加一個rcu-head結構,可能如下所示:
struct foo {
int a;
char b;
long c;
struct rcu_head rcu;
};
foo_update_a()
函數示例如下:
/*
* Create a new struct foo that is the same as the one currently
* * pointed to by gbl_foo, except that field "a" is replaced
* * with "new_a". Points gbl_foo to the new structure, and
* * frees up the old structure after a grace period. *
* Uses rcu_assign_pointer() to ensure that concurrent readers
* * see the initialized version of the new structure.
* * Uses call_rcu() to ensure that any readers that might have
* * references to the old structure complete before freeing the * old structure.
* */
void foo_update_a(int new_a) {
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
spin_lock(&foo_mutex);
old_fp = rcu_dereference_protected(gbl_foo, lockdep_is_held(&foo_mutex));
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp);
spin_unlock(&foo_mutex);
call_rcu(&old_fp->rcu, foo_reclaim);
}
// The foo_reclaim() function might appear as follows:
void foo_reclaim(struct rcu_head *rp) {
struct foo *fp = container_of(rp, struct foo, rcu);
foo_cleanup(fp->a);
kfree(fp);
}
container_of()
原語是一個宏,給定指向結構的指針,結構的類型以及結構內的指向字段,該宏將返回指向結構開頭的指針。
使用 call_rcu()
可使 foo_update_a()
的調用方立即重新獲得控制權,而不必擔心新近更新的元素的舊版本。 它還清楚地顯示了更新程序 foo_update_a()
和回收程序 foo_reclai()
之間的RCU區別。
總結:
- 在從受RCU保護的數據結構中刪除數據元素之后,請使用
call_rcu()
-以注冊一個回調函數,該函數將在所有可能引用該數據項的RCU讀取側完成后調用。 - 如果
call_rcu()
的回調除了在結構上調用kfree()
之外沒有做其他事情,則可以使用kfree_rcu()
代替call_rcu()
來避免編寫自己的回調:kfree_rcu(old_fp,rcu)
rcu_assign_pointer()
原型: void rcu_assign_pointer(p, typeof(p) v);
rcu_assign_pointer()
通過宏實現。將新指針賦給RCU結構體,賦值前的讀者看到的還是舊的指針。
更新者使用這個函數為受rcu保護的指針分配一個新值,以便安全地將更新的值更改傳遞給讀者。
此宏不計算rvalue,但它執行某CPU體系結構所需的內存屏障指令。保證內存屏障前的指令一定會先於內存屏障后的指令被執行。
它用於記錄(1)哪些指針受RCU保護以及(2)給定結構可供其他CPU訪問的點。
rcu_assign_pointer()
最常通過_rcu列表操作原語(例如list_add_rcu()
)間接使用。
rcu_dereference()
原型: typeof(p) rcu_dereference(p);
與rcu_assign_pointer()
類似,rcu_dereference()
也必須通過宏實現。
讀者通過rcu_dereference()
獲取受保護的RCU指針,該指針返回一個可以安全解除引用的值。
請注意,rcu_dereference()
實際上並未取消對指針的引用,相反,它保護指針供以后取消引用。
它還針對給定的CPU體系結構執行任何所需的內存屏障指令。 當前,只有Alpha CPU架構才需要rcu_dereference()
中的內存屏障-在其他CPU上,它編譯為無內容,甚至編譯器指令也沒有。
常見的編碼實踐是使用rcu_dereference()
將一個受rcu保護的指針復制到一個局部變量,然后解引用這個局部變量,例如:
p = rcu_dereference(head.next);
return p->data;
然而,上述情況可以整合成如下一句:
return rcu_dereference(head.next)->data;
如果您要從受rcu保護的結構中獲取多個字段,那么使用局部變量當然是首選的。重復的rcu_dereference()調用看起來很糟糕,不能保證在關鍵部分發生更新時返回相同的指針,並且會在Alpha cpu上產生不必要的開銷。
注意,rcu_dereference()
返回的值僅在封閉的RCU讀端臨界區[1]內有效。
例如,以下內容是不合法的:
rcu_read_lock();
p = rcu_dereference(head.next);
rcu_read_unlock();
x = p->address; /* BUG!!! */
rcu_read_lock();
y = p->data; /* BUG!!! */
rcu_read_unlock();
將一個RCU讀臨界區獲得的引用保留到另一個是非法的;同事,將一個鎖定的臨界區的引用放在另一個中使用也是非法的。
與rcu_assign_pointer()
一樣,rcu_dereference()
的重要功能是記錄哪些指針受RCU保護,尤其是標記一個隨時可能更改的指針,包括緊隨rcu_dereference()
之后。
通常通過_rcu列表操作基元(例如list_for_each_entry_rcu()
)間接使用rcu_dereference()
。
變量rcu_dereference_protected()可以在RCU讀取臨界區外使用,只要使用情況受到更新者代碼獲取的鎖的保護即可。
下圖展示了不同角色之間的通信。
rcu_assign_pointer()
+--------+
+---------------------->| 讀者 |---------+
| +--------+ |
| | |
| | | Protect:
| | | rcu_read_lock()
| | | rcu_read_unlock()
| rcu_dereference() | |
+---------+ | |
| 更新者 |<----------------+ |
+---------+ V
| +-----------+
+----------------------------------->| 回收者 |
+-----------+
推遲、等待:
synchronize_rcu() & call_rcu()
RCU基礎結構會觀察rcu_read_lock(),rcu_read_unlock(),synchronize_rcu() 和call_rcu() 調用的時間順序,以確定何時(1)syncnize_rcu()調用何時可以返回,以及(2)call_rcu() 回調可以被調用。
RCU基礎結構的有效實現大量使用批處理,以便在相應API的許多使用上分攤其開銷。
在Linux內核中至少有三種RCU用法。上圖顯示了最常見的一種。在更新端,rcu_assign_pointer()、sychronize_rcu()和call_rcu()
這三種基本類型使用的原語是相同的。但是為了保護(在讀端),使用的原語根據不同的口味而有所不同:
a.
rcu_read_lock() / rcu_read_unlock()
rcu_dereference()
b.
rcu_read_lock_bh() / rcu_read_unlock_bh()
local_bh_disable() / local_bh_enable()
rcu_dereference_bh()
c.
rcu_read_lock_sched() / rcu_read_unlock_sched()
preempt_disable() / preempt_enable()
local_irq_save() / local_irq_restore()
hardirq enter / hardirq exit
NMI enter / NMI exit
rcu_dereference_sched()
上述三種類型的使用方法如下:
- a. RCU應用於普通的數據結構。
- b. RCU應用於可能遭受遠程拒絕服務攻擊的網絡數據結構。
- c. RCU應用於調度器和中斷/ nmi處理器任務。
同樣,大多數用途是(a)。 (b)和(c)情況對於專門用途很重要,但相對較少見。
核心API使用示例
本節展示如何簡單使用核心RCU API來保護指向動態分配結構的全局指針。
更多的典型用法在 listRCU.txt , arrayRCU.txt , NMI-RCU.txt中被使用。
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex); // 定義spin鎖
struct foo __rcu *gbl_foo; // 聲明一個受保護的指針
/*
* Create a new struct foo that is the same as the one currently
* pointed to by gbl_foo, except that field "a" is replaced
* with "new_a". Points gbl_foo to the new structure, and
* frees up the old structure after a grace period.
*
* Uses rcu_assign_pointer() to ensure that concurrent readers
* see the initialized version of the new structure.
*
* Uses synchronize_rcu() to ensure that any readers that might
* have references to the old structure complete before freeing
* the old structure.
*/
void foo_update_a(int new_a)
{
struct foo *new_fp;
struct foo *old_fp;
new_fp = kmalloc(sizeof(*new_fp), GFP_KERNEL);
spin_lock(&foo_mutex); // 更新操作上鎖
old_fp = rcu_dereference_protected(gbl_foo, lockdep_is_held(&foo_mutex));
*new_fp = *old_fp;
new_fp->a = new_a;
rcu_assign_pointer(gbl_foo, new_fp); // 確保並行的讀者看到新結構的舊版本
spin_unlock(&foo_mutex);
synchronize_rcu(); // 確保所有引用舊的數據結構的讀者在釋放舊數據結構之前都已經完成操作,退出了臨界區
kfree(old_fp);
}
/*
* Return the value of field "a" of the current gbl_foo
* structure. Use rcu_read_lock() and rcu_read_unlock()
* to ensure that the structure does not get deleted out
* from under us, and use rcu_dereference() to ensure that
* we see the initialized version of the structure (important
* for DEC Alpha and for people reading the code).
*/
int foo_get_a(void) {
int retval;
rcu_read_lock();
retval = rcu_dereference(gbl_foo)->a;
rcu_read_unlock();
return retval;
}
總結如下:
- 使用
rcu_read_lock()
與rcu_read_unlock()
保證其處於讀端臨界區; - 在讀端臨界區內,使用
rcu_dereference()
解引用受RCU保護的指針 - 使用一些可靠的方案保證並行更新操作不會互相干擾,如鎖或者向量
- 使用
rcu_assign_pointer()
更新受rcu保護的指針。這個原語保護並發讀不受更新操作(而不是並發更新)的影響!但是,仍然需要使用鎖(或類似的東西)來防止並發rcu_assign_pointer()原語相互干擾。 - 使用
synchronize_rcu()
在從受RCU保護的數據結構中刪除一個數據元素之后,但是在回收/釋放數據元素之前,為了等待所有可能正在引用那個數據項的RCU讀端臨界區完成。
FAQs
rcu_dereference() vs rcu_dereference_protected()?
簡而言之:
rcu_dereference()
應該在閱讀方使用,受rcu_read_lock()
保護。rcu_dereference_protected()
應該由單個寫者在在寫入側(更新側)使用,或者由鎖定保護,這會阻止多個寫入器同時修改解除引用的指針.在這種情況下,指針不能在當前線程之外進行修改,因此既不需要編譯器也不需要cpu-barrier.
使用rcu_dereference總是安全的,並且其性能損失(與之相比rcu_dereference_protected)很低.
精確描述了rcu_dereference_protected在內核4.6:
/**
* rcu_dereference_protected() - fetch RCU pointer when updates prevented
* @p: The pointer to read, prior to dereferencing
* @c: The conditions under which the dereference will take place
*
* Return the value of the specified RCU-protected pointer, but omit
* both the smp_read_barrier_depends() and the READ_ONCE(). This
* is useful in cases where update-side locks prevent the value of the
* pointer from changing. Please note that this primitive does -not-
* prevent the compiler from repeating this reference or combining it
* with other references, so it should not be used without protection
* of appropriate locks.
*
* This function is only for update-side use. Using this function
* when protected only by rcu_read_lock() will result in infrequent
* but very ugly failures.
*/
參考
NFVschool 微信公共號,關注最前沿的網絡技術。