最近搞NVMe驱动需求分析,对volatile这个单词实在是再熟悉不过了。 而在C语言中,有一个关键字就叫做volatile, 其字面意思是"挥发性的, 不稳定的,可改变的"。 那么,究竟嘛意思? 先看看文章volatile: The Multithreaded Programmer's Best Friend的解释,

The volatile keyword was devised to prevent compiler optimizations that might
render code incorrect in the presence of certain asynchronous events.

设计关键字volatile的目的就是阻止可能发生的编译器优化。在某些异步事件存在的情况下,编译器对代码做了优化之后将导致代码不能正确地工作。

再看看维基百科对volatile的解释,

In computer programming, particularly in the C, C++, C#,   and Java programming
languages, the volatile keyword indicates that a value may change between
different accesses, even if it does not appear to be modified. This keyword
prevents an optimizing compiler from optimizing away subsequent reads or writes
and thus incorrectly reusing a stale value or omitting writes. Volatile values
primarily arise in hardware access (memory-mapped I/O), where reading from or
writing to memory is used to communicate with peripheral devices, and in
threading, where a different thread may have modified a value.

下面举个例子, 写一小段C代码,然后比对一下。

  • 01 - foo1.c
  • 02 - foo2.c
  • 03 - Makefile

01 - foo1.c

 /**
* foo1.c - int foo is not defined as volatile
*/ static int foo; static int bar(void)
{
foo = ;
while (foo != 0xFF)
;
return foo;
} int main(int argc, char *argv[])
{
return bar();
}

02 - foo2.c

 /**
* foo2.c - int foo is defined as volatile
*/ static volatile int foo; static int bar(void)
{
foo = ;
while (foo != 0xFF)
;
return foo;
} int main(int argc, char *argv[])
{
return bar();
}

03 - Makefile

CC         = gcc
CFLAGS += -g -Wall all: foo1 foo2 foo1: foo1.c
${CC} ${CFLAGS} -o $@ $< foo2: foo2.c
${CC} ${CFLAGS} -o $@ $< clean:
rm -f *.o
clobber: clean
rm -f foo1 foo2
cl: clobber

04 - diff foo1.c foo2.c

$ diff foo1.c foo2.c
2c2
< * foo1.c - int foo is not defined as volatile
---
> * foo2.c - int foo is defined as volatile
5c5
< static int foo;
---
> static volatile int foo;

foo1.c 与foo2.c的差别在于int foo是否被申明为volatile。

第1步: 设CFLAGS="", 编译一下; 然后用gdb反汇编

$ export CFLAGS=""
$ make
gcc -g -Wall -o foo1 foo1.c
gcc -g -Wall -o foo2 foo2.c
  • gdb foo1, 将结果存入foo1.00.out
$ gdb foo1
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x0804840e <+>: push ebp
0x0804840f <+>: mov ebp,esp
0x08048411 <+>: call 0x80483ed <bar>
0x08048416 <+>: pop ebp
0x08048417 <+>: ret
End of assembler dump.
(gdb) disas bar
Dump of assembler code for function bar:
0x080483ed <+>: push ebp
0x080483ee <+>: mov ebp,esp
0x080483f0 <+>: mov DWORD PTR ds:0x804a020,0x0
0x080483fa <+>: nop
0x080483fb <+>: mov eax,ds:0x804a020
0x08048400 <+>: cmp eax,0xff
0x08048405 <+>: jne 0x80483fb <bar+>
0x08048407 <+>: mov eax,ds:0x804a020
0x0804840c <+>: pop ebp
0x0804840d <+>: ret
End of assembler dump.
(gdb)
  • gdb foo2, 将结果存入foo2.00.out
$ gdb foo2
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x0804840e <+>: push ebp
0x0804840f <+>: mov ebp,esp
0x08048411 <+>: call 0x80483ed <bar>
0x08048416 <+>: pop ebp
0x08048417 <+>: ret
End of assembler dump.
(gdb) disas bar
Dump of assembler code for function bar:
0x080483ed <+>: push ebp
0x080483ee <+>: mov ebp,esp
0x080483f0 <+>: mov DWORD PTR ds:0x804a020,0x0
0x080483fa <+>: nop
0x080483fb <+>: mov eax,ds:0x804a020
0x08048400 <+>: cmp eax,0xff
0x08048405 <+>: jne 0x80483fb <bar+>
0x08048407 <+>: mov eax,ds:0x804a020
0x0804840c <+>: pop ebp
0x0804840d <+>: ret
End of assembler dump.
(gdb)
  • diff foo1.00.out foo2.00.out
