当前位置: 首页 > news >正文

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 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. 结语

大多数人可能对编译器的工作过程都不会很了解,在这些依赖编译器实现的场合,如果我们无法确定编译器的编译结果,那么查看编译结果的反汇编代码将是一个不错的选择。

8. 参考资料

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

http://www.lryc.cn/news/357885.html

相关文章:

  • Discuz!X3.4论坛网站公安备案号怎样放到网站底部?
  • LPDDR6带宽预计将翻倍增长:应对低功耗挑战与AI时代能源需求激增
  • 云原生架构内涵_3.主要架构模式
  • 宏基因组分析流程(Metagenomic workflow)202405|持续更新
  • 一千题,No.0037(组个最小数)
  • PV PVC
  • 深入理解Nginx配置文件:全面指南
  • 【传知代码】自监督高效图像去噪(论文复现)
  • linnux上安装php zip(ZipArchive)、libzip扩展
  • 油封制品中各种橡胶材料的差异
  • 梳理清楚的echarts地图下钻和标点信息组件
  • 【busybox记录】【shell指令】readlink
  • C++之vector
  • 【简单介绍下idm有那些优势】
  • MyBatis系统学习 - 使用Mybatis完成查询单条,多条数据,模糊查询,动态设置表名,获取自增主键
  • Generative Action Description Prompts for Skeleton-based Action Recognition
  • 动手学深度学习(Pytorch版)代码实践 -深度学习基础-02线性回归基础版
  • 信息学奥赛初赛天天练-15-阅读程序-深入解析二进制原码、反码、补码,位运算技巧,以及lowbit的神奇应用
  • 期权具体怎么交易详细的操作流程?
  • 系统架构设计师【第3章】: 信息系统基础知识 (核心总结)
  • Linux 驱动设备匹配过程
  • 游戏子弹类python设计与实现详解
  • Python基础学习笔记(六)——列表
  • 帝国CMS跳过选择会员类型直接注册方法
  • 【python】python tkinter 计算器GUI版本(模仿windows计算器 源码)【独一无二】
  • 黑马es数据同步mq解决方案
  • 通过LLM多轮对话生成单元测试用例
  • [Redis]String类型
  • Ai速递5.29
  • Android9.0 MTK平台如何增加一个系统应用