2013年9月16日 星期一

C語言: 認識關鍵字volatile

前言:

前面文章中C語言的幾道問題提道 : 

volatile是C程序員和嵌入式系統程序員的最基本的問題。搞嵌入式的家夥們經常同硬體、中斷、RTOS等等打交道,所有這些都要求用到volatile變量。不懂得volatile的內容將會帶來災難


正文 :  Introduction to the Volatile Keyword   (認識關鍵字Volatile)

The use of volatile is poorly understood by many programmers. This is not surprising, as most C texts dismiss it in a sentence or two.

很多程式師對於volatile的用法都不是很熟悉。這並不奇怪,很多介紹C語言的書籍對於他的用法都閃爍其辭。

Have you experienced any of the following in your C/C++ embedded code?
•        Code that works fine-until you turn optimization on
•        Code that works fine-as long as interrupts are disabled
•        Flaky hardware drivers
•        Tasks that work fine in isolation-yet crash when another task is enabled

在你們使用C/C++語言開發嵌入式系統的時候,遇到過以下的情況麼?
•       
一打開編譯器的編譯優化選項,代碼就不再正常工作了;
•        中斷似乎總是程式異常的元兇;
•        硬體驅動工作不穩定;
•        多工系統中,單個任務工作正常,加入任何其他任務以後,系統就崩潰了。

If you answered yes to any of the above, it's likely that you didn't use the C keyword volatile. You aren't alone. The use of volatile is poorly understood by many programmers. This is not surprising, as most C texts dismiss it in a sentence or two.

如果你曾經向別人請教過和以上類似的問題,至少說明,你還沒有接觸過C語言關鍵字volatile的用法。這種情況,你不是第一個遇到。很多程式師對於volatile都幾乎一無所知。大部分介紹C語言的文獻對於它都閃爍其辭。

volatile is a qualifier that is applied to a variable when it is declared. It tells the compiler that the value of the variable may change at any time-without any action being taken by the code the compiler finds nearby. The implications of this are quite serious. However, before we examine them, let's take a look at the syntax.

Volatile
是一個變數聲明限定詞它告訴編譯器,它所修飾的變數的值可能會在任何時刻被意外的更新,即便與該變數相關的上下文沒有任何對其進行修改的語句。造成這種意外更新的原因相當複雜。在我們分析這些原因之前,我們先回顧一下與其相關的語法。

Syntax (
語法)

To declare a variable volatile, include the keyword volatile before or after the data type in the variable definition. For instance both of these declarations will declare foo to be a volatile integer:

要想給一個變數加上volatile限定,只需要在變數類型聲明附之前/後加入一個volatile關鍵字就可以了。下面的兩個實例是等效的,它們都是將foo聲明為一個需要被即時更新int型變數。

volatile int foo;
int volatile foo;

Now, it turns out that pointers to volatile variables are very common. Both of these declarations declare foo to be a pointer to a volatile integer:

同樣,聲明一個指向volatile型變數的pointer也是非常類似的。下面的兩個聲明都是將foo定義為一個指向volatile integer型變數的pointer。

volatile int * foo;
int volatile * foo;