$ diff foo1..out foo2..out
1c1
< $ gdb foo1
---
> $ gdb foo2

发现foo1和foo2反汇编后并没有什么不同。 这就对了,因为编译的时候没有带优化选项啊!

第2步: 设CFLAGS="-O1", 编译一下; 然后用gdb反汇编

$ export CFLAGS="-O1"
$ make
gcc -O1 -g -Wall -o foo1 foo1.c
gcc -O1 -g -Wall -o foo2 foo2.c
  • gdb foo1, 将结果存入foo1.01.out
$ gdb foo1
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x080483ed <+>: mov DWORD PTR ds:0x804a020,0x0
0x080483f7 <+>: jmp 0x80483f7 <main+>
End of assembler dump.
(gdb) disas bar
No symbol "bar" in current context.
(gdb) q
  • gdb foo2, 将结果存入foo2.01.out
$ gdb foo2
(gdb) set disassembly-flavor intel
(gdb) disas main
Dump of assembler code for function main:
0x080483ed <+>: mov DWORD PTR ds:0x804a020,0x0
0x080483f7 <+>: mov eax,ds:0x804a020
0x080483fc <+>: cmp eax,0xff
0x08048401 <+>: jne 0x80483f7 <main+>
0x08048403 <+>: mov eax,ds:0x804a020
0x08048408 <+>: ret
End of assembler dump.
(gdb) disas bar
No symbol "bar" in current context.
(gdb) q
  • 用meld diff foo1.01.out foo2.01.out, 截图如下

从上图中,我们不难发现,bar()函数被优化掉了,而且优化后的汇编代码也不相同,因为我们在gcc中使用了"-O1"选项。

  • 在foo1.c中, 
         static int foo;

         static int bar(void)
{
foo = ;
while (foo != 0xFF)
;
return foo;
}

L5定义了static int foo, 当"-O1"被指定的时候,整个函数bar()都被优化掉了,因为foo等于0, 肯定不等于0xFF, 于是L9-L11为一个死循环。

$ gdb foo1
(gdb) disas /m main
Dump of assembler code for function main:
foo = ; while (foo != 0xFF)
;
return foo;
} int main(int argc, char *argv[])
{
0x080483ed <+>: mov DWORD PTR ds:0x804a020,0x0
0x080483f7 <+>: jmp 0x80483f7 <main+> End of assembler dump.

下面的汇编代码正好与L10-L12对应

0x080483f7 <+>:    jmp    0x80483f7 <main+>
  • 在foo2.c中,
         static volatile int foo;

         static int bar(void)
{
foo = ;
while (foo != 0xFF)
;
return foo;
}

L5定义了static volatile int foo, 因为有了volatile关键字,所以编译器gcc在处理L10-L12的时候,每次都要从内存中读取foo的值,

$ gdb foo2
(gdb) disas /m main
Dump of assembler code for function main:
foo = ; while (foo != 0xFF)
0x080483f7 <+>: mov eax,ds:0x804a020
0x080483fc <+>: cmp eax,0xff
0x08048401 <+>: jne 0x80483f7 <main+> ;
return foo;
0x08048403 <+>: mov eax,ds:0x804a020 } int main(int argc, char *argv[])
{
0x080483ed <+>: mov DWORD PTR ds:0x804a020,0x0 return bar();
}
0x08048408 <+>: ret End of assembler dump.

在上面的反汇编代码中, foo的内存位置为ds:0x804a020中,那么,C代码

            while (foo != 0xFF)
;
return foo;

对应于汇编代码

0x080483f7 <+>: mov eax,ds:0x804a020    ; load var foo from memory
0x080483fc <+>: cmp eax,0xff ; compare var foo with 0xff i.e. eax - 0xff
0x08048401 <+>: jne 0x80483f7 <main+> ; if not zero (i.e. foo != 0xff), jmp back to main+10
0x08048403 <+>: mov eax,ds:0x804a020 ; move var foo to eax as return value

参考资料:

虽然C提供了关键字volatile,但是在Linux内核编程中禁止使用这一关键字。具体原因,请阅读下面两个链接:

小结: 用volatile修饰的变量不允许编译器对与它有关的运算做任何优化。用volatile定义的变量可能会在程序外被改变,所以每次都必须从内存中读取,而不能把它放在cache或寄存器中重复使用。另外,切记切记,在Linux内核开发中使用volatile被认为是邪恶(evil)的!

