簡體   English   中英

64 位計算機上的哪些類型在 gnu C 和 gnu C++ 中自然是原子的? -- 意味着它們具有原子讀取和原子寫入

[英]Which types on a 64-bit computer are naturally atomic in gnu C and gnu C++? -- meaning they have atomic reads, and atomic writes

注意:對於這個問題,我不是在談論 C 或 C++語言標准。 相反,我說的是特定架構的 gcc 編譯器實現,因為語言標准對原子性的唯一保證是在 C11 或更高版本中使用_Atomic類型或在 C++11 或更高版本中使用std::atomic<>類型。 另請參閱此問題底部的我的更新。

在任何架構上,某些數據類型可以原子讀取和寫入,而其他數據類型將占用多個時鍾周期,並且可能在操作中間被中斷,如果該數據在線程之間共享,則會導致損壞。

8 位單核 AVR 微控制器(例如:Arduino Uno、Nano 或 Mini 使用的 ATmega328 MCU)上,只有8 位數據類型具有原子讀寫(使用 gcc 編譯器和 gnu C 或 gnu C++語)。 我在不到 2 天的時間內進行了 25 小時的調試馬拉松,然后在這里寫了這個答案 另請參閱此問題的底部以獲取更多信息。 以及使用使用 AVR-libc 庫的 gcc 編譯器編譯時具有自然原子寫入和自然原子讀取的 AVR 8 位微控制器的 8 位變量的文檔。

(32 位)STM32 單核微控制器上,任何32 位或更小的數據類型絕對是自動原子的(當使用 gcc 編譯器和 gnu C 或 gnu C++ 語言編譯時,因為ISO C 和 C++ 對此不做任何保證直到 2011 版本在 C11 中具有_Atomic類型,在 C++11 中具有std::atomic<>類型)。 這包括bool / _Boolint8_t / uint8_tint16_t / uint16_tint32_t / uint32_tfloat所有指針 唯一的原子類型是int64_t / uint64_tdouble (8 個字節)和long double (也是 8 個字節)。 我在這里寫過:

  1. 哪些變量類型/大小在 STM32 微控制器上是原子的?
  2. 讀取由 ISR 更新的 64 位變量
  3. 為了實現原子訪問保護,在 STM32 微控制器中禁用和重新啟用中斷的各種方法是什么?

現在我需要知道我的64 位 Linux 計算機 哪些類型絕對是自動原子的?

我的電腦有一個 x86-64 處理器和 Linux Ubuntu 操作系統。

我可以使用 Linux 頭文件和 gcc 擴展。

我在 gcc 源代碼中看到了一些有趣的東西,表明至少32 位int類型是原子的。 例如:Gnu++ 標頭<bits/atomic_word.h> ,它存儲在我的計算機上的/usr/include/x86_64-linux-gnu/c++/8/bits/atomic_word.h中,並且是在線的,它包含以下內容:

typedef int _Atomic_word;

因此, int顯然是原子的。

並且 Gnu++ 標頭<bits/types.h>包含在<ext/atomicity.h>中,並存儲在我的計算機上的/usr/include/x86_64-linux-gnu/bits/types.h中,包含以下內容:

/* C99: An integer type that can be accessed as an atomic entity,
   even in the presence of asynchronous interrupts.
   It is not currently necessary for this to be machine-specific.  */
typedef int __sig_atomic_t;

所以,再一次, int顯然是原子的。

這是一些示例代碼來顯示我在說什么......

...當我說我想知道哪些類型具有自然原子讀取和自然原子寫入,但沒有原子增量、減量或復合賦值。

volatile bool shared_bool;
volatile uint8_t shared u8;
volatile uint16_t shared_u16;
volatile uint32_t shared_u32;
volatile uint64_t shared_u64;
volatile float shared_f; // 32-bits
volatile double shared_d; // 64-bits

// Task (thread) 1
while (true)
{
    // Write to the values in this thread.
    //
    // What I write to each variable will vary. Since other threads are reading
    // these values, I need to ensure my *writes* are atomic, or else I must
    // use a mutex to prevent another thread from reading a variable in the
    // middle of this thread's writing.
    shared_bool = true;
    shared_u8 = 129;
    shared_u16 = 10108;
    shared_u32 = 130890;
    shared_f = 1083.108;
    shared_d = 382.10830;
}

// Task (thread) 2
while (true)
{
    // Read from the values in this thread.
    //
    // What thread 1 writes into these values can change at any time, so I need
    // to ensure my *reads* are atomic, or else I'll need to use a mutex to
    // prevent the other thread from writing to a variable in the midst of
    // reading it in this thread.
    if (shared_bool == whatever)
    {
        // do something
    }
    if (shared_u8 == whatever)
    {
        // do something
    }
    if (shared_u16 == whatever)
    {
        // do something
    }
    if (shared_u32 == whatever)
    {
        // do something
    }
    if (shared_u64 == whatever)
    {
        // do something
    }
    if (shared_f == whatever)
    {
        // do something
    }
    if (shared_d == whatever)
    {
        // do something
    }
}

