RCU機制及發布訂閱

2021-08-03 04:54:15 字數 4087 閱讀 4644

簡介

rcu(read-copy update)是資料同步的一種方式,在當前的linux核心中發揮著重要的作用。rcu主要針對的資料物件是鍊錶,目的是提高遍歷讀取資料的效率,為了達到目的使用rcu機制讀取資料的時候不對鍊錶進行耗時的加鎖操作。這樣在同一時間可以有多個執行緒同時讀取該鍊錶,並且允許乙個執行緒對鍊錶進行修改(修改的時候,需要加鎖)。rcu適用於需要頻繁的讀取資料,而相應修改資料並不多的情景,例如在檔案系統中,經常需要查詢定位目錄,而對目錄的修改相對來說並不多,這就是rcu發揮作用的最佳場景。

linux核心原始碼當中,關於rcu的文件比較齊全,你可以在 /documentation/rcu/ 目錄下找到這些檔案。paul e. mckenney 是核心中rcu原始碼的主要實現者,他也寫了很多rcu方面的文章。他把這些文章和一些關於rcu的**的鏈結整理到了一起。

在rcu的實現過程中,我們主要解決以下問題:

1,在讀取過程中,另外乙個執行緒刪除了乙個節點。刪除執行緒可以把這個節點從鍊錶中移除,但它不能直接銷毀這個節點,必須等到所有的讀取執行緒讀取完成以後,才進行銷毀操作。rcu中把這個過程稱為寬限期(grace period)。

2,在讀取過程中,另外乙個執行緒插入了乙個新節點,而讀執行緒讀到了這個節點,那麼需要保證讀到的這個節點是完整的。這裡涉及到了發布-訂閱機制(publish-subscribe mechanism)。

3, 保證讀取鍊錶的完整性。新增或者刪除乙個節點,不至於導致遍歷乙個鍊錶從中間斷開。但是rcu並不保證一定能讀到新增的節點或者不讀到要被刪除的節點。

通過例子,方便理解這個內容。以下例子修改於paul的文章。

[cpp]view plain

copy

struct foo ;  

define_spinlock(foo_mutex);  

struct foo *gbl_foo;  

void foo_read (void)  

void foo_update( foo* new_fp )    

如上的程式,是針對於全域性變數gbl_foo的操作。假設以下場景。有兩個執行緒同時執行 foo_ read和foo_update的時候,當foo_ read執行完賦值操作後,執行緒發生切換;此時另乙個執行緒開始執行foo_update並執行完成。當foo_ read執行的程序切換回來後,執行dosomething 的時候,fp已經被刪除,這將對系統造成危害。為了防止此類事件的發生,rcu裡增加了乙個新的概念叫寬限期(grace period)。如下圖所示:

圖中每行代表乙個執行緒,最下面的一行是刪除執行緒,當它執行完刪除操作後,執行緒進入了寬限期。寬限期的意義是,在乙個刪除動作發生後,它必須等待所有在寬限期開始前已經開始的讀執行緒結束,才可以進行銷毀操作。這樣做的原因是這些執行緒有可能讀到了要刪除的元素。圖中的寬限期必須等待1和2結束;而讀執行緒5在寬限期開始前已經結束,不需要考慮;而3,4,6也不需要考慮,因為在寬限期結束後開始後的執行緒不可能讀到已刪除的元素。為此rcu機制提供了相應的api來實現這個功能。                             

[cpp]view plain

copy

void foo_read(void)  

void foo_update( foo* new_fp )    

其中foo_read中增加了rcu_read_lock和rcu_read_unlock,這兩個函式用來標記乙個rcu讀過程的開始和結束。其實作用就是幫助檢測寬限期是否結束。foo_update增加了乙個函式synchronize_rcu(),呼叫該函式意味著乙個寬限期的開始,而直到寬限期結束,該函式才會返回。我們再對比著圖看一看,執行緒1和2,在synchronize_rcu之前可能得到了舊的gbl_foo,也就是foo_update中的old_fp,如果不等它們執行結束,就呼叫kfee(old_fp),極有可能造成系統崩潰。而3,4,6在synchronize_rcu之後執行,此時它們已經不可能得到old_fp,此次的kfee將不對它們產生影響。

