Linux 编译屏障之 ACCESS_ONCE()

文章目录

  • 1. 前言
  • 2. 背景
  • 3. 为什么要有 ACCESS_ONCE() ?
  • 4. ACCESS_ONCE() 代码实现
  • 5. ACCESS_ONCE() 实例分析
  • 6. ACCESS() 的演进
  • 7. 参考资料

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 BorntraegerGCC 4.6/4.7 上发现,ACCESS_ONCE()union 类型上无法正常工作,Christian BorntraegerLinus 讨论能否通过一种 workaround 方式,来绕过这个问题。
简而言之,就是 ACCESS_ONCE() 强制将变量视为 volatile 类型,即使它(就像内核中的几乎所有变量一样)不是这样声明的。Christian Borntraeger 报告的问题是,如果传入 GCC 4.64.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. 参考资料

[1] https://lwn.net/Articles/508991/
[2] https://lwn.net/Articles/624126/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/18754.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于匹配追踪和最大重叠离散小波变换的ECG心电信号R波检测(MATLAB 2018a)

准确识别心电信号的R波是进行HRV分析的前提。因此,开发出准确的心电信号R波检测方法十分重要。近几十年来,提出的R峰检测方法主要分为两个阶段。第1阶段是预处理阶段,目的是对受不同噪声影响的原始心电信号进行降噪处理,从而实现增…

基于SpringBoot+Html+Mysql的餐厅点餐管理系统外卖点餐系统

博主介绍: 大家好,本人精通Java、Python、C#、C、C编程语言,同时也熟练掌握微信小程序、Php和Android等技术,能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验,能够为学生提供各类…

算法与数据结构高手养成:朴素的贪心法(上)最优化策略

✨✨ 欢迎大家来访Srlua的博文(づ ̄3 ̄)づ╭❤~✨✨ 🌟🌟 欢迎各位亲爱的读者,感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢,在这里我会分享我的知识和经验。&am…

沃飞长空总部落地成都高新,为蓉低空经济发展助力!

5月25日,吉利科技集团与成都高新区签署合作协议,吉利科技集团旗下沃飞长空全球总部落地成都高新区。 根据协议,沃飞长空全球总部项目落地成都未来科技城,将布局总部办公、研发和生产制造低空出行航空器等业务。双方将积极发挥各自…

图片去水印工具(低调用哦)

一、简介 1、它是一款专业的图像编辑工具,旨在帮助用户轻松去除照片中不需要的元素或修复照片中的缺陷。无论是修复旧照片、消除拍摄时的不良构图,还是删除照片中的杂乱元素,都能帮助用户快速实现这些目标。其功能强大且操作简单&#xff0c…

【蓝桥杯嵌入式】第十四届省赛 更新中

0 前言 刚刚做完第十四届的省赛,这届题目比我想象中的要简单;不过我去年参加的14届单片机的省赛都比往年的国赛还难,挺离谱的~ 1 展示 1.1 源码 1.2 演示视频 1.3 题目展示 2 CubeMX配置(第十四届省赛真题) 设置下载线 HSE时钟设置 时钟树…

微信小程序-常用的视图容器类组件

一.组件分类 小程序中的组件也是由宿主环境提供的,开发者可以基于组件快速搭建出漂亮的页面结构。 官方把小程序的组件分为了9大类: (1) 视图容器 (2) 基础内容 (3) 表单组件 (4)导航组件 (5) 媒体组件 (6) map 地图组件 (7) canvas 画布组件 (8) 开放能力 (9) 无…

Android Studio中xml预览不显示问题解决办法

具体表现: Android Studio的XML中可以看到视图的相对位置结构,但是看不到具体的模样,全黑,等于半摸黑调UI(由于我已经修好了所以没有截图)。不是所有的项目都会这样,一部分项目是正常的。 或许…

Sping源码(九)—— Bean的初始化(非懒加载)— getMergedLocalBeanDefinition

序言 前两篇文章介绍了Bean初始化之前的一些准备工作,包括设置BeanFacroty的ConversionService属性以及将Bean进行冻结。这篇文章将会进入到preInstantiateSingletons方法。进一步了解Bean的初始化流程。 preInstantiateSingletons public void preInstantiateSin…

