1. 程式人生 > >Linux併發與同步(二)RCU

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()