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