2015年3月4日 星期三

[Linux Kernel] ACCESS_ONCE Macro

最近看Kernel code,看到這個ACCESS_ONCE巨集,仔細看了它的定義發現挺有趣的,順便記錄一下。 

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


沒有留言: