rcu的理解

RCU(Read-copy update)是於2012年10月引入內核的同步機制,是讀寫鎖的一種.

RCU的updater(寫者)會先複製一份指針指向的數據進行修改,然後修改指針指向修改後的數據,然後就等啊等啊等.
爲啥要等啊?
因爲原先指針指向的數據上可能有讀者(reader),reader自然不會負責回收這個數據所佔的空間.如果updater立即回收,乃乾的偷天換日的勾當就暴露了.
等到啥時候纔是頭呢? 這個時機是所有讀者cpu(實際上不是所有,而是修改指針那一刻的readers)都經歷了一次'quiescent state'
這保證了所有的cpu都不在使用這份數據的原版
術語介紹:
    quiescent state:
        當某個cpu不位於臨界區時,稱其處於該狀態,意思是不吵鬧着要臨界區資源
        但實際中,只是檢測是否處於用戶態上下文或idle線程上下文且不處於中斷上下文
    grace period:
        updater修改指針後,開始等待所有cpu都經歷一次quiescent state,當條件滿足,說明一個'grace period'的等待完成了.時機已經成熟
updaters之間要使用額外的鎖同步

SRCU使得grace period相較之前的實現縮短了不少,這對KVM意義重大.

rcu_access_index()和rcu_access_pointer()
    它們只是讀內存而已.不會觸及鎖依賴問題.所以可以在RCU臨界區之外使用

rcu_dereference_raw_notrace()
rcu_is_watching()
hlist_for_each_entry_rcu_notrace()
    鎖的依賴分析會使用RCU機制,提供這些原語用於依賴分析中來避免第歸

rcu_lockdep_assert()用於檢查當前是否滿足指定的RCU臨界區條件,需要CONFIG_PROVE_RCU=y

RCU_NONIDLE()臨時性在非idle狀態執行某個語句
rcu_is_watching()檢查非srcu的讀臨界區是否合法,與RCU_NONIDLE()對應
在idle狀態中進行跟蹤分析時,要使用*_rcuidle()形式

synchronize_rcu_expedited()相對於synchronize_rcu()會讓grace period縮短,但會造成所有cpu的瞬時延遲

call_rcu()異步執行grace period回調(垃圾回收/析構?)的接口
rcu_barrier()用於等待所有RCU回調執行完畢
kfree_rcu()雖然簡便,但需要注意,它只是自動執行釋放內存的任務.如果還有其他的析構動作,考慮前兩個方法
call_rcu()&rcu_barrier()與kfree_rcu()
    這常用於不想等待,註冊個回調,把空間回收的任務交代出去,然後就可以幹別的事了
    前兩者的方式代碼量要多些,而且如果是在模塊中使用,在模塊卸載前要調rcu_barrier()
    kfree_rcu()代碼簡潔,且不用在模塊卸載前調rcu_barrier()

rcu_read_lock_held()用於判定是不是位於RCU讀臨界區中,但在CONFIG_PROVE_RCU=n的時候不一定準

*_bh()代表臨界區中禁止了softirq,讓臨界區處理不受softirq打擾.用來避免因網絡DOS攻擊導致的OOM.

*_sched()
    代表臨界區中禁止了搶佔,雖然禁止搶佔有相同效果,但這是官方版本,高大上.
    CONFIG_TREE_PREEMPT_RCU=y的時候就表這樣想了,因爲RCU臨界區是可以搶佔的了

srcu
    指在讀臨界區中是允許睡眠的RCU
    但不代表可以在讀臨界區中調用synchronize_srcu()
    這種鎖有內存表示(就像其他鎖一樣)

可以用RCU_INIT_POINTER()替代rcu_assign_pointer()的情況
    指針賦值NULL
    讀者還沒生出來,比如初始化
    指針指向的數據有讀者但數據的改動部分讀者恰好不使用
    指針指向的數據有讀者但讀者用原來的數據不會出問題

__rcu用於修飾RCU中的指針

RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基於其原理命名的。對於被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然後對副本進行修改,最後使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。這個時機就是所有引用該數據的CPU都退出對共享數據的操作。

