正確處理和使用 rcu_dereference() 的返回值

正確處理和使用地址和資料依賴關係對於正確使用 RCU 等機制至關重要。為此,從 rcu_dereference() 系列原語返回的指標帶有地址和資料依賴關係。這些依賴關係從 rcu_dereference() 宏載入指標延伸到以後使用該指標來計算後續記憶體訪問的地址(表示地址依賴關係)或後續記憶體訪問寫入的值(表示資料依賴關係)。

大多數情況下,這些依賴關係會被保留,允許您自由地使用來自 rcu_dereference() 的值。例如,解引用(字首“*”)、欄位選擇(“->”)、賦值(“=”)、取地址(“&”)、型別轉換以及常量的加法或減法都可以自然而安全地工作。但是,由於當前的編譯器沒有考慮地址或資料依賴關係,因此仍然有可能遇到問題。

請遵循以下規則來保留從您對 rcu_dereference() 及其朋友的呼叫中發出的地址和資料依賴關係,從而使您的 RCU 讀取器正常工作。

  • 您必須使用 rcu_dereference() 系列原語之一來載入受 RCU 保護的指標,否則 CONFIG_PROVE_RCU 會報錯。更糟糕的是,由於編譯器和 DEC Alpha 可以進行的各種操作,您的程式碼可能會出現隨機的記憶體損壞錯誤。如果沒有 rcu_dereference() 原語之一,編譯器可以重新載入該值,並且您的程式碼會因為單個指標的兩個不同值而感到快樂!如果沒有 rcu_dereference(),DEC Alpha 可以載入指標,解引用該指標,並返回先於指標儲存的初始化的資料。(稍後會注意到,在最近的核心中,READ_ONCE() 也阻止 DEC Alpha 玩這些技巧。)

    此外,rcu_dereference() 中的 volatile 強制型別轉換阻止編譯器推匯出生成的指標值。請參閱標題為“編譯器知道得太多的示例”的部分,以瞭解編譯器實際上可以推匯出指標的確切值,從而導致錯誤排序的示例。

  • 在新增資料但在讀取器訪問該結構時從未刪除資料的特殊情況下,可以使用 READ_ONCE() 代替 rcu_dereference()。在這種情況下,READ_ONCE() 的使用承擔了在 v4.15 中刪除的 lockless_dereference() 原語的角色。

  • 您只能在指標值上使用 rcu_dereference()。編譯器對整數值的瞭解太多,無法信任它將依賴關係傳遞給整數運算。但是,有少數例外情況,即您可以暫時將指標強制轉換為 uintptr_t,以便

    • 設定位並清除該指標的必須為零的低階位。這顯然意味著該指標必須具有對齊約束,例如,這對於 char* 指標通常起作用。

    • XOR 位來轉換指標,就像在一些經典的 buddy-allocator 演算法中所做的那樣。

    在用它做任何其他事情之前,將值強制轉換回指標非常重要。

  • 使用“+”和“-”中綴算術運算子時,避免取消。例如,對於給定的變數“x”,對於 char* 指標,避免使用“(x-(uintptr_t)x)”。編譯器有權用零替換此類表示式,因此後續訪問不再依賴於 rcu_dereference(),同樣可能由於錯誤排序而導致錯誤。

    當然,如果“p”是來自 rcu_dereference() 的指標,並且“a”和“b”是恰好相等的整數,則表示式“p+a-b”是安全的,因為它的值仍然必須依賴於 rcu_dereference(),從而保持正確的順序。

  • 如果您使用 RCU 來保護 JITed 函式,以便將“()”函式呼叫運算子應用於從 rcu_dereference()(直接或間接)獲得的值,則可能需要直接與硬體互動以重新整理指令快取。當新 JITed 函式使用先前 JITed 函式使用的相同記憶體時,某些系統會出現此問題。

  • 解引用時不要使用關係運算符(“==”、“!=”、“>”、“>=”、“<”或“<=”)的結果。例如,以下(非常奇怪的)程式碼有錯誤

    int *p;
    int *q;
    
    ...
    
    p = rcu_dereference(gp)
    q = &global_q;
    q += p > &oom_p;
    r1 = *q;  /* BUGGY!!! */
    

    與之前一樣,出現此錯誤的原因是關係運算符通常使用分支進行編譯。與之前一樣,儘管 ARM 或 PowerPC 等弱記憶體機器確實在這些分支之後對儲存進行排序,但可以推測載入,這可能會再次導致錯誤排序的錯誤。

  • 比較從 rcu_dereference() 獲得的指標與非 NULL 值時要非常小心。正如 Linus Torvalds 所解釋的那樣,如果兩個指標相等,編譯器可以將您要比較的指標替換為從 rcu_dereference() 獲得的指標。例如

    p = rcu_dereference(gp);
    if (p == &default_struct)
            do_default(p->a);
    

    因為編譯器現在知道“p”的值正是變數“default_struct”的地址,因此可以自由地將此程式碼轉換為以下程式碼

    p = rcu_dereference(gp);
    if (p == &default_struct)
            do_default(default_struct.a);
    

    在 ARM 和 Power 硬體上,現在可以推測從“default_struct.a”載入,使其可能在 rcu_dereference() 之前發生。這可能會導致由於錯誤排序而導致的錯誤。

    但是,在以下情況下,比較是可以的

    • 比較是針對 NULL 指標的。如果編譯器知道指標為 NULL,那麼您最好不要解引用它。如果比較不相等,編譯器就不會更聰明瞭。因此,將來自 rcu_dereference() 的指標與 NULL 指標進行比較是安全的。

    • 比較後永遠不會解引用指標。由於沒有後續解引用,因此編譯器不能使用從比較中學到的任何內容來重新排序不存在的後續解引用。掃描受 RCU 保護的迴圈連結串列時經常發生這種比較。

      請注意,如果在 RCU 讀取端臨界區之外進行指標比較,並且從不解引用指標,則應使用 rcu_access_pointer() 代替 rcu_dereference()。在大多數情況下,最好透過直接測試 rcu_access_pointer() 返回值而不將其分配給變數來避免意外解引用。

      在 RCU 讀取端臨界區內,幾乎沒有理由使用 rcu_access_pointer()

    • 比較是針對引用“很久以前”初始化的記憶體的指標。出現這種情況是安全的,原因在於即使發生錯誤排序,錯誤排序也不會影響比較之後的訪問。那麼“很久以前”到底有多久?以下是一些可能性

      • 編譯時。

      • 啟動時間。

      • 模組程式碼的模組初始化時間。

      • kthread 程式碼的 kthread 建立之前。

      • 在之前獲取我們現在持有的鎖期間。

      • 計時器處理程式的 mod_timer() 時間之前。

      還有許多其他可能性涉及 Linux 核心的各種原語,這些原語會導致程式碼稍後被呼叫。

    • 要比較的指標也來自 rcu_dereference()。在這種情況下,兩個指標都依賴於一個 rcu_dereference() 或另一個,因此無論哪種方式,您都可以獲得正確的排序。

      也就是說,這種情況可能會使某些 RCU 用法錯誤更容易發生。至少如果在測試期間發生,這可能是一件好事。標題為“放大 RCU 用法錯誤的示例”的部分中顯示了此類 RCU 用法錯誤的示例。

    • 比較之後的所有訪問都是儲存,因此控制依賴性保留了所需的排序。也就是說,很容易弄錯控制依賴性。有關更多詳細資訊,請參閱 Documentation/memory-barriers.txt 的“控制依賴性”部分。

    • 指標不相等並且編譯器沒有足夠的資訊來推斷指標的值。請注意,rcu_dereference() 中的 volatile 強制型別轉換通常會阻止編譯器瞭解太多資訊。

      但是,請注意,如果編譯器知道指標只採用兩個值之一,則不相等比較將提供編譯器推斷指標值所需的精確資訊。

  • 停用編譯器可能提供的任何值推測最佳化,尤其是在您使用基於反饋的最佳化(採用從先前執行中收集的資料)時。此類值推測最佳化透過設計重新排序操作。

    此規則有一個例外:利用分支預測硬體的值推測最佳化在強排序系統(例如 x86)上是安全的,但在弱排序系統(例如 ARM 或 Power)上則不安全。明智地選擇您的編譯器命令列選項!