话说C语言的关键字volatile的更多相关文章

  1. C语言中关键字volatile的含义【转】

    本文转载自:http://m.jb51.net/article/37489.htm 本篇文章是对C语言中关键字volatile的含义进行了详细的分析介绍,需要的朋友参考下 volatile 的意思是“ ...

  2. C语言关键字-volatile

    1.C语言关键字volatile     C 语言关键字volatile(注意它是用来修饰变量而不是上面介绍的__volatile__)表明某个变量的值可能在外部被改变,因此对这些变量的存取 不能缓存 ...

  3. C语言中关键字auto、static、register、const、volatile、extern的作用

    原文:C语言中关键字auto.static.register.const.volatile.extern的作用 关键字auto.static.register.const.volatile.exter ...

  4. 【转】话说C语言const用法

    原文:话说C语言const用法 const在C语言中算是一个比较新的描述符,我们称之为常量修饰符,意即其所修饰的对象为常量(immutable). 我们来分情况看语法上它该如何被使用. 1.函数体内修 ...

  5. 第二十一节:Java语言基础-关键字,标识符,注释,常量和变量,运算符

    Java语言基础-关键字,标识符,注解,常量和变量,运算符 class Demo { public static void main(String[] args){ System.out.printl ...

  6. java中关键字volatile的误解和使用

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

  7. Java 关键字volatile的解释

    volatile 关键字特征: 1.可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.可以禁止线程的工作内存对volatile修饰的变量进行缓存,并将修改的变量立即写入主存. 2. ...

  8. Docs-.NET-C#-指南-语言参考-关键字:C# 关键字

    ylbtech-Docs-.NET-C#-指南-语言参考-关键字:C# 关键字 1.返回顶部 1. C# 关键字 2017/03/07 关键字是预定义的保留标识符,对编译器有特殊意义. 除非前面有 @ ...

  9. [CareerCup] 13.5 Volatile Keyword 关键字volatile

    13.5 What is the significance of the keyword "volatile" in C 这道题考察我们对于关键字volatile的理解,顾名思义, ...

随机推荐

  1. 微服务中的健康监测以及其在ASP.NET Core服务中实现运行状况检查

    1 .什么是健康检查? 健康检查几乎就是名称暗示的.它是一种检查您的应用程序是否健康的方法.随着越来越多的应用程序转向微服务式架构,健康检查变得尤其重要(Health Check).虽然微服务架构有很 ...

  2. java分页实例Demo

    前两天测试过的一个分页的demo,在网上看到的,挺好的,就写了下来. 分页也是web里面必须的,有使用的价值. demo文件打包上传了,链接:http://pan.baidu.com/s/1o6sME ...

  3. SQL Server数据库的基础脚本编程

    数据库脚本的基础编程 Go批量处理语句 用于同时处理多条语句 use指定数据库或表 use master --创建数据库 go use Student --创建表(Student)表示数据库 go 创 ...

  4. java—实现一个监听器HttpServletRequest的创建销毁、在线人数 (56)

    在JavaWeb中的监听器分类 在Javaweb中存在三个被监听对象: HttpServletRequest HttpSessoin ServletContext 监听者 被监听者 监听到事件对象 H ...

  5. 【Oracle 12c】最新CUUG OCP-071考试题库(59题)

    59.(16-8)choose two: Which two statements are true regarding the USING and ON clauses in table joins ...

  6. “全栈2019”Java第一百零六章:匿名内部类与抽象类接口注意事项

    难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...

  7. 数论入门2——gcd,lcm,exGCD,欧拉定理,乘法逆元,(ex)CRT,(ex)BSGS,(ex)Lucas,原根,Miller-Rabin,Pollard-Rho

    数论入门2 另一种类型的数论... GCD,LCM 定义\(gcd(a,b)\)为a和b的最大公约数,\(lcm(a,b)\)为a和b的最小公倍数,则有: 将a和b分解质因数为\(a=p1^{a1}p ...

  8. Python装饰器(函数)

    闭包 1.作用域L_E_G_B(局部.内嵌.全局...): x=10#全局 def f(): a=5 #嵌套作用域 def inner(): count = 7 #局部变量 print a retur ...

  9. angular核心原理解析3:指令的执行过程

    指令的执行过程分析. 我们知道指令的执行分两个阶段,一个是compile,一个是link. 我们可以在指令中自定义compile和link. 首先,我们来讲解如何自定义link函数 举个例子: < ...

  10. RHEL配置本地yum

    RHEL(即Red Hat Enterprise Linux的缩写)配置本地yum 提前将 rhel-server-6.7-x86_64-dvd.iso 文件上传到服务器上 1.在根目录创建文件夹/m ...