Linux RCU 機制詳解

来源:https://www.cnblogs.com/linhaostudy/archive/2018/02/23/8463529.html
-Advertisement-
Play Games

1、簡介: RCU(Read-Copy Update)是數據同步的一種方式,在當前的Linux內核中發揮著重要的作用。 RCU主要針對的數據對象是鏈表,目的是提高遍歷讀取數據的效率,為了達到目的使用RCU機制讀取數據的時候不對鏈表進行耗時的加鎖操作。這樣在同一時間可以有多個線程同時讀取該鏈表,並且允 ...


1、簡介:

RCU(Read-Copy Update)是數據同步的一種方式,在當前的Linux內核中發揮著重要的作用。

RCU主要針對的數據對象是鏈表,目的是提高遍歷讀取數據的效率,為了達到目的使用RCU機制讀取數據的時候不對鏈表進行耗時的加鎖操作。這樣在同一時間可以有多個線程同時讀取該鏈表,並且允許一個線程對鏈表進行修改(修改的時候,需要加鎖)。

 

2、應用場景:

RCU適用於需要頻繁的讀取數據,而相應修改數據並不多的情景,例如在文件系統中,經常需要查找定位目錄,而對目錄的修改相對來說並不多,這就是RCU發揮作用的最佳場景。

 

3、相應資料:

Linux內核源碼當中,關於RCU的文檔比較齊全,你可以在 /Documentation/RCU/ 目錄下找到這些文件。

Paul E. McKenney 是內核中RCU源碼的主要實現者,他也寫了很多RCU方面的文章。他把這些文章和一些關於RCU的論文的鏈接整理到了一起。相應鏈接如下:

http://www2.rdrop.com/users/paulmck/RCU/

 

4、實現過程:

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

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

2,在讀取過程中,另外一個線程插入了一個新節點,而讀線程讀到了這個節點,那麼需要保證讀到的這個節點是完整的。這裡涉及到了發佈-訂閱機制(Publish-Subscribe Mechanism)。

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

 

4.1 寬限期:

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

 1 struct foo{
 2     int a;
 3     char b;
 4     long c;
 5 };
 6 
 7 DEFINE_SPINLOCK(foo_mutex);
 8 
 9 void foo_read(void)
10 {
11     foo *fp = gbl_foo;
12     if( fp != NULL )
13     {
14         dosomthing(fp->a, fp->b, fp->c);
15     }
16 }
17 
18 void foo_update(foo * new_fp)
19 {
20     spin_lock(&foo_mutex);
21     foo *old_fp = gbl_foo;
22     gbl_foo = new_fp;
23     spin_unlock(&foo_mutex);
24 }

 如上的程式,是針對於全局變數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來實現這個功能。

 

 1 void foo_read(void)
 2 {
 3     rcu_read_lock();
 4     foo *fp = gbl_foo;
 5     if( fp != NULL )
 6         dosomthing(fp->a, fp->b, fp->c);
 7     rcu_read_unlock();
 8 }
 9 
10 void foo_update(foo *new_fp)
11 {
12     spin_lock(&foo_mutex);
13     foo *old_fp = gbl_foo;
14     gbl_foo = new_fp;
15     spin_unlock(&foo_mutex);
16     synchronize_rcu();
17     kfree(old_fp);
18 }

 

 

其中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實現中最複雜的部分,原因是在提高讀數據性能的同時,刪除數據的性能也不能太差。

 

 4.2 訂閱——發佈機制:

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

 1 void foo_update(foo *new_fp)
 2 {
 3     spin_lock(&foo_mutex);
 4     foo *old_fp = gbl_foo;
 5     
 6     new_fp->a = 1;
 7     new_fp->b = 'b';
 8     new_fp->c = 100;
 9     
10     gbl_foo = new_fp;
11     spin_unlock(&foo_mutex);
12     synchronize_rcu();
13     kfree(old_fp);
14 }

 

 

這段代碼中,我們期望的是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的實現比較簡單,如下:

1 #define rcu_assign_pointer(p, v) \
2     __rcu_assign_pointer((p), (v), __rcu)

 

1 #define RCU_INIT_POINTER(p, v) \
2         p = (typeof(*v) __force __rcu *)(v)

 

 

在DEC Alpha CPU機器上還有一種更強悍的優化,如下所示:

