Volatile的陷阱

最近寫的關于在嵌入式開發中常遇到的關于volatile關鍵字使用的短文,都是些通用的技術,貼上來share

?

對于volatile關鍵字,大部分的C語言教材都是一筆帶過,并沒有做太過深入的分析,所以這里簡單整理了一些關于volatile的使用注意事項。實際上從語法上來看volatileconst是一樣的,但是如果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的數據內容。

volatileconst的合用

從字面上看,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的,不能被修改。



危險的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[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關鍵字,用起來還是有非常多的講究在里面的,大家一定要引起重視。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/383326.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/383326.shtml
英文地址,請注明出處:http://en.pswp.cn/news/383326.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

c++中靜態成員變量和靜態成員函數

靜態成員變量 在一個類中,若將一個成員變量聲明為static,這種成員成為靜態成員變量,與一般的數據成員不同,無論建立了多少個對象,都只想有一個靜態數據的拷貝,靜態成員變量,屬于某個類,所有對象…

單列模式(餓漢)

單例模式案例 目的&#xff1a;為了讓類中只有一個實例&#xff0c;實例不需要自己釋放將 默認構造 和 拷貝構造 私有化內部維護一個 對象的指針私有化唯一指針對外提供getinstance方法來訪問這個指針保證類中只能實例化唯一 一個對象 主席案例 #include<iostream>usin…

Makefile札記

Makefile中: ? 的區別 在Makefile中我們經常看到 : ? 這幾個賦值運算符&#xff0c;那么他們有什么區別呢&#xff1f;我們來做個簡單的實驗 新建一個Makefile&#xff0c;內容為&#xff1a; ifdef DEFINE_VRE VRE “Hello World!” else endif ifeq ($(OPT),define) VRE…

c++中this指針基本概念和使用

class Person { public:int m_A;//非靜態成員變量&#xff0c;屬于對象上void func(/*Person * this*/){}; //非靜態成員函數 不屬于對象身上static int m_B;//靜態成員函數&#xff0c;不屬于對象上static void fun2(){};//靜態成員函數 &#xff0c;不屬于對象身上//double …

通用Makefile實現

Makefile是Linux下程序開發的自動化編譯工具&#xff0c;一個好的Makefile應該準確的識別編譯目標與源文件的依賴關系&#xff0c;并且有著高效的編譯效率&#xff0c;即每次重新make時只需要處理那些修改過的文件即可。Makefile擁有很多復雜的功能&#xff0c;這里不可能也沒必…

c++中空指針訪問成員函數

如果成員函數沒有用到this &#xff0c;那么空指針可以直接訪問 如果成員函數用到this 指針&#xff0c;就要注意&#xff0c;要判斷是否為空&#xff0c;防止程序崩潰 #include<iostream>using namespace std;class Person{public:void show(){//沒有 用到this指針&am…

從0開始python學習-35.allure報告企業定制

目錄 1. 搭建allure環境 2. 生成報告 3. logo定制 4. 企業級報告內容或層級定制 5. allure局域網查看 1. 搭建allure環境 1.1 JDK&#xff0c;使用PyCharm 找到pycharm安裝目錄找到java.exe記下jbr目錄的完整路徑&#xff0c;eg: C:\Program Files\JetBrains\PyCharm Com…

grep 常用命令

這個--include選項,可以這樣使用: grep -rn --include*.c --include*.h re . 可以指定多次, 如果真是上面的這種情況, 其實可以用 grep -rn --include*.[ch] re . 但是, 如果源文件中含有C源代碼,上面的方法就不湊效了, 因為[]中只能放一個字符. grep -rn --include*.{cp…

c++中友元函數詳解

友元 友元分為&#xff1a;友元函數和友元類 友元提供了一種突破封裝的方式&#xff0c;有時提供了便利。但是友元會增加耦合度&#xff0c;破壞了封裝&#xff0c;所以友元不宜多 用。 全局函數做友元函數 全局函數寫到類中做聲明 并且最前面寫關鍵字 friend 友元函數可訪問…

Linux時間函數札記

關于gmtime、gmtime_r、localtime、localtime_r 測試環境&#xff1a;vmware 7 Redhat5.5&#xff0c;系統時間使用UTC&#xff0c;時區為上海。 1、函數功能介紹 使用man gmtime或man localtime都可以的得到這幾個函數的介紹。原型如下&#xff1a; struct tm *gmtime(const …

c++實現順序表的相關操作

Myarray.h文件 #pragma once#include<iostream>using namespace std;class MyArray { public:MyArray();//默認構造 默認100容量MyArray(int capacity);MyArray(const MyArray& array);~MyArray();//尾插法void Push_Back(int val);//根據索引獲取值int getData(int…

系統架構札記

什么是高內聚、低耦合&#xff1f; 起因&#xff1a;模塊獨立性指每個模塊只完成系統要求的獨立子功能&#xff0c;并且與其他模塊的聯系最少且接口簡單&#xff0c;兩個定性的度量標準――耦合性和內聚性。 耦合性也稱塊間聯系。指軟件系統結構中各模塊間相互聯系緊密程度的一…

c++中運算符重載(加號運算,左移運算,前置后置++運算符,賦值運算,關系運算,函數運算)

運算符重載注意 重載的運算符要易讀內置的數據類型的表達式的運算符是不可以改變的不要重載&& 和 | | 運算符&#xff0c;[]和->運算符只能通過成員函數進行重載<<和>>只能通過全局函數配合友元函數進行重載 加號運算符重載 如果想讓自定義數據類型 進…

linux fstab解讀

fstab這個文件挺有用的。 從左到右&#xff1a; /dev/device mount-point type rules dump fsck 1. /dev/device: 不用說了吧&#xff1f;例如&#xff0c;/dev/hda1為M$-Win9x下的c:盤。 2. mount-point: 掛載點。例如&#xff0c;把/dev/hda1掛到/mnt/mywinc下。 3. type: ex…

c++實現字符串類的封裝

MyString.h文件 #define _CRT_SECURE_NO_WARNINGS#pragma once#include<iostream>#include<string>using namespace std;class MyString{friend ostream & operator<<(ostream & cout, MyString & str);friend istream & operator>>(…

c++中的繼承--1(引出,繼承方式,繼承的對象模型)

繼承的引出 概念&#xff1a; 繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段&#xff0c;它允許程序員在保持原有類特 性的基礎上進行擴展&#xff0c;增加功能&#xff0c;這樣產生新的類&#xff0c;稱派生類。繼承呈現了面向對象程序設計的層次結構…

Makefile經典教程(掌握這些足夠)

makefile很重要 什么是makefile&#xff1f;或許很多Winodws的程序員都不知道這個東西&#xff0c;因為那些Windows的IDE都為你做了這個工作&#xff0c;但我覺得要作一個好的和professional的程序員&#xff0c;makefile還是要懂。這就好像現在有這么多的HTML的編輯器&#xf…

c++中的繼承--2(繼承中的析構函數和構造函數,繼承中同名成員,繼承中靜態成員)

繼承中的構造函數和析構函數 繼承中的構造和析構順序 子類創建對象時&#xff0c;先調用父類的構造&#xff0c;然后調用自身構造析構順序與構造順序相反子類不會繼承父類的構造函數和析構函數如果父類中沒有合適默認構造&#xff0c;那么子類可以利用初始化列表的方式顯示的…

Linux鎖機制和線程安全

鎖機制是多線程編程中最常用的同步機制&#xff0c;用來對多線程間共享的臨界區進行保護。 1. 互斥鎖&#xff1a;pthread_mutex&#xff0c;屬于sleep-waiting類型的鎖 pthread_mutex_t *mutex; int pthread_mutex_int(mutex, attr) //以動態方式創建互斥鎖&#xff0c;參…

c++中的繼承--3(多繼承問題,菱形繼承)

繼承中的多繼承 #include<iostream>using namespace std;class Base1 { public:Base1(){m_A 10;} public:int m_A;};class Base2 { public:Base2(){m_A 10;} public:int m_B;int m_A;};class Son :public Base1, public Base2 {public:int m_C;int m_D; };void test01…