volatile陷阱

對於volatile關鍵字,大部分的C語言教材都是一筆帶過,並沒有做太過深入的分析,所以這裏簡單整理了一些關於volatile的使用注意事項。實際上從語法上來看volatile和const是一樣的,但是如果const用錯,幾乎不會有什麼問題;而volatile用錯,後果可能很嚴重。所以在volatile的使用上,建議大家還是儘量求穩,少用一些沒有切實把握的技巧。

注意volatile修飾的是誰

首先來看下面兩個定義的區別:

uchar * volatile reg;

這行代碼裏volatile修飾的是reg這個變量。所以這裏實際上是定義了一個uchar類型的指針,並且這個指針變量本身是volatile 的。但是指針所指的內容並不是volatile的!在實際使用的時候,編譯器對代碼中指針變量reg本身的操作不會進行優化,但是對reg所指的內容 *reg卻會作爲non-volatile內容處理,對*reg的操作還是會被優化。通常這種寫法一般用在對共享指針的聲明上,即這個指針變量有可能會被中斷等函數修改。將其定義爲volatile以後,編譯器每次取指針變量的值的時候都會從內存中載入,這樣即使這個變量已經被別的程序修改了當前函數用的時候也能得到修改後的值(否則通常只在函數開始取一次放在寄存器裏,以後就一直使用寄存器內的副本)。

volatile uchar *reg;

這行代碼裏volatile修飾的是指針所指的內容。所以這裏定義了一個uchar類型的指針,並且這個指針指向的是一個volatile的對象。但是指針變量本身並不是volatile的。如果對指針變量reg本身進行計算或者賦值等操作,是可能會被編譯器優化的。但是對reg所指向的內容 *reg的引用卻禁止編譯器優化。因爲這個指針所指的是一個volatile的對象,所以編譯器必須保證對*reg的操作都不被優化。通常在驅動程序的開發中,對硬件寄存器指針的定義,都應該採用這種形式。

volatile uchar * volatile reg;

這樣定義出來的指針就本身是個volatile的變量,又指向了volatile的數據內容。

volatile與const的合用

從字面上看,volatile和const似乎是一個對象的兩個對立屬性,是互斥的。但是實際上,兩者是有可能一起修飾同一個對象的。看看下面這行聲明:

extern const volatile unsigned int rt_clock;

這是在RTOS系統內核中常見的一種聲明:rt_clock通常是指系統時鐘,它經常被時鐘中斷進行更新。所以它是volatile,易變的。因此在用的時候,要讓編譯器每次從內存裏面取值。而rt_clock通常只有一個寫者(時鐘中斷),其他地方對其的使用通常都是隻讀的。所以將其聲明爲 const,表示這裏不應該修改這個變量。所以volatile和const是兩個不矛盾的東西,並且一個對象同時具備這兩種屬性也是有實際意義的。

  注意

在上面這個例子裏面,要注意聲明和定義時對const的使用:

在需要讀寫rt_clock變量的中斷處理程序裏面,應該如下定義(define)此變量:

volatile unsigned int rt_clock;

而在提供給外部用戶使用的頭文件裏面,可以將此變量聲明(declare)爲:

extern const volatile unsigned int rt_clock;

這樣是沒有問題的。但是切記一定不能反過來,即定義一個const的變量:

const unsigned int a;

但是卻聲明爲非const變量:

extern unsigned int a;

這樣萬一在用戶函數裏面對a進行了寫操作,結果是Undefined。

再看另一個例子:

volatile struct devregs * const dvp = DEVADDR;

這裏的volatile和const實際上是分別修飾了兩個不同的對象:volatile修飾的是指針dvp所指的類型爲struct devregs的數據結構,這個結構對應者設備的硬件寄存器,所以是易變的,不能被優化的;而後面的const修飾的是指針變量dvp。因爲硬件寄存器的地址是一個常量,所以將這個指針變量定義成const的,不能被修改。

危險的volatile用法

下面將列舉幾種對volatile的不當使用和可能導致的非預期的結果。

1.例:定義爲volatile的結構體成員

考察下面對一個設備硬件寄存器結構類型的定義:

struct devregs{ 
    unsigned short volatile csr; 
    unsigned short const volatile data; 
};

我們的原意是希望聲明一個設備的硬件寄存器組。其中有一個16bit的CSR控制/狀態寄存器,這個寄存器可以由程序向設備寫入控制字,也可以由硬件設備設置反映其工作狀態。另外還有一個16bit的DATA數據寄存器,這個寄存器只會由硬件來設置,由程序進行讀入。

看起來,這個結構的定義沒有什麼問題,也相當符合實際情況。但是如果執行下面這樣的代碼時,會發生什麼情況呢?

struct devregs * const dvp = DEVADDR; 
 
while ((dvp->csr & (READY | ERROR)) == 0) 
    ; /* NULL - wait till done */

通過一個non-volatile的結構體指針,去訪問被定義爲volatile的結構體成員,編譯器將如何處理?答案是:Undefined!C99 標準沒有對編譯器在這種情況下的行爲做規定。所以編譯器有可能正確地將dvp->csr作爲volatile的變量來處理,使程序運行正常;也有可能就將dvp->csr作爲普通的non-volatile變量來處理,在while當中優化爲只有開始的時候取值一次,以後每次循環始終使用第一次取來的值而不再從硬件寄存器裏讀取,這樣上面的代碼就有可能陷入死循環!!