放大 RCU 用法錯誤的示例

由於更新程式可以與 RCU 讀取器併發執行,因此 RCU 讀取器可能會看到過時和/或不一致的值。如果 RCU 讀取器需要新鮮或一致的值(有時需要),他們需要採取適當的預防措施。要了解這一點,請考慮以下程式碼片段

struct foo {
        int a;
        int b;
        int c;
};
struct foo *gp1;
struct foo *gp2;

void updater(void)
{
        struct foo *p;

        p = kmalloc(...);
        if (p == NULL)
                deal_with_it();
        p->a = 42;  /* Each field in its own cache line. */
        p->b = 43;
        p->c = 44;
        rcu_assign_pointer(gp1, p);
        p->b = 143;
        p->c = 144;
        rcu_assign_pointer(gp2, p);
}

void reader(void)
{
        struct foo *p;
        struct foo *q;
        int r1, r2;

        rcu_read_lock();
        p = rcu_dereference(gp2);
        if (p == NULL)
                return;
        r1 = p->b;  /* Guaranteed to get 143. */
        q = rcu_dereference(gp1);  /* Guaranteed non-NULL. */
        if (p == q) {
                /* The compiler decides that q->c is same as p->c. */
                r2 = p->c; /* Could get 44 on weakly order system. */
        } else {
                r2 = p->c - r1; /* Unconditional access to p->c. */
        }
        rcu_read_unlock();
        do_something_with(r1, r2);
}

