contributed by < ccs100203
>
linux2022
READ_ONCE
為防止編譯器做相關最佳化工作的 macro,以下程式碼是 READ_ONCE
可能的實作
READ_ONCE
巨集先在傳入 __read_once_size
時將參數轉型成 void *
,限制傳入的參數是 scalar type,然後在存取前轉換為 volatile uint64_t *
等等包含 volatile
的型態,就可以避免編譯器對其進行優化。
可以看到 void *res
就是將 __c
轉型後傳入的變數,因為 union 內的記憶體位置相同,所以在 __read_once_size
內改動 res
的值,就會相對應的改到 __val
上,所以程式就是借此將 x
的值複製到 __val
中,並作為回傳值。
sizeof(__c)
> sizeof(__val)
這裡是用 char
也就是 uint8_t
作為 __c
的型態,使用 uint8_t
到 uint64_t
中最小的型態可以避免 union 佔用多餘的空間,也可以避免 array 的範圍超過傳入的 __val
的大小。
假設 union 是下列情況:
此時的 sizeof(__c)
> sizeof(__val)
,在之後對 res
操作會有一定的風險,因為 __val
只佔 __c
的 1 byte 而已,有可能在對 __c
存取後卻沒更新到 __val
上。
不過其實在 little endian 的情況下是不會有問題的,因為 __val
對齊在 __c
從 LSB 開始的 1 byte,所以更新 __c
時還是可以同時改到 __val
上,但如果是在 big endian 上的機器上,我想就會發生沒有更新到 __val
的情況。
char __c[1]
改成 char *__c
如果把 char __c[1]
改成 char *__c
,程式的執行結果會是錯誤的。
因為 *__c
會把它記憶體位置上所放的值當作 address 使用 (也就是指向該 address),所以在對 res
操作時,改到的會是該指標所指向的位置,而不是 union 的記憶體位置,所以 __val
不會得到正確的結果。
char __c[1]
x
: 0xf3fREAD_ONCE(x)
: 0xf3faddr. of union | __val | __c | dereference on __c | |
---|---|---|---|---|
before __read_once_size |
0x7fffffffdc10 | 0x7fffffffdd10 | 0x7fffffffdc10 | 0x10 |
after __read_once_size |
0x7fffffffdc10 | 0xf3f | 0x7fffffffdc10 | 0x3f |
文字訊息不要用圖片來展現!
2022/03/19 已修改
union 的位置是 0x7fffffffdc10,在進入 __read_once_size
之前,union 上放著一個 uninitialized value 0x7fffffffdd10,此時印出 *__c
,會是印出 0x7fffffffdd10 的最後一個 byte 0x10。(因為 __c
是 1 byte)
在做完 __read_once_size
後,已經將 x
的值 0xf3f 放到 __val
上,此時印出 *__c
,會是印出 0xf3f 的最後一個 byte 0x3f,再來程式就會將 __val
回傳回去並得到 0xf3f。
char *__c
x
: 0xf3fREAD_ONCE(x)
: 0xffffdd10addr. of union | __val | __c | dereference on __c | |
---|---|---|---|---|
before __read_once_size |
0x7fffffffdc10 | 0x7fffffffdd10 | 0x7fffffffdd10 | 0x1 |
after __read_once_size |
0x7fffffffdc10 | 0x7fffffffdd10 | 0x7fffffffdd10 | 0x3f |
union 的位置與一開始放在 union 內的值和上面相同,但可以發現在 *__c
會得到不同的結果。
*__c
會指向內容尚未初始化的地址 0x7fffffffdd10
,所以在印出 dereference 的結果時會是印出 0x7fffffffdd10 上目前的值 0x1。
而在做完 __read_once_size
後,因為 *res
內含值存於記憶體地址 0x7fffffffdd10,所以可以看到 __val
上的值仍然沒變,變的是記憶體位置 0x7fffffffdd10 上的值,這樣在最後回傳 __val
時就會得到錯誤的結果。
memcpy
避免編譯器做了非預期的最佳化好奇為什麼用 memcpy
可以避免編譯器進行優化,雖然還沒找到相關說明,不過有找到在 linux 的 xdp_sample.bpf.h 中某一種實作 __read_once_size
的方式,他是用
asm volatile ("" : : : "memory")
去設置 compiler barrier,避免其進行優化。
在使用 volatile
的情況下,該變數也必須要有 atomic 的性質,所以就必須透過一些 lock 去進行保護,既然已經需要使用 lock,那麼在適當使用 spinlocks, mutexes, memory barriers 等保護 critical section 的機制下,其實就可以捨去 volatile
。
In properly-written kernel code,
volatile
can only serve to slow things down.
在正確撰寫的 kernel code 中,volatile
只會有負面效果,像是導致執行速度變慢。
volatile
on spin_lock假設現在有一個 share_data
,雖然 compiler 認為他知道 share_data
的值並想要進行優化,但他可以透過 share_data
被 spin_lock
保護這件事得知這個變數可能會被其他程式修改,所以 compiler 就會跳過對 share_data
的優化。那既然已經需要使用 lock,compiler 也會在看到 lock 時就避免優化,那自然可以省去使用 volatile
。
volatile
on memory-mapped I/O registersvolatile
一開始設計的目的是為了 memory-mapped I/O registers,在 kernel 中, register accesses 同樣是需要被 lock 保護的。但因為並不是所有架構都能直接透過 pointer 直接存取 I/O memory,所以 I/O memory accesses 總是會透過呼叫 accessor functions 來進行,那麼就可以用 lock 對其保護,而不需要 volatile
。
volatile
on busy-waiting或許程式撰寫者會想要在等待 busy-waiting
時加上 volatile
,但因為 cpu_relax()
會做 lower CPU power 或 yield 等動作,這其實就等同於做了 compiler barrier,所以不需要使用 volatile
。
volatile
的情況volatile
on architectures where direct I/O memory access does work
前面提到的 access function,在可以直接存取 I/O memory 的情況下可能需要使用
volatile
,因為每次的 accessor call 都是一個小小的 critical section。
volatile
keyword to asm statements will prevent this removal.
一段會改變 memory 的 Inline assembly code,當他沒有任何 side effects 時,GCC 可能會直接將其刪除,所以需要透過加入
volatile
去保護他。
volatile
.
每次存取的值都會改變的變數稱為 jiffies,因為這不需要 lock 就可以直接存取,所以要用
volatile
進行保護,但 Linus 說這是 Linux 中的 "stupid legacy"。
volatile
.
如果一個指向 coherent memory 內的資料結構的 pointer,可能被 I/O devices 更改的話,可以合法的加入
volatile
。像是網路卡中的 ring buffer,網卡會改變裡頭的 pointer 去紀錄存取的位置。
根據 C11 5.1.2.3/2 中定義 side effects
Accessing a volatile object, modifying an object, modifying a file, or calling a function that does any of those operations are all side effects, which are changes in the state of the execution environment.
可以簡單將 side effects 分類為:
volatile
variable而前面提到的 inline assembly code 可能被優化,可以參考 C11 5.1.2.3/4
In the abstract machine, all expressions are evaluated as specified by the semantics. An actual implementation need not evaluate part of an expression if it can deduce that its value is not used and that no needed side effects are produced (including any caused by calling a function or accessing a volatile object).
若是一個 implementation 沒有 side effects,那麼就不用被視為一個 expression,也可以直接推斷出他的值。
文章最後提到,現在繳交的 patch 若有使用 volatile
的話,容易被當成 bug,且會受到額外的審查。
現在非常歡迎協助刪除 volatile
的 patch,但要確保有仔細思考過裡頭的 concurrency issues。
TODO