李浩: 再谈 volatile 关键字

Linux阅码场 2021-03-03 00:00

本文内容:volatile关键字的含义,它与barrier()和编译乱序的关系,以及内核里面READ_ONCE()、WRITE_ONCE()的实现原理。

作者简介:李浩,就职于南京富士通南大软件,熟悉 x86 架构,对内存和文件系统有些研究。

最常见的用法

如果一个变量被声明为 volatile 的,就是告诉编译器即使我们当前编译的代码不会修改这个变量,该变量对应的内存数据也可能会由于其他原因而被修改,这可能的原因有很多,比如该变量对应的内存位置是使用 memory mapped I/O 机制映射的一个外设端口,即我们本质上是在访问一个硬件寄存器,它的值的变化当然不受程序控制。

那为什么要告诉编译器这个信息呢?因为这样的话,生成汇编代码时,每次使用该变量时都会去对内存位置做一次读访问以获取最新的值。相反,如果不加 volatile,那么编译器为了效率,很可能先把该变量加载到寄存器,以后需要用时就都去读这个寄存器了,不会再去读内存,即使内存的数据变动了我们的代码也不知道,还在用寄存器里的老数据。我们常用的 ioread 函数就封装了 volatile 操作,保证能读到最新数据,具体定义可以参见 build_mmio_read

但要注意,除了这种 memory mapped I/O 以及其他少数几个特殊情况[1],如果一个变量可能被多个过程并发访问,这种情况不应该使用 volatile 关键字来保证每个过程都能看到该变量的最新值,正确的做法是使用锁来保护它,加锁成功后只需要把被保护变量从内存读一次扔到寄存器就行了,后面都用寄存器的值,这样效率高,在我们出临界区之前锁机制会保证不会有其他过程来修改此变量,所以寄存器里的数据一直是有效的。这个时候如果画蛇添足把被保护变量声明为 volatile,会阻止编译器在临界区内对该变量的读取优化,每次都要从内存读,这显然没必要。

阻止编译乱序

volatile 的另一种用法需要结合 READ_ONCE/WRITE_ONCE 这两个宏来看,内核注释里提到这两个宏有阻止编译乱序的作用。

The compiler is also forbidden from reordering successive instances ofREAD_ONCE and WRITE_ONCE

我们下文以 READ_ONCE 读取变量为例展开分析。

从内核对这个宏的定义来看,它的本质其实就是使用 volatile 关键字对变量做了类型修饰,怎么看都不像是能起到阻止乱序的作用。

#define READ_ONCE(x) \({\ compiletime_assert_rwonce_type(x); \ __READ_ONCE(x); \})#define __READ_ONCE(x) (*(const volatile __unqual_scalar_typeof(x) *)&(x))

所以我们只好试一试。

首先是一段 C 语言代码:

int a, b;int i, j;
void foo(){ a = i; b = j/16;}

使用 gcc -O2 example.c -S 生成汇编:

movl j(%rip), %edx // 读取 jmovl i(%rip), %eax // 读取 itestl %edx, %edxmovl %eax, a(%rip)leal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, b(%rip)

很明显地看到 i 和 j 的读取顺序与 C 语言语句颠倒了。那么为了阻止这种优化,我们首先试下编译屏障 barrier(),看看效果如何。

#define barrier() __asm__ __volatile__("": : :"memory")
int a, b;int i, j;
void foo(){ a = i; barrier(); b = j/16;}

汇编如下:

movl i(%rip), %eax // 读取 imovl %eax, a(%rip) // 写入 a-------------------------------- 屏障在此movl j(%rip), %edx // 读取 jtestl %edx, %edxleal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, b(%rip) // 写入 b

显然,barrier() 编译屏障很管用,它告诉编译器:在 barrier() 前后是两个世界,屏障前的语句不能跑到屏障后,反之亦然,也就是编译乱序不能穿透屏障。所以,读取 i 写入 a 和 读取 j 写入 b 这两组操作被屏障隔离了。

在见识了编译屏障的作用后,我们再试试 volatile 究竟有没有起到类似的作用。

#define __READ_ONCE(x) (*(const volatile int *)&(x))
int a, b;int i, j;
void foo(){ a = __READ_ONCE(i); b = __READ_ONCE(j)/16;}

汇编如下:

movl i(%rip), %eax // 读取 imovl j(%rip), %edx // 读取 jmovl %eax, a(%rip) // 写入 atestl %edx, %edxleal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, b(%rip) // 写入 b

