Linux 编译屏障之 ACCESS_ONCE()
文章目录
- 1. 前言
- 2. 背景
- 3. 为什么要有 ACCESS_ONCE() ?
- 4. ACCESS_ONCE() 代码实现
- 5. ACCESS_ONCE() 实例分析
- 6. ACCESS() 的演进
- 7. 结语
- 8. 参考资料
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文基于 LWN 文章
ACCESS_ONCE()
ACCESS_ONCE() and compiler bugs
以及其它相关资料文档,经笔者理解后整理而成。本文并非对原文一对一的翻译,这一点提请读者注意。
3. 为什么要有 ACCESS_ONCE() ?
即使是内核源代码的普通读者,最终也可能会遇到对 ACCESS_ONCE()
宏的调用。我们可能不会停下来理解这个宏的含义,但事实表明,很多内核开发者都可能对它的作用没有明确的概念。本文试图解释它为什么存在以及何时必须使用它。你可能想知道为什么这很重要,归根结底,如果没有明确的告知
,C 编译器
将假定它正在编译的程序的地址空间中只有一个执行线程
。并发性不是内置在 C 语言本身
,因此处理并发访问
的机制必须建立在语言之上
,ACCESS_ONCE()
就是这样一种处理并发访问的机制之一。
这个宏的功能实际上从它的名字中得到了很好的描述:其目的是确保生成的代码精确访问一次
作为参数传递给它的变量的值
,即不是从寄存器或缓存等其它地方访问变量的副本,而是从变量所在内存地址直接访问,一如接下来的 volatile
相关描述。
在说到为什么要有 ACCESS_ONCE()
之前,得先说说 volatile
这个变量修饰符。如果我们将某个变量加上 volatile
修饰,如:
volatile int variable;
将指示编译器总是从内存读取变量的值
,而不是用之前某个时刻预先读到寄存器的值。这意味,一旦给变量加上了 volatile
修饰符,这种总是从内存读取变量的值
的操作,就会伴随变量的整个生存周期
。但这对于我们的程序来说,并不一定总是正确的,可能程序只要求
变量在某个特定上下文
(如代码临界区
)时,需要从内存读取变量的值
,这时候可以去掉变量声明中的 volatile
修饰符,接前面的例子,变量的定义变成:
int variable;
然后在需要的地方通过 ACCESS_ONCE()
访问变量,ACCESS_ONCE()
临时加上 volatile
保证从内存读取变量的值,类似这样:
temp = *(volatile int *)&variable;
而在其它地方,我们正常访问变量(不再通过 ACCESS_ONCE()
访问),类似这样:
temp = variable;
这样编译器可以优化代码,如将变量读取缓存到寄存器,然后再从寄存器读取变量的值,以优化访问速度。换句话说,volatile
关键字的目的是抑制优化
,但对于同一个变量,我们并非总是要
在任何访问它的抑制优化
,通常只需要在特定上下文抑制对它的访问优化,这时候 ACCESS_ONCE()
就应运而生了。
前面提到了 volatile
,Linux 内核代码只在极少数情形下适用于 volatile
。更多关于 Linux 内核下 volatile
的话题,可参考 Linux: 为什么不应该在内核代码中使用 volatile ?
4. ACCESS_ONCE() 代码实现
/* include/linux/compiler.h *//** Prevent the compiler from merging or refetching accesses. The compiler* is also forbidden from reordering successive instances of ACCESS_ONCE(),* but only when the compiler is aware of some particular ordering. One way* to make the compiler aware of ordering is to put the two invocations of* ACCESS_ONCE() in different C statements.** ACCESS_ONCE will only work on scalar types. For union types, ACCESS_ONCE* on a union member will work as long as the size of the member matches the* size of the union and the size is smaller than word size.** The major use cases of ACCESS_ONCE used to be (1) Mediating communication* between process-level code and irq/NMI handlers, all running on the same CPU,* and (2) Ensuring that the compiler does not fold, spindle, or otherwise* mutilate accesses that either do not require ordering or that interact* with an explicit memory barrier or atomic instruction that provides the* required ordering.** If possible use READ_ONCE()/WRITE_ONCE() instead.*/
#define __ACCESS_ONCE(x) ({ \__maybe_unused typeof(x) __var = (__force typeof(x)) 0; \(volatile typeof(x) *)&(x); })
#define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
从代码我们了解到,ACCESS_ONCE()
的工作原理
是将相关变量暂时转换为 volatile 类型
。考虑到优化编译器带来的各种问题,对数据的大多数并发访问都受到(或肯定应该)受到锁的保护。自旋锁
和互斥锁
都充当优化屏障
,也就是说,它们可以防止屏障一侧的优化延续到另一侧
。如果代码只访问锁保护的共享变量,并且该变量只能在释放锁(并由不同的线程持有)时更改,则编译器不会产生细微的问题。只有在没有锁
(或显式屏障:编译屏障、内存屏障或等同事物)的情况下
访问共享数据的地方,才需要使用 ACCESS_ONCE()
。
5. ACCESS_ONCE() 实例分析
例如,考虑 kernel/mutex.c
中的以下代码片段:
for (;;) {struct task_struct *owner;owner = ACCESS_ONCE(lock->owner);if (owner && !mutex_spin_on_owner(lock, owner))break;/* ... */
这段代码的意图是,希望在当前所有者 lock->owner
放弃互斥锁时快速获取互斥锁,而无需进入睡眠状态。编译器开发人员可能会热衷于优化所有代码逻辑,对于上面代码片段,他们可能得出这样的结论:由于上述代码片段没有修改 lock->owner
,因此没有必要每次都通过循环实际获取其值
。然后,编译器可能会将代码重新排列为如下内容:
owner = ACCESS_ONCE(lock->owner);
for (;;) {if (owner && !mutex_spin_on_owner(lock, owner))break;
编译器没考虑到
的是 lock->owner 可能正在被另一个执行线程更改
,结果是代码在多次执行循环时无法知道任何此类更改,从而导致令人不快的结果。ACCESS_ONCE()
调用可防止此优化发生,使用 ACCESS_ONCE()
后代码将按预期执行。
6. ACCESS() 的演进
由于 ACCESS_ONCE()
依赖于编译器
的实现,ACCESS_ONCE()
要能正常工作,所使用的编译器,必须按 ACCESS_ONCE()
所期望的那样工作。世事无常,有时候总是事与愿违。早期的 ACCESS_ONCE()
实现如下:
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
在这个实现版本下,Christian Borntraeger
报告了这样一个 ACCESS_ONCE()
相关的问题:compiler bug gcc4.6/4.7 with ACCESS_ONCE and workarounds 。简单来说,就是 Christian Borntraeger
在 GCC 4.6/4.7
上发现,ACCESS_ONCE()
在 union
类型上无法正常工作,Christian Borntraeger
和 Linus
讨论能否通过一种 workaround
方式,来绕过这个问题。
简而言之,就是 ACCESS_ONCE()
强制将变量视为 volatile 类型
,即使它(就像内核中的几乎所有变量一样)不是这样声明的。Christian Borntraeger
报告的问题是,如果传入 GCC 4.6
和 4.7
的变量不是标量类型
,则 GCC 4.6 和 4.7
将删除 volatile 修饰符
。例如,如果 x 是 int
,则工作正常
,但如果 x 具有更复杂的类型
,则不能正常工作
。例如,ACCESS_ONCE()
通常与页表项一起使用,页表项被定义为具有 pte_t
类型:
typedef struct {unsigned long pte;
} pte_t;
在这种情况下,volatile
的语义将在 bug 编译器中丢失,从而导致内核 bug 。Christian Borntraeger
开始寻找解决问题的方法,却被告知正常的内核实践是尽可能避免绕过编译器错误;相反,有缺陷的版本应该简单地在内核构建系统中被列入黑名单。但是 GCC 4.6 和 4.7
安装在很多系统上;将它们列入黑名单会给许多用户带来不便。而且,正如 Linus
所说,除了列入黑名单之外,还有其他方法。一种方法是修改对 ACCESS_ONCE()
调用,以指向相关非标量类型的标量部分
。因此,如果原始的执行如下作:
pte_t p = ACCESS_ONCE(pte);
我们可以按如下修改,通过直接访问基础标量类型
的方式,以绕过 GCC 4.6 和 4.7
的 bug:
unsigned long p = ACCESS_ONCE(pte->pte);
但是,这种的更改需要审核所有 ACCESS_ONCE()
调用,以查找使用非标量类型的调用,这将是一个漫长且容易出错的过程。 Christian Borntraeger
探索的另一种方法是删除一些有问题的 ACCESS_ONCE()
调用,然后通过 barrier()
放入编译器屏障进行替代。在许多情况下,放入编译器屏障
就足够了,但在其他情况下则不然。同样,这需要进行详细的审计,并且没有什么可以阻止新代码添加错误的 ACCESS_ONCE()
调用。因此,Christian Borntraeger
走上了改变 ACCESS_ONCE()
的道路,简单地禁止使用非标量类型
。最终,ACCESS_ONCE()
演变成如下所示版本:
#define __ACCESS_ONCE(x) ({ \__maybe_unused typeof(x) __var = 0; \(volatile typeof(x) *)&(x); })
#define ACCESS_ONCE(x) (*__ACCESS_ONCE(x))
如果将非标量类型
传递到宏中,则此版本将导致编译失败
。但是,需要使用非标量类型
的情况呢?对于这些情况,Christian Borntraeger
引入了 2
个新的宏,READ_ONCE()
和 ASSIGN_ONCE()
。前者的定义如下:
static __always_inline void __read_once_size(volatile void *p, void *res, int size)
{switch (size) {case 1: *(u8 *)res = *(volatile u8 *)p; break;case 2: *(u16 *)res = *(volatile u16 *)p; break;case 4: *(u32 *)res = *(volatile u32 *)p; break;
#ifdef CONFIG_64BITcase 8: *(u64 *)res = *(volatile u64 *)p; break;
#endif}
}#define READ_ONCE(p) \({ typeof(p) __val; __read_once_size(&p, &__val, sizeof(__val)); __val; })
从本质上讲,READ_ONCE()
是通过将变量强制使用标量类型
来工作,即使传入的变量没有这样的类型。事实上,最新的 READ_ONCE()
版本已经可以处理标量类型
,且包含了更多的 Linux 内核赋予的语义:
#define __READ_ONCE_SIZE \
({ \switch (size) { \case 1: *(__u8 *)res = *(volatile __u8 *)p; break; \case 2: *(__u16 *)res = *(volatile __u16 *)p; break; \case 4: *(__u32 *)res = *(volatile __u32 *)p; break; \case 8: *(__u64 *)res = *(volatile __u64 *)p; break; \default: /* 处理非标量类型 */ \barrier(); \__builtin_memcpy((void *)res, (const void *)p, size); \barrier(); \} \
})static __always_inline
void __read_once_size(const volatile void *p, void *res, int size)
{__READ_ONCE_SIZE;
}
更多关于 READ_ONCE()
的实现,将在另外的文章里介绍。另外,ASSIGN_ONCE()
已经不再存在,我们就不再考古了。
7. 结语
大多数人可能对编译器的工作过程都不会很了解,在这些依赖编译器实现的场合,如果我们无法确定编译器的编译结果,那么查看编译结果的反汇编代码
将是一个不错的选择。
8. 参考资料
[1] https://lwn.net/Articles/508991/
[2] https://lwn.net/Articles/624126/
相关文章:

Linux 编译屏障之 ACCESS_ONCE()
文章目录 1. 前言2. 背景3. 为什么要有 ACCESS_ONCE() ?4. ACCESS_ONCE() 代码实现5. ACCESS_ONCE() 实例分析6. ACCESS() 的演进7. 结语8. 参考资料 1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做…...

Discuz!X3.4论坛网站公安备案号怎样放到网站底部?
Discuz!网站的工信部备案号都知道在后台——全局——站点信息——网站备案信息代码填写,那公安备案号要添加在哪里呢?并没有看到公安备案号填写栏,今天驰网飞飞和你分享 1)工信部备案号和公安备案号统一填写到网站备案…...

LPDDR6带宽预计将翻倍增长:应对低功耗挑战与AI时代能源需求激增
在当前科技发展的背景下,低能耗问题成为了业界关注的焦点。国际能源署(IEA)近期报告显示,日常的数字活动对电力消耗产生显著影响——每次Google搜索平均消耗0.3瓦时(Wh),而向OpenAI的ChatGPT提出的每一次请求则消耗2.9…...

云原生架构内涵_3.主要架构模式
云原生架构有非常多的架构模式,这里列举一些对应用收益更大的主要架构模式,如服务化架构模式、Mesh化架构模式、Serverless模式、存储计算分离模式、分布式事务模式、可观测架构、事件驱动架构等。 1.服务化架构模式 服务化架构是云时代构建云原生应用的…...

宏基因组分析流程(Metagenomic workflow)202405|持续更新
Logs 增加R包pctax内的一些帮助上游分析的小脚本(2024.03.03)增加Mmseqs2用于去冗余,基因聚类的速度非常快,且随序列量线性增长(2024.03.12)更新全文细节(2024.05.29) 注意&#x…...

一千题,No.0037(组个最小数)
给定数字 0-9 各若干个。你可以以任意顺序排列这些数字,但必须全部使用。目标是使得最后得到的数尽可能小(注意 0 不能做首位)。例如:给定两个 0,两个 1,三个 5,一个 8,我们得到的最…...

PV PVC
默写 1 如何将pod创建在指定的Node节点上 node亲和、pod亲和、pod反亲和: 调度策略 匹配标签 操作符 nodeAffinity 主机 In,NotIn,Exists,DoesNotExist,Gt,Lt podAffinity …...

深入理解Nginx配置文件:全面指南
Nginx 是一个高性能的 HTTP 服务器和反向代理服务器,也是一个电子邮件(IMAP/POP3)代理服务器。由于其高效性和灵活性,Nginx 被广泛应用于各种 web 服务中。本文将详细介绍 Nginx 配置文件的结构和主要配置项,帮助你深入…...

【传知代码】自监督高效图像去噪(论文复现)
前言:在数字化时代,图像已成为我们生活、工作和学习的重要组成部分。然而,随着图像获取方式的多样化,图像质量问题也逐渐凸显出来。噪声,作为影响图像质量的关键因素之一,不仅会降低图像的视觉效果…...

linnux上安装php zip(ZipArchive)、libzip扩展
安装顺序: 安装zip(ZipArchive),需要先安装libzip扩展 安装libzip,需要先安装cmake 按照cmake、libzip、zip的先后顺序安装 下面的命令都是Linux命令 1、安装cmake 确认是否已安装 cmake --version cmake官网 未安装…...

油封制品中各种橡胶材料的差异
在机械系统中,油封起着关键的作用,其主要功能是防止润滑剂泄漏和污染物进入。油封的性能很大程度上取决于所用的橡胶材料。不同的橡胶化合物各有其独特的特性、优点和应用场景。本文将详细探讨油封制品中各种橡胶材料的差异,重点分析其特性、…...

梳理清楚的echarts地图下钻和标点信息组件
效果图 说明 默认数据没有就是全国地图, $bus.off("onresize")是地图容器变化刷新地图适配的,可以你们自己写 getEchartsFontSize是适配字体大小的,getEchartsFontSize(0.12) 12 mapScatter是base64图片就是图上那个标点的底图 Ge…...

【busybox记录】【shell指令】readlink
目录 内容来源: 【GUN】【readlink】指令介绍 【busybox】【readlink】指令介绍 【linux】【readlink】指令介绍 使用示例: 打印符号链接或规范文件名的值 - 默认输出 打印符号链接或规范文件名的值 - 打印规范文件的全路径 打印符号链接或规范文…...

C++之vector
1、标准库的vector类型 2、vector对象的初始化 3、vector常用成员函数 #include <vector> #include <algorithm> #include <iostream> using namespace std;typedef vector<int> INTVEC;// 普通方法 //void showVec(const INTVEC& vec) // 这边如…...

【简单介绍下idm有那些优势】
🎥博主:程序员不想YY啊 💫CSDN优质创作者,CSDN实力新星,CSDN博客专家 🤗点赞🎈收藏⭐再看💫养成习惯 ✨希望本文对您有所裨益,如有不足之处,欢迎在评论区提出…...

MyBatis系统学习 - 使用Mybatis完成查询单条,多条数据,模糊查询,动态设置表名,获取自增主键
上篇博客我们围绕Mybatis链接数据库进行了相关概述,并对Mybatis的配置文件进行详细的描述,本篇博客也是建立在上篇博客之上进行的,在上面博客搭建的框架基础上,我们对MyBatis实现简单的增删改查操作进行重点概述,在MyB…...

Generative Action Description Prompts for Skeleton-based Action Recognition
标题:基于骨架的动作识别的生成动作描述提示 源文链接:https://openaccess.thecvf.com/content/ICCV2023/papers/Xiang_Generative_Action_Description_Prompts_for_Skeleton-based_Action_Recognition_ICCV_2023_paper.pdfhttps://openaccess.thecvf.c…...

动手学深度学习(Pytorch版)代码实践 -深度学习基础-02线性回归基础版
02线性回归基础版 主要内容 数据生成:使用线性模型 ( y X*w b ) 加上噪声生成人造数据集。数据读取:通过小批量读取数据集来实现批量梯度下降,打乱数据顺序并逐批返回特征和标签。模型参数初始化:随机初始化权重和偏置&#x…...

信息学奥赛初赛天天练-15-阅读程序-深入解析二进制原码、反码、补码,位运算技巧,以及lowbit的神奇应用
更多资源请关注纽扣编程微信公众号 1 2021 CSP-J 阅读程序1 阅读程序(程序输入不超过数组或字符串定义的范围;判断题正确填 √,错误填;除特 殊说明外,判断题 1.5 分,选择题 3 分) 源码 #in…...

期权具体怎么交易详细的操作流程?
期权就是股票,唯一区别标的物上证指数,会看大盘吧,交易两个方向认购做多,认沽做空,双向t0交易,期权具体交易流程可以理解选择方向多和空,选开仓的合约,买入开仓和平仓没了࿰…...

系统架构设计师【第3章】: 信息系统基础知识 (核心总结)
文章目录 3.1 信息系统概述3.1.1 信息系统的定义3.1.2 信息系统的发展3.1.3 信息系统的分类3.1.4 信息系统的生命周期3.1.5 信息系统建设原则3.1.6 信息系统开发方法 3.2 业务处理系统(TPS)3.2.1 业务处理系统的概念3.2.2 业务处理系统的功能 …...

Linux 驱动设备匹配过程
一、Linux 驱动-总线-设备模型 1、驱动分层 Linux内核需要兼容多个平台,不同平台的寄存器设计不同导致操作方法不同,故内核提出分层思想,抽象出与硬件无关的软件层作为核心层来管理下层驱动,各厂商根据自己的硬件编写驱动…...

游戏子弹类python设计与实现详解
新书上架~👇全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我👆,收藏下次不迷路┗|`O′|┛ 嗷~~ 目录 一、引言 二、子弹类设计思路 1. 属性定义 2. 方法设计 三、子弹类实现详解 1. 定义子弹…...

Python基础学习笔记(六)——列表
目录 一、一维列表的介绍和创建二、序列的基本操作1. 索引的查询与返回2. 切片3. 序列加 三、元素的增删改1. 添加元素2. 删除元素3. 更改元素 四、排序五、列表生成式 一、一维列表的介绍和创建 列表(list),也称数组,是一种有序、…...

帝国CMS跳过选择会员类型直接注册方法
国CMS因允许多用户组注册,所以在注册页面会有一个选择注册用户组的界面,即使网站只用了一个用户组也会出现。 如果想去掉这个页面,直接进入注册页面,那么可按以下办法修改 打开 e/class/user.php 文件 查找: $chan…...

【python】python tkinter 计算器GUI版本(模仿windows计算器 源码)【独一无二】
👉博__主👈:米码收割机 👉技__能👈:C/Python语言 👉公众号👈:测试开发自动化【获取源码商业合作】 👉荣__誉👈:阿里云博客专家博主、5…...

黑马es数据同步mq解决方案
方式一:同步调用 优点:实现简单,粗暴 缺点:业务耦合度高 方式二:异步通知 优点:低耦含,实现难度一般 缺点:依赖mq的可靠性 方式三:监听binlog 优点:完全解除服务间耦合 缺点:开启binlog增加数据库负担、实现复杂度高 利用MQ实现mysql与elastics…...

通过LLM多轮对话生成单元测试用例
通过LLM多轮对话生成单元测试用例 代码 在采用 随机生成pytorch算子测试序列且保证算子参数合法 这种方法之前,曾通过本文的方法生成算子组合测试用例。目前所测LLM生成的代码均会出现BUG,且多次交互后仍不能解决.也许随着LLM的更新,这个问题会得到解决.记录备用。 代码 impo…...

[Redis]String类型
基本命令 set命令 将 string 类型的 value 设置到 key 中。如果 key 之前存在,则覆盖,无论原来的数据类型是什么。之前关于此 key 的 TTL 也全部失效。 set key value [expiration EX seconds|PX milliseconds] [NX|XX] 选项[EX|PX] EX seconds⸺使用…...

Ai速递5.29
全球AI新闻速递 1.摩尔线程与无问芯穹合作,实现国产 GPU 端到端 AI 大模型实训。 2.宝马工厂:机器狗上岗,可“嗅探”故障隐患。 3.ChatGPT:macOS 开始公测。 4.Stability AI:推出Stable Assistant,可用S…...