1、概念
??volatile 是 C/C++ 語言中的一個類型修飾符,用于告知編譯器:該變量的值可能會在程序控制流之外被意外修改(如硬件寄存器、多線程共享變量或信號處理函數等),因此編譯器不應對其進行激進的優化(如緩存到寄存器或消除冗余讀取)。
??在程序運行時,編譯器通常會假設變量的值僅由當前線程或函數內的代碼修改,并據此進行優化(如循環內變量提升、指令重排等)。然而,在嵌入式開發、設備驅動編程或多線程環境中,某些變量的值可能被外部因素(如硬件中斷、信號處理器、其他線程)異步修改。此時,若未使用 volatile 修飾,編譯器可能生成錯誤的優化代碼,導致程序行為異常。
簡而言之,volatile 的作用是:
- 阻止編譯器優化:強制每次訪問變量時都從內存讀取,而非使用寄存器中的緩存值;
- 確保內存可見性:防止編譯器重排或省略對變量的訪問,保證操作順序符合預期;
- 適用于特殊場景:如硬件寄存器映射、信號處理、多線程共享變量(需配合其他同步機制)
volatile 并不解決所有并發問題(如原子性),但它是底層編程中確保正確內存訪問的重要工具。
2、代碼測試
下面是在 ARM 平臺的 C 語言測試,因為 ARM 是弱內存模型,更容易復現問題。
/** volatile_test.c*/#include <stdio.h>
#include <signal.h>
#include <unistd.h>// 全局變量,使用或不使用volatile修飾
int flag = 0; // 嘗試改為 volatile int flag = 0; 觀察不同結果void handler(int sig) {flag = 1;printf("Signal handler set flag to 1\n");
}int main() {signal(SIGALRM, handler);alarm(1); // 1秒后發送SIGALRM信號while(!flag) {// 空循環等待flag變化}printf("Main loop detected flag change\n");return 0;
}
2.1 測試結果
不使用 volatile 關鍵字,程序會卡死在 while 循環中:
liang@liang-virtual-machine:~/cfp$../arm-none-linux-gnueabi-gcc -O3 -g -o volatile_test volatile_test.c
liang@liang-virtual-machine:~/cfp$ ./volatile_test
Signal handler set flag to 1
使用 volatile,程序正常退出
liang@liang-virtual-machine:~/cfp$../arm-none-linux-gnueabi-gcc -O3 -g -o volatile_test volatile_test.c
liang@liang-virtual-machine:~/cfp$ ./volatile_test
Signal handler set flag to 1
Main loop detected flag change
liang@liang-virtual-machine:~/cfp$
2.2 反匯編
不使用 volatile:
liang@liang-virtual-machine:~/cfp$../arm-none-linux-gnueabi-objdump -S volatile_test
......
int main() {83cc: e92d4010 push {r4, lr}signal(SIGALRM, handler);83d0: e59f1030 ldr r1, [pc, #48] ; 8408 <main+0x3c>83d4: e3a0000e mov r0, #14 ; 0xe83d8: ebffffc5 bl 82f4 <_init+0x38>alarm(1); // 1秒后發送SIGALRM信號83dc: e3a00001 mov r0, #1 ; 0x183e0: ebffffc9 bl 830c <_init+0x50>83e4: e59f3020 ldr r3, [pc, #32] ; 840c <main+0x40>83e8: e5932000 ldr r2, [r3] ; 從內存讀取 flag 值到 r283ec: e3520000 cmp r2, #0 ; 0x0 ; 比較 r2 的值83f0: 1a000000 bne 83f8 <main+0x2c> ; 如果 r2≠0,跳轉到 83f8 位置83f4: eafffffe b 83f4 <main+0x28> ; 無條件跳轉到自身(無限循環)while(!flag) {// 空循環等待flag變化}printf("Main loop detected flag change\n");83f8: e59f0010 ldr r0, [pc, #16] ; 8410 <main+0x44>83fc: ebffffc5 bl 8318 <_init+0x5c>return 0;
}
......
??可以看到,編譯器對 while 循環做了優化。編譯器只在循環開始前讀取一次 flag 的值到寄存器 r2。編譯器認為 flag 在循環內不會被修改,之后循環中不再重新從內存讀取 flag。同時,直接做了一個無條件跳轉到自身的優化:
83f4: eafffffe b 83f4 <main+0x28> ; 無條件跳轉到自身(無限循環)
??而對比使用 volatile 關鍵字,可以看到編譯器沒有對 while 循環做優化,每次循環都重新讀取 flag 的值:
liang@liang-virtual-machine:~/cfp$../arm-none-linux-gnueabi-objdump -S volatile_test
......
int main() {83cc: e92d4010 push {r4, lr}signal(SIGALRM, handler);83d0: e59f102c ldr r1, [pc, #44] ; 8404 <main+0x38>83d4: e3a0000e mov r0, #14 ; 0xe83d8: ebffffc5 bl 82f4 <_init+0x38>alarm(1); // 1秒后發送SIGALRM信號83dc: e3a00001 mov r0, #1 ; 0x183e0: ebffffc9 bl 830c <_init+0x50>83e4: e59f201c ldr r2, [pc, #28] ; 8408 <main+0x3c>while(!flag) {83e8: e5923000 ldr r3, [r2] ; 每次循環都重新讀取flag83ec: e3530000 cmp r3, #0 ; 0x0 ; 如果≠0跳轉到退出83f0: 0afffffc beq 83e8 <main+0x1c> ; 繼續循環// 空循環等待flag變化}printf("Main loop detected flag change\n");83f4: e59f0010 ldr r0, [pc, #16] ; 840c <main+0x40>83f8: ebffffc6 bl 8318 <_init+0x5c>return 0;
}
......