可以看到 i 和 j 的读取顺序被保证了。但是注意,volatile 毕竟不是编译屏障,不能把第一条 C 语句和第二条语句完全隔离开,所以我们用 __READ_ONCE 能保证的也只是 i 和 j 的读取顺序,其他的写入顺序或者读写之间的顺序无法被保证 (比如读取 j 和写入 a 就颠倒了)。

那编译器为何会对 volatile 有这样的约束行为呢,这是因为 C 标准做出了如下规定:

The least requirements on a conforming implementation are:
At sequence points, volatile objects are stable in the sense that previous accesses are complete and subsequent accesses have not yet occurred.
.........
The following are the sequence points described in 5.1.2.3:
The end of a full expression: an initializer (6.7.8); the expression in an expressionstatement (6.8.3); the controlling expression of a selection statement (if or switch)(6.8.4); the controlling expression of a while or do statement (6.8.5); each of theexpressions of a for statement (6.8.5.3); the expression in a return statement(6.8.6.4).

这里引出了 sequence point 的概念,简单来说就是 sequence point 之前的表达式所造成的影响不能扩散到 sequence point 之后。尤其是对于 volatile 变量来说,以一个 sequence point 为分界点,对于前面 volatile 变量的访问必须完成,且对于后面 volatile 变量的访问必须没有开始。遵照如上标准,; 就是个 sequence point,那么 a = __READ_ONCE(i) 和 b = __READ_ONCE(j)/16 之间隔着一个 sequence point,所以对 i 的访问必须放在 j 之前。

但需要注意的是,编译器只是保证 volatile 变量与 volatile 变量的读取不会被乱序,但是 non-volatile 变量和 volatile 变量的读取顺序依然是可以被乱序的。

比如我们把 j 的 __READ_ONCE 去掉:

#define __READ_ONCE(x) (*(const volatile int *)&(x))
int a, b;int i, j;
void foo(){ a = __READ_ONCE(i); b = j/16;}

产生的汇编如下:

movl j(%rip), %edx // 读取 jmovl i(%rip), %eax // 读取 itestl %edx, %edxmovl %eax, a(%rip)leal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, b(%rip)

可以看到 i 和 j 的读取顺序又颠倒了。

到这里,我们就把 READ_ONCE 也即 volatile 在变量读取中的作用分析完了,它可以保证变量严格地按照代码给出的顺序去读。同理,WRITE_ONCE 则是保证了变量的写入顺序。

那么如果 READ_ONCE 和 WRITE_ONCE 两者混合使用,又会怎样呢。其实,按照 C 标准,没有特指这个保序只针对读与读或写与写,所以读写混合的顺序也会得到保证。下面举个例子:

int a, b;int i;
void foo(){ a = i/16; b = 0;}

这个函数的汇编如下:

movl $0, b(%rip) // 写入 bmovl i(%rip), %edx // 读取 itestl %edx, %edxleal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, a(%rip) // 写入 a

可以看到读取 i 、写入 a、写入 b 这三者的顺序已经彻底打乱了。

我们用上 volatile

#define __READ_ONCE(x) (*(const volatile int *)&(x))#define __WRITE_ONCE(x, val) do {*(volatile typeof(x) *)&(x) = (val);} while(0)
int a, b;int i;
void foo(){ a = __READ_ONCE(i)/16; __WRITE_ONCE(b, 0);}

生成的汇编如下:

movl i(%rip), %edx // 读取 imovl $0, b(%rip) // 写入 btestl %edx, %edxleal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, a(%rip) // 写入 a

可见,i 的读取和 b 的写入是严格按照 C 代码的顺序来的,说明 volatile 生效了。但是 a 的写入被放到 b 写入的后面了,这是因为 a 在被写入时没有被 volatile 修饰。如果我们把代码改成这样:

__WRITE_ONCE(a, __READ_ONCE(i)/16);__WRITE_ONCE(b, 0);

生成的汇编就会如下:

movl i(%rip), %edx // 读取 itestl %edx, %edxleal 15(%rdx), %eaxcmovns %edx, %eaxsarl $4, %eaxmovl %eax, a(%rip) // 写入 amovl $0, b(%rip) // 写入 b

可以看到现在的顺序和 C 代码完全对应了。不过其实可以写的更简单一点,因为 a 需要靠 i 算出来,有计算依赖,所以编译器会保证 i 的读取在 a 写入之前,第一行的 __READ_ONCE 可以去掉,写成下面这样效果是一样的:

__WRITE_ONCE(a, i/16);__WRITE_ONCE(b, 0);

后记