您可能會感到驚訝,結果(r1 == 143 && r2 == 44)是可能的,但不應該感到驚訝。畢竟,在 reader() 載入到“r1”和載入到“r2”之間,更新程式可能已被第二次呼叫。由於編譯器和 CPU 的某些重新排序而可能發生相同的事實無關緊要。

但是,如果讀取器需要一致的檢視呢?

那麼一種方法是使用鎖定,例如,如下所示

struct foo {
        int a;
        int b;
        int c;
        spinlock_t lock;
};
struct foo *gp1;
struct foo *gp2;

void updater(void)
{
        struct foo *p;

        p = kmalloc(...);
        if (p == NULL)
                deal_with_it();
        spin_lock(&p->lock);
        p->a = 42;  /* Each field in its own cache line. */
        p->b = 43;
        p->c = 44;
        spin_unlock(&p->lock);
        rcu_assign_pointer(gp1, p);
        spin_lock(&p->lock);
        p->b = 143;
        p->c = 144;
        spin_unlock(&p->lock);
        rcu_assign_pointer(gp2, p);
}

void reader(void)
{
        struct foo *p;
        struct foo *q;
        int r1, r2;

        rcu_read_lock();
        p = rcu_dereference(gp2);
        if (p == NULL)
                return;
        spin_lock(&p->lock);
        r1 = p->b;  /* Guaranteed to get 143. */
        q = rcu_dereference(gp1);  /* Guaranteed non-NULL. */
        if (p == q) {
                /* The compiler decides that q->c is same as p->c. */
                r2 = p->c; /* Locking guarantees r2 == 144. */
        } else {
                spin_lock(&q->lock);
                r2 = q->c - r1;
                spin_unlock(&q->lock);
        }
        rcu_read_unlock();
        spin_unlock(&p->lock);
        do_something_with(r1, r2);
}

一如既往,請使用正確的工具來完成工作!

編譯器知道得太多的示例

如果從 rcu_dereference() 獲得的指標與某個其他指標不相等,則編譯器通常不知道第一個指標的值可能是什麼。這種知識的缺乏阻止了編譯器執行原本可能會破壞 RCU 所依賴的排序保證的最佳化。rcu_dereference() 中的 volatile 強制型別轉換應該阻止編譯器猜測該值。

但是如果沒有 rcu_dereference(),編譯器比您預期的知道的更多。請考慮以下程式碼片段

struct foo {
        int a;
        int b;
};
static struct foo variable1;
static struct foo variable2;
static struct foo *gp = &variable1;

void updater(void)
{
        initialize_foo(&variable2);
        rcu_assign_pointer(gp, &variable2);
        /*
         * The above is the only store to gp in this translation unit,
         * and the address of gp is not exported in any way.
         */
}

int reader(void)
{
        struct foo *p;

        p = gp;
        barrier();
        if (p == &variable1)
                return p->a; /* Must be variable1.a. */
        else
                return p->b; /* Must be variable2.b. */
}

因為編譯器可以看到對“gp”的所有儲存,所以它知道“gp”的唯一可能值是一方面是“variable1”,另一方面是“variable2”。因此,reader() 中的比較告訴編譯器“p”的精確值,即使在不相等的情況下也是如此。這允許編譯器使返回值獨立於從“gp”的載入,從而破壞此載入與返回值載入之間的排序。這可能導致“p->b”在弱排序系統上返回預初始化垃圾值。

簡而言之,當您要解引用生成的指標時,rcu_dereference()不是可選的。

