C语言:关键字volatile详解!
一、volatile 介紹
參看:volatile詳解
參看:C Language Keywords
?
Indicates that a variable can be changed by a background routine.
Keyword?volatile?is an extreme opposite of?const.It indicates that a variable may be changed in a way which is absolutely unpredictable by analysing the normal program flow (for example, a variable which may be changed by an interrupt handler). This keyword uses the following syntax:
volatile data-definition;?
Every reference to the variable will reload the contents from memory rather than take advantage of situations where a copy can be in a register.
?
翻譯:
表示一個變量也許會被后臺程序改變,關鍵字 volatile 是與 const 絕對對立的。它指示一個變量也許會被某種方式修改,這種方式按照正常程序流程分析是無法預知的(例如,一個變量也許會被一個中斷服務程序所修改)。這個關鍵字使用下列語法定義:
?
volatile data-definition;?
變量如果加了 volatile 修飾,則會從內存重新裝載內容,而不是直接從寄存器拷貝內容。?
?
volatile應用比較多的場合,在中斷服務程序和cpu相關寄存器的定義。
示例:
volatile 用于相關寄存器定義
//編譯led.c文件#define GPC1CON *((volatile unsigned int*)0xE0200080) #define GPC1DAT *((volatile unsigned int*)0xE0200084) #define GPC1PUD *((volatile unsigned int*)0xE0200088) //隱式聲明 void delay (unsigned int); void led_test (void) {//配置相應管腳為輸出功能 GPC1_3GPC1CON &= ~(0x0f << 12);GPC1CON |= (1 << 12);//GPC1_4為輸出功能GPC1CON |= (1 << 16);//禁止內部上拉下拉功能GPC1PUD &= ~(0x03 << 6);GPC1PUD &= ~(0x03 << 8);while (1) {//燈亮GPC1DAT |= (1 << 3);GPC1DAT |= (1 << 4);delay (0x100000);//燈滅GPC1DAT &= ~(1 << 3);GPC1DAT &= ~(1 << 4);delay (0x100000);} }void delay (unsigned int n) {unsigned int i = 0;for (i = n; i != 0; i--); }編譯: arm-linux-gcc -c led.c -o led.o –nostdlib 不使用標準庫,生成led.o文件二、為什么使用 volatile
我們上一篇文章講到了 const 和 volatile 關鍵字是一種類型修飾符。volatile 的作用?是作為指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。
?
現在考慮一個問題,編譯器如何對代碼進行優化的?
我們看一個例子:
//示例一 #include <stdio.h> int main (void) {int i = 10;int a = i; //優化int b = i;printf ("i = %d\n", b);return 0; } //編譯優化、查看匯編 gcc -O2 -S test.c cat test.s .file "test.c".section .rodata.str1.1,"aMS",@progbits,1 .LC0:.string "i = %d\n".section .text.startup,"ax",@progbits.p2align 4,,15.globl main.type main, @function main: .LFB22:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5andl $-16, %espsubl $16, %espmovl $10, 8(%esp)movl $.LC0, 4(%esp)movl $1, (%esp)call __printf_chkxorl %eax, %eaxleave.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc .LFE22:.size main, .-main.ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3".section .note.GNU-stack,"",@progbits //示例二 #include <stdio.h> int main (void) {volatile int i = 10;int a = i; //未優化int b = i;printf ("i = %d\n", b);return 0; } //編譯優化、查看匯編 gcc -O2 -S test.c cat test.s .file "test.c".section .rodata.str1.1,"aMS",@progbits,1 .LC0:.string "i = %d\n".section .text.startup,"ax",@progbits.p2align 4,,15.globl main.type main, @function main: .LFB22:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5andl $-16, %espsubl $32, %espmovl $10, 28(%esp)movl 28(%esp), %eaxmovl 28(%esp), %eaxmovl $.LC0, 4(%esp)movl $1, (%esp)movl %eax, 8(%esp)call __printf_chkxorl %eax, %eaxleave.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc .LFE22:.size main, .-main.ident "GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3".section .note.GNU-stack,"",@progbits比較:
可以清楚的看到:使用 volatile 的代碼編譯未優化。
volatile 指出 i 是隨時可能發生變化的,每次使用它的時候必須從 i的地址中讀取,因而編譯器生成的匯編代碼會重新從i的地址讀取數據放在 b 中。而優化做法是,由于編譯器發現兩次從 i讀數據的代碼之間的代碼沒有對 i 進行過操作,它會自動把上次讀的數據放在 b 中。而不是重新從 i 里面讀。這樣以來,如果 i是一個寄存器變量或者表示一個端口數據就容易出錯,所以說?volatile 可以保證對特殊地址的穩定訪問。
?
如果上述例子,還是不夠明顯:
#include <stdio.h> #include <sys/timeb.h> long long getSystemTime() { struct timeb t; ftime(&t); return 1000 * t.time + t.millitm; } #define TIME 1000000000 int main(void) { volatile int a, b = TIME; /* volatile修飾變量 */ int x, y = TIME; /* 一般變量 */ long long start = 0, end = 0; start=getSystemTime(); for (a = 0; a < b; a++); end=getSystemTime(); printf("vloatile修飾變量用時: %lld ms\n", end - start); start=getSystemTime(); for (x = 0; x < y; x++); end=getSystemTime(); printf("一般變量用時: %lld ms\n", end - start); return 0; } ??
編譯:gcc test.c 輸出結果: vloatile修飾變量用時: 3738 ms 一般變量用時: 3742 ms 優化編譯:gcc -O2 test.c 輸出結果: vloatile修飾變量用時: 3550 ms 一般變量用時: 0 ms可明顯看出:
for(int i=0; i<100000; i++);
這個語句用來測試空循環的速度的,但是編譯器肯定要把它優化掉,根本就不執行。
如果你寫成,
for(volatile int i=0; i<100000; i++);
它就會執行了。
?
我們用上面的例子基本已經搞明白,volatile 不會被編譯器優化了,現在講點理論知識。
參看:C語言中volatile關鍵字的作用
?
1、編譯器優化介紹:
由于內存訪問速度遠不及CPU處理速度,為提高機器整體性能,在硬件上引入硬件高速緩存Cache,加速對內存的訪問。另外在現代CPU中指令的執行并不一定嚴格按照順序執行,沒有相關性的指令可以亂序執行,以充分利用CPU的指令流水線,提高執行速度。以上是硬件級別的優化。再看軟件一級的優化:一種是在編寫代碼時由程序員優化,另一種是由編譯器進行優化。編譯器優化常用的方法有:將內存變量緩存到寄存器;調整指令順序充分利用CPU指令流水線,常見的是重新排序讀寫指令。對常規內存進行優化的時候,這些優化是透明的,而且效率很好。由編譯器優化或者硬件重新排序引起的問題的解決辦法是在從硬件(或者其他處理器)的角度看必須以特定順序執行的操作之間設置內存屏障(memory barrier),Linux 提供了一個宏解決編譯器的執行順序問題。
void Barrier(void)
這個函數通知編譯器插入一個內存屏障,但對硬件無效,編譯后的代碼會把當前CPU寄存器中的所有修改過的數值存入內存,需要這些數據的時候再重新從內存中讀出。
2、volatile總是與優化有關,編譯器有一種技術叫做數據流分析,分析程序中的變量在哪里賦值、在哪里使用、在哪里失效,分析結果可以用于常量合并,常量傳播等優化,進一步可以消除一些代碼。但有時這些優化不是程序所需要的,這時可以用volatile關鍵字禁止做這些優化。
volatile的本意是“易變的” 因為訪問寄存器要比訪問內存單元快的多,所以編譯器一般都會作減少存取內存的優化,但有可能會讀臟數據。當要求使用volatile聲明變量值的時候,系統總是重新從它所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。精確地說就是,遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問;如果不使用valatile,則編譯器將對所聲明的語句進行優化。(簡潔的說就是:volatile關鍵詞影響編譯器編譯的結果,用volatile聲明的變量表示該變量隨時可能發生變化,與該變量有關的運算,不要進行編譯優化,以免出錯)
?
三、volatile 使用
1、并行設備的硬件寄存器(如:狀態寄存器)
存儲器映射的硬件寄存器通常也要加 voliate,因為每次對它的讀寫都可能有不同意義。
例如:
假設要對一個設備進行初始化,此設備的某一個寄存器為0xff800000。
經過編譯器優化后,編譯器認為前面循環半天都是廢話,對最后的結果毫無影響,因為最終只是將output這個指針賦值為 9,所以編譯器最后給你編譯編譯的代碼結果相當于:
int init(void) {*output = 9; }如果你對此外部設備進行初始化的過程是必須是像上面代碼一樣順序的對其賦值,顯然優化過程并不能達到目的。反之如果你不是對此端口反復寫操作,而是反復讀操作,其結果是一樣的,編譯器在優化后,也許你的代碼對此地址的讀操作只做了一次。然而從代碼角度看是沒有任何問題的。這時候就該使用volatile通知編譯器這個變量是一個不穩定的,在遇到此變量時候不要優化。
?
再例如上面提到的?volatile 用于相關寄存器定義
//編譯led.c文件#define GPC1CON *((volatile unsigned int*)0xE0200080) #define GPC1DAT *((volatile unsigned int*)0xE0200084) #define GPC1PUD *((volatile unsigned int*)0xE0200088) //隱式聲明 void delay (unsigned int); void led_test (void) {//配置相應管腳為輸出功能 GPC1_3GPC1CON &= ~(0x0f << 12);GPC1CON |= (1 << 12);//GPC1_4為輸出功能GPC1CON |= (1 << 16);//禁止內部上拉下拉功能GPC1PUD &= ~(0x03 << 6);GPC1PUD &= ~(0x03 << 8);while (1) {//燈亮GPC1DAT |= (1 << 3);GPC1DAT |= (1 << 4);delay (0x100000);//燈滅GPC1DAT &= ~(1 << 3);GPC1DAT &= ~(1 << 4);delay (0x100000);} }void delay (unsigned int n) {unsigned int i = 0;for (i = n; i != 0; i--); }編譯: arm-linux-gcc -c led.c -o led.o –nostdlib 不使用標準庫,生成led.o文件#define GPC1CON *((volatile unsigned int*)0xE0200080) ??怎么理解?
這里其實就是定義了一個指針變量。
GPC1CON 為寄存器名稱、0xE0200080 為寄存器地址、(volatile unsigned int*) 為強制類型轉換。
我們知道 volatile 和 const 一樣為類型修飾符,不改變變量類型。
?
寄存器地址為什么要加 volatile 修飾呢?
是因為,這些寄存器里面的值是隨時變化的。如果我們沒有將這個地址強制類型轉換成 volatile,那么我們在使用GPC1CON 這個寄存器的時候,?會直接從 CPU 的寄存器中取值。因為之前GPC1CON ?被訪問過,也就是之前就從內存中取出?GPC1CON 的值保存到某個寄存器中。之所以直接從寄存器中取值,而不去內存中取值,是因為編譯器優化代碼的結果(訪問 CPU寄存器比訪問 RAM 快的多)。用 volatile 關鍵字對?0xE0200080 ?進行強制轉換,使得每一次訪問?GPC1CON 時,執行部件都會從?0xE0200080 ?這個內存單元中取出值來賦值給?GPC1CON ?。
?
2、一個中斷服務子程序中會訪問到的非自動變量(Non-automatic variables)
由于訪問寄存器的速度要快過RAM,所以編譯器一般都會作減少存取外部RAM的優化,例如:
static int i=0; //i 為非自動變量 int main(void) {...while (1){ if (i) dosomething(); } } /* Interrupt service routine. */ void ISR_2(void) {i=1; }程序的本意是希望 ISR_2 中斷產生時,在main函數中調用 dosomething 函數,但是,由于編譯器判斷在 main 函數里面沒有修改過 i,因此可能只執行一次對從i到某寄存器的讀操作,然后每次if判斷都只使用這個寄存器里面的“i副本”,導致 dosomething 永遠也不會被調用。如果將變量加上 volatile 修飾,則編譯器保證對此變量的讀寫操作都不會被優化(肯定執行)。此例中i也應該如此說明。
?
3、多線程應用中被幾個任務共享的變量
當兩個線程都要用到某一個變量且該變量的值會被改變時,應該用 volatile 聲明,該關鍵字的作用是防止優化編譯器把變量從內存裝入CPU寄存器中。如果變量被裝入寄存器,那么兩個線程有可能一個使用內存中的變量,一個使用寄存器中的變量,這會造成程序的錯誤執行。volatile的意思是讓編譯器每次操作該變量時一定要從內存中真正取出,而不是使用已經存在寄存器中的值,如下:
volatile BOOL bStop = FALSE; //bStop 為共享全局變量 (1) 在一個線程中: while( !bStop ) { ... } bStop = FALSE; return; (2) 在另外一個線程中,要終止上面的線程循環: bStop = TRUE; while( bStop );等待上面的線程終止,如果bStop不使用volatile申明,那么這個循環將是一個死循環,因為bStop已經讀取到了寄存器中,寄存器中bStop的值永遠不會變成FALSE,加上volatile,程序在執行時,每次均從內存中讀出bStop的值,就不會死循環了。
?
四、volatile 問題和總結
volatile 常見的幾個面試題:
1、一個參數既可以是const還可以是volatile嗎?
可以,例如只讀的狀態寄存器。它是 volatile 因為它可能被意想不到地改變。它是 const 因為 程序不應該試圖去修改它。
?
2、一個指針可以是 volatile 嗎?
可以,當一個中服務子程序修改一個指向一個 buffer 的指針時。
3、下面的函數有什么錯誤:?
int square(volatile int *ptr) { return *ptr * *ptr; }這段代碼的目的是用來返指針*ptr指向值的平方,但是,由于*ptr指向一個volatile型參數,編譯器將產生類似下面的代碼:
int square(volatile int *ptr) { int a,b; a = *ptr; b = *ptr; return a * b; }由于*ptr的值可能被意想不到地該變,因此a和b可能是不同的。結果,這段代碼可能返不是你所期望的平方值!正確的代碼如下:?
long square(volatile int *ptr) { int a; a = *ptr; return a * a; }總結:
?
volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改。volatile 提醒編譯器它后面所定義的變量隨時都有可能改變,因此編譯后的程序每次需要存儲或讀取這個變量的時候,都會直接從變量地址中讀取數據。如 果沒有 volatile 關鍵字,則編譯器可能優化讀取和存儲,可能暫時使用寄存器中的值,如果這個變量由別的程序更新了的話,將出現不一致的現象。所以遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。
總結
以上是生活随笔為你收集整理的C语言:关键字volatile详解!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: const与#define相比,区别和优
- 下一篇: 进程同步的5种机制