Linux 內核:RCU機制與使用
背景
學習Linux源碼的時候,發現很多熟悉的數據結構多了__rcu
后綴,因此了解了一下這些內容。
介紹
RCU(Read-Copy Update)是數據同步的一種方式,在當前的Linux內核中發揮着重要的作用。RCU主要針對的數據對象是鏈表,目的是提高遍歷讀取數據的效率,為了達到目的使用RCU機制讀取數據的時候不對鏈表進行耗時的加鎖操作。這樣在同一時間可以有多個線程同時讀取該鏈表,並且允許一個線程對鏈表進行修改(修改的時候,需要加鎖)。RCU適用於需要頻繁的讀取數據,而相應修改數據並不多的情景,例如在文件系統中,經常需要查找定位目錄,而對目錄的修改相對來說並不多,這就是RCU發揮作用的最佳場景。
RCU(Read-Copy Update),是 Linux 中比較重要的一種同步機制。顧名思義就是“讀,拷貝更新”,再直白點是“隨意讀,但更新數據的時候,需要先復制一份副本,在副本上完成修改,再一次性地替換舊數據”。這是 Linux 內核實現的一種針對“讀多寫少”的共享數據的同步機制。
Linux內核源碼當中,關於RCU的文檔比較齊全,你可以在 Documentation/RCU/
目錄下找到這些文件。Paul E. McKenney 是內核中RCU源碼的主要實現者,他也寫了很多RCU方面的文章。他把這些文章和一些關於RCU的論文的鏈接整理到了一起:http://www2.rdrop.com/users/paulmck/RCU/
RCU機制解決了什么
在RCU的實現過程中,我們主要解決以下問題:
1、在讀取過程中,另外一個線程刪除了一個節點。刪除線程可以把這個節點從鏈表中移除,但它不能直接銷毀這個節點,必須等到所有的讀取線程讀取完成以后,才進行銷毀操作。RCU中把這個過程稱為寬限期(Grace period)。
2、在讀取過程中,另外一個線程插入了一個新節點,而讀線程讀到了這個節點,那么需要保證讀到的這個節點是完整的。這里涉及到了發布-訂閱機制(Publish-Subscribe Mechanism)。
3、保證讀取鏈表的完整性。新增或者刪除一個節點,不至於導致遍歷一個鏈表從中間斷開。但是RCU並不保證一定能讀到新增的節點或者不讀到要被刪除的節點。
RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基於其原理命名的。對於被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然后對副本進行修改,最后使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。那么這個“適當的時機”是怎么確定的呢?這是由內核確定的,也是我們后面討論的重點。
RCU原理
RCU實際上是一種改進的rwlock,讀者幾乎沒有什么同步開銷,它不需要鎖,不使用原子指令,而且在除alpha的所有架構上也不需要內存柵(Memory Barrier),因此不會導致鎖競爭,內存延遲以及流水線停滯。不需要鎖也使得使用更容易,因為死鎖問題就不需要考慮了。寫者的同步開銷比較大,它需要延遲數據結構的釋放,復制被修改的數據結構,它也必須使用某種鎖機制同步並行的其它寫者的修改操作。
讀者必須提供一個信號給寫者以便寫者能夠確定數據可以被安全地釋放或修改的時機。有一個專門的垃圾收集器來探測讀者的信號,一旦所有的讀者都已經發送信號告知它們都不在使用被RCU保護的數據結構,垃圾收集器就調用回調函數完成最后的數據釋放或修改操作。
RCU與rwlock的不同之處是:它既允許多個讀者同時訪問被保護的數據,又允許多個讀者和多個寫者同時訪問被保護的數據(注意:是否可以有多個寫者並行訪問取決於寫者之間使用的同步機制),讀者沒有任何同步開銷,而寫者的同步開銷則取決於使用的寫者間同步機制。但RCU不能替代rwlock,因為如果寫比較多時,對讀者的性能提高不能彌補寫者導致的損失。
讀者在訪問被RCU保護的共享數據期間不能被阻塞,這是RCU機制得以實現的一個基本前提,也就說當讀者在引用被RCU保護的共享數據期間,讀者所在的CPU不能發生上下文切換,spinlock和rwlock都需要這樣的前提。寫者在訪問被RCU保護的共享數據時不需要和讀者競爭任何鎖,只有在有多於一個寫者的情況下需要獲得某種鎖以與其他寫者同步。
寫者修改數據前首先拷貝一個被修改元素的副本,然后在副本上進行修改,修改完畢后它向垃圾回收器注冊一個回調函數以便在適當的時機執行真正的修改操作。等待適當時機的這一時期稱為grace period,而CPU發生了上下文切換稱為經歷一個quiescent state,grace period就是所有CPU都經歷一次quiescent state所需要的等待的時間。垃圾收集器就是在grace period之后調用寫者注冊的回調函數來完成真正的數據修改或數據釋放操作的。
要想使用好RCU,就要知道RCU的實現原理。我們拿linux 2.6.21 kernel的實現開始分析,為什么選擇這個版本的實現呢?因為這個版本的實現相對較為單純,也比較簡單。當然之后內核做了不少改進,如搶占RCU、可睡眠RCU、分層RCU。但是基本思想都是類似的。所以先從簡單入手。
首先,上一節我們提到,寫者在訪問它時首先拷貝一個副本,然后對副本進行修改,最后使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。而這個“適當的時機”就是所有CPU經歷了一次進程切換(也就是一個grace period)。為什么這么設計?因為RCU讀者的實現就是關搶占執行讀取,讀完了當然就可以進程切換了,也就等於是寫者可以操作臨界區了。
那么就自然可以想到,內核會設計兩個元素,來分別表示寫者被掛起的起始點,以及每cpu變量,來表示該cpu是否經過了一次進程切換(quies state)。
就是說,當寫者被掛起后,
1)重置每cpu變量,值為0。
2)當某個cpu經歷一次進程切換后,就將自己的變量設為1。
3)當所有的cpu變量都為1后,就可以喚醒寫者了。
下面我們來分別看linux里是如何完成這三步的。
從一個例子開始
我們從一個例子入手,這個例子來源於linux kernel文檔中的whatisRCU.txt。這個例子使用RCU的核心API來保護一個指向動態分配內存的全局指針。
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo *gbl_foo;
void foo_read (void)
{
foo *fp = gbl_foo;
if ( fp != NULL )
dosomething(fp->a, fp->b , fp->c );
}
void foo_update( foo* new_fp )
{
spin_lock(&foo_mutex);
foo *old_fp = gbl_foo;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
kfee(old_fp);
}
如上代碼所示,RCU被用來保護全局指針struct foo *gbl_foo
。
foo_get_a()
用來從RCU保護的結構中取得gbl_foo的值。
而foo_update_a()
用來更新被RCU保護的gbl_foo的值(更新其a成員)。
首先,我們思考一下,為什么要在foo_update_a()
中使用自旋鎖foo_mutex
呢?假設中間沒有使用自旋鎖.那foo_update_a()
的代碼如下:
void foo_read(void)
{
rcu_read_lock();
foo *fp = gbl_foo;
if ( fp != NULL )
dosomething(fp->a,fp->b,fp->c);
rcu_read_unlock();
}
void foo_update( foo* new_fp )
{
spin_lock(&foo_mutex);
foo *old_fp = gbl_foo;
gbl_foo = new_fp;
spin_unlock(&foo_mutex);
synchronize_rcu();
kfee(old_fp);
}
假設A進程在上圖—-標識處被B進程搶點.B進程也執行了goo_ipdate_a().等B執行完后,再切換回A進程.此時,A進程所持的old_fd實際上已經被B進程給釋放掉了.此后A進程對old_fd的操作都是非法的。所以在此我們得到一個重要結論:RCU允許多個讀者同時訪問被保護的數據,也允許多個讀者在有寫者時訪問被保護的數據(但是注意:是否可以有多個寫者並行訪問取決於寫者之間使用的同步機制)。
說明:本文中說的進程不是用戶態的進程,而是內核的調用路徑,也可能是內核線程或軟中斷等。
RCU的核心API
另外,我們在上面也看到了幾個有關RCU的核心API。它們為別是:
rcu_read_lock()
rcu_read_unlock()
synchronize_rcu()
rcu_assign_pointer()
rcu_dereference()
其中,rcu_read_lock()
和rcu_read_unlock()
用來保持一個讀者的RCU臨界區.在該臨界區內不允許發生上下文切換。
為什么不能發生切換呢?因為內核要根據“是否發生過切換”來判斷讀者是否已結束讀操作,我們后面再分析。
而下列的函數用於實現內存屏障的作用。
- rcu_dereference():讀者調用它來獲得一個被RCU保護的指針。
- rcu_assign_pointer():寫者使用該函數來為被RCU保護的指針分配一個新的值。
注意,synchronize_rcu()
:這是RCU的核心所在,它掛起寫者,等待讀者都退出后釋放老的數據。
增加鏈表項
Linux kernel 中利用 RCU 往鏈表增加項的源碼如下:
#define list_next_rcu(list) (*((struct list_head __rcu **)(&(list)->next)))
static inline void __list_add_rcu(struct list_head *new,
struct list_head *prev, struct list_head *next)
{
new->next = next;
new->prev = prev;
rcu_assign_pointer(list_next_rcu(prev), new);
next->prev = new;
}
list_next_rcu() 函數中的 rcu 是一個供代碼分析工具 Sparse 使用的編譯選項,規定有 rcu 標簽的指針不能直接使用,而需要使用 rcu_dereference() 返回一個受 RCU 保護的指針才能使用。rcu_dereference() 接口的相關知識會在后文介紹,這一節重點關注 rcu_assign_pointer() 接口。首先看一下 rcu_assign_pointer() 的源碼:
#define __rcu_assign_pointer(p, v, space) \
({ \
smp_wmb(); \
(p) = (typeof(*v) __force space *)(v); \
})
上述代碼的最終效果是把 v 的值賦值給 p,關鍵點在於第 3 行的內存屏障。什么是內存屏障(Memory Barrier)呢?CPU 采用流水線技術執行指令時,只保證有內存依賴關系的指令的執行順序,例如 p = v; a = *p;,由於第 2 條指令訪問的指針 p 所指向的內存依賴於第 1 條指令,因此 CPU 會保證第 1 條指令在第 2 條指令執行前執行完畢。但對於沒有內存依賴的指令,例如上述 __list_add_rcu() 接口中,假如把第 8 行寫成 prev->next = new;,由於這個賦值操作並沒涉及到對 new 指針指向的內存的訪問,因此認為不依賴於 6,7 行對 new->next 和 new->prev 的賦值,CPU 有可能實際運行時會先執行 prev->next = new; 再執行 new->prev = prev;,這就會造成 new 指針(也就是新加入的鏈表項)還沒完成初始化就被加入了鏈表中,假如這時剛好有一個讀者剛好遍歷訪問到了該新的鏈表項(因為 RCU 的一個重要特點就是可隨意執行讀操作),就會訪問到一個未完成初始化的鏈表項!通過設置內存屏障就能解決該問題,它保證了在內存屏障前邊的指令一定會先於內存屏障后邊的指令被執行。這就保證了被加入到鏈表中的項,一定是已經完成了初始化的。
最后提醒一下,這里要注意的是,如果可能存在多個線程同時執行添加鏈表項的操作,添加鏈表項的操作需要用其他同步機制(如 spin_lock 等)進行保護。
訪問鏈表項
Linux kernel 中訪問 RCU 鏈表項常見的代碼模式是:
rcu_read_lock();
list_for_each_entry_rcu(pos, head, member) {
// do something with `pos`
}
rcu_read_unlock();
這里要講到的 rcu_read_lock() 和 rcu_read_unlock(),是 RCU “隨意讀” 的關鍵,它們的效果是聲明了一個讀端的臨界區(read-side critical sections)。在說讀端臨界區之前,我們先看看讀取鏈表項的宏函數 list_for_each_entry_rcu。追溯源碼,獲取一個鏈表項指針主要調用的是一個名為 rcu_dereference() 的宏函數,而這個宏函數的主要實現如下:
#define __rcu_dereference_check(p, c, space) \
({ \
typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); \
rcu_lockdep_assert(c, "suspicious rcu_dereference_check()" \
" usage"); \
rcu_dereference_sparse(p, space); \
smp_read_barrier_depends(); \
((typeof(*p) __force __kernel *)(_________p1)); \
})
第 3 行:聲明指針 _p1 = p;
第 7 行:smp_read_barrier_depends();
第 8 行:返回 _p1;
上述兩塊代碼,實際上可以看作這樣一種模式:
rcu_read_lock();
p1 = rcu_dereference(p);
if (p1 != NULL) {
// do something with p1, such as:
printk("%d\n", p1->field);
}
rcu_read_unlock();
根據 rcu_dereference() 的實現,最終效果就是把一個指針賦值給另一個,那如果把上述第 2 行的 rcu_dereference() 直接寫成 p1 = p 會怎樣呢?在一般的處理器架構上是一點問題都沒有的。但在 alpha 上,編譯器的 value-speculation 優化選項據說可能會“猜測” p1 的值,然后重排指令先取值 p1->field~ 因此 Linux kernel 中,smp_read_barrier_depends() 的實現是架構相關的,arm、x86 等架構上是空實現,alpha 上則加了內存屏障,以保證先獲得 p 真正的地址再做解引用。因此上一節 “增加鏈表項” 中提到的 “__rcu” 編譯選項強制檢查是否使用 rcu_dereference() 訪問受 RCU 保護的數據,實際上是為了讓代碼擁有更好的可移植性。
現在回到讀端臨界區的問題上來。多個讀端臨界區不互斥,即多個讀者可同時處於讀端臨界區中,但一塊內存數據一旦能夠在讀端臨界區內被獲取到指針引用,這塊內存塊數據的釋放必須等到讀端臨界區結束,等待讀端臨界區結束的 Linux kernel API 是synchronize_rcu()。讀端臨界區的檢查是全局的,系統中有任何的代碼處於讀端臨界區,synchronize_rcu() 都會阻塞,知道所有讀端臨界區結束才會返回。為了直觀理解這個問題,舉以下的代碼實例:
/* `p` 指向一塊受 RCU 保護的共享數據 */
/* reader */
rcu_read_lock();
p1 = rcu_dereference(p);
if (p1 != NULL) {
printk("%d\n", p1->field);
}
rcu_read_unlock();
/* free the memory */
p2 = p;
if (p2 != NULL) {
p = NULL;
synchronize_rcu();
kfree(p2);
}
用以下圖示來表示多個讀者與內存釋放線程的時序關系:
上圖中,每個讀者的方塊表示獲得 p 的引用(第5行代碼)到讀端臨界區結束的時間周期;t1 表示 p = NULL 的時間;t2 表示 synchronize_rcu() 調用開始的時間;t3 表示 synchronize_rcu() 返回的時間。我們先看 Reader1,2,3,雖然這 3 個讀者的結束時間不一樣,但都在 t1 前獲得了 p 地址的引用。t2 時調用 synchronize_rcu(),這時 Reader1 的讀端臨界區已結束,但 Reader2,3 還處於讀端臨界區,因此必須等到 Reader2,3 的讀端臨界區都結束,也就是 t3,t3 之后,就可以執行 kfree(p2) 釋放內存。synchronize_rcu() 阻塞的這一段時間,有個名字,叫做 Grace period。而 Reader4,5,6,無論與 Grace period 的時間關系如何,由於獲取引用的時間在 t1 之后,都無法獲得 p 指針的引用,因此不會進入 p1 != NULL 的分支。
刪除鏈表項
知道了前邊說的 Grace period,理解鏈表項的刪除就很容易了。常見的代碼模式是:
p = seach_the_entry_to_delete();
list_del_rcu(p->list);
synchronize_rcu();
kfree(p);
其中 list_del_rcu() 的源碼如下,把某一項移出鏈表:
/* list.h */
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
/* rculist.h */
static inline void list_del_rcu(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->prev = LIST_POISON2;
}
根據上一節“訪問鏈表項”的實例,假如一個讀者能夠從鏈表中獲得我們正打算刪除的鏈表項,則肯定在 synchronize_rcu()
之前進入了讀端臨界區,synchronize_rcu()
就會保證讀端臨界區結束時才會真正釋放鏈表項的內存,而不會釋放讀者正在訪問的鏈表項。
更新鏈表項
前文提到,RCU 的更新機制是 “Copy Update”,RCU 鏈表項的更新也是這種機制,典型代碼模式是:
p = search_the_entry_to_update();
q = kmalloc(sizeof(*p), GFP_KERNEL);
*q = *p;
q->field = new_value;
list_replace_rcu(&p->list, &q->list);
synchronize_rcu();
kfree(p);
其中第 3,4 行就是復制一份副本,並在副本上完成更新,然后調用 list_replace_rcu() 用新節點替換掉舊節點。源碼如下:
其中第 3,4 行就是復制一份副本,並在副本上完成更新,然后調用 list_replace_rcu() 用新節點替換掉舊節點,最后釋放舊節點內存。
list_replace_rcu() 源碼如下:
static inline void list_replace_rcu(struct list_head *old,
struct list_head *new)
{
new->next = old->next;
new->prev = old->prev;
rcu_assign_pointer(list_next_rcu(new->prev), new);
new->next->prev = new;
old->prev = LIST_POISON2;
}
RCU鏈表API應用示例
下面看下RCU list API的幾個應用示例。
只有增加和刪除的鏈表操作
在這種應用情況下,絕大部分是對鏈表的遍歷,即讀操作,而很少出現的寫操作只有增加或刪除鏈表項,並沒有對鏈表項的修改操作,這種情況使用RCU非常容易,從rwlock轉換成RCU非常自然。路由表的維護就是這種情況的典型應用,對路由表的操作,絕大部分是路由表查詢,而對路由表的寫操作也僅僅是增加或刪除,因此使用RCU替換原來的rwlock順理成章。系統調用審計也是這樣的情況。
這是一段使用rwlock的系統調用審計部分的讀端代碼:
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后將變成:
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的寫端代碼:
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后寫端代碼變成為:
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非常容易。
寫端需要對鏈表條目進行修改操作
如果寫者需要對鏈表條目進行修改,那么就需要首先拷貝要修改的條目,然后修改條目的拷貝,等修改完畢后,再使用條目拷貝取代要修改的條目,要修改條目將被在經歷一個grace period后安全刪除。
對於系統調用審計代碼,並沒有這種情況。這里假設有修改的情況,那么使用rwlock的修改代碼應當如下:
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,修改代碼應當為;
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 */
}
修改操作立即可見
前面兩種情況,讀者能夠容忍修改可以在一段時間后看到,也就說讀者在修改后某一時間段內,仍然看到的是原來的數據。在很多情況下,讀者不能容忍看到舊的數據,這種情況下,需要使用一些新措施,如System V IPC,它在每一個鏈表條目中增加了一個deleted字段,標記該字段是否刪除,如果刪除了,就設置為真,否則設置為假,當代碼在遍歷鏈表時,核對每一個條目的deleted字段,如果為真,就認為它是不存在的。
還是以系統調用審計代碼為例,如果它不能容忍舊數據,那么,讀端代碼應該修改為:
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標志。此外,該函數如果搜索到條目,返回時應當保持該條目的鎖。
寫端的刪除操作將變成:
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或其他的鎖機制來與其他寫者同步。