volatile 这种防止乱序的作用在 Java 中相当清晰,JVM 本身就类似于一个操作系统,Java 编译为字节码后也有指令重排导致编译乱序的问题,所以 Java 中的 volatile 关键字明确带有阻止优化的作用,这已经在 Java 开发者中成为了常识,而 C 语言中的 volatile 却稍显隐晦。

References

[1] 特殊情况: 

https://www.kernel.org/doc/html/latest/process/volatile-considered-harmful.html


更多精彩,尽在"Linux阅码场",扫描下方二维码关注

别忘了分享、点赞或者在看哦~


Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
评论
  • 概述 说明(三)探讨的是比较器一般带有滞回(Hysteresis)功能,为了解决输入信号转换速率不够的问题。前文还提到,即便使能滞回(Hysteresis)功能,还是无法解决SiPM读出测试系统需要解决的问题。本文在说明(三)的基础上,继续探讨为SiPM读出测试系统寻求合适的模拟脉冲检出方案。前四代SiPM使用的高速比较器指标缺陷 由于前端模拟信号属于典型的指数脉冲,所以下降沿转换速率(Slew Rate)过慢,导致比较器检出出现不必要的问题。尽管比较器可以使能滞回(Hysteresis)模块功
    coyoo 2024-12-03 12:20 170浏览
  • 11-29学习笔记11-29学习笔记习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记
    youyeye 2024-12-02 23:58 92浏览
  • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
    刘旷 2024-12-02 09:32 138浏览
  • TOF多区传感器: ND06   ND06是一款微型多区高集成度ToF测距传感器,其支持24个区域(6 x 4)同步测距,测距范围远达5m,具有测距范围广、精度高、测距稳定等特点。适用于投影仪的无感自动对焦和梯形校正、AIoT、手势识别、智能面板和智能灯具等多种场景。                 如果用ND06进行手势识别,只需要经过三个步骤: 第一步&
    esad0 2024-12-04 11:20 103浏览
  • 作为优秀工程师的你,已身经百战、阅板无数!请先醒醒,新的项目来了,这是一个既要、又要、还要的产品需求,ARM核心板中一个处理器怎么能实现这么丰富的外围接口?踌躇之际,你偶阅此文。于是,“潘多拉”的魔盒打开了!没错,USB资源就是你打开新世界得钥匙,它能做哪些扩展呢?1.1  USB扩网口通用ARM处理器大多带两路网口,如果项目中有多路网路接口的需求,一般会选择在主板外部加交换机/路由器。当然,出于成本考虑,也可以将Switch芯片集成到ARM核心板或底板上,如KSZ9897、
    万象奥科 2024-12-03 10:24 93浏览
  • 遇到部分串口工具不支持1500000波特率,这时候就需要进行修改,本文以触觉智能RK3562开发板修改系统波特率为115200为例,介绍瑞芯微方案主板Linux修改系统串口波特率教程。温馨提示:瑞芯微方案主板/开发板串口波特率只支持115200或1500000。修改Loader打印波特率查看对应芯片的MINIALL.ini确定要修改的bin文件#查看对应芯片的MINIALL.ini cat rkbin/RKBOOT/RK3562MINIALL.ini修改uart baudrate参数修改以下目
    Industio_触觉智能 2024-12-03 11:28 110浏览
  • 当前,智能汽车产业迎来重大变局,随着人工智能、5G、大数据等新一代信息技术的迅猛发展,智能网联汽车正呈现强劲发展势头。11月26日,在2024紫光展锐全球合作伙伴大会汽车电子生态论坛上,紫光展锐与上汽海外出行联合发布搭载紫光展锐A7870的上汽海外MG量产车型,并发布A7710系列UWB数字钥匙解决方案平台,可应用于数字钥匙、活体检测、脚踢雷达、自动泊车等多种智能汽车场景。 联合发布量产车型,推动汽车智能化出海紫光展锐与上汽海外出行达成战略合作,联合发布搭载紫光展锐A7870的量产车型
    紫光展锐 2024-12-03 11:38 126浏览
  •         温度传感器的精度受哪些因素影响,要先看所用的温度传感器输出哪种信号,不同信号输出的温度传感器影响精度的因素也不同。        现在常用的温度传感器输出信号有以下几种:电阻信号、电流信号、电压信号、数字信号等。以输出电阻信号的温度传感器为例,还细分为正温度系数温度传感器和负温度系数温度传感器,常用的铂电阻PT100/1000温度传感器就是正温度系数,就是说随着温度的升高,输出的电阻值会增大。对于输出
    锦正茂科技 2024-12-03 11:50 141浏览
  • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
    丙丁先生 2024-12-01 17:37 114浏览
  • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
    晶台光耦 2024-12-02 10:40 143浏览
我要评论
0
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