1. 程式人生 > >C語言再學習 -- 關鍵字volatile

C語言再學習 -- 關鍵字volatile

上週確實事情挺多的,年會、公司聚餐,一到過年就有忙不完的事分心。還好C語言再學習總結的已經差不多了,年前也不展開別的了,接下來這十幾天、總結幾篇典型的面試題吧。

言歸正傳,接下來看看關鍵字 volatile。

一、volatile 介紹

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_3
    GPC1CON &= ~(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_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$16, %esp
	movl	$10, 8(%esp)
	movl	$.LC0, 4(%esp)
	movl	$1, (%esp)
	call	__printf_chk
	xorl	%eax, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.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_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	andl	$-16, %esp
	subl	$32, %esp
	movl	$10, 28(%esp)
	movl	28(%esp), %eax
	movl	28(%esp), %eax
	movl	$.LC0, 4(%esp)
	movl	$1, (%esp)
	movl	%eax, 8(%esp)
	call	__printf_chk
	xorl	%eax, %eax
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.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 不會被編譯器優化了,現在講點理論知識。

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。

int  *output = (unsigned  int *)0xff800000;//定義一個IO埠;
int   init(void)
{
      int i;
      for(i=0;i< 10;i++){
         *output = i;
}
}
經過編譯器優化後,編譯器認為前面迴圈半天都是廢話,對最後的結果毫無影響,因為最終只是將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_3
    GPC1CON &= ~(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 關鍵字,則編譯器可能優化讀取和儲存,可能暫時使用暫存器中的值,如果這個變數由別的程式更新了的話,將出現不一致的現象。所以遇到這個關鍵字宣告的變數,編譯器對訪問該變數的程式碼就不再進行優化,從而可以提供對特殊地址的穩定訪問