contributed by < kevinshieh0225
>
4/12 一對一討論
memory-barriers document - The Linux Kernel Archives
memory-barriers document - 整理版本
多核的計算機示意圖如下:
CPU 會和不同裝置溝通並做記憶體存取操作,然而相對 CPU 執行速度,其他裝置的速度是非常慢的,這將導致 CPU 在進行記憶體存取操作時需要停下來等待記憶體存取(stall)。於是 CPU 准許諸多手段以提昇效能、盡可能減少 pipeline stall 的發生,包括 out-of-order execution 、 load speculation, branch prediction
可參考 現代處理器設計:原理和關鍵特徵 有更深入探討。
以上手段雖然保證單執行序下的結果一致,但當多執行序的情況下,我們無法保證指令執行順序並可能造成對共用記憶體操作結果的不一致,以此為例:
我們無法保證 CPU 各自執行指令的順序,導致指令執行的順序有 種排列可能,並且會導致四種不同的輸出結果:
以下列舉 CPU 的可預期行為,可見於 Dependency ordering 的簡報:
READ-WRITE
order for generally ordered by all CPU architectures, but not all cases. Discuss in later section.就算使用 READ_ONCE
WRITE_ONCE
確保執行順序,也不保證多執行序下記憶體操作的一致性,詳細原因可參考 Memory Barriers: a Hardware View for Software Hackers
DEC alpha 架構處理器在 linux v4.15 把 memory barrior 加入到 READ_ONCE()
當中,而在原文件中稱此為 data dependency barriers。
但也有你不該期待 CPU 的行為假設:
READ_ONCE()
and WRITE_ONCE()
. Without them, the compiler is within its rights to do all sorts of “creative” transformations注意,以上保證並不包括 bitfields
的操作,也盡可能不要用 bitfields
在並行同步的演算法中。
以上保證只針對那些 "properly alligned(由記憶體分配對齊的) and sized scalar(大小等價於 char
, short
, int
, long
) variables"。
由上一章我們得知 CPU 為了提高執行效能會無所不用其極的對 memory access operation 做調整,然而這將導致 CPU 之間執行上的結果一致性問題。
memory barrier 的目的就是保證在此指令前後執行的結果與變化,能保證其他執行單元在對應的 barrier 指令前能夠注意到、接收到 (perceive)。(通常 memory barrier 會成對使用,詳情可看 SMP Barrier Pairing)
memory barrier 並非代表說,在這個 mb 指令以前的記憶體操作都會被執行並同步完畢,mb 只是畫了一條線告訴大家記憶體指令間的關係、使用規則與執行限制。
mb 也並不會廣播這個指令執行的資訊給其他的 CPU ,如果要完整的保護執行一致性需要使用 barrier pair 來達到目的。
(聽起來很抽象,可以參考 Dive deeper into write/read memory barrier 筆記一探 mb 和硬體執行上的操作。)
Explicit Memory Barrier 主要可分為四種基本類型:
wmb
指令前的 STORE
operations 會先發生於(happen-before)後面的 STORE
。wmb
只規範 stores 的順序,並不影響 loads。rmb
指令前的 LOAD
operations 會先發生於(happen-before)後面的 LOAD
。rmb
只規範 loads 的順序,並不影響 stores。mb
指令前的 LOAD
,STORE
operations 會先發生於(happen-before)後面的 LOAD
,STORE
。另外也有 Implicit Memory Barrier 的類型:
利用 ACQUIRE+RELEASE
來達到 memory barrier 的效果:
ACQUIRE
指令後的記憶體操作會在這個指令順序後執行(ACQUIRE
前的記憶體操作仍可能在指令順序後完成)。包括 LOCK operations
, smp_load_acquire()
, smp_cond_load_acquire()
。RELEASE
指令前的記憶體操作會在這個指令順序前執行(RELEASE
前的記憶體操作仍可能在指令順序前完成)。包括 UNLOCK operations
, smp_store_release()
。ACQUIRE+RELEASE
確保在這 critical section 的記憶體操作會在這個 critical section 內完成,但他並不保證其他的記憶體操作執行的時間點,故而無法達到完整 memory barrier 的保證。
使用 memory barrier 的前提是 CPU 執行的程式間彼此會互相影響,如果彼此間沒有影響的話那就沒有必要使用。
Control Dependencies 很不容易維持,現代編譯器容易在 branch 操作中做出指令重序,就算使用了 volatile
還是可能重排,而適時需要 memory barrier 的協助。這裡簡述幾種情形,詳細請參考原文。
請注意:
Control dependencies 通常也會和其他 barriers 搭配使用,control dependencies 也可以視作另類的屏障手段。
READ-READ
不保證順序,必須使用 rmb
來限制順序。考慮以下例子如果不使用 rmb
,編譯器可能會先做 branch prediction 認為 q>0
,並先執行了 b
的讀取。
我們先前提到 control dependencies 保證 READ-WRITE
的順序,但請注意如果少了 READ_ONCE
WRITE_ONCE
任一,編譯器還是可能會對程式碼進行重排。除了 READ
, WRITE
指令可能會被和其他讀寫操作合併最佳化,考慮以下例子,如果編譯器認為 a
總是非零,那麼編譯器會直接除去 if-statement
:
所以一定要有 READ_ONCE
WRITE_ONCE
缺一不可。
但也不總是保證加了就不會被最佳化,考慮以下例子:
If both legs of the “if” statement begin with identical stores to the same variable, then those stores must be ordered, either by preceding both of them with
smp_mb()
or by usingsmp_store_release()
to carry out the stores.Please note that it is not sufficient to use
barrier()
at beginning of each leg of the “if” statement because, as shown by the example above, optimizing compilers can destroy the control dependency while respecting the letter of thebarrier()
law.
在 CPU-CPU 彼此影響的情境需要 memory barrier 確保執行正確性,但注意特定的 memory barrier 務必搭配使用才能發揮功能。
General barrier
因應情境可以搭配所有上述種類的 barrier type。(不包含 multicopy atomicity)
ACQUIRE barrier
多與 RELEASE
搭配,但也可以和其他 barrier 搭配。
Write barrier
可以和 data dependency barrier
, control dependency
, Read barrier
, ACQUIRE
, RELEASE
, 或是 General barrier
做搭配。
而 data dependency barrier
, control dependency
, Read barrier
可以和 Write barrier
, ACQUIRE
, RELEASE
, 或是 General barrier
做搭配。
最後我們透過範例來觀察 Memory Barrier 指令如何影響記憶體操作的順序。
假設我們有一段指令操作:
由於 write barrier
的緣故,我們要求 wmb
以前的指令(STORE A = 1, B = 2, C = 3
)必須在下次 STORE D = 4, E = 5
執行以前做完。於是乎 memory operation order 可以如下圖所示:
Load Speculation:CPU 多半會先假設(speculate)load
的值,因為 load
的執行時間較長,且會受 system bus 運輸資料的速度影響,所以 CPU 如果得知未來有 load
的指令,並發現此時沒有其他 load
佔用 system bus,那就會提早執行 load
。這使的 load
幾乎可以在執行指令時即刻完成,因為 CPU 早就先把存值載入了。
在此我們放入 read barrier
確保多 CPU 下的正確性,考慮以下:
如果在過程中 A 都並未發生改變可能執行狀況如下:CPU 在預先得知會執行 LOAD A
,於是預先載入數值,等到準備執行時確定沒有收到 invalidate,再正式使用執行 LOAD A
。
然而如果 A 被其他 CPU update and invalidate,如果沒有 rmb
的限制,可能因為 invalidate 傳入的比較慢,而 CPU 使用了舊的 speculate load;由於使用 rmb
,讓我們能優先確認 invalidate queue 以防出錯:
write barrier
保證 STORE
的 partial order;read barrier
保證 LOAD
的 partial order,參考以下範例:
如果只用 write barrier
,CPU 2 可以根據執行的效率改變讀取的執行順序,而使的執行順序一致性失敗:
今天再增加一個 read barrier
:
即可讓 A 的執行結果符合預期:
最後我們針對 Explicit kernel barriers 的類型做討論:
compiler barrier 只有 general 版的 barrier()
,限制編譯器對於指令前後的 memory access ,不會被 reorder 到指令的另一側去,確保 data dependency 的關係。READ_ONCE()
和 WRITE_ONCE()
可被視為較弱的 barrier()
。
compiler barrier 雖確保 data dependency ,然而無法確保 CPU 讀寫的存值是 up-to-date 的,比如 store buffers/Invalidate queue 改變的快取一致性。這時需要透過 CPU Memory Barrier 來保障讀寫一致性。
相關定義可參考 linux/include/asm-generic/barrier.h, linux/tools/testing/selftests/rcutorture/formal/srcu-cbmc/src/barriers.h。
kernel/workqueue.c
讓我們從核心程式碼 kernel/workqueue.c
中一探 memory barrier 使用的範例:
在討論裡面的 mb 應用前,先來了解一下 workqueue
在做什麼。
sturct workqueue
是任務佇列的結構體,紀錄受安排執行的任務(work item
)列表。
struct work_struct
是一個指向任務函式的結構體,包裝我們的任務單位 work item
。
行程透過 alloc_workqueue
來建立任務佇列,並設定這個任務佇列的屬性:
在 kernel/workqueue_internal.h
定義的 struct worker
是執行任務的執行序。每個 cpu 被分配到兩個 worker-pool
,一個執行普通優先級的 work item
,一個執行高優先級的 work item
。
另外也有 dynamic unbound worker-pools
來執行 unbound workqueue
的任務,也就是協助分配這些未指定 cpu worker 的 wq。
worker_thread
進到 mb 主題之前,先理解 worker_thread
的執行脈落:
每一種 worker 都執行 worker_thread
,透過 goto
來決定此刻 worker_thread
的執行狀態:
goto sleep
:如果無分配 workqueue
則休眠goto woke_up
:被分配 workqueue
醒來準備做事goto recheck
:持續執行任務並確認還有沒有剩餘的 workqueue
以 process_scheduled_works
尋訪 worker->scheduled
並執行任務:
process_one_work
讓 worker_thread
執行 work item
。函式進行流程如下:
在步驟 3.
時改變了 work item
內 PENDING
的屬性,代表這個 work item
準備要被執行了,不再是等待執行的狀態。
這時我們看到了 memory barrier 的使用。
set_work_pool_and_clear_pending
這個函式要更改 work item 的狀態成正在執行,而不再是等待執行的狀態,程式碼如下:
我覺的他的註解範例有問題,後面會提出我的理解。
set_work_pool_and_clear_pending
就這三行指令:
這個過程確保了:
STORE
的順序務必遵循 before STORE
-> set_work_data()
-> after STORE
set_work_data()
做完後,預期後面執行的 LOAD
都必須確保指令過後的狀態是否有更動(不能直接使用 specultative LOAD 的結果,記得確認 invalidate queue)。對應到上面的範例,會發現因為 smp_mb()
確保了 LOAD
的順序,不會因為 speculative LOAD 而使用了舊資訊。 。所以原本的註解範例應該改成:
後來再想想,也不是他寫錯,只是他想表達的方式和邏輯和我不同而已。