如果你使用一個volatile的指針來指向一個非volatile的對象。比如將一個non-volatile的結構體地址賦給一個 volatile的指針,這樣對volatile指針所指結構體的使用都會被編譯器認爲是volatile的,即使原本那個對象沒有被聲明爲 volatile。然而反過來,如果將一個volatile對象的地址賦給一個non-volatile的普通指針,通過這個指針訪問volatile對象的結果是undefined,是危險的。

所以對於本例中的代碼,我們應該修改成這樣:

struct devregs { 
    unsigned short csr; 
    unsigned short data; 
}; 
 
volatile struct devregs * const dvp = DEVADDR;

這樣我們才能保證通過dvp指針去訪問結構體成員的時候,都是作爲volatile來處理的。

2.例:定義爲volatile的結構體類型

考察如下代碼:

volatile struct devregs { 
    /* stuff */ 
} dev1; 
......; 
struct devregs dev2;

作者的目的也許是希望定義一個volatile的結構體類型,然後順便定義一個這樣的volatile結構體變量dev1。後來又需要一個這種類型的變量,因此又定義了一個dev2。然而,第二次所定義的dev2變量實際上是non-volatile的!!因爲實際上在定義結構體類型時的那個 volatile關鍵字,修飾的是dev1這個變量而不是struct devregs類型的結構體!!

所以這個代碼應該改寫成這樣:

typedef volatile struct devregs { 
    /* stuff */ 
} devregs_t; 
 
devregs_t dev1; 
......; 
devregs_t dev2;

這樣我們才能得到兩個volatile的結構體變量。

3.例:多次的間接指針引用

考察如下代碼:

/* DMA buffer descriptor */ 
struct bd { 
    unsigned int state; 
    unsigned char *data_buff; 
}; 
 
struct devregs { 
    unsigned int csr; 
    struct bd *tx_bd; 
    struct bd *rx_bd; 
}; 
 
volatile struct devregs * const dvp = DEVADDR; 
 
/* send buffer */ 
dvp->tx_bd->state = READY; 
while ((dvp->tx_bd->state & (EMPTY | ERROR)) == 0) 
    ; /* NULL - wait till done */

這樣的代碼常用在對一些DMA設備的發送Buffer處理上。通常這些Buffer Descriptor(BD)當中的狀態會由硬件進行設置以告訴軟件Buffer是否完成發送或接收。但是請注意,上面的代碼中對dvp->tx_bd->state的操作實際上是non-volatile的!這樣的操作有可能因爲編譯器對其讀取的優化而導致後面陷入死循環。

因爲雖然dvp已經被定義爲volatile的指針了,但是也只有其指向的devregs結構才屬於volatile object的範圍。也就是說,將dvp聲明爲指向volatile數據的指針可以保障其所指的volatile object之內的tx_bd這個結構體成員自身是volatile變量,但是並不能保障這個指針變量所指的數據也是volatile的(因爲這個指針並沒有被聲明爲指向volatile數據的指針)。

要讓上面的代碼正常工作,可以將數據結構的定義修改成這樣:

struct devregs { 
    unsigned int csr; 
    volatile struct bd *tx_bd; 
    volatile struct bd *rx_bd; 
};

這樣可以保證對state成員的處理也是volatile的。不過最爲穩妥和清晰的辦法還是這樣:

volatile struct devregs * const dvp = DEVADDR; 
volatile struct bd *tx_bd = dvp->tx_bd; 
 
tx_bd->state = READY; 
while ((tx_bd->state & (EMPTY | ERROR)) == 0) 
    ; /* NULL - wait till done */

這樣在代碼裏面能絕對保證數據結構的易變性,即使數據結構裏面沒有定義好也不會有關係。而且對於日後的維護也有好處:因爲這樣從代碼裏一眼就能看出哪些數據結構的訪問是必須保證volatile的。

4.例:到底哪個volatile可能無效

就在你看過前面幾個例子,感覺自己可能已經都弄明白了的時候,請看最後這個例子:

struct hw_bd { 
    ......; 
    volatile unsigned char * volatile buffer; 
}; 

struct hw_bd *bdp; 

......; 
bdp->buffer = ...; ① 
bdp->buffer[i] = ...; ②

請問上面標記了①和②的兩行代碼,哪個是確實在訪問volatile對象,而哪個又是undefined的結果?

答案是:②是volatile的,①是undefined。來看本例的數據結構示意圖:

        (non-volatile)
 bdp -->+-------------+
        |             |
        |   ... ...   |
        |             |
        +-------------+    (volatile)   
        |    buffer   |-->+------------+
        +-------------+   |            |
                          |            |
                          |            |
                          +------------+
                          |  buffer[i] |
                          +------------+
                          |            |
                          |            |
                          +------------+
buffer成員本身是通過一個non-volatile的指針bdp訪問的,按照C99標準的定義,這就屬於undefined的情況,因此對bdp->buffer的訪問編譯器不一定能保證是volatile的;

雖然buffer成員本身可能不是volatile的變量,但是buffer成員是一個指向volatile對象的指針。因此對buffer成員所指對象的訪問編譯器可以保證是volatile的,所以bdp->buffer[i]是volatile的。

所以,看似簡單的volatile關鍵字,用起來還是有非常多的講究在裏面的,大家一定要引起重視。

發佈了192 篇原創文章 · 獲贊 151 · 訪問量 97萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章