英語

核心併發清理器 (KCSAN)

核心併發清理器 (KCSAN) 是一種動態競爭檢測器,它依賴於編譯時插樁,並使用基於觀察點的取樣方法來檢測競爭。KCSAN 的主要目的是檢測資料競爭

用法

GCC 和 Clang 都支援 KCSAN。對於 GCC,我們需要 11 或更高版本,對於 Clang 也需要 11 或更高版本。

要啟用 KCSAN,請使用以下配置核心

CONFIG_KCSAN = y

KCSAN 提供了幾個其他配置選項來自定義行為(有關更多資訊,請參閱lib/Kconfig.kcsan中的相應幫助文字)。

錯誤報告

典型的資料競爭報告如下所示

==================================================================
BUG: KCSAN: data-race in test_kernel_read / test_kernel_write

write to 0xffffffffc009a628 of 8 bytes by task 487 on cpu 0:
 test_kernel_write+0x1d/0x30
 access_thread+0x89/0xd0
 kthread+0x23e/0x260
 ret_from_fork+0x22/0x30

read to 0xffffffffc009a628 of 8 bytes by task 488 on cpu 6:
 test_kernel_read+0x10/0x20
 access_thread+0x89/0xd0
 kthread+0x23e/0x260
 ret_from_fork+0x22/0x30

value changed: 0x00000000000009a6 -> 0x00000000000009b2

Reported by Kernel Concurrency Sanitizer on:
CPU: 6 PID: 488 Comm: access_thread Not tainted 5.12.0-rc2+ #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

報告的頭部提供了涉及競爭的函式的簡短摘要。接下來是涉及資料競爭的 2 個執行緒的訪問型別和堆疊跟蹤。如果 KCSAN 還觀察到值的更改,則分別在“value changed”行上顯示觀察到的舊值和新值。

另一種不太常見的資料競爭報告如下所示

==================================================================
BUG: KCSAN: data-race in test_kernel_rmw_array+0x71/0xd0

race at unknown origin, with read to 0xffffffffc009bdb0 of 8 bytes by task 515 on cpu 2:
 test_kernel_rmw_array+0x71/0xd0
 access_thread+0x89/0xd0
 kthread+0x23e/0x260
 ret_from_fork+0x22/0x30

value changed: 0x0000000000002328 -> 0x0000000000002329

Reported by Kernel Concurrency Sanitizer on:
CPU: 2 PID: 515 Comm: access_thread Not tainted 5.12.0-rc2+ #1
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.14.0-2 04/01/2014
==================================================================

此報告是在無法確定其他競爭執行緒的情況下生成的,但由於被監視的記憶體位置的資料值已更改而推斷出存在競爭。這些報告始終顯示“value changed”行。此類報告的常見原因是競爭執行緒中缺少插樁,但也可能由於例如 DMA 訪問而發生。只有在啟用CONFIG_KCSAN_REPORT_RACE_UNKNOWN_ORIGIN=y時才會顯示此類報告,預設情況下啟用。

選擇性分析

