ACCESS_ONCE顧名思義,就是確實地讀取所指定記憶體位址的內容值,且僅限這一次。所以在這個巨集肯定有volatile關鍵字,其原始定義如下:
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
使用情境
底下程式碼擷取於kernel/locking/mutex.c (linux-v4.0-rc1)
while (true) {
struct task_struct *owner;
...
/*
* If there's an owner, wait for it to either
* release the lock or go to sleep.
*/
owner = ACCESS_ONCE(lock->owner);
if (owner && !mutex_spin_on_owner(lock, owner))
break;
...
然而,如果沒有加入ACCESS_ONCE macro,程式碼如下:
while (true) {
struct task_struct *owner;
...
/*
* If there's an owner, wait for it to either
* release the lock or go to sleep.
*/
owner = lock->owner;
if (owner && !mutex_spin_on_owner(lock, owner))
break;
...
由於,編譯器優化關係[1]發現"owner = lock->owner"在這個while loop沒有被更改,所以編譯器把這行程式碼放到while loop外面,如此不用每次都實際地讀取lock->owner,其程式碼變成:
struct task_struct *owner;
owner = lock->owner;
while (true) {
...
if (owner && !mutex_spin_on_owner(lock, owner))
break;
...
問題來了,"lock->owner"有可能被其它task修改,如此造成資料不一致。
因此使用ACCESS_ONCE可以防止編譯器做相關優化工作,並確保每次都能到實體記憶體位址讀取。其做法便是將某參數暫時性地轉換成具volatile型態。如此,存取該參數在非引入ACESS_ONCE macro的地方 (不具volatile特性),仍可享用編譯器優化的好處。
延伸閱讀
2014 11月 Christian Borntraeger在lkml提出ACCESS_ONCE用gcc 4.6/4.7編繹所造成的問題,詳見compiler bug gcc4.6/4.7 with ACCESS_ONCE and workarounds。其問題在於: 如果所傳入的參數是non-scalar型態,gcc 4.6/4.7會把volatile關鍵字自動拿掉,如下程式碼:
typedef struct {
unsigned long pte;
} pte_t;
pte_t pte;
pte_t p = ACCESS_ONCE(pte);
上述程式碼 (存取struct) 可能被編譯器優化 (因為gcc 4.6/4.7針對non-scalar型態會自動地去掉volatile)。
最直覺的解法就是存取scalar-type的參數,如下所示:
unsigned long p = ACCESS_ONCE(pte->pte);
但此方法需要更改所有使用ACCESS_ONCE的程式碼,這將是一件很無聊的工作。經過一番lkml討論,Christian決定更改ACCESS_ONCE (參照lkml patch set),其程式碼如下:
#define __ACCESS_ONCE(x) ({ \
__maybe_unused typeof(x) __var = (__force typeof(x)) 0; \
(volatile typeof(x) *)&(x); })
#define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
其解法就是限制ACCESS_ONCE只能傳入scalar type參數 (union也可以,不過,有些限制,詳見include/linux/compiler.h檔裡面的註解)。
如果傳入non-scalar type參數,會發生編繹錯誤。示範程式碼與編繹結果如下:
1: #include <stdio.h>
2:
3: #define __maybe_unused __attribute__((unused))
4:
5: #define __ACCESS_ONCE(x) ({ \
6: __maybe_unused typeof(x) __var = 0; \
7: (volatile typeof(x) *)&(x); })
8:
9: #define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
10:
11: typedef struct {
12: unsigned long pte;
13: } pte_t;
14:
15: int main(void)
16: {
17: pte_t pte;
18:
19: ACCESS_ONCE(pte);
20:
21: return 0;
22: }
23:
$ gcc -o access_once access_once.c
access_once.c: In function ‘main’:
access_once.c:19:2: error: invalid initializer
ACCESS_ONCE(pte);
^
其出錯原因在於這段程式碼 "__maybe_unused typeof(x) __var = 0;",它限制所傳入參數必須為scalar type參數 (因為 "__var = 0")。如果傳入一結構型態,則必須"__var = {0}",才能避免編繹錯誤。只能說Linux Kernel好多程式藝術在裡面啊!!!!!
因此,如果要存取non-scalar type參數,請改用READ_ONCE與ASSIGN_ONCE,如此便能避免編繹錯誤。
在linux-4.0-rc1原始碼,__ACCESS_ONCE導入一新patch,用以這個編繹警告 "Using plain integer as NULL pointer",詳見lkml - kernel: Fix sparse warning for ACCESS_ONCE。
#define __ACCESS_ONCE(x) ({ \
__maybe_unused typeof(x) __var = (__force typeof(x)) 0; \
(volatile typeof(x) *)&(x); })
#define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
[Reference]
ACCESS_ONCE()
ACCESS_ONCE() and compiler bugs
沒有留言:
張貼留言