1 void foo_read(void)
2 {
3     rcu_read_lock();
4     foo *fp = gbl_foo;
5     if( fp != NULL )
6         dosomthing(fp->a, fp->b, fp->c);
7     rcu_read_unlock();
8 }

 

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

 1 #define rcu_dereference_check(p, c) \
 2     __rcu_dereference_check((p), rcu_read_lock_held() || (c), __rcu)
 3 
 4 #define __rcu_dereference_check(p, c, space) \
 5     ({ \
 6         typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); \
 7         rcu_lockdep_assert(c, "suspicious rcu_dereference_check()" \
 8                       " usage"); \
 9         rcu_dereference_sparse(p, space); \
10         smp_read_barrier_depends(); \
11         ((typeof(*p) __force __kernel *)(_________p1)); \
12     })
13 
14 static inline int rcu_read_lock_held(void)
15 {
16     if (!debug_lockdep_rcu_enabled())
17         return 1;
18     if (rcu_is_cpu_idle())
19         return 0;
20     if (!rcu_lockdep_current_cpu_online())
21         return 0;
22     return lock_is_held(&rcu_lock_map);
23 }

 

這段代碼中加入了調試信息,去除調試信息,可以是以下的形式(其實這也是舊版本中的代碼):

1 #define rcu_dereference_check(p) ({\
2                                     typeof(p) _____p1  = p; \
3                                     smp_read_barrier_depends(); \
4                                     (_____p1); \
5                                   })

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

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

 

4.3 數據讀取的完整性:

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

 

如圖我們在原list中加入一個節點new到A之前,所要做的第一步是將new的指針指向A節點,第二步才是將Head的指針指向new。這樣做的目的是當插入操作完成第一步的時候,對於鏈表的讀取並不產生影響,而執行完第二步的時候,讀線程如果讀到new節點,也可以繼續遍歷鏈表。如果把這個過程反過來,第一步head指向new,而這時一個線程讀到new,由於new的指針指向的是Null,這樣將導致讀線程無法讀取到A,B等後續節點。從以上過程中,可以看出RCU並不保證讀線程讀取到new節點。如果該節點對程式產生影響,那麼就需要外部調用做相應的調整。如在文件系統中,通過RCU定位後,如果查找不到相應節點,就會進行其它形式的查找,相關內容等分析到文件系統的時候再進行敘述。

 

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

 

如圖我們希望刪除B,這時候要做的就是將A的指針指向C,保持B的指針,然後刪除程式將進入寬限期檢測。由於B的內容並沒有變更,讀到B的線程仍然可以繼續讀取B的後續節點。B不能立即銷毀,它必須等待寬限期結束後,才能進行相應銷毀操作。由於A的節點已經指向了C,當寬限期開始之後所有的後續讀操作通過A找到的是C,而B已經隱藏了,後續的讀線程都不會讀到它。這樣就確保寬限期過後,刪除B並不對系統造成影響。

 

5、小結:

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 10069 Accepted Submission(s): 4289 Problem Descri ...
  • 1 2 3 ' /> 4 ' CommandName="btnUpdate" /> 5 6 7 8 string Id = e.CommandArgument.ToString(); 9 ... ...
  • 1. 資料庫訪問性能優化 資料庫的連接和關閉 訪問資料庫資源需要創建連接、打開連接和關閉連接幾個操作。這些過程需要多次與資料庫交換信息以通過身份驗證,比較耗費伺服器資源。ASP.NET中提供了連接池(Connection Pool)改善打開和關閉資料庫對性能的影響。系統將用戶的資料庫連接放在連接池中 ...
  • 運行程式,如果資料庫已經存在,則刪除重建。當打開 連接以及單獨使用OpenAsync和ExecuteNonQueryAsync方法執行SQL命令時,我們使用了I/O非同步操作。 在這個任務完成後,我們創建了一張新的表並插入了一些數據,除了之前提到的方法,我們還使用了ExceuteS... ...
  • 2017晃眼就過去了,2018開工了。閑暇之餘,我想為自己訂個小小目標。 作為一名.NET菜逼程式員。 肯定是希望自己在2018學的更多,做的更多,具體的我想了想: 2017年工作也算比較順利,換了兩家公司,感覺任務都不是特別繁重,也很少加班。第一家公司是做WPF的,我之前沒有做過,但公司願意給我時 ...
  • 1、破解版下載&安裝 參考:https://bbs.feng.com/read htm tid 6939481.html 2、session導入 查看 SecureCRT Preferences General Configuration Paths ,就可以查看session具體目錄,將已有ses ...
  • 前言 今天部門老大開了一個會,期間說到,以後我們部署東西,都是純粹命令部署的,但是用習慣了windows的自己,剛開始用Linux真的是一種折磨,但是折磨歸折磨,想進步就要能夠忍受折磨,所以,還能怎麼辦呢?只好在虛擬機中裝一個沒有界面的centOS,然後藉助secureCRT進行相關連接了.... ...
  • HAProxy介紹 HAProxy HAProxy支持兩種主要的代理模式 HAProxy負載均衡策略非常多 HAProxy優缺點 HAProxy功能 HAProxy 組成 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...