C _Atomic類型和 C++ std::atomic<>類型

我知道 C11 及更高版本提供_Atomic類型,例如:

const _Atomic int32_t i;
// or (same thing)
const atomic_int_least32_t i;

看這里:

  1. https://en.cppreference.com/w/c/thread
  2. https://en.cppreference.com/w/c/language/atomic

C++11 及更高版本提供std::atomic<>類型,例如:

const std::atomic<int32_t> i;
// or (same thing)
const atomic_int32_t i;

看這里:

  1. https://en.cppreference.com/w/cpp/atomic/atomic

這些 C11 和 C++11 “原子”類型提供原子讀取和原子寫入以及原子遞增運算符、遞減運算符和復合賦值......

……但這不是我真正要說的。

我想知道哪些類型只有自然原子讀取和自然原子寫入。 對於我所說的,遞增、遞減和復合賦值不會是自然原子的。


2022 年 4 月 14 日更新

我與 ST 的某人進行了一些聊天,似乎 STM32 微控制器僅保證在這些條件下對某些大小的變量進行原子讀寫:

  1. 你使用匯編。
  2. 您使用 C11 _Atomic類型或 C++11 std::atomic<>類型。
  3. 您將 gcc 編譯器與 gnu 語言和 gcc 擴展一起使用。
    1. 我對最后一個最感興趣,因為這是我在這個問題頂部的假設的症結所在,過去 10 年似乎一直基於這一點,而我沒有意識到這一點。 我想幫助查找 gcc 編譯器手冊以及其中解釋這些顯然存在的原子訪問保證的地方。 我們應該檢查:
      1. 適用於 8 位 AVR ATmega 微控制器的 AVR gcc 編譯器手冊。
      2. 適用於 32 位 ST 微控制器的 STM32 gcc 編譯器手冊。
      3. x86-64 gcc 編譯器手冊??--如果存在這樣的東西,適用於我的 64 位 Ubuntu 計算機。

到目前為止我的研究:

  1. AVR gcc:不存在 avr gcc 編譯器手冊 相反,請在此處使用 AVR-libc 手冊: https ://www.nongnu.org/avr-libc/ -->“用戶手冊”鏈接。

    1. <util/atomic>部分中的 AVR-libc 用戶手冊支持我的說法,即AVR 上的 8 位類型在由 gcc 編譯時,已經具有自然原子讀取自然原子寫入,當它暗示 8 位讀取和寫入時通過說已經是原子的(強調添加):

    需要原子訪問的典型示例是在主執行路徑和 ISR 之間共享的16(或更多)位變量

    1. 它談論的是 C 代碼,而不是匯編,因為它在該頁面上提供的所有示例都是用 C 語言編寫的,包括volatile uint16_t ctr變量的示例,緊跟在該引用之后。

從語言標准的角度來看,答案非常簡單:它們都不是“絕對自動”的 atomic

首先,區分“原子”的兩種含義很重要。

  • 一個是關於信號的原子 例如,這可以確保當您在sig_atomic_t上執行x = 5,當前線程中調用的信號處理程序將看到舊值或新值。 這通常只需通過在一條指令中進行訪問即可完成,因為信號只能由硬件中斷觸發,而硬件中斷只能在指令之間到達。 例如,x86 add dword ptr [var], 12345 ,即使沒有lock前綴,在這個意義上也是原子的。

  • 另一個相對於線程是原子的,因此同時訪問該對象的另一個線程將看到正確的值。 這更難做對。 特別是, sig_atomic_t類型的普通變量對於線程來說不是原子的。 你需要_Atomicstd::atomic來獲得它。

請注意,您的實現為其類型選擇的內部名稱不是任何證據。 typedef int _Atomic_word; 我當然不會推斷“ int顯然是原子的”; 我不知道實施者在什么意義上使用“原子”這個詞,或者它是否准確(例如,遺留代碼可以使用)。 如果他們想做出這樣的承諾,它會在文檔中,而不是在應用程序程序員永遠不會看到的bits標頭中無法解釋的typedef中。


您的硬件可能使某些類型的訪問“自動原子化”這一事實並不能告訴您 C/C++ 級別的任何內容。 例如,在 x86 上,對自然對齊變量的普通全尺寸加載和存儲確實是原子的。 但是在沒有std::atomic的情況下,編譯器沒有義務發出普通的全尺寸加載和存儲; 它有權變得聰明並以其他方式訪問這些變量。 它“知道”這不會有問題,因為並發訪問將是數據競爭,當然程序員永遠不會編寫帶有數據競爭的代碼,不是嗎?

作為一個具體示例,請考慮以下代碼:

