Linux中的RCU機制
時間:2021-11-12 14:00:15
手機看文章
掃描二維碼
隨時隨地手機看文章
[導(dǎo)讀]內(nèi)容基本原理使用方法GP的生命周期QS的判定與標(biāo)記優(yōu)勢何在存在的問題RCU機制是自內(nèi)核2.5版本引入的(2002年10月),而后不斷完善,其在Linux的locking機制中的使用占比也是逐年攀升。1.基本原理RCU的基本思想是這樣的:先創(chuàng)建一個舊數(shù)據(jù)的copy,然后writer...
內(nèi)容
RCU機制是自內(nèi)核2.5版本引入的(2002年10月),而后不斷完善,其在Linux的locking機制中的使用占比也是逐年攀升。 1.基本原理RCU的基本思想是這樣的:先創(chuàng)建一個舊數(shù)據(jù)的copy,然后writer更新這個copy,最后再用新的數(shù)據(jù)替換掉舊的數(shù)據(jù)。這樣講似乎比較抽象,那么結(jié)合一個實例來看或許會更加直觀。
假設(shè)有一個單向鏈表,其中包含一個由指針p指向的節(jié)點: 現(xiàn)在,我們要使用RCU機制來更新這個節(jié)點的數(shù)據(jù),那么首先需要分配一段新的內(nèi)存空間(由指針q指向),用于存放這個copy。 然后將p指向的節(jié)點數(shù)據(jù),以及它和下一節(jié)點[11, 4, 8]的關(guān)系,都完整地copy到q指向的內(nèi)存區(qū)域中。 接下來,writer會修改這個copy中的數(shù)據(jù)(將[5, 6, 7]修改為[5, 2, 3])。 修改完成之后,writer就可以將這個更新“發(fā)布”了(publish),對于reader來說就“可見”了。因此,pubulish之后才開始讀取操作的reader(比如讀節(jié)點[1, 2, 3]的下一個節(jié)點),得到的就是新的數(shù)據(jù)[5, 2, 3](圖中紅色邊框表示有reader在引用)。 而在publish之前就開始讀取操作的reader則不受影響,依然使用舊的數(shù)據(jù)[5, 6, 7]。 等到所有引用舊數(shù)據(jù)區(qū)的reader都完成了相關(guān)操作,writer才會釋放由p指向的內(nèi)存區(qū)域。 可見,在此期間,reader如果讀取這個節(jié)點的數(shù)據(jù),得到的要么全是舊的數(shù)據(jù),要么全是新的數(shù)據(jù),反正不會是「半新半舊」的數(shù)據(jù),數(shù)據(jù)的一致性是可以保證的。
重要的是,RCU中的reader不用像rwlock中的reader那樣,在writer操作期間必須spin等待了。 RCU的全稱是"read copy update",可以這樣來理解:read和進(jìn)行copy的線程并行,目的是為了update。好像有點"copy on write"的意思?反正有人覺得RCU的命名不夠準(zhǔn)確,寧愿叫它"publish protocol"(比如 Fedor Pikus)。不管怎樣,RCU的命名已經(jīng)成了業(yè)界默認(rèn)的,我們還是就叫它RCU吧。
那RCU具體應(yīng)該如何使用呢?這得走進(jìn)真正的代碼,才能一探究竟。2.使用方法前面的例子為了簡化,采用的是單向鏈表來演示,這里我們切換到Linux中常用的雙向鏈表上來。由于RCU可理解為是基于rwlock演進(jìn)而來的,所以筆者將結(jié)合上文講解的rwlock的用法,來對比討論RCU的使用。
假設(shè)現(xiàn)在reader正在遍歷/查詢一個鏈表,而writer正在刪除該鏈表中的一個節(jié)點。那么,使用rwlock(左)和RCU(右)來實現(xiàn)的讀取一側(cè)的代碼分別是這樣的: 同rwlock類似,rcu_read_lock()和rcu_read_unlock()界定了RCU讀取一側(cè)的critical section。如果在內(nèi)核配置時選擇了"CONFIG_PREEMPT",那么這2個函數(shù)實際要做的工作僅僅是分別關(guān)閉和打開CPU的可搶占性而已,等同于preempt_disable()和preempt_enable()。
這種命名,體現(xiàn)了RCU和rwlock的「一脈相承」,但在RCU的讀取一側(cè),其實并沒有什么"lock",所以可能命名為rcu_enter()和rcu_exit()之類的更加貼切。
在寫入一側(cè)的RCU實現(xiàn)中,為了防止多個writer對鏈表的同時操作,使用了一個標(biāo)準(zhǔn)的spinlock。list_del_rcu()的實現(xiàn)和普通的list_del()基本一致,但多了一個對"prev"指針的"poison"處理,以避免接下來reader再通過該節(jié)點訪問前向節(jié)點。
static inline void list_del_rcu(struct list_head *entry){
__list_del_entry(entry);
entry->prev = LIST_POISON2;}
此外,在調(diào)用kfree()釋放節(jié)點之前,多了一個synchronize_rcu()函數(shù)。synchronize就是「同步」,那它在和誰同步呢?
就是前面說的那些“引用舊數(shù)據(jù)區(qū)的reader”啦,因為此時它們可能還在引用指針p。這相當(dāng)于給了這些reader一個優(yōu)雅退出的寬限區(qū),因此這段同步等待的時間被稱為Grace Period(簡稱GP)。 不過,必須是在synchronize之前就已經(jīng)進(jìn)入critical section的reader才可以,至于之后的reader么,直接讀新的數(shù)據(jù)就可以了,用不著writer來等待。
比如下面這個場景中,作為writer的CPU 1只會等待CPU 0,CPU 0離開critical section后就結(jié)束同步,而不會理會CPU 2。 也許,把synchronize_rcu()改名成wait_for_readers_to_leave()會更加直觀。
第一個參數(shù)的類型是struct rcu_head,它的定義是這樣的:struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_headCPU調(diào)用call_rcu()后就可以離開去做其他事情了,之后它完全可能再次調(diào)用call_rcu(),所以它每次注冊的回調(diào)函數(shù),需要通過"next"指針排隊串接起來,等grace period結(jié)束后,依次執(zhí)行。如果需要處理的回調(diào)函數(shù)比較多,可能需要分批進(jìn)行。
第二個參數(shù)就是前面講的回調(diào)函數(shù),其功能主要就是釋放掉“舊指針”指向的內(nèi)存空間。
來看一個使用call_rcu()的具體實例:call_rcu(
基本原理
使用方法
- GP 的生命周期
QS 的判定與標(biāo)記
優(yōu)勢何在
存在的問題
RCU機制是自內(nèi)核2.5版本引入的(2002年10月),而后不斷完善,其在Linux的locking機制中的使用占比也是逐年攀升。
假設(shè)有一個單向鏈表,其中包含一個由指針p指向的節(jié)點:
重要的是,RCU中的reader不用像rwlock中的reader那樣,在writer操作期間必須spin等待了。
那RCU具體應(yīng)該如何使用呢?這得走進(jìn)真正的代碼,才能一探究竟。2.使用方法前面的例子為了簡化,采用的是單向鏈表來演示,這里我們切換到Linux中常用的雙向鏈表上來。由于RCU可理解為是基于rwlock演進(jìn)而來的,所以筆者將結(jié)合上文講解的rwlock的用法,來對比討論RCU的使用。
假設(shè)現(xiàn)在reader正在遍歷/查詢一個鏈表,而writer正在刪除該鏈表中的一個節(jié)點。那么,使用rwlock(左)和RCU(右)來實現(xiàn)的讀取一側(cè)的代碼分別是這樣的:
這種命名,體現(xiàn)了RCU和rwlock的「一脈相承」,但在RCU的讀取一側(cè),其實并沒有什么"lock",所以可能命名為rcu_enter()和rcu_exit()之類的更加貼切。
在寫入一側(cè)的RCU實現(xiàn)中,為了防止多個writer對鏈表的同時操作,使用了一個標(biāo)準(zhǔn)的spinlock。list_del_rcu()的實現(xiàn)和普通的list_del()基本一致,但多了一個對"prev"指針的"poison"處理,以避免接下來reader再通過該節(jié)點訪問前向節(jié)點。
static inline void list_del_rcu(struct list_head *entry){
__list_del_entry(entry);
entry->prev = LIST_POISON2;}
此外,在調(diào)用kfree()釋放節(jié)點之前,多了一個synchronize_rcu()函數(shù)。synchronize就是「同步」,那它在和誰同步呢?
就是前面說的那些“引用舊數(shù)據(jù)區(qū)的reader”啦,因為此時它們可能還在引用指針p。這相當(dāng)于給了這些reader一個優(yōu)雅退出的寬限區(qū),因此這段同步等待的時間被稱為Grace Period(簡稱GP)。
比如下面這個場景中,作為writer的CPU 1只會等待CPU 0,CPU 0離開critical section后就結(jié)束同步,而不會理會CPU 2。
等待與回調(diào)如果grace period的時間比較長,writer這么干等著,豈不是會影響這個CPU上更高優(yōu)先級的任務(wù)執(zhí)行?在這種情況下,可以使用基于callback機制的call_rcu()來替換synchronize_rcu()。void call_rcu(struct rcu_head *head, rcu_callback_t func);call_rcu()會注冊一個回調(diào)函數(shù)"func",當(dāng)所有的reader都退出critical section后,該回調(diào)函數(shù)將被執(zhí)行。
第一個參數(shù)的類型是struct rcu_head,它的定義是這樣的:struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_headCPU調(diào)用call_rcu()后就可以離開去做其他事情了,之后它完全可能再次調(diào)用call_rcu(),所以它每次注冊的回調(diào)函數(shù),需要通過"next"指針排隊串接起來,等grace period結(jié)束后,依次執(zhí)行。如果需要處理的回調(diào)函數(shù)比較多,可能需要分批進(jìn)行。
第二個參數(shù)就是前面講的回調(diào)函數(shù),其功能主要就是釋放掉“舊指針”指向的內(nèi)存空間。
來看一個使用call_rcu()的具體實例:call_rcu(