可能希望為特定的訪問、函式、編譯單元或整個子系統停用資料競爭檢測。對於靜態黑名單,可以使用以下選項

  • KCSAN 理解data_race(expr)註釋,它告訴 KCSAN 由於expr中的訪問而導致的任何資料競爭都應被忽略,並且當遇到資料競爭時產生的結果行為被認為是安全的。有關更多資訊,請參見LKMM 中的“標記共享記憶體訪問”

  • data_race(...)類似,型別限定符__data_racy可用於記錄由於訪問變數而導致的所有資料競爭都是有意的,並且應被 KCSAN 忽略

    struct foo {
        ...
        int __data_racy stats_counter;
        ...
    };
    
  • 可以使用函式屬性__no_kcsan來停用對整個函式的資料競爭檢測

    __no_kcsan
    void foo(void) {
        ...
    

    要動態限制生成報告的函式,請參閱DebugFS 介面黑名單/白名單功能。

  • 要停用特定編譯單元的資料競爭檢測,請新增到Makefile

    KCSAN_SANITIZE_file.o := n
    
  • 要停用Makefile中列出的所有編譯單元的資料競爭檢測,請新增到相應的Makefile

    KCSAN_SANITIZE := n
    

此外,可以告訴 KCSAN 根據偏好顯示或隱藏整個類別的資料競爭。可以透過以下 Kconfig 選項更改這些設定

  • CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY:如果啟用,並且透過觀察點觀察到衝突的寫入,但觀察到記憶體位置的資料值保持不變,則不報告資料競爭。

  • CONFIG_KCSAN_ASSUME_PLAIN_WRITES_ATOMIC:預設情況下,假定最大字長的純對齊寫入是原子的。假定此類寫入不受導致資料競爭的不安全編譯器最佳化影響。該選項導致 KCSAN 不報告由於衝突而導致的資料競爭,其中唯一的純訪問是對齊的寫入,最大為字長。

  • CONFIG_KCSAN_PERMISSIVE:啟用額外的寬鬆規則,以忽略某些常見的資料競爭類別。與上述不同,這些規則更復雜,涉及值更改模式、訪問型別和地址。此選項取決於CONFIG_KCSAN_REPORT_VALUE_CHANGE_ONLY=y。有關詳細資訊,請參見kernel/kcsan/permissive.h。建議僅關注來自特定子系統而不是整個核心的報告的測試人員和維護人員停用此選項。

要使用盡可能嚴格的規則,請選擇CONFIG_KCSAN_STRICT=y,這將 KCSAN 配置為儘可能密切地遵循 Linux 核心記憶體一致性模型 (LKMM)。

DebugFS 介面

檔案/sys/kernel/debug/kcsan提供了以下介面

  • 讀取/sys/kernel/debug/kcsan返回各種執行時統計資訊。

  • onoff寫入/sys/kernel/debug/kcsan允許分別開啟或關閉 KCSAN。

  • !some_func_name寫入/sys/kernel/debug/kcsan會將some_func_name新增到報告過濾器列表中,該列表(預設情況下)會將報告資料競爭列入黑名單,其中頂部堆疊幀之一是列表中的函式。

  • blacklistwhitelist寫入/sys/kernel/debug/kcsan會更改報告過濾行為。例如,黑名單功能可用於消除頻繁發生的資料競爭;白名單功能可以幫助重現和測試修復。

調整效能

影響 KCSAN 整體效能和錯誤檢測能力的核心引數作為核心命令列引數公開,其預設值也可以透過相應的 Kconfig 選項更改。

  • kcsan.skip_watch (CONFIG_KCSAN_SKIP_WATCH):在設定另一個觀察點之前,要跳過的每個 CPU 記憶體操作的數量。更頻繁地設定觀察點將導致觀察到的競爭的可能性增加。此引數對整體系統效能和競爭檢測能力具有最顯著的影響。

  • kcsan.udelay_task (CONFIG_KCSAN_UDELAY_TASK):對於任務,設定觀察點後要延遲執行的微秒數。較大的值會導致我們可能觀察到競爭的視窗增加。

  • kcsan.udelay_interrupt (CONFIG_KCSAN_UDELAY_INTERRUPT):對於中斷,設定觀察點後要延遲執行的微秒數。中斷具有更嚴格的延遲要求,它們的延遲通常應小於為任務選擇的延遲。

它們可以在執行時透過/sys/module/kcsan/parameters/進行調整。

資料競爭

在執行中,如果兩個記憶體訪問衝突、它們在不同的執行緒中同時發生,並且至少其中一個是純訪問,則這兩個記憶體訪問形成資料競爭;如果兩者都訪問相同的記憶體位置,並且至少一個是寫入,則它們衝突。有關更深入的討論和定義,請參見LKMM 中的“純訪問和資料競爭”

與 Linux 核心記憶體一致性模型 (LKMM) 的關係

LKMM 定義了各種記憶體操作的傳播和排序規則,這使開發人員能夠推理併發程式碼。最終,這允許確定併發程式碼的可能執行,以及該程式碼是否沒有資料競爭。

KCSAN 知道標記的原子操作 (READ_ONCEWRITE_ONCEatomic_*等)以及記憶體屏障所暗示的排序保證的子集。使用CONFIG_KCSAN_WEAK_MEMORY=y,KCSAN 模擬載入或儲存緩衝,並且可以檢測到缺少的smp_mb()smp_wmb()smp_rmb()smp_store_release()以及所有具有等效隱含屏障的atomic_*操作。

請注意,KCSAN 不會報告由於缺少記憶體排序而導致的所有資料競爭,特別是當需要記憶體屏障來禁止後續記憶體操作在屏障之前重新排序時。因此,開發人員應仔細考慮保持未檢查的所需記憶體排序要求。

超出資料競爭的競爭檢測

對於具有複雜併發設計的程式碼,競爭條件錯誤可能並不總是表現為資料競爭。如果併發執行的操作導致意外的系統行為,則會發生競爭條件。另一方面,資料競爭是在 C 語言級別定義的。以下宏可用於檢查併發程式碼的屬性,其中錯誤不會表現為資料競爭。

ASSERT_EXCLUSIVE_WRITER

ASSERT_EXCLUSIVE_WRITER (var)

斷言沒有併發寫入var

引數

var

要斷言的變數

描述

斷言沒有併發寫入var;允許其他讀取器。此斷言可用於指定併發程式碼的屬性,其中違規行為無法檢測為正常的資料競爭。

例如,如果我們只有一個寫入器,但有多個併發讀取器,為了避免資料競爭,所有這些訪問都必須標記;即使併發標記的寫入與單個寫入器競爭也是錯誤。不幸的是,由於被標記,它們不再是資料競爭。對於這種情況,我們可以使用以下宏

void writer(void) {
        spin_lock(&update_foo_lock);
        ASSERT_EXCLUSIVE_WRITER(shared_foo);
        WRITE_ONCE(shared_foo, ...);
        spin_unlock(&update_foo_lock);
}
void reader(void) {
        // update_foo_lock does not need to be held!
        ... = READ_ONCE(shared_foo);
}

注意

如果適用,ASSERT_EXCLUSIVE_WRITER_SCOPED()執行更徹底的檢查,如果存在預期的沒有併發寫入的明確範圍。

ASSERT_EXCLUSIVE_WRITER_SCOPED

ASSERT_EXCLUSIVE_WRITER_SCOPED (var)

斷言在範圍內沒有併發寫入var

引數

var

要斷言的變數

描述

ASSERT_EXCLUSIVE_WRITER()的範圍變體。

斷言在引入它的範圍的持續時間內沒有併發寫入var。與多個ASSERT_EXCLUSIVE_WRITER()相比,這提供了一種更好的方式來完全覆蓋封閉範圍,並增加了 KCSAN 檢測競爭訪問的可能性。

例如,它允許查詢僅由於範圍本身內的狀態更改而發生的競爭條件錯誤

void writer(void) {
        spin_lock(&update_foo_lock);
        {
                ASSERT_EXCLUSIVE_WRITER_SCOPED(shared_foo);
                WRITE_ONCE(shared_foo, 42);
                ...
                // shared_foo should still be 42 here!
        }
        spin_unlock(&update_foo_lock);
}
void buggy(void) {
        if (READ_ONCE(shared_foo) == 42)
                WRITE_ONCE(shared_foo, 1); // bug!
}
ASSERT_EXCLUSIVE_ACCESS

ASSERT_EXCLUSIVE_ACCESS (var)

斷言沒有併發訪問var

引數

var

要斷言的變數

描述

斷言沒有併發訪問var(沒有讀取器或寫入器)。此斷言可用於指定併發程式碼的屬性,其中違規行為無法檢測為正常的資料競爭。

例如,在確定沒有物件的其他使用者後,期望獨佔訪問,但實際上並沒有釋放該物件。我們可以檢查此屬性是否實際保持如下

if (refcount_dec_and_test(&obj->refcnt)) {
        ASSERT_EXCLUSIVE_ACCESS(*obj);
        do_some_cleanup(obj);
        release_for_reuse(obj);
}
  1. 如果適用,ASSERT_EXCLUSIVE_ACCESS_SCOPED()執行更徹底的檢查,如果存在預期的沒有併發訪問的明確範圍。

  2. 對於釋放物件的情況,KASAN更適合檢測釋放後使用錯誤。

注意

ASSERT_EXCLUSIVE_ACCESS_SCOPED

ASSERT_EXCLUSIVE_ACCESS_SCOPED (var)

斷言在範圍內沒有併發訪問var

引數

var

要斷言的變數

描述

ASSERT_EXCLUSIVE_ACCESS()的範圍變體。

斷言在引入它的範圍的整個持續時間內沒有併發訪問var(沒有讀取器或寫入器)。與多個ASSERT_EXCLUSIVE_ACCESS()相比,這提供了一種更好的方式來完全覆蓋封閉範圍,並增加了 KCSAN 檢測競爭訪問的可能性。

ASSERT_EXCLUSIVE_BITS

ASSERT_EXCLUSIVE_BITS (var, mask)

斷言沒有併發寫入var中位元的子集

引數

var

要斷言的變數

mask

僅檢查對mask中設定的位元的修改

描述

ASSERT_EXCLUSIVE_WRITER()的位元粒度變體。

斷言沒有併發寫入var中位元的子集;允許併發讀取器。與其他(字粒度)斷言相比,此斷言捕獲了更詳細的位級別屬性。僅檢查在mask中設定的位元的併發修改,同時忽略剩餘的位元,即忽略對 ~mask 位元的併發寫入(或讀取)。

將此用於變數,其中某些位元不得併發修改,但期望其他位元併發修改。

例如,變數在初始化後,某些位元是隻讀的,但其他位元仍可能併發修改。讀取器可能希望斷言這是真實的,如下所示

ASSERT_EXCLUSIVE_BITS(flags, READ_ONLY_MASK);
foo = (READ_ONCE(flags) & READ_ONLY_MASK) >> READ_ONLY_SHIFT;
ASSERT_EXCLUSIVE_BITS(flags, READ_ONLY_MASK);
foo = (flags & READ_ONLY_MASK) >> READ_ONLY_SHIFT;

另一個可以使用它的示例是,僅當持有適當的鎖時,才可以修改var的某些位元,但其他位元仍可以併發修改。寫入器,其中其他位元可能會併發更改,可以使用如下斷言

spin_lock(&foo_lock);
ASSERT_EXCLUSIVE_BITS(flags, FOO_MASK);
old_flags = flags;
new_flags = (old_flags & ~FOO_MASK) | (new_foo << FOO_SHIFT);
if (cmpxchg(&flags, old_flags, new_flags) != old_flags) { ... }
spin_unlock(&foo_lock);

注意

假定緊隨ASSERT_EXCLUSIVE_BITS()之後的訪問僅訪問遮蔽的位元,並且 KCSAN 樂觀地假定它是安全的,即使存在資料競爭,並且從 KCSAN 的角度來看,使用 READ_ONCE() 標記它是可選的。但是,我們告誡您,這樣做仍然是明智的,因為我們無法在位操作方面推斷所有編譯器最佳化(在讀取器和寫入器端)。如果您確定不會出錯,我們可以簡單地將上述內容寫成

實現細節

KCSAN 依賴於觀察到兩次訪問同時發生。至關重要的是,我們希望 (a) 增加觀察到競爭的機會(特別是對於很少表現出來的競爭),以及 (b) 能夠實際觀察到它們。我們可以透過注入各種延遲來實現 (a),並透過使用地址觀察點(或斷點)來實現 (b)。

如果我們故意停止記憶體訪問,同時為它的地址設定了觀察點,然後觀察到觀察點觸發,則對同一地址的兩次訪問剛剛競爭。使用硬體觀察點,這是DataCollider中採用的方法。與 DataCollider 不同,KCSAN 不使用硬體觀察點,而是依賴於編譯器插樁和“軟觀察點”。

在 KCSAN 中,觀察點使用一種有效的編碼來實現,該編碼將訪問型別、大小和地址儲存在一個長整型中;使用“軟觀察點”的好處是可移植性和更大的靈活性。然後,KCSAN 依賴於編譯器對純訪問進行插樁。對於每個插樁的純訪問

  1. 檢查是否存在匹配的觀察點;如果存在,並且至少一次訪問是寫入,則我們遇到了競爭訪問。

  2. 定期地,如果沒有匹配的觀察點存在,則設定一個觀察點並暫停一小段隨機延遲。

  3. 還要在延遲之前檢查資料值,並在延遲之後重新檢查資料值;如果值不匹配,則我們推斷出未知來源的競爭。

為了檢測純訪問和標記訪問之間的資料競爭,KCSAN 還會註釋標記的訪問,但僅用於檢查是否存在觀察點;即,KCSAN 永遠不會在標記的訪問上設定觀察點。透過從不在標記的操作上設定觀察點,如果對併發訪問的變數的所有訪問都已正確標記,則 KCSAN 永遠不會觸發觀察點,因此永遠不會報告訪問。

模擬弱記憶體

KCSAN 檢測由於缺少記憶體屏障而導致的資料競爭的方法是基於模擬訪問重新排序(使用CONFIG_KCSAN_WEAK_MEMORY=y)。還選擇為其設定觀察點的每個純記憶體訪問,以便在其函式範圍內模擬重新排序(最多 1 次進行中的訪問)。

一旦選擇了訪問進行重新排序,就會針對直到函式範圍結束的每個其他訪問檢查該訪問。如果遇到適當的記憶體屏障,則將不再考慮該訪問進行模擬重新排序。

當記憶體操作的結果應透過屏障排序時,KCSAN 可以檢測到僅由於缺少屏障而發生的衝突的資料競爭。考慮以下示例

int x, flag;
void T1(void)
{
    x = 1;                  // data race!
    WRITE_ONCE(flag, 1);    // correct: smp_store_release(&flag, 1)
}
void T2(void)
{
    while (!READ_ONCE(flag));   // correct: smp_load_acquire(&flag)
    ... = x;                    // data race!
}

當啟用弱記憶體建模時,KCSAN 可以考慮T1中的x進行模擬重新排序。在寫入flag之後,再次檢查x是否存在併發訪問:因為T2能夠在寫入flag之後繼續進行,因此檢測到資料競爭。在適當的位置使用正確的屏障後,在正確釋放flag之後,將不再考慮x進行重新排序,並且不會檢測到資料競爭。

複雜性的有意權衡以及實際限制意味著只能檢測到由於缺少記憶體屏障而導致的資料競爭的子集。使用當前可用的編譯器支援,該實現僅限於模擬“緩衝”(延遲訪問)的影響,因為執行時無法“預取”訪問。還請記住,僅為純訪問設定觀察點,並且 KCSAN 僅模擬重新排序的訪問型別。這意味著不會模擬標記訪問的重新排序。

上述情況的一個結果是,獲取操作不需要屏障插樁(沒有預取)。此外,引入地址或控制依賴關係的標記訪問不需要特殊處理(不能重新排序標記的訪問,不能預取稍後依賴的訪問)。

關鍵屬性

  1. 記憶體開銷: 整體記憶體開銷只有幾個 MiB,具體取決於配置。當前實現使用一個小的長整型陣列來編碼觀察點資訊,這可以忽略不計。

  2. 效能開銷: KCSAN 的執行時旨在最小化,使用一種有效的觀察點編碼,該編碼不需要在快速路徑中獲取任何共享鎖。對於在具有 8 個 CPU 的系統上的核心引導

    • 使用預設 KCSAN 配置時,速度降低 5.0 倍;

    • 僅來自執行時快速路徑開銷的速度降低 2.8 倍(設定非常大的KCSAN_SKIP_WATCH並取消設定KCSAN_SKIP_WATCH_RANDOMIZE)。

  3. 註釋開銷: 除了 KCSAN 執行時之外,需要最少的註釋。因此,隨著核心的發展,維護開銷最小。

  4. 檢測來自裝置的競爭寫入: 由於在設定觀察點時檢查資料值,因此也可以檢測到來自裝置的競爭寫入。

  5. 記憶體排序: KCSAN 僅知道 LKMM 排序規則的子集;這可能會導致錯過的資料競爭(假陰性)。

  6. 分析準確性: 對於觀察到的執行,由於使用取樣策略,分析是不健全的(可能存在假陰性),但旨在是完整的(沒有假陽性)。

備選方案考慮

可以在核心執行緒清理器 (KTSAN)中找到核心的另一種資料競爭檢測方法。KTSAN 是一種發生在之前的資料競爭檢測器,它顯式地建立了記憶體操作之間發生在之前的順序,然後可以將其用於確定資料競爭中定義的資料競爭。

為了構建正確的發生在之前的關係,KTSAN 必須知道 LKMM 和同步原語的所有排序規則。不幸的是,任何遺漏都會導致大量假陽性,這在包括許多自定義同步機制的核心環境中尤其有害。為了跟蹤發生在之前的關係,KTSAN 的實現需要每個記憶體位置的元資料(影子記憶體),對於每個頁面,這對應於 4 個頁面的影子記憶體,並且可以在大型系統上轉換為數十 GiB 的開銷。