unsigned x;

unsigned foo(void) {
    return (x >> 8) & 0xffff;
}

加載一個不錯的 32 位整數變量,然后是一些算術。 還有什么比這更無辜? 但是請查看 GCC 11.2 -O2發出的程序集,嘗試使用 godbolt

foo:
        movzx   eax, WORD PTR x[rip+1]
        ret

哦親愛的。 部分負載,並且未對齊引導。 AFAIK x86 沒有提供關於未對齊負載的原子性承諾。


這是另一個有趣的例子,這次是在 ARM64 上。 根據 ARMv8-A 架構參考手冊的 B2.2.1,對齊的 64 位存儲是原子的。 所以這看起來不錯:

unsigned long x;

void bar(void) {
    x = 0xdeadbeefdeadbeef;
}

但是,GCC 11.2 -O2 給出了( godbolt ):

bar:
        adrp    x1, .LANCHOR0
        add     x2, x1, :lo12:.LANCHOR0
        mov     w0, 48879
        movk    w0, 0xdead, lsl 16
        str     w0, [x1, #:lo12:.LANCHOR0]
        str     w0, [x2, 4]
        ret

那是兩個 32 位的str ,無論如何都不是原子的。 讀者可能會很好地閱讀0x00000000deadbeef

為什么要這樣做? 在寄存器中實現一個 64 位常量需要 ARM64 上的幾條指令,其指令大小是固定的。 但是值的兩半是相等的,那么為什么不實現 32 位值並將其存儲到每一半呢?

(如果你執行unsigned long *p; *p = 0xdeadbeefdeadbeef那么你會得到stp w1, w1, [x0] ( godbolt )。這看起來更有希望,因為它是一條指令,但實際上仍然是兩個單獨的寫入線程之間的原子性。)


用戶 supercat 對是否並發無序寫入以及共享內存未定義行為的防護? 還有另一個很好的 ARM32 Thumb 示例,其中 C 源代碼要求加載一次unsigned short ,但生成的代碼會加載兩次。 在存在並發寫入的情況下,您可能會得到“不可能”的結果。

可以在 x86-64( godbolt )上引發同樣的問題:

_Bool x, y, z;

void foo(void) {
    _Bool tmp = x;
    y = tmp;
    // imagine elaborate computation here that needs lots of registers
    z = tmp;
}

GCC 將重新加載x而不是溢出tmp 在 x86 上,您可以只用一條指令加載一個全局指令,但溢出到堆棧至少需要兩條指令。 因此,如果x正在同時被線程或信號/中斷修改,那么之后的assert(y == z)可能會失敗。


假設超出語言實際保證的任何內容確實是不安全的,除非您使用std::atomic否則什么都不是。 現代編譯器非常清楚語言規則的確切限制,並積極優化。 他們可以並且將會破壞假設他們會做“自然”的代碼,如果這超出了語言所承諾的范圍,他們會經常以人們意想不到的方式去做。

在 8 位 AVR 微控制器(例如:Arduino Uno 或 Mini 使用的 ATmega328 MCU)上,只有 8 位數據類型具有原子讀寫。

僅在您使用匯編程序而不是 C 語言編寫代碼的情況下。

在(32 位)STM32 微控制器上,任何 32 位或更小的數據類型絕對是自動原子的。

僅在您使用匯編程序而不是 C 編寫代碼的情況下。此外,僅當 ISA 保證生成的指令是原子指令時,我不記得這是否適用於所有 ARM 指令。

這包括 bool/_Bool、int8_t/uint8_t、int16_t/uint16_t、int32_t/uint32_t、float 和所有指針。

不,這絕對是錯誤的。

現在我需要知道我的 64 位 Linux 計算機。 哪些類型絕對是自動原子的?

與 AVR 和 STM32 中相同的類型:無。

這一切都歸結為 C 中的變量訪問不能保證是原子的,因為它可能在多個指令中執行。 或者在某些情況下,ISA 不保證原子性的指令。

在 C(和 C++)中,唯一可以被視為原子的類型是具有 C11/C++11 中的_Atomic限定符的類型。 時期。

我在 EE這個答案是重復的。 它明確地解決了微控制器的情況、競爭條件、 volatile的使用、危險的優化等。它還包含一種防止中斷競爭條件的簡單方法,適用於所有不能中斷中斷的 MCU。 該答案的引用:

在編寫 C 語言時,必須保護 ISR 和后台程序之間的所有通信免受競爭條件的影響。 總是,每次,沒有例外。 MCU 數據總線的大小無關緊要,因為即使您在 C 中進行單個 8 位復制,該語言也無法保證操作的原子性。 除非您使用 C11 功能_Atomic否則不會。 如果此功能不可用,則必須使用某種信號量或在讀取期間禁用中斷等。內聯匯編器是另一種選擇。 volatile 不保證原子性。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM