RCU實際上是一種改進的rwlock,讀者幾乎沒有什麼同步開銷,它不需要鎖,不使 用原子指令,而且在除alpha的所有架構上也不需要內存柵(Memory Barrier),因此不會導致鎖競爭,內存延遲以及流水線停滯。不需要鎖也使得使用更容易,因爲死鎖問題就不需要考慮了。寫者的同步開銷比較大,它需要 延遲數據結構的釋放,複製被修改的數據結構,它也必須使用某種鎖機制同步並行的其它寫者的修改操作。讀者必須提供一個信號給寫者以便寫者能夠確定數據可以 被安全地釋放或修改的時機。有一個專門的垃圾收集器來探測讀者的信號,一旦所有的讀者都已經發送信號告知它們都不在使用被RCU保護的數據結構,垃圾收集 器就調用回調函數完成最後的數據釋放或修改操作。 RCU與rwlock的不同之處是:它既允許多個讀者同時訪問被保護的數據,又允許多個讀者和多個寫者同時訪問被保護的數據(注意:是否可以有多個寫者並 行訪問取決於寫者之間使用的同步機制),讀者沒有任何同步開銷,而寫者的同步開銷則取決於使用的寫者間同步機制。但RCU不能替代rwlock,因爲如果 寫比較多時,對讀者的性能提高不能彌補寫者導致的損失。
讀者在訪問被RCU保護的 共享數據期間不能被阻塞,也就說當讀者在引用被RCU保護的共享數據期間,讀者所在的CPU不能發生上下文切換,spinlock和rwlock都需要這 樣的前提。寫者在訪問被RCU保護的共享數據時不需要和讀者競爭任何鎖,只有在有多於一個寫者的情況下需要獲得某種鎖以與其他寫者同步。寫者修改數據前首 先拷貝一個被修改元素的副本,然後在副本上進行修改,修改完畢後它向垃圾回收器註冊一個回調函數以便在所有讀執行單元已經完成對臨界區的訪問進行修改操 作。
RCU的API如下:
#define rcu_read_lock() preempt_disable()
#define rcu_read_unlock() preempt_enable()
例子:
struct my_stuff *stuff;
rcu_read_lock();
stuff = find_the_stuff(args...);
do_something_with(stuff);
rcu_read_unlock();
讀者在讀取由RCU保護的共享數據時使用rcu_read_lock()標記它進入讀端臨界區。
rcu_read_unlock()與rcu_read_lock配對使用,用以標記讀者退出讀端臨界區。
#define rcu_read_lock_bh() local_bh_disable()
#define rcu_read_unlock_bh() local_bh_enable()
rcu_read_lock_bh() 只在修改是通過 call_rcu_bh 進行的情況下使用,因爲 call_rcu_bh將把 softirq 的執行完畢也認爲是一個 quiescent state,因此如果修改是通過 call_rcu_bh 進行的,在進程上下文的讀端臨界區必須使用rcu_read_lock_bh()。
注:rcu_read_lock和rcu_read_lock_bh實質作用只是禁止內核的搶佔調度。
synchronize_rcu()
在linux/kernel/rcupdate.c定義如下: 618void synchronize_rcu(void) 619{ 620 struct rcu_synchronize rcu; 621 622 init_completion(&rcu.completion); 623 /* Will wake me after RCU finished */ 624 call_rcu(&rcu.head, wakeme_after_rcu); 625 626 /* Wait for it */ 627 wait_for_completion(&rcu.completion); 628} |
該函數由RCU寫端調用,它將阻塞寫者,直到所有讀執行單元完成對臨界區的訪問後,寫者纔可以繼續下一步操
作。如果有多個RCU寫端調用該函數,他們將在所有讀執行單元完成對臨界區的訪問後全部被喚醒。
需要指出的是,函數synchronize_rcu 的實現實際上使用函數call_rcu。
synchronize_sched()
在linux/include/linux/rcupdate.h定義如下:
270#define synchronize_sched() synchronize_rcu() |
void fastcall call_rcu(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
struct rcu_head {
struct rcu_head *next;
void (*func)(struct rcu_head *head);
};
其中:fastcall加快訪問速度.
函 數call_rcu也由RCU寫端調用,它不會使寫者阻塞,因而可以在中斷上下文或softirq使用。該函數將把函數func掛接到RCU回調函數鏈 上,然後立即返回。一旦所有的CPU都已經完成端臨界區操作,該函數將被調用來釋放刪除的將絕不在被應用的數據。參數head用於記錄回調函數 func,一般該結構會作爲被RCU保護的數據結構的一個字段,以便省去單獨爲該結構分配內存的操作。
void fastcall call_rcu_bh(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
函數call_ruc_bh功能幾乎與call_rcu完全相同,唯一差別就是它把softirq的完成也當作經歷一個quiescent state,因此如果寫端使用了該函數,在進程上下文的讀端必須使用rcu_read_lock_bh。
#define rcu_dereference(p) ({ /
typeof(p) _________p1 = p; /
smp_read_barrier_depends(); /
(_________p1); /
})
該宏用於在RCU讀端臨界區獲得一個RCU保護的指針,該指針可以在以後安全地引用,內存柵只在alpha架構上才使用。
RCU還增加了鏈表操作的RCU版本,因爲對於RCU,對共享數據的操作必須保證能夠被沒有使用同步機制的讀者看到,所以內存柵是非常必要的。
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
該函數把鏈表項new插入到RCU保護的鏈表head的開頭。使用內存柵保證了在引用這個新插入的鏈表項之前,新鏈表項的鏈接指針的修改對所有讀者是可見的。
static inline void list_add_tail_rcu(struct list_head *new,
struct list_head *head)
該函數類似於list_add_rcu,它將把新的鏈表項new添加到被RCU保護的鏈表的末尾。
static inline void list_del_rcu(struct list_head *entry)
在linux/include/linux/list.h定義如下: 202static inline void list_del_rcu(struct list_head *entry) 203{ 204 __list_del(entry->prev, entry->next); 205 entry->prev = LIST_POISON2; 206} |
該函數從RCU保護的鏈表中移走指定的鏈表項entry,並且把entry的prev指針設置爲LIST_POISON2,但是並沒有把entry的next指針設置爲LIST_POISON1,因爲該指針可能仍然在被讀者用於遍歷該鏈表。
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
該函數是RCU新添加的函數,並不存在非RCU版本。它使用新的鏈表項new取代舊的鏈表項old,內存柵保證在引用新的鏈表項之前,它的鏈接指針的修正對所有讀者可見。
list_for_each_rcu(pos, head)
599
/**
|
該宏用於遍歷由RCU保護的鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu鏈表操作函數(如list_add_rcu)併發運行。
list_for_each_safe_rcu(pos, n, head)
該宏類似於list_for_each_rcu,但不同之處在於它允許安全地刪除當前鏈表項pos。
list_for_each_entry_rcu(pos, head, member)
在linux/include/linux/list.h定義如下:5
/**
其中list_entry((head)->next, typeof(*pos), member);定義爲:425#define list_entry(ptr, type, member) / 426 container_of(ptr, type, member) |
這個 container_of(ptr, type, member)宏說了很多次了,取得type數據結構的首地址。
該宏類似於list_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構。可以比較代碼發現和list_for_each_rcu(pos, head)的區別。
list_for_each_continue_rcu(pos, head)
該宏用於在退出點之後繼續遍歷由RCU保護的鏈表head。
static inline void hlist_del_rcu(struct hlist_node *n)
它從由RCU保護的哈希鏈表中移走鏈表項n,並設置n的ppre指針爲LIST_POISON2,但並沒有設置next爲LIST_POISON1,因爲該指針可能被讀者使用用於遍利鏈表。
static inline void hlist_add_head_rcu(struct hlist_node *n,
struct hlist_head *h)
該函數用於把鏈表項n插入到被RCU保護的哈希鏈表的開頭,但同時允許讀者對該哈希鏈表的遍歷。內存柵確保在引用新鏈表項之前,它的指針修正對所有讀者可見。
hlist_for_each_rcu(pos, head)
該宏用於遍歷由RCU保護的哈希鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu哈希鏈表操作函數(如hlist_add_rcu)併發運行。
hlist_for_each_entry_rcu(tpos, pos, head, member)
類似於hlist_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構哈希鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構。
現 代產品應用:在絕大部分爲讀而只有極少部分爲寫的情況下,它是非常高效的,因此在路由表維護、系統調用審計、SELinux的AVC、dcache和 IPC等代碼部分中,使用它來取代rwlock來獲得更高的性能。但是,它也有缺點,延後的刪除或釋放將佔用一些內存(如:在執行寫操作只能等到讀操作全 部執行完成,纔會釋放原來的數據的指針指向新的數據),尤其是對嵌入式系統,這可能是非常昂貴的內存開銷。此外,寫者的開銷比較大,尤其是對於那些無法容 忍舊數據的情況以及不只一個寫者的情況,寫者需要spinlock或其他的鎖機制來與其他寫者同步。