Linux併發與同步(二)RCU
概述
RCU是Read-Copy-Update的縮寫。於linux2.5版本開發期間加入並順利被社群接納。現在廣泛應用於指標及核心連結串列的保護。RCU相對其它併發訪問保護的鎖,具體更好的效能,因為嚴格意義上來說,RCU並不是一個鎖。但RCU對記憶體是有一定的開銷的。
為何需要RCU
- 效能問題,無需獲得鎖,效能更好。
- 讀寫執行緒可併發執行。
RCU使用介面
rcu_read_lock() //RCU讀臨界區開始
rcu_read_unlock() //RCU讀臨界區結束
synchronize_rcu() //同步等待所有現存讀訪問完成
call_rcu() //註冊一個回撥函式,等所有讀訪問完成後,呼叫回撥函式摧毀舊資料
rcu_assign_pointer() //釋出更新後的資料
rcu_dereference() //獲取RCU保護和指標
RCU使用條件
- 對共享資料訪問大多是隻讀,寫訪問相對很少(如檔案系統中查詢目錄,但修改目錄相對較少)
- RCU保護的程式碼範圍內不能睡眠
- 受保護的資源必須通過指標訪問
-讀者對新舊資料不是非常敏感
RCU使用範例
我們從一個例子入手,這個例子來源於linux kernel文件中的whatisRCU.txt。這個例子使用RCU的核心API來保護一個指向動態分配記憶體的全域性指標。
struct foo {
int a;
char b;
long c;
};
DEFINE_SPINLOCK(foo_mutex);
struct foo *gbl_foo;
//寫執行緒
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);//RCU本身不能保護寫的併發,用spinlock保護
old_fp = gbl_foo;
*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);
}
//讀執行緒
int foo_get_a(void)
{
int retval;
rcu_read_lock(); //RCU讀開始
retval = rcu_dereference(gbl_foo)->a; //獲取保護的指標
rcu_read_unlock(); //RCU讀結束
return retval;
}
RCU實現原理
RCU機制記錄了指向共享資料結構的指標的所有使用者,在該結構將要改變時,首先建立一個副本,在副本中修改。在所有讀訪問使用者結束對舊副本的讀取後,指標可以替換為指向新的修改過的副本,然後將舊資料摧毀。
RCU實現分析
寬限期(Grace Period)
通常把寫者開始更新,到所有讀者完成訪問這段時間叫做寬限期(Grace Period)。核心中實現寬限期等待的函式是synchronize_rcu。
如何判斷渡過了寬限期?
我們先看一下讀的鎖標誌:
static inline void __rcu_read_lock(void)
{
preempt_disable();
}
static inline void __rcu_read_unlock(void)
{
preempt_enable();
}
這時是否度過寬限期的判斷就比較簡單:每個CPU都經過一次搶佔。因為發生搶佔,就說明不在rcu_read_lock和rcu_read_unlock之間,必然已經完成訪問或者還未開始訪問。
靜態期(synchronize_rcu)
kernel把這個完成搶佔的狀態稱為quiescent state。每個CPU在時鐘中斷的處理函式中,都會判斷當前CPU是否度過quiescent state。
void update_process_times(int user_tick)
{
......
rcu_check_callbacks(cpu, user_tick);
......
}
void rcu_check_callbacks(int cpu, int user)
{
......
if (user || rcu_is_cpu_rrupt_from_idle()) {
/*在使用者態上下文,或者idle上下文,說明已經發生過搶佔*/
rcu_sched_qs(cpu);
rcu_bh_qs(cpu);
} else if (!in_softirq()) {
/*僅僅針對使用rcu_read_lock_bh型別的rcu,不在softirq,
*說明已經不在read_lock關鍵區域*/
rcu_bh_qs(cpu);
}
rcu_preempt_check_callbacks(cpu);
if (rcu_pending(cpu))
invoke_rcu_core();
......
}
經典RCU與Tree RCU
linux2.6.29之前的RCU通常被稱為經典RCU(classic RCU).經典RCU在大型系統中遇到了效能問題,後來由Tree RCU解決對應的效能問題。目前的核心大多使用Tree RCU.
Tree RCU採用樹狀結構:
核心中RCU相關連結串列操作
為了操作連結串列,在include/linux/rculist.h有一套專門的RCU API。如:list_entry_rcu、list_add_rcu、list_del_rcu、list_for_each_entry_rcu等。即對所有kernel 的list的操作都有一個對應的RCU操作。其將指標的獲取替換為使用rcu_dereference。
rculist.h (include\linux)
list_entry_rcu()
list_add_rcu()
list_add_tail_rcu()