因此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之後調用寫者註冊的回調函數來完成真正的數據修改或數據釋放操作的。

以下以鏈表元素刪除爲例詳細說明這一過程。

寫者要從鏈表中刪除元素 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。

圖 2 使用 RCU 進行鏈表刪除操作
圖 2 使用 RCU 進行鏈表刪除操作

三、RCU 實現機制

按照第二節所講原理,對於讀者,RCU 僅需要搶佔失效,因此獲得讀鎖和釋放讀鎖分別定義爲:

#define rcu_read_lock()         preempt_disable()
#define rcu_read_unlock()       preempt_enable()

它們有一個變種:

#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。該函數也會被時鐘中斷觸發調用。

時鐘中斷觸發垃圾收集器運行,它會檢查:

  1. 否在該CPU上有需要處理的回調函數並且已經經過一個grace period;
  2. 否沒有需要處理的回調函數但有註冊的回調函數;
  3. 否該CPU已經完成回調函數的處理;
  4. 否該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 API

rcu_read_lock()

讀者在讀取由RCU保護的共享數據時使用該函數標記它進入讀端臨界區。

rcu_read_unlock()

該函數與rcu_read_lock配對使用,用以標記讀者退出讀端臨界區。夾在這兩個函數之間的代碼區稱爲"讀端臨界區"(read-side critical section)。讀端臨界區可以嵌套,如圖3,臨界區2被嵌套在臨界區1內。

圖 3 嵌套讀端臨界區示例
圖 3 嵌套讀端臨界區示例

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,因此當前它們的功能實際是完全等同的,但是將來將可能有大的變化,因此務必根據需求選擇恰當的函數。

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。

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。

#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的開頭。使用內存柵保證了在引用這個新插入的鏈表項之前,新鏈表項的鏈接指針的修改對所有讀者是可見的。

static inline void list_add_tail_rcu(struct list_head *new,
                                        struct list_head *head)

該函數類似於list_add_rcu,它將把新的鏈表項new添加到被RCU保護的鏈表的末尾。

static inline void list_del_rcu(struct list_head *entry)

該函數從RCU保護的鏈表中移走指定的鏈表項entry,並且把entry的prev指針設置爲LIST_POISON2,但是並沒有把entry的next指針設置爲LIST_POISON1,因爲該指針可能仍然在被讀者用於便利該鏈表。

static inline void list_replace_rcu(struct list_head *old, struct list_head *new)

該函數是RCU新添加的函數,並不存在非RCU版本。它使用新的鏈表項new取代舊的鏈表項old,內存柵保證在引用新的鏈表項之前,它的鏈接指針的修正對所有讀者可見。

list_for_each_rcu(pos, head)

該宏用於遍歷由RCU保護的鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu鏈表操作函數(如list_add_rcu)併發運行。

list_for_each_safe_rcu(pos, n, head)

該宏類似於list_for_each_rcu,但不同之處在於它允許安全地刪除當前鏈表項pos。

list_for_each_entry_rcu(pos, head, member)

該宏類似於list_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構。

list_for_each_continue_rcu(pos, head)

該宏用於在退出點之後繼續遍歷由RCU保護的鏈表head。

static inline void hlist_del_rcu(struct hlist_node *n)

它從由RCU保護的哈希鏈表中移走鏈表項n,並設置n的ppre指針爲LIST_POISON2,但並沒有設置next爲LIST_POISON1,因爲該指針可能被讀者使用用於遍利鏈表。

static inline void hlist_add_head_rcu(struct hlist_node *n,
                                        struct hlist_head *h)

該函數用於把鏈表項n插入到被RCU保護的哈希鏈表的開頭,但同時允許讀者對該哈希鏈表的遍歷。內存柵確保在引用新鏈表項之前,它的指針修正對所有讀者可見。

hlist_for_each_rcu(pos, head)

該宏用於遍歷由RCU保護的哈希鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu哈希鏈表操作函數(如hlist_add_rcu)併發運行。

hlist_for_each_entry_rcu(tpos, pos, head, member)

類似於hlist_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構哈希鏈表,當前鏈表項pos爲一包含struct list_head結構的特定的數據結構。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章