您應該使用 rcu_dereference() 系列的哪個成員?

首先,請避免使用 rcu_dereference_raw(),並且也請避免使用 rcu_dereference_check()rcu_dereference_protected(),第二個引數的常量值為 1(或者就此而言,為 true)。在排除這些注意事項之後,以下是一些關於在各種情況下使用哪個 rcu_dereference() 成員的指導

  1. 如果訪問需要在 RCU 讀取端臨界區內,請使用 rcu_dereference()。使用新的統一 RCU 風格,使用 rcu_read_lock()、停用 bottom halves 的任何操作、停用中斷的任何操作或停用搶佔的任何操作來進入 RCU 讀取端臨界區。請注意,自旋鎖臨界區也是隱含的 RCU 讀取端臨界區,即使它們是可搶佔的,因為它們是在使用 CONFIG_PREEMPT_RT=y 構建的核心中。

  2. 如果訪問可能一方面在 RCU 讀取端臨界區內,另一方面受(例如)my_lock 保護,請使用 rcu_dereference_check(),例如

    p1 = rcu_dereference_check(p->rcu_protected_pointer,
                               lockdep_is_held(&my_lock));
    
  3. 如果訪問可能一方面在 RCU 讀取端臨界區內,另一方面受 my_lock 或 your_lock 保護,請再次使用 rcu_dereference_check(),例如

    p1 = rcu_dereference_check(p->rcu_protected_pointer,
                               lockdep_is_held(&my_lock) ||
                               lockdep_is_held(&your_lock));
    
  4. 如果訪問在更新端,以便它始終受 my_lock 保護,請使用 rcu_dereference_protected()

    p1 = rcu_dereference_protected(p->rcu_protected_pointer,
                                   lockdep_is_held(&my_lock));
    

    這可以擴充套件到處理上面#3 中的多個鎖,並且兩者都可以擴充套件到檢查其他條件。

  5. 如果保護由呼叫者提供,因此此程式碼未知,那麼 rcu_dereference_raw() 是合適的罕見情況。此外,當 lockdep 表示式過於複雜時,rcu_dereference_raw() 可能是合適的,除非在這種情況下,更好的方法可能是仔細檢視您的同步設計。儘管如此,在資料鎖定的情況下,大量的鎖或引用計數器中的任何一個都足以保護指標,因此 rcu_dereference_raw() 確實有其位置。

    但是,與當前核心中的使用數量相比,它的位置可能要小得多。其同義詞 rcu_dereference_check( ... , 1) 及其近親 rcu_dereference_protected(... , 1) 也是如此。

RCU 保護指標的稀疏檢查

稀疏靜態分析工具檢查對 RCU 保護指標的非 RCU 訪問,這可能會由於涉及發明的載入以及可能的載入撕裂的編譯器最佳化而導致“有趣的”錯誤。例如,假設有人錯誤地做了類似的事情

p = q->rcu_protected_pointer;
do_something_with(p->a);
do_something_else_with(p->b);

如果暫存器壓力很高,編譯器可能會最佳化掉“p”,從而將程式碼轉換為類似的內容

do_something_with(q->rcu_protected_pointer->a);
do_something_else_with(q->rcu_protected_pointer->b);

如果在 q->rcu_protected_pointer 在此期間發生更改,這可能會讓您的程式碼非常失望。這也不是一個理論問題:早在 1990 年代初期,正是這種錯誤使 Paul E. McKenney(以及他的一些無辜的同事)損失了一個三天的週末。

載入撕裂當然可能導致解引用一對指標的混合,這也可能使您的程式碼非常失望。

只需將程式碼改為以下形式即可避免這些問題

p = rcu_dereference(q->rcu_protected_pointer);
do_something_with(p->a);
do_something_else_with(p->b);

不幸的是,在審查期間,這些型別的錯誤可能非常難以發現。這就是稀疏工具以及“__rcu”標記發揮作用的地方。如果您使用“__rcu”標記指標宣告(無論是在結構中還是作為形式引數),這將告訴稀疏如果直接訪問此指標,則會發出警告。如果使用 rcu_dereference() 及其朋友訪問未標記為“__rcu”的指標,它也會導致稀疏發出警告。例如,->rcu_protected_pointer 可以宣告如下

struct foo __rcu *rcu_protected_pointer;

“__rcu”的使用是選擇性的。如果您選擇不使用它,則應忽略稀疏警告。