zoukankan      html  css  js  c++  java
  • 话说C语言的关键字volatile

    最近搞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

     1 /**
     2  * foo1.c - int foo is not defined as volatile
     3  */
     4 
     5 static int foo;
     6 
     7 static int bar(void)
     8 {
     9         foo = 0;
    10         while (foo != 0xFF)
    11                 ;
    12         return foo;
    13 }
    14 
    15 int main(int argc, char *argv[])
    16 {
    17         return bar();
    18 }

    02 - foo2.c

     1 /**
     2  * foo2.c - int foo is defined as volatile
     3  */
     4 
     5 static volatile int foo;
     6 
     7 static int bar(void)
     8 {
     9         foo = 0;
    10         while (foo != 0xFF)
    11                 ;
    12         return foo;
    13 }
    14 
    15 int main(int argc, char *argv[])
    16 {
    17         return bar();
    18 }

    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 <+0>:     push   ebp
       0x0804840f <+1>:     mov    ebp,esp
       0x08048411 <+3>:     call   0x80483ed <bar>
       0x08048416 <+8>:     pop    ebp
       0x08048417 <+9>:     ret
    End of assembler dump.
    (gdb) disas bar
    Dump of assembler code for function bar:
       0x080483ed <+0>:     push   ebp
       0x080483ee <+1>:     mov    ebp,esp
       0x080483f0 <+3>:     mov    DWORD PTR ds:0x804a020,0x0
       0x080483fa <+13>:    nop
       0x080483fb <+14>:    mov    eax,ds:0x804a020
       0x08048400 <+19>:    cmp    eax,0xff
       0x08048405 <+24>:    jne    0x80483fb <bar+14>
       0x08048407 <+26>:    mov    eax,ds:0x804a020
       0x0804840c <+31>:    pop    ebp
       0x0804840d <+32>:    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 <+0>:     push   ebp
       0x0804840f <+1>:     mov    ebp,esp
       0x08048411 <+3>:     call   0x80483ed <bar>
       0x08048416 <+8>:     pop    ebp
       0x08048417 <+9>:     ret
    End of assembler dump.
    (gdb) disas bar
    Dump of assembler code for function bar:
       0x080483ed <+0>:     push   ebp
       0x080483ee <+1>:     mov    ebp,esp
       0x080483f0 <+3>:     mov    DWORD PTR ds:0x804a020,0x0
       0x080483fa <+13>:    nop
       0x080483fb <+14>:    mov    eax,ds:0x804a020
       0x08048400 <+19>:    cmp    eax,0xff
       0x08048405 <+24>:    jne    0x80483fb <bar+14>
       0x08048407 <+26>:    mov    eax,ds:0x804a020
       0x0804840c <+31>:    pop    ebp
       0x0804840d <+32>:    ret
    End of assembler dump.
    (gdb)
    • diff foo1.00.out foo2.00.out
    $ diff foo1.00.out foo2.00.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 <+0>:     mov    DWORD PTR ds:0x804a020,0x0
       0x080483f7 <+10>:    jmp    0x80483f7 <main+10>
    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 <+0>:     mov    DWORD PTR ds:0x804a020,0x0
       0x080483f7 <+10>:    mov    eax,ds:0x804a020
       0x080483fc <+15>:    cmp    eax,0xff
       0x08048401 <+20>:    jne    0x80483f7 <main+10>
       0x08048403 <+22>:    mov    eax,ds:0x804a020
       0x08048408 <+27>:    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中, 
         5    static int foo;
         6    
         7    static int bar(void)
         8    {
         9        foo = 0;
        10        while (foo != 0xFF)
        11            ;
        12        return foo;
        13    }

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

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

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

    0x080483f7 <+10>:    jmp    0x80483f7 <main+10>
    • 在foo2.c中,
         5    static volatile int foo;
         6    
         7    static int bar(void)
         8    {
         9        foo = 0;
        10        while (foo != 0xFF)
        11            ;
        12        return foo;
        13    }

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

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

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

        10        while (foo != 0xFF)
        11            ;
        12        return foo;

    对应于汇编代码

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

    参考资料:

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

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

  • 相关阅读:
    6种负载均衡算法
    Java中volatile关键字
    剑指offer练习
    linux系统查看IP地址,不显示IP地址或者只显示127.0.0.1
    Nginx负载均衡配置
    集群应用Session一致性实现的三种方案
    rabbitMQ学习
    JDK1.8在LINUX下安装步骤
    ecplise部署gradle web项目
    Kubernetes下的应用监控解决方案
  • 原文地址:https://www.cnblogs.com/idorax/p/7561793.html
Copyright © 2011-2022 走看看