常用的 Git 命令

切換分支 流程 // 查看分支狀態 git status // 保存本分支 git add . // 提交本分支 git commit -m 保存 // 推送到遠程 git push // 切換分支 git checkout BranchName創建新分支流程 // 查看分支狀態 git status // 保存分支內容 git add . // 提交到本地 git commit -m 保…

这个开源的多模态模型无敌。。。

InternVL 由 OpenGVLab 开发,是一个开源的多模态对话模型,其性能接近商业化的 GPT-4V 模型。 GPT-4V 是 OpenAI 去年推出的多模态模型,使用它你可以分析所需的任何类型的图像并获取有关该图像的信息。 1. InternVL 开源模型 而今天的主角研究…

香港苏州商会、香港青年科学家协会博士团参观李良济,加强人才交流,促进科创合作与共赢

近日,香港苏州商会、香港青年科学家协会联合主办的苏港青年科创交流会成功举行,香港青年科学家协会博士团神州行苏州站启动。 5月26日,香港苏州商会及香港青年科学家协会博士团走进李良济,先后参观了李良济中医药文化展厅&#xf…

2024年5月29号PMP每日三题含答案

2024年5月29号PMP每日三题含答案 1.一位团队成员通知项目经理有一个问题可能会破坏项目。项目经理将该问题添加到问题日志中,并要求团队找到解决方案。 项目经理下一步应该怎么做? A.更新风险登记册 B.修订项目管理计划 C.确定适当的风险应对 D.通知干系…

【图论】树的重心

树的重心 删去这一点后每一块中点数最为平均 统计以u为根的子树点数个数&#xff08;加上一个本身&#xff0c;从哪里来的&#xff09;&#xff0c;这里点都是一样的 s846. 树的重心 - AcWing题库 #include <iostream> #include <algorithm> #include <cst…

【Qt之·类QCoreApplication】

系列文章目录 文章目录 前言一、QCoreApplication的概述1.1 QCoreApplication的作用1.2 QCoreApplication在应用程序中的角色和功能1.3 QCoreApplication成员函数 二、QCoreApplication的常用功能2.1 事件循环的概念和作用2.2 处理应用程序事件循环2.3 QCoreApplication如何处理…

16、24年--信息系统工程——软件工程

本章主要靠选择题,4分左右,案例涉及概率不大,论文不会单独考,多看课本原文。 1、架构设计 软件架构为软件系统提供了一个结构、行为和属性的高级抽象,由构件的描述、构件的相互作用(连接件)、指导构件集成的模式以及这些模式的约束组成。软件架构不仅指定了系统的组织架…

前端面试题(14)|求职季面试题分享|答案

1、你知道Xss和Csrf攻击吗&#xff1f; - XSS(跨站脚本攻击)是指攻击者将恶意代码注入到网页中,当用户访问该页面时,恶意代码会在用户的浏览器中执行,从而窃取用户的信息或执行恶意操作。 - CSRF(跨站请求伪造)是指攻击者盗用用户的身份,以用户的名义发送恶意请求,从而执行非法…

Caused by: java.util.ConcurrentModificationException

Caused by: java.util.ConcurrentModificationException 1. 关于 ConcurrentModificationException 报错2. 报错日志3. 代码逻辑doInBackground4. 修改方案5. 方案解析 1. 关于 ConcurrentModificationException 报错 在 Java 中&#xff0c;当尝试在迭代过程中修改对象的结构…

【星海出品】Langchain

Introduction 数学字符大全 | LangChain LLM语言模型 P(Ω1,Ω2,Ω3)N元语法模型:使用统计的方式进行语言模型的建模序列到序列模型(Seq2Seq),采用编码器-解码器的结构设计基于机器学习的语言模型:使用RNN模型进行语言模型的建模使用了 自注意力机制(Self-Attention Me…

游戏联运平台如何助力游戏行业飞速发展?

随着科技的进步和互联网的普及&#xff0c;游戏行业正以前所未有的速度飞速发展。在这个过程中&#xff0c;游戏联运平台凭借其独特的优势和功能&#xff0c;成为了推动游戏行业腾飞的关键力量。本文将探讨游戏联运平台如何助力游戏行业实现飞速发展。 一、游戏联运平台的定义与…