衆所周知,爲了保護共享數據,須要一些同步機制,如自旋鎖(spinlock),讀寫鎖(rwlock),它們使用起來很是簡單,並且是一種頗有效的同步機制,在UNIX系統和Linux系統中獲得了普遍的使用。可是隨着計算機硬件的快速發展,得到這種鎖的開銷相對於CPU的速度在成倍地增長,緣由很簡單,CPU的速度與訪問內存的速度差距愈來愈大,而這種鎖使用了原子操做指令,它須要原子地訪問內存,也就說得到鎖的開銷與訪存速度相關,另外在大部分非x86架構上獲取鎖使用了內存柵(Memory Barrier),這會致使處理器流水線停滯或刷新,所以它的開銷相對於CPU速度而言就愈來愈大。表1數據證實了這一點。html
表1是在700MHz的奔騰III機器上的基本操做的開銷,在該機器上一個時鐘週期可以執行兩條整數指令。在1.8GHz的奔騰4機器上, 原子加1指令的開銷要比700MHz的奔騰III機器慢75納秒(ns),儘管CPU速度快兩倍多。node
這種鎖機制的另外一個問題在於其可擴展性,在多處理器系統上,可擴展性很是重要,不然根本沒法發揮其性能。圖1代表了Linux上各類鎖的擴展性。linux
注:refcnt表示自旋鎖與引用記數一塊兒使用。數組
讀寫鎖rwlock在兩個CPU的狀況下性能反倒比一個CPU的差,在四個CPU的狀況下,refcnt的性能要高於rwlock,refcnt大約是理論性能的45%,而rwlock是理論性能的39%,自旋縮spinlock的性能明顯好於refcnt和rwlock,但它也只達到了理性性能的57%,brlock(Big Reader Lock)性能能夠線性擴展。Brlock是由Redhat的Ingo Molnar實現的一個高性能的rwlock,它適用於讀特多而寫特少的狀況,讀者得到brlock的開銷很低,但寫者得到鎖的開銷很是大,並且它只預約義了幾個鎖,用戶沒法隨便定義並使用這種鎖,它也須要爲每一個CPU定義一個鎖狀態數組,所以這種鎖並無被做爲rwlock的替代方案普遍使用,只是在一些特別的地方使用到。安全
正是在這種背景下,一個高性能的鎖機制RCU呼之欲出,它克服了以上鎖的缺點,具備很好的擴展性,可是這種鎖機制的使用範圍比較窄,它只適用於讀多寫少的狀況,如網絡路由表的查詢更新、設備狀態表的維護、數據結構的延遲釋放以及多徑I/O設備的維護等。網絡
RCU並非新的鎖機制,它只是對Linux內核而言是新的。早在二十世紀八十年代就有了這種機制,並且在生產系數據結構
統中使用了這種機制,但這種早期的實現並不太好,在二十世紀九十年代出現了一個比較高效的實現,而在linux中是在開發內核2.5.43中引入該技術的並正式包含在2.6內核中。架構
RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基於其原理命名的。對於被RCU保護的共享數據結構,讀者不須要得到任何鎖就能夠訪問它,但寫者在訪問它時首先拷貝一個副本,而後對副本進行修改,最後使用一個回調(callback)機制在適當的時機把指向原來數據的指針從新指向新的被修改的數據。這個時機就是全部引用該數據的CPU都退出對共享數據的操做。併發
所以RCU其實是一種改進的rwlock,讀者幾乎沒有什麼同步開銷,它不須要鎖,不使用原子指令,並且在除alpha的全部架構上也不須要內存柵(Memory Barrier),所以不會致使鎖競爭,內存延遲以及流水線停滯。不須要鎖也使得使用更容易,由於死鎖問題就不須要考慮了。寫者的同步開銷比較大,它須要延遲數據結構的釋放,複製被修改的數據結構,它也必須使用某種鎖機制同步並行的其它寫者的修改操做。讀者必須提供一個信號給寫者以便寫者可以肯定數據能夠被安全地釋放或修改的時機。有一個專門的垃圾收集器來探測讀者的信號,一旦全部的讀者都已經發送信號告知它們都不在使用被RCU保護的數據結構,垃圾收集器就調用回調函數完成最後的數據釋放或修改操做。 RCU與rwlock的不一樣之處是:它既容許多個讀者同時訪問被保護的數據,又容許多個讀者和多個寫者同時訪問被保護的數據(注意:是否能夠有多個寫者並行訪問取決於寫者之間使用的同步機制),讀者沒有任何同步開銷,而寫者的同步開銷則取決於使用的寫者間同步機制。但RCU不能替代rwlock,由於若是寫比較多時,對讀者的性能提升不能彌補寫者致使的損失。ide
讀者在訪問被RCU保護的共享數據期間不能被阻塞,這是RCU機制得以實現的一個基本前提,也就說當讀者在引用被RCU保護的共享數據期間,讀者所在的CPU不能發生上下文切換,spinlock和rwlock都須要這樣的前提。寫者在訪問被RCU保護的共享數據時不須要和讀者競爭任何鎖,只有在有多於一個寫者的狀況下須要得到某種鎖以與其餘寫者同步。寫者修改數據前首先拷貝一個被修改元素的副本,而後在副本上進行修改,修改完畢後它向垃圾回收器註冊一個回調函數以便在適當的時機執行真正的修改操做。等待適當時機的這一時期稱爲grace period,而CPU發生了上下文切換稱爲經歷一個quiescent state,grace period就是全部CPU都經歷一次quiescent state所須要的等待的時間。垃圾收集器就是在grace period以後調用寫者註冊的回調函數來完成真正的數據修改或數據釋放操做的。
如下以鏈表元素刪除爲例詳細說明這一過程。
寫者要從鏈表中刪除元素 B,它首先遍歷該鏈表獲得指向元素 B 的指針,而後修改元素 B 的前一個元素的 next 指針指向元素 B 的 next 指針指向的元素C,修改元素 B 的 next 指針指向的元素 C 的 prep 指針指向元素 B 的 prep指針指向的元素 A,在這期間可能有讀者訪問該鏈表,修改指針指向的操做是原子的,因此不須要同步,而元素 B 的指針並無去修改,由於讀者可能正在使用 B 元素來獲得下一個或前一個元素。寫者完成這些操做後註冊一個回調函數以便在 grace period 以後刪除元素 B,而後就認爲已經完成刪除操做。垃圾收集器在檢測到全部的CPU不在引用該鏈表後,即全部的 CPU 已經經歷了 quiescent state,grace period 已通過去後,就調用剛纔寫者註冊的回調函數刪除了元素 B。
按照第二節所講原理,對於讀者,RCU 僅須要搶佔失效,所以得到讀鎖和釋放讀鎖分別定義爲:
1
2
|
#define rcu_read_lock() preempt_disable()
#define rcu_read_unlock() preempt_enable()
|
它們有一個變種:
1
2
|
#define rcu_read_lock_bh() local_bh_disable()
#define rcu_read_unlock_bh() local_bh_enable()
|
這個變種只在修改是經過 call_rcu_bh 進行的狀況下使用,由於 call_rcu_bh將把 softirq 的執行完畢也認爲是一個 quiescent state,所以若是修改是經過 call_rcu_bh 進行的,在進程上下文的讀端臨界區必須使用這一變種。
每個 CPU 維護兩個數據結構rcu_data,rcu_bh_data,它們用於保存回調函數,函數call_rcu和函數call_rcu_bh用戶註冊回調函數,前者把回調函數註冊到rcu_data,然後者則把回調函數註冊到rcu_bh_data,在每個數據結構上,回調函數被組成一個鏈表,先註冊的排在前頭,後註冊的排在末尾。
當在CPU上發生進程切換時,函數rcu_qsctr_inc將被調用以標記該CPU已經經歷了一個quiescent state。該函數也會被時鐘中斷觸發調用。
時鐘中斷觸發垃圾收集器運行,它會檢查:
若是以上四個條件只要有一個知足,它就調用函數rcu_check_callbacks。
函數rcu_check_callbacks首先檢查該CPU是否經歷了一個quiescent state,若是:
1. 當前進程運行在用戶態;
或
2. 當前進程爲idle且當前不處在運行softirq狀態,也不處在運行IRQ處理函數的狀態;
那麼,該CPU已經經歷了一個quiescent state,所以經過調用函數rcu_qsctr_inc標記該CPU的數據結構rcu_data和rcu_bh_data的標記字段passed_quiesc,以記錄該CPU已經經歷一個quiescent state。
不然,若是當前不處在運行softirq狀態,那麼,只標記該CPU的數據結構rcu_bh_data的標記字段passed_quiesc,以記錄該CPU已經經歷一個quiescent state。注意,該標記只對rcu_bh_data有效。
而後,函數rcu_check_callbacks將調用tasklet_schedule,它將調度爲該CPU設置的tasklet rcu_tasklet,每個CPU都有一個對應的rcu_tasklet。
在時鐘中斷返回後,rcu_tasklet將在softirq上下文被運行。
rcu_tasklet將運行函數rcu_process_callbacks,函數rcu_process_callbacks可能作如下事情:
1. 開始一個新的grace period;這經過調用函數rcu_start_batch實現。
2. 運行須要處理的回調函數;這經過調用函數rcu_do_batch實現。
3. 檢查該CPU是否經歷一個quiescent state;這經過函數rcu_check_quiescent_state實現
若是尚未開始grace period,就調用rcu_start_batch開始新的grace period。調用函數rcu_check_quiescent_state檢查該CPU是否經歷了一個quiescent state,若是是而且是最後一個經歷quiescent state的CPU,那麼就結束grace period,並開始新的grace period。若是有完成的grace period,那麼就調用rcu_do_batch運行全部須要處理的回調函數。函數rcu_process_callbacks將對該CPU的兩個數據結構rcu_data和rcu_bh_data執行上述操做。
rcu_read_lock()
讀者在讀取由RCU保護的共享數據時使用該函數標記它進入讀端臨界區。
rcu_read_unlock()
該函數與rcu_read_lock配對使用,用以標記讀者退出讀端臨界區。夾在這兩個函數之間的代碼區稱爲"讀端臨界區"(read-side critical section)。讀端臨界區能夠嵌套,如圖3,臨界區2被嵌套在臨界區1內。
synchronize_rcu()
該函數由RCU寫端調用,它將阻塞寫者,直到通過grace period後,即全部的讀者已經完成讀端臨界區,寫者才能夠繼續下一步操做。若是有多個RCU寫端調用該函數,他們將在一個grace period以後所有被喚醒。注意,該函數在2.6.11及之前的2.6內核版本中爲synchronize_kernel,只是在2.6.12才改名爲synchronize_rcu,但在2.6.12中也提供了synchronize_kernel和一個新的函數synchronize_sched,由於之前有不少內核開發者使用synchronize_kernel用於等待全部CPU都退出不可搶佔區,而在RCU設計時該函數只是用於等待全部CPU都退出讀端臨界區,它可能會隨着RCU實現的修改而發生語意變化,所以爲了預先防止這種狀況發生,在新的修改中增長了專門的用於其它內核用戶的synchronize_sched函數和只用於RCU使用的synchronize_rcu,如今建議非RCU內核代碼部分不使用synchronize_kernel而使用synchronize_sched,RCU代碼部分則使用synchronize_rcu,synchronize_kernel之因此存在是爲了保證代碼兼容性。
synchronize_kernel()
其餘非RCU的內核代碼使用該函數來等待全部CPU處在可搶佔狀態,目前功能等同於synchronize_rcu,但如今已經不建議使用,而使用synchronize_sched。
synchronize_sched()
該函數用於等待全部CPU都處在可搶佔狀態,它能保證正在運行的中斷處理函數處理完畢,但不能保證正在運行的softirq處理完畢。注意,synchronize_rcu只保證全部CPU都處理完正在運行的讀端臨界區。 注:在2.6.12內核中,synchronize_kernel和synchronize_sched都實際使用synchronize_rcu,所以當前它們的功能實際是徹底等同的,可是未來將可能有大的變化,所以務必根據需求選擇恰當的函數。
1
2
3
4
5
6
|
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);
};
|
函數 call_rcu 也由 RCU 寫端調用,它不會使寫者阻塞,於是能夠在中斷上下文或 softirq 使用,而 synchronize_rcu、synchronize_kernel 和synchronize_shced 只能在進程上下文使用。該函數將把函數 func 掛接到 RCU回調函數鏈上,而後當即返回。一旦全部的 CPU 都已經完成端臨界區操做,該函數將被調用來釋放刪除的將毫不在被應用的數據。參數 head 用於記錄回調函數 func,通常該結構會做爲被 RCU 保護的數據結構的一個字段,以便省去單獨爲該結構分配內存的操做。須要指出的是,函數 synchronize_rcu 的實現實際上使用函數call_rcu。
1
2
|
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。
1
2
3
4
5
|
#define rcu_dereference(p) ({ \
typeof(p) _________p1 = p; \
smp_read_barrier_depends(); \
(_________p1); \
})
|
該宏用於在RCU讀端臨界區得到一個RCU保護的指針,該指針能夠在之後安全地引用,內存柵只在alpha架構上才使用。
除了這些API,RCU還增長了鏈表操做的RCU版本,由於對於RCU,對共享數據的操做必須保證可以被沒有使用同步機制的讀者看到,因此內存柵是很是必要的。
static inline void list_add_rcu(struct list_head *new, struct list_head *head) 該函數把鏈表項new插入到RCU保護的鏈表head的開頭。使用內存柵保證了在引用這個新插入的鏈表項以前,新鏈表項的連接指針的修改對全部讀者是可見的。
1
2
|
static inline void list_add_tail_rcu(struct list_head *new,
struct list_head *head)
|
該函數相似於list_add_rcu,它將把新的鏈表項new添加到被RCU保護的鏈表的末尾。
1
|
static inline void list_del_rcu(struct list_head *entry)
|
該函數從RCU保護的鏈表中移走指定的鏈表項entry,而且把entry的prev指針設置爲LIST_POISON2,可是並無把entry的next指針設置爲LIST_POISON1,由於該指針可能仍然在被讀者用於便利該鏈表。
1
|
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
|
該函數是RCU新添加的函數,並不存在非RCU版本。它使用新的鏈表項new取代舊的鏈表項old,內存柵保證在引用新的鏈表項以前,它的連接指針的修正對全部讀者可見。
1
|
list_for_each_rcu(pos, head)
|
該宏用於遍歷由RCU保護的鏈表head,只要在讀端臨界區使用該函數,它就能夠安全地和其它_rcu鏈表操做函數(如list_add_rcu)併發運行。
1
|
list_for_each_safe_rcu(pos, n, head)
|
該宏相似於list_for_each_rcu,但不一樣之處在於它容許安全地刪除當前鏈表項pos。
1
|
list_for_each_entry_rcu(pos, head, member)
|
該宏相似於list_for_each_rcu,不一樣之處在於它用於遍歷指定類型的數據結構鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構。
1
|
list_for_each_continue_rcu(pos, head)
|
該宏用於在退出點以後繼續遍歷由RCU保護的鏈表head。
1
|
static inline void hlist_del_rcu(struct hlist_node *n)
|
它從由RCU保護的哈希鏈表中移走鏈表項n,並設置n的ppre指針爲LIST_POISON2,但並無設置next爲LIST_POISON1,由於該指針可能被讀者使用用於遍利鏈表。
1
2
|
static inline void hlist_add_head_rcu(struct hlist_node *n,
struct hlist_head *h)
|
該函數用於把鏈表項n插入到被RCU保護的哈希鏈表的開頭,但同時容許讀者對該哈希鏈表的遍歷。內存柵確保在引用新鏈表項以前,它的指針修正對全部讀者可見。
1
|
hlist_for_each_rcu(pos, head)
|
該宏用於遍歷由RCU保護的哈希鏈表head,只要在讀端臨界區使用該函數,它就能夠安全地和其它_rcu哈希鏈表操做函數(如hlist_add_rcu)併發運行。
1
|
hlist_for_each_entry_rcu(tpos, pos, head, member)
|
相似於hlist_for_each_rcu,不一樣之處在於它用於遍歷指定類型的數據結構哈希鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構。
在 linux 2.6 內核中,RCU 被內核使用的愈來愈普遍。下面是在最新的 2.6.12內核中搜索獲得的RCU使用狀況統計表。
從以上統計結果能夠看出,RCU已經在網絡驅動層、網絡核心層、IPC、dcache、內存設備層、軟RAID層、系統調用審計和SELinux中使用。從全部RCU API的使用統計彙總(表 10),不難看出,RCU已是一個很是重要的內核鎖機制。
所以,如何正確使用 RCU 對於內核開發者而言很是重要。
下面部分將就 RCU 的幾種典型應用狀況詳細講解。
1.只有增長和刪除的鏈表操做
在這種應用狀況下,絕大部分是對鏈表的遍歷,即讀操做,而不多出現的寫操做只有增長或刪除鏈表項,並無對鏈表項的修改操做,這種狀況使用RCU很是容易,從rwlock轉換成RCU很是天然。路由表的維護就是這種狀況的典型應用,對路由表的操做,絕大部分是路由表查詢,而對路由表的寫操做也僅僅是增長或刪除,所以使用RCU替換原來的rwlock瓜熟蒂落。系統調用審計也是這樣的狀況。
這是一段使用rwlock的系統調用審計部分的讀端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
read_lock(&auditsc_lock);
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
read_unlock(&auditsc_lock);
return state;
}
}
read_unlock(&auditsc_lock);
return AUDIT_BUILD_CONTEXT;
}
|
使用RCU後將變成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
rcu_read_lock();
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry_rcu(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
rcu_read_unlock();
return state;
}
}
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
|
這種轉換很是直接,使用rcu_read_lock和rcu_read_unlock分別替換read_lock和read_unlock,鏈表遍歷函數使用_rcu版本替換就能夠了。
使用rwlock的寫端代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
write_lock(&auditsc_lock);
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del(&e->list);
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT; /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{
write_lock(&auditsc_lock);
if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add(&entry->list, list);
} else {
list_add_tail(&entry->list, list);
}
write_unlock(&auditsc_lock);
return 0;
}
|
使用RCU後寫端代碼變成爲:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
/* Do not use the _rcu iterator here, since this is the only
* deletion routine. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del_rcu(&e->list);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{
if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add_rcu(&entry->list, list);
} else {
list_add_tail_rcu(&entry->list, list);
}
return 0;
}
|
對於鏈表刪除操做,list_del替換爲list_del_rcu和call_rcu,這是由於被刪除的鏈表項可能還在被別的讀者引用,因此不能當即刪除,必須等到全部讀者經歷一個quiescent state才能夠刪除。另外,list_for_each_entry並無被替換爲list_for_each_entry_rcu,這是由於,只有一個寫者在作鏈表刪除操做,所以沒有必要使用_rcu版本。
一般狀況下,write_lock和write_unlock應當分別替換成spin_lock和spin_unlock,可是對於只是對鏈表進行增長和刪除操做並且只有一個寫者的寫端,在使用了_rcu版本的鏈表操做API後,rwlock能夠徹底消除,不須要spinlock來同步讀者的訪問。對於上面的例子,因爲已經有audit_netlink_sem被調用者保持,因此spinlock就沒有必要了。
這種狀況容許修改結果延後必定時間纔可見,並且寫者對鏈表僅僅作增長和刪除操做,因此轉換成使用RCU很是容易。
2.寫端須要對鏈表條目進行修改操做
若是寫者須要對鏈表條目進行修改,那麼就須要首先拷貝要修改的條目,而後修改條目的拷貝,等修改完畢後,再使用條目拷貝取代要修改的條目,要修改條目將被在經歷一個grace period後安全刪除。
對於系統調用審計代碼,並無這種狀況。這裏假設有修改的狀況,那麼使用rwlock的修改代碼應當以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static inline int audit_upd_rule(struct audit_rule *rule,
struct list_head *list,
__u32 newaction,
__u32 newfield_count)
{
struct audit_entry *e;
struct audit_newentry *ne;
write_lock(&auditsc_lock);
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
e->rule.action = newaction;
e->rule.file_count = newfield_count;
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT; /* No matching rule */
}
|
若是使用RCU,修改代碼應當爲;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
static inline int audit_upd_rule(struct audit_rule *rule,
struct list_head *list,
__u32 newaction,
__u32 newfield_count)
{
struct audit_entry *e;
struct audit_newentry *ne;
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
ne = kmalloc(sizeof(*entry), GFP_ATOMIC);
if (ne == NULL)
return -ENOMEM;
audit_copy_rule(&ne->rule, &e->rule);
ne->rule.action = newaction;
ne->rule.file_count = newfield_count;
list_replace_rcu(e, ne);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
|
3.修改操做當即可見
前面兩種狀況,讀者可以容忍修改能夠在一段時間後看到,也就說讀者在修改後某一時間段內,仍然看到的是原來的數據。在不少狀況下,讀者不能容忍看到舊的數據,這種狀況下,須要使用一些新措施,如System V IPC,它在每個鏈表條目中增長了一個deleted字段,標記該字段是否刪除,若是刪除了,就設置爲真,不然設置爲假,當代碼在遍歷鏈表時,覈對每個條目的deleted字段,若是爲真,就認爲它是不存在的。
仍是以系統調用審計代碼爲例,若是它不能容忍舊數據,那麼,讀端代碼應該修改成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
rcu_read_lock();
list_for_each_entry_rcu(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
spin_lock(&e->lock);
if (e->deleted) {
spin_unlock(&e->lock);
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
rcu_read_unlock();
return state;
}
}
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
|
注意,對於這種狀況,每個鏈表條目都須要一個spinlock保護,由於刪除操做將修改條目的deleted標誌。此外,該函數若是搜索到條目,返回時應當保持該條目的鎖,由於只有這樣,才能看到新的修改的數據,不然,仍然可能看到就的數據。
寫端的刪除操做將變成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
/* Do not use the _rcu iterator here, since this is the only
* deletion routine. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
spin_lock(&e->lock);
list_del_rcu(&e->list);
e->deleted = 1;
spin_unlock(&e->lock);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
|
刪除條目時,須要標記該條目爲已刪除。這樣讀者就能夠經過該標誌當即得知條目是否已經刪除。
RCU是2.6內核引入的新的鎖機制,在絕大部分爲讀而只有極少部分爲寫的狀況下,它是很是高效的,所以在路由表維護、系統調用審計、SELinux的AVC、dcache和IPC等代碼部分中,使用它來取代rwlock來得到更高的性能。可是,它也有缺點,延後的刪除或釋放將佔用一些內存,尤爲是對嵌入式系統,這多是很是昂貴的內存開銷。此外,寫者的開銷比較大,尤爲是對於那些沒法容忍舊數據的狀況以及不僅一個寫者的狀況,寫者須要spinlock或其餘的鎖機制來與其餘寫者同步。
在做者先前的兩篇文章"Linux 實時技術與典型實現分析, 第 1 部分: 介紹"和"Linux 實時技術與典型實現分析, 第 2 部分: Ingo Molnar 的實時補丁"中,Ingo Molnar的實時實現要求RCU讀端臨界區可搶佔,而RCU的實現的前提是讀端臨界區不可搶佔,所以如何解決這一矛盾但同時不損害RCU的性能是RCU將來的一大挑戰。
[1] Linux RCU實現者之一Paul E. McKenney的RCU資源連接,http://www.rdrop.com/users/paulmck/rclock/。
[2] Paul E. McKenney的博士論文,"Exploiting Deferred Destruction: An Analysis of Read-Copy Update Techniques in Operating System Kernels",http://www.rdrop.com/users/paulmck/rclock/RCUdissertation.2004.07.14e1.pdf。
[3] Paul E. McKenney's paper in Ottawa Linux Summit 2002, Read-Copy Update,http://www.rdrop.com/users/paulmck/rclock/rcu.2002.07.08.pdf。
[4] Linux Journal在2003年10月對RCU的簡介, Kernel Korner - Using RCU in the Linux 2.5 Kernel,http://linuxjournal.com/article/6993。
[5] Scaling dcache with RCU, http://linuxjournal.com/article/7124。
[6] Patch: Real-Time Preemption and RCU,http://lwn.net/Articles/128228/。
[7] Using Read-Copy Update Techniques for System V IPC in the Linux 2.5 Kernel, http://www.rdrop.com/users/paulmck/rclock/rcu.FREENIX.2003.06.14.pdf。
[8] Linux 2.6.12 kernel source。
[9] Linux kernel documentation, Documentation/RCU/*。
轉自:https://www.ibm.com/developerworks/cn/linux/l-rcu/