RCU(Read-Copy Update)是Linux內核比較成熟的新型讀寫鎖,具備較高的讀寫併發性能,經常用在須要互斥的性能關鍵路徑。在kernel中,rcu有tiny rcu和tree rcu兩種實現,tiny rcu更加簡潔,一般用在小型嵌入式系統中,tree rcu則被普遍使用在了server, desktop以及android系統中。本文將以tree rcu爲分析對象。android
1 如何度過寬限期 併發
RCU的核心理念是讀者訪問的同時,寫者能夠更新訪問對象的副本,但寫者須要等待全部讀者完成訪問以後,才能刪除老對象。這個過程實現的關鍵和難點就在於如何判斷全部的讀者已經完成訪問。一般把寫者開始更新,到全部讀者完成訪問這段時間叫作寬限期(Grace Period)。內核中實現寬限期等待的函數是synchronize_rcu。ide
1.1 讀者鎖的標記 函數
在普通的TREE RCU實現中,rcu_read_lock和rcu_read_unlock的實現很是簡單,分別是關閉搶佔和打開搶佔:性能
staticinlinevoid __rcu_read_lock(void){preempt_disable();} staticinlinevoid __rcu_read_unlock(void){preempt_enable();}
這時是否度過寬限期的判斷就比較簡單:每一個CPU都通過一次搶佔。由於發生搶佔,就說明不在rcu_read_lock和rcu_read_unlock之間,必然已經完成訪問或者還未開始訪問。ui
1.2 每一個CPU度過quiescnet state spa
接下來咱們看每一個CPU上報完成搶佔的過程。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);}elseif(!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();......}
這裏補充一個細節說明,Tree RCU有多個類型的RCU State,用於不一樣的RCU場景,包括rcu_sched_state、rcu_bh_state和rcu_preempt_state。不一樣的場景使用不一樣的RCU API,度過寬限期的方式就有所區別。例如上面代碼中的rcu_sched_qs和rcu_bh_qs,就是爲了標記不一樣的state度過quiescent state。普通的RCU例如內核線程、系統調用等場景,使用rcu_read_lock或者rcu_read_lock_sched,他們的實現是同樣的;軟中斷上下文則可使用rcu_read_lock_bh,使得寬限期更快度過。 3d
細分這些場景是爲了提升RCU的效率。rcu_preempt_state將在下文進行說明。code
1.3 彙報寬限期度過
每一個CPU度過quiescent state以後,須要向上彙報直至全部CPU完成quiescent state,從而標識寬限期的完成,這個彙報過程在軟中斷RCU_SOFTIRQ中完成。軟中斷的喚醒則是在上述的時鐘中斷中進行。
update_process_times
-> rcu_check_callbacks
-> invoke_rcu_core
RCU_SOFTIRQ軟中斷處理的彙報流程以下:
rcu_process_callbacks
-> __rcu_process_callbacks
-> rcu_check_quiescent_state
-> rcu_report_qs_rdp
-> rcu_report_qs_rnp
其中rcu_report_qs_rnp是從葉子節點向根節點的遍歷過程,同一個節點的子節點都經過quiescent state後,該節點也設置爲經過。
這個樹狀的彙報過程,也就是「Tree RCU」這個名字得來的原因。
樹結構每層的節點數量和葉子節點數量由一系列的宏定義來決定:
#define MAX_RCU_LVLS 4#define RCU_FANOUT_1 (CONFIG_RCU_FANOUT_LEAF)#define RCU_FANOUT_2 (RCU_FANOUT_1 * CONFIG_RCU_FANOUT)#define RCU_FANOUT_3 (RCU_FANOUT_2 * CONFIG_RCU_FANOUT)#define RCU_FANOUT_4 (RCU_FANOUT_3 * CONFIG_RCU_FANOUT) #if NR_CPUS <= RCU_FANOUT_1# define RCU_NUM_LVLS 1# define NUM_RCU_LVL_0 1# define NUM_RCU_LVL_1 (NR_CPUS)# define NUM_RCU_LVL_2 0# define NUM_RCU_LVL_3 0# define NUM_RCU_LVL_4 0#elif NR_CPUS <= RCU_FANOUT_2# define RCU_NUM_LVLS 2# define NUM_RCU_LVL_0 1# define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)# define NUM_RCU_LVL_2 (NR_CPUS)# define NUM_RCU_LVL_3 0# define NUM_RCU_LVL_4 0#elif NR_CPUS <= RCU_FANOUT_3# define RCU_NUM_LVLS 3# define NUM_RCU_LVL_0 1# define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_2)# define NUM_RCU_LVL_2 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)# define NUM_RCU_LVL_3 (NR_CPUS)# define NUM_RCU_LVL_4 0#elif NR_CPUS <= RCU_FANOUT_4# define RCU_NUM_LVLS 4# define NUM_RCU_LVL_0 1# define NUM_RCU_LVL_1 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_3)# define NUM_RCU_LVL_2 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_2)# define NUM_RCU_LVL_3 DIV_ROUND_UP(NR_CPUS, RCU_FANOUT_1)# define NUM_RCU_LVL_4 (NR_CPUS)
1.3 寬限期的發起與完成
全部寬限期的發起和完成都是由同一個內核線程rcu_gp_kthread來完成。經過判斷rsp->gp_flags & RCU_GP_FLAG_INIT來決定是否發起一個gp;經過判斷! (rnp->qsmask) && !rcu_preempt_blocked_readers_cgp(rnp))來決定是否結束一個gp。
發起一個GP時,rsp->gpnum++;結束一個GP時,rsp->completed = rsp->gpnum。
1.4 rcu callbacks處理
rcu的callback一般是在sychronize_rcu中添加的wakeme_after_rcu,也就是喚醒synchronize_rcu的進程,它正在等待GP的結束。
callbacks的處理一樣在軟中斷RCU_SOFTIRQ中完成
rcu_process_callbacks
-> __rcu_process_callbacks
-> invoke_rcu_callbacks
-> rcu_do_batch
-> __rcu_reclaim
這裏RCU的callbacks鏈表採用了一種分段鏈表的方式,整個callback鏈表,根據具體GP結束的時間,分紅若干段:nxtlist -- *nxttail[RCU_DONE_TAIL] -- *nxttail[RCU_WAIT_TAIL] -- *nxttail[RCU_NEXT_READY_TAIL] -- *nxttail[RCU_NEXT_TAIL]。
rcu_do_batch只處理nxtlist -- *nxttail[RCU_DONE_TAIL]之間的callbacks。每一個GP結束都會從新調整callback所處的段位,每一個新的callback將會添加在末尾,也就是*nxttail[RCU_NEXT_TAIL]。
2 可搶佔的RCU
若是config文件定義了CONFIG_TREE_PREEMPT_RCU=y,那麼sychronize_rcu將默認使用rcu_preempt_state。這類rcu的特色就在於read_lock期間是容許其它進程搶佔的,所以它判斷寬限期度過的方法就不太同樣。
從rcu_read_lock和rcu_read_unlock的定義就能夠知道,TREE_PREEMPT_RCU並非以簡單的通過搶佔爲CPU渡過GP的標準,而是有個rcu_read_lock_nesting計數
void __rcu_read_lock(void){current->rcu_read_lock_nesting++;barrier();/* critical section after entry code. */} void __rcu_read_unlock(void){struct task_struct *t = current; if(t->rcu_read_lock_nesting !=1){--t->rcu_read_lock_nesting;}else{barrier();/* critical section before exit code. */t->rcu_read_lock_nesting = INT_MIN;barrier();/* assign before ->rcu_read_unlock_special load */if(unlikely(ACCESS_ONCE(t->rcu_read_unlock_special)))rcu_read_unlock_special(t);barrier();/* ->rcu_read_unlock_special load before assign */t->rcu_read_lock_nesting =0;}}
當搶佔發生時,__schedule函數會調用rcu_note_context_switch來通知RCU更新狀態,若是當前CPU處於rcu_read_lock狀態,當前進程將會放入rnp->blkd_tasks阻塞隊列,並呈如今rnp->gp_tasks鏈表中。
從上文1.3節寬限期的結束處理過程咱們能夠知道,rcu_gp_kthread會判斷! (rnp->qsmask) && !rcu_preempt_blocked_readers_cgp(rnp))兩個條件來決定GP是否完成,其中!rnp->qsmask表明每一個CPU都通過一次quiescent state,quiescent state的定義與傳統RCU一致;!rcu_preempt_blocked_readers_cgp(rnp)這個條件就表明了rcu是否還有阻塞的進程。