Volatile pointers to non-volatile variables are very rare (I think I've used them once), but I'd better go ahead and give you the syntax:

一個Volatile型的pointer指向一個非volatile型變數的情況非常少見(我想,我可能使用過一次),儘管如此,我還是要給出他的語法:

int * volatile foo;

And just for completeness, if you really must have a volatile pointer to a volatile variable, then:

最後一種形式,針對你真的需要一個volatile型的指標指向一個volatile型的情形:

int volatile * volatile foo;

Incidentally, for a great explanation of why you have a choice of where to place volatile and why you should place it after the data type (for example, int volatile * foo), consult Dan Sak's column, "Top-Level cv-Qualifiers in Function Parameters" (February 2000, p. 63).

順便說一下,如果你想知道關於我們需要在什麼時候在什麼地方使用volatile”為什麼我們需要volatile放在變數類型後面(例如,int volatile * foo這類問題的詳細內容,請參考Dan Sak`s的專題,Top-Levelcv-Qualifiers in Function Parameters

Finally, if you apply volatile to a struct or union, the entire contents of the struct/union are volatile. If you don't want this behavior, you can apply the volatile qualifier to the individual members of the struct/union.

最後,如果你將volatile應用在struct或者是union上,那麼該struct/union內的所有內容就都帶有volatile屬性了。如果你並不想這樣(牽一髮而動全身),你可以僅僅在struct/union中的某一個成員上單獨使用該限定。

Use(
使用)

A variable should be declared volatile whenever its value could change unexpectedly. In practice, only three types of variables could change:

•        Memory-mapped peripheral registers
•        Global variables modified by an interrupt service routine
•        Global variables within a multi-threaded application

當一個變數的內容可能會被意想不到的更新時(by hardware),一定要使用volatile來聲明該變數。通常,只有三種類型的變數會發生這種意外
•       
在記憶體中進行位址映射的周邊暫存器(peripheral registers)
•        在中斷處理程式中(interrupt service routine)可能被修改的總體變數(Global variables)
•        多線程應用程式中(multi-threaded application) 的總體變數(Global variables)

Note : (added by  
一心の流)
"A book on c" 作者AL Kelley/Ira PohL對 volatile 這樣說明 :
"The qualifier volatile indicates that the object may be acted on by the hardware."

example :
 extern const volatile int real_time_clock;

Because  const is also qualifier, the object may not be assigned to. incremented, or decremented within the program . The hardware can change the clock, but the code cannot.

Peripheral registers(周邊暫存器)

Embedded systems contain real hardware, usually with sophisticated peripherals. These peripherals contain registers whose values may change asynchronously to the program flow. As a very simple example, consider an 8-bit status register at address 0x1234. It is required that you poll the status register until it becomes non-zero. The nave and incorrect implementation is as follows:

嵌入式系統的硬體實體中,通常包含一些複雜的週邊設備。這些設備中包含的暫存器,其值往往隨著程式的流程同步的進行改變。在一個非常簡單的例子中,假設我們有一個8位元的狀態暫存器映射在位址0x1234上。系統需要我們一直監測狀態暫存器的值,直到它的值不為0為止。通常錯誤的實現方法是:

uint * ptr = (uint *) 0x1234; // Wait for register to become non-zero.
等待暫存器變為非0
while (*ptr == 0);
// Do something else.
作其他事情

This will almost certainly fail as soon as you turn the optimizer on, since the compiler will generate assembly language that looks something like this:

一旦你打開了優化選項,這種寫法肯定會失敗,編譯器就會生成類似如下的組合語言代碼:

              mov    ptr, #0x1234     

              mov    a, @ptr loop
              bz       loop

The rationale of the optimizer is quite simple: having already read the variable's value into the accumulator (on the second line), there is no need to reread it, since the value will always be the same. Thus, in the third line, we end up with an infinite loop. To force the compiler to do what we want, we modify the declaration to:

優化的工作原理非常簡單一旦我們我們將一個變數讀入暫存器中(參照代碼的第二行),如果(從變數相關的上下文看)變數的值總是不變的,那麼就沒有必要(從記憶體中)從新讀取他。在代碼的第三行中,我們使用一個無限迴圈來結束。為了強迫編譯器按照我們的意願進行編譯,我們修改指標的聲明為:

uint volatile * ptr = (uint volatile *) 0x1234;


The assembly language now looks like this:

對應的彙編代碼為:
          mov     ptr, #0x1234
loop    mov     a, @ptr    
          bz        loop

The desired behavior is achieved.

我們需要的功能實現了!

Subtler problems tend to arise with registers that have special properties. For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.

對於一些較為特殊的暫存器,(我們上面提到的方法)會導致一些難以想像的錯誤。事實上,很多周邊暫存器在讀取一次以後就會被清除。這種情況下,多餘的讀取操作會導致意想不到的錯誤。


Interrupt service routines (
中斷處理程式)

Interrupt service routines often set variables that are tested in main line code. For example, a serial port interrupt may test each received character to see if it is an ETX character (presumably signifying the end of a message). If the character is an ETX, the ISR might set a global flag. An incorrect implementation of this might be:

中斷處理程式經常負責更新一些在主程序中被查詢的變數的值。例如,一個串列通訊中斷會檢測接收到的每一個位元組是否為ETX信號(以便來確認一個消息(message)的結束標誌)。如果其中的一個位元組為ETX,中斷處理程式就是修改一個global flag。一個錯誤的實現方法可能為:

int etx_rcvd = FALSE;       //初始化etx_rcvd 變數
void main()
{
    ...
    while (!ext_rcvd)
    {
        // Wait
    }
    ...
}

interrupt void rx_isr(void)
{
    ...
    if (ETX == rx_char)
    {
        etx_rcvd = TRUE;
    }
    ...
}

With optimization turned off, this code might work. However, any half decent optimizer will "break" the code. The problem is that the compiler has no idea that etx_rcvd can be changed within an ISR. As far as the compiler is concerned, the expression !ext_rcvd is always true, and, therefore, you can never exit the while loop. Consequently, all the code after the while loop may simply be removed by the optimizer. If you are lucky, your compiler will warn you about this. If you are unlucky (or you haven't yet learned to take compiler warnings seriously), your code will fail miserably. Naturally, the blame will be placed on a "lousy optimizer."

在編譯優化選項關閉的時候,代碼可能會工作的很好。但是,即便是任何半吊子的優化,也會破壞這個代碼的意圖。問題就在於,編譯器並不知道etx_rcvd會在中斷處理程式中被更新。在編譯器可以檢測的上下文內,運算式!ext_rcvd總是為"真",所以,你就永遠無法從迴圈中跳出。因此,該迴圈後面的代碼會被當作不可達到的內容而被編譯器的優化選項簡單的刪除掉。如果你比較幸運,你的編譯器也許會給你一個相關的警告;如果你沒有那麼幸運(或者你沒有注意到這些警告),你的代碼就會導致嚴重的錯誤。通常,就會有人抱怨該死的優化選項

The solution is to declare the variable etx_rcvd to be volatile. Then all of your problems (well, some of them anyway) will disappear.

解決這個問題的方法很簡單:將變數etx_rcvd聲明為volatile。然後,所有的(至少是一部分症狀)那些錯誤症狀就會消失。

Multi-threaded applications (
多線程應用程式)

Despite the presence of queues, pipes, and other scheduler-aware communications mechanisms in real-time operating systems, it is still fairly common for two tasks to exchange information via a shared memory location (that is, a global). When you add a pre-emptive scheduler to your code, your compiler still has no idea what a context switch is or when one might occur. Thus, another task modifying a shared global is conceptually identical to the problem of interrupt service routines discussed previously. So all shared global variables should be declared volatile. For example:

在即時操作系統中(RTOS),除去佇列(queues)、管道(pipes)以及其他調度相關的通訊結構,在兩個任務之間採用共用的記憶體空間(shared memory location)(就是全局共用, global)實現資料的交換仍然是相當常見的方法。當你將一個優先權調度器應用于你的代碼時,編譯器仍然不知道某一程式段分支選擇的實際工作方式以及什麼時候某一分支情況會發生。這是因為,另外一個任務修改一個共用的總體變數在概念上通常和前面中斷處理程式中提到的情形是一樣的。所以,(這種情況下)所有共用的總體變數都要被聲明為volatile。例如:

int cntr;
void task1(void)
{
    cntr = 0;
    while (cntr == 0)
    {
        sleep(1);
    }
    ...
}

void task2(void)
{
    ...
    cntr++;
    sleep(10);
    ...
}

This code will likely fail once the compiler's optimizer is enabled. Declaring cntr to be volatile is the proper way to solve the problem.

一旦編譯器的優化選項被打開,這段代碼的執行通常會失敗。將cntr聲明為volatile是解決問題的好辦法。

Final thoughts(
反思)

Some compilers allow you to implicitly declare all variables as volatile. Resist this temptation, since it is essentially a substitute for thought. It also leads to potentially less efficient code.

一些編譯器允許我們隱含的聲明所有的變數為volatile。最好抵制這種便利的誘惑,因為它很容易讓我們不動腦子,而且,這也常常會產生一個效率相對較低的代碼。

Also, resist the temptation to blame the optimizer or turn it off. Modern optimizers are so good that I cannot remember the last time I came across an optimization bug. In contrast, I come across failures to use volatile with depressing frequency.

所以,我們又詛咒編譯優化或者簡單的關掉這一選項來抵制這些誘惑。現在的編譯優化已經相當聰明,我不記得在編譯優化中找到過什麼錯誤。與之相比,為了解決一些錯誤,我卻常常使用瘋狂數量的volatile

If you are given a piece of flaky code to "fix," perform a grep for volatile. If grep comes up empty, the examples given here are probably good places to start looking for problems.

如果你恰巧有一段代碼需要去修正,先搜索一下有沒有volatile關鍵字。如果找不到volatile,那麼這個代碼很可能會是一個很好的實例來檢測前面提到過的各種錯誤。

Nigel Jones is a consultant living in Maryland. When not underwater, he can be found slaving away on a diverse range of embedded projects. He can be reached at NAJones@compuserve.com.

Nigel Jones
在馬里蘭從事顧問工作。除了為各類嵌入式專案開發充當顧問,他平時的一大愛好就是潛水。你可以通過發送郵件到NAJones@compuserve.com與其取得聯繫。




另一篇有用文章Post給大家參考

Volatile的陷阱 

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

1) 注意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的資料內容。

2) volatile
const的合用 

  從字面上看,volatileconst似乎是一個物件的兩個對立屬性,是互斥的。但是實際上,兩者是有可能一起修飾同一個物件的。看看下面這行聲明:
extern const volatile unsigned int rt_clock;
這是在RTOS系統內核中常見的一種聲明:rt_clock通常是指系統時鐘,它經常被時鐘中斷進行更新。所以它是volatile,易變的。因此在用的時候,要讓編譯器每次從記憶體裏面取值。而rt_clock通常只有一個寫者(時鐘中斷),其他地方對其的使用通常都是唯讀的。所以將其聲明為 const,表示這裏不應該修改這個變數。所以volatileconst是兩個不矛盾的東西,並且一個物件同時具備這兩種屬性也是有實際意義的。

  
注意
在上面這個例子裏面,要注意聲明和定義時對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;
這裏的volatileconst實際上是分別修飾了兩個不同的物件:volatile修飾的是指標dvp所指的類型為struct devregs的資料結構,這個結構對應者設備的硬體寄存器,所以是易變的,不能被優化的;而後面的const修飾的是指標變數dvp。因為硬體寄存器的位址是一個常量,所以將這個指標變數定義成const的,不能被修改。

3)危險的volatile用法

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

例:定義為volatile的結構體成員

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

struct devregs{
    unsigned short volatile csr;
    unsigned short const volatile data;
};
我們的原意是希望聲明一個設備的硬體寄存器組。其中有一個16bitCSR控制/狀態寄存器,這個寄存器可以由程式向設備寫入控制字,也可以由硬體設備設置反映其工作狀態。另外還有一個16bitDATA資料寄存器,這個寄存器只會由硬體來設置,由程式進行讀入。

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

struct devregs * const dvp = DEVADDR;

while ((dvp->csr & (READY | ERROR)) == 0)
    ; /* NULL - wait till done */
通過一個non-volatile的結構體指標,去訪問被定義為volatile的結構體成員,編譯器將如何處理?答案是:UndefinedC99 標準沒有對編譯器在這種情況下的行為做規定。所以編譯器有可能正確地將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來處理的。

例:定義為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的結構體變數。

例:多次的間接指針引用

考察如下代碼:

/* 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 DescriptorBD)當中的狀態會由硬體進行設置以告訴軟體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的。

例:到底哪個volatile可能無效

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

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

struct hw_bd *bdp;

......;
bdp->buffer = ... ;    ①
*(bdp->buffer) = ...; ②请问上面标记了①和②的两行代码,哪个是确实在访问volatile对象,而哪个又是undefined的结果?

答案是:②是volatile的,①是undefined。

  
來看本例的資料結構示意圖:

        (non-volatile)
bdp -->+-------------+
             |                   |
             |   ... ...         |
             |                   |
            +-------------+      (volatile) 
             |    buffer    |-->+------------+
            +-------------+    |                  |
                                      |                  |
                                      |                  |
                                     +------------+
                                      |  buffer      |
                                    +------------+
                                      |                  |
                                      |                  |
                                    +------------ +

buffer成員本身是通過一個non-volatile的指標bdp訪問的,按照C99標準的定義,這就屬於undefined的情況,因此對bdp->buffer的訪問編譯器不一定能保證是volatile的;
雖然buffer成員本身可能不是volatile的變數,但是buffer成員是一個指向volatile物件的指標。因此對buffer成員所指物件的訪問編譯器可以保證是volatile的,所以bdp->buffervolatile的。

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

再舉個例子

關鍵字volatile 是什麼
將一個變數說明為volatile表示這個變數是“易變的”。如果一個變數會被其他引用改變,或在其他並行的任務中會被改變(例如中斷服務程式),都要顯式地說明為“volatile,否則在編譯器優化階段會作出錯誤的判斷,例如將這個變數讀入寄存器以後,在沒有對這個變數賦值以前,會一直使用寄存器中的值,而實際上這個變數的值可能已經被一個指標引用改變了,或者是在中斷服務程式中被改變了,下面這個例子說明這種錯誤:
有一個變數 T,在定時中斷中每隔一個固定時間減一,然後在主程序中等待它減到"0"
unsigned char T;
void T0_int(void) interrupt 1
{
     ...
    T--;
    ...
}
void main(void)
{
    ...
   T=10;
    while (T!=0); /* 這在某些編譯器中將成為一個閉環,而不是預想的等待一段時間*/
   ...
}
正確的寫法應該是將第一句改為:
volatile unsigned char T;
*注:這個例子並不是針對特定的編譯器,所以可能在有些編譯器中能正確編譯。
但作為一個健康的程式,一定要注意這一點,否則即使能得到正確結果,也會給程式移植或升級帶來意想不到的問題。

有關Dan Sak`s相關文章

沒有留言:

張貼留言

注意:只有此網誌的成員可以留言。