寬限期是rcu實現中最複雜的部分,原因是在提高讀資料效能的同時,刪除資料的效能也不能太差。

當前使用的編譯器大多會對**做一定程度的優化,cpu也會對執行指令做一些優化調整,目的是提高**的執行效率,但這樣的優化,有時候會帶來不期望的結果。如例:

[cpp]view plain

copy

void foo_update( foo* new_fp )    

這段**中,我們期望的是6,7,8行的**在第10行**之前執行。但優化後的**並不對執行順序做出保證。在這種情形下,乙個讀執行緒很可能讀到 new_fp,但new_fp的成員賦值還沒執行完成。當讀執行緒執行dosomething(fp->a, fp->b , fp->c ) 的 時候,就有不確定的引數傳入到dosomething,極有可能造成不期望的結果,甚至程式崩潰。可以通過優化屏障來解決該問題,rcu機制對優化屏障做了包裝,提供了專用的api來解決該問題。這時候,第十行不再是直接的指標賦值,而應該改為 :

rcu_assign_pointer(gbl_foo,new_fp);

rcu_assign_pointer的實現比較簡單,如下:

[cpp]view plain

copy

#define rcu_assign_pointer(p, v) \

__rcu_assign_pointer((p), (v), __rcu)  

#define __rcu_assign_pointer(p, v, space) \

do  while (0)  

我們可以看到它的實現只是在賦值之前加了優化屏障 smp_wmb來確保**的執行順序。另外就是巨集中用到的__rcu,只是作為編譯過程的檢測條件來使用的。

在dec alpha cpu機器上還有一種更強悍的優化,如下所示:

[cpp]view plain

copy

void foo_read(void)    

第六行的 fp->a,fp->b,fp->c會在第3行還沒執行的時候就預先判斷執行,當他和foo_update同時執行的時候,可能導致傳入dosomething的一部分屬於舊的gbl_foo,而另外的屬於新的。這樣導致執行結果的錯誤。為了避免該類問題,rcu還是提供了巨集來解決該問題:

[cpp]view plain

copy

#define rcu_dereference(p) rcu_dereference_check(p, 0)

#define rcu_dereference_check(p, c) \

__rcu_dereference_check((p), rcu_read_lock_held() || (c), __rcu)  

#define __rcu_dereference_check(p, c, space) \

()  

static

inline

int rcu_read_lock_held(void)    

這段**中加入了除錯資訊,去除除錯資訊,可以是以下的形式(其實這也是舊版本中的**):

[cpp]view plain

copy

#define rcu_dereference(p)     ()  

在賦值後加入優化屏障smp_read_barrier_depends()。

我們之前的第四行**改為 foo *fp = rcu_dereference(gbl_foo);,就可以防止上述問題。

還是通過例子來說明這個問題:

我們再看一下刪除乙個節點的例子:

rcu的原理並不複雜,應用也很簡單。但**的實現確並不是那麼容易,難點都集中在了寬限期的檢測上,後續分析源**的時候,我們可以看到一些極富技巧的實現方式。

Redis發布訂閱機制

redis是乙個開源的記憶體資料庫,它以鍵值對的形式儲存資料。由於資料儲存在記憶體中,因此redis的速度很快,但是每次重啟redis服務時,其中的資料也會丟失,因此,redis也提供了持久化儲存機制,將資料以某種形式儲存在檔案中,每次重啟時,可以自動從檔案載入資料到記憶體當中。redis的架構包括...

Redis 發布訂閱機制詳解

程序間的一種訊息通訊模式 傳送者 pub 傳送訊息,訂閱者 sub 接收訊息。聯想諸多訊息中介軟體的發布訂閱模式,但是redis大多用來作為基於記憶體的分布式快取,企業中訊息中介軟體多用activemq ribbitmq等。下圖展示了頻道channel1,以及訂閱這個頻道的三個客戶端 client2...

Redis之發布 訂閱機制

相關命令 publish 發布 subscribe 訂閱 psubscribe 一種訂閱符合給定模式的所有頻道的方法 unsubscribe 退訂 punsubscribe 退訂乙個訂閱的模式這些命令被廣泛用於構建即時通訊應用,比如網路聊天室 chatroom 和實時廣播 實時提醒等。redis相關...