以下是關于volatile關鍵的外文描述:
By declaring an object volatile, the compiler is informed that the value of the object can change beyond the compiler’s control. The compiler must also assume that any accesses can have side effects—thus all accesses to the volatile object must be preserved.
There are three main reasons for declaring an object volatile:
Shared access; the object is shared between several tasks in a multitasking environment
Trigger access; as for a memory-mapped SFR where the fact that an access occurshas an effect
Modified access; where the contents of the object can change in ways not known tothe compiler.
1、Shared access
the object is shared between several tasks in a multitasking environment。
當同一全局變量在多個線程之間被共享時,有可能會出現同步錯誤,編譯器可能會將訪問該全局變量的代碼優化為訪問某個寄存器,而不會再次訪問相應的內存,導致程序運行錯誤。
測試代碼如下:
?
static struct rt_thread v_thread1; static char v_thread1_stack[8192]; static struct rt_thread v_thread2; static char v_thread2_stack[8192]; static int flag; static int count; static void rt_init_thread1_entry(void *parameter) { ? ?while(1) ? { ? ? ? ?rt_thread_mdelay(300); ? ? ? ?flag = 1; ? ? ? ?rt_thread_mdelay(300); ? ? ? ?flag = 0; ? ? ? ?if(count++ > 10) ? ? ? { ? ? ? ? ? ?rt_kprintf("thread1 exit. "); ? ? ? ? ? ?flag = 1; ? ? ? ? ? ?return; ? ? ? } ? } } static void rt_init_thread2_entry(void *parameter) { ? ?while(1) ? { ? ? ? ?while(flag==0); ? ? ? ?rt_kprintf("thread2 running. "); ? ? ? ?rt_thread_mdelay(100); ? ? ? ?if(count++ > 10) ? ? ? { ? ? ? ? ? ?rt_kprintf("thread2 exit. "); ? ? ? ? ? ?return; ? ? ? } ? } } int volatile_test() { ? ?rt_err_t result = RT_EOK; ? ?result = rt_thread_init(&v_thread1, "vth1", ? ? ? ? ? ? ? ? ? ? ? ? ? ?rt_init_thread1_entry, ? ? ? ? ? ? ? ? ? ? ? ? ? ?RT_NULL, ? ? ? ? ? ? ? ? ? ? ? ? ? ?v_thread1_stack, sizeof(v_thread1_stack), ? ? ? ? ? ? ? ? ? ? ? ? ? ?RT_THREAD_PRIORITY_MAX / 3 - 1 , 20); ? ?if (result == RT_EOK) ? ? ? ?rt_thread_startup(&v_thread1); ? ?result = rt_thread_init(&v_thread2, "vth2", ? ? ? ? ? ? ? ? ? ? ? ? ? ?rt_init_thread2_entry, ? ? ? ? ? ? ? ? ? ? ? ? ? ?RT_NULL, ? ? ? ? ? ? ? ? ? ? ? ? ? ?v_thread2_stack, sizeof(v_thread2_stack), ? ? ? ? ? ? ? ? ? ? ? ? ? ?RT_THREAD_PRIORITY_MAX / 3, 20); ? ?if (result == RT_EOK) ? ? ? ?rt_thread_startup(&v_thread2); ? ?return 0; } MSH_CMD_EXPORT(volatile_test, run volatile_test);
?
上面的測試代碼在 O0 優化時正常運行,打印結果如下:
?
msh />volatile_test thread2 running. msh />thread2 running. thread2 running. thread2 running. thread2 running. thread2 running. thread2 running. thread2 running. thread2 running. thread2 exit. thread1 exit.
?
但是如果開啟 O3 優化,則打印結果如下:
?
msh />volatile_test thread1 exit.
?
也就是說 thread2 永遠得不到運行,那么原因是什么呢,請看下圖的反匯編,語句
?
while(flag==0);
?
被優化成了如下匯編:
?
00108b4c: ? ldr ? ? r3, [r4, #+288] # 第一次讀取 flag 的實際值到 r3 00108b50: ? cmp ? ? r3, #0 ? ? ? ? # 對比 r3 的值是否為 0 00108b54: ? bne ? ? +0 ? ? ; ? ? ? # 如果不為 0 則跳轉 00108b58: ? b ? ? ? -8 ? ? ; ? ? ? # 再次跳轉回 cmp 語句繼續循環
?
也就是說,整個程序被翻譯成,只讀取一次 flag 的實際值,后續一直使用 r3 寄存器中的值來進行對比,而第一次讀取到的 r3 值為零,因此 while 的條件將永遠成立,thread2 永遠也得不到執行。
2、Trigger access
as for a memory-mapped SFR(特殊功能寄存器)where the fact that an access occurs has an effect。
當讀取類似串口設備的數據寄存器時,一定要加上 volatile,因為該地址寄存器中的數值可能會發生改變,如果不加 volatile,可能會發現讀取的數據是錯誤的。
3、Modified access
where the contents of the object can change in ways not known to the compiler.
對象的內容可能會被以編譯器不清楚的方式被修改,例如在內核態與用戶態的程序在不同的虛擬地址訪問同一塊物理內存,此時如果不加上 volatile,則外部的修改無法被感知到,造成程序錯誤。
關于優化錯誤
如果系統在低優化等級能正常運行,但是在高優化的情況下的無法正常運行,首先懷疑兩個方面:
是否是一些關鍵操作沒有添加 volatile
是否是有內存寫穿(因為不同的優化等級改變了內存排布導致寫穿位置發生改變)
4、如何避免關鍵操作被優化
情況一
如果發現加上了 printf 打印,或者調用了某個外部函數,系統就正常運行了,也要懷疑是否出現了變量訪問被優化的情況,因為如果加上了外部函數(非本文件中的函數或其他庫中的函數)調用,則編譯器無法確定被引用的變量是否被外部函數所改變,因而會自動從原有地址重新讀取該變量的值。
如果修改上面的測試代碼,在 while 循環中加入 rt_kprintf 打印如下:
?
while(flag==0) { ? ?rt_kprintf("5 "); }
?
則程序仍然正常運行,原因就是編譯器不知道 rt_kprintf 函數是否會修改 flag 變量,因此編譯器會嘗試每次都重新讀取 flag 的值。
情況二
還可以使用另外一種方式來解決這個問題,如下:
?
while(flag==0) { ? ?asm volatile ("":::"memory"); }
?
If our instruction modifies memory in an unpredictable fashion, add "memory" to the list of clobbered registers. This will cause GCC to not keep memory values cached in registers across the assembler instruction. We also have to add the volatile keyword if the memory affected is not listed in the inputs or outputs of the asm.
這將會告訴編譯器,經過一些指令后,memory 中的數據已經發生了變化,GCC 將不會再使用寄存器作為數據的緩存。因此再次使用這些數據時,會從內存中重新嘗試讀取。使用關鍵字 volatile 也可以達到同樣的效果。
以下描述摘自 《GCC-Inline-Assembly-HOWTO》 :
?
Some instructions clobber some hardware registers. We have to list those registers in the clobber-list, ie the field after the third ’:’ in the asm function. This is to inform gcc that we will use and modify them ourselves. So gcc will not assume that the values it loads into these registers will be valid. We shoudn’t list the input and output registers in this list. Because, gcc knows that "asm" uses them (because they are specified explicitly as constraints). If the instructions use any other registers, implicitly or explicitly (and the registers are not present either in input or in the output constraint list), then those registers have to be specified in the clobbered list. If our instruction can alter the condition code register, we have to add "cc" to the list of clobbered registers.
?
4、結論
關于 volatile 關鍵字,最重要的是要認識到一點,即是否在編譯器清楚的范圍之外,所操作的變量有可能被改變,如果有這種可能性,則一定要添加上 volatile 關鍵字,以避免這種錯誤。
歸根結底,是要確定代碼在真實運行的狀態下,當其訪問某個變量時,是否真正地從這個變量所在的地址重新讀取該變量的值,而不是直接使用上次存儲在某個寄存器中的值。
審核編輯:湯梓紅
評論
查看更多