内存访问顺序 - part2: 屏障及Linux kernel中屏障的使用
文章目录
- 屏障是什么
- Linux Kernel 中的屏障
- Linux 屏障 API
- 一般的屏障
- 强制性屏障
- SMP 条件屏障
- 隐式屏障
- 其他屏障
- 屏障的开销
- 未来的文章
本文翻译自 Memory access ordering part 2: Barriers and the Linux kernel.
该系列有 3 篇文章,其余两篇是:
- Memory Access Ordering - an introduction,译文:内存访问顺序 - part1: 介绍
- Memory access ordering part 3: Memory access ordering in the Arm Architecture,译文:内存访问顺序 - part3: ARM体系架构中的内存访问顺序
作者 Leif Lindholm 在 ARM 工作多年,经验丰富,虽然这篇文章发表于 2013 年,但是还是很有借鉴价值的。
前一篇文章 介绍了内存访问顺序的概念。但是它没有提供任何解决问题的办法,也没有具体说明这种排序的重要性。
当今,并非所有的软件开发者都需要深入了解内存访问顺序或屏障。除非您的代码直接与硬件打交道,或者直接与在其他内核上执行的代码打交道,或者直接加载或生成要执行的指令,否则大多数情况下您的代码都会正常工作。
- 如果您与硬件的交互完全是通过设备驱动程序进行的(即没有设备控制器直接映射到您的应用程序中),那么设备驱动程序有责任保证执行顺序。
- 如果您与运行在不同内核上的软件的通信使用到了多线程API,例如使用了
Pthreads
或Java 线程
,那么该API有责任保证执行顺序。 - 如果您的程序在具备分页功能的操作系统上执行,那么操作系统有责任保证执行顺序。
但是,如果您正在编写设备驱动程序,或实现自己的线程通信,或创建 JIT 编译器,则如果不了解屏障
(barrier
)的正确使用可能会导致意外的难以诊断的问题。如果您的程序要求特定的内存访问顺序才能被系统中的多个内核或设备看到,那么该解决方案称为屏障。
尽管底层的体系结构概念本身很有趣,但它们并不是大多数与屏障相关的软件开发人员所需要知道的。因此,本文仅涉及Linux
内核中的屏障使用。我保证在以后的文章中再讲详细的细节。
屏障是什么
屏障
(barrier
)在某些体系结构中称为栅栏
(fence
),是一种显示地执行某种类型的内存访问顺序的操作。
从较高的层次上来讲,这可能意味着编译器指令防止加载/存储操作在源代码中跨行重新排序,但是允许编译器对任意一侧的内存访问或同一侧的其他访问进行重新排序。从较低的层次上来讲,这可能意味着使用专用指令停止内核的运行,直至确保所有先前的内存访问对系统中的其他代理可见。代理是系统中能够启动总线事务的任何设备——例如处理器或DMA控制器。
图1是屏障影响加载-存储(load-store
)指令顺序的例子。
图中Load2
依赖Store1
。例如,Store1
可能是对配置存储器的写操作,其需要重新映射外设的物理地址,然后由Load2
读取。注意,在没有地址依赖关系的情况下,屏障两侧的访问仍然可以自由地重新排序。
Linux Kernel 中的屏障
由于供应商,体系结构和整个系统组件整体之间的编译器指令,屏障指令和其他系统操作会有所不同,因此Linux内核定义了一组需要为每个体系结构实现的可移植的屏障操作集。由于受支持的具有最弱内存模型的体系结构(实际上是允许最多重新排序的模型)是DEC Alpha
,因此将其用做参考体系结构。在这方面,没有其他体系结构可以超越DEC Alpha
,但是ARMv7-A
非常接近。Linux内核中可用屏障的完整文档在Linux/Documentation/memory-barrier.txt
,这里只做一个简短的介绍。
Linux 屏障 API
一般的屏障
一般的屏障对运行时没有影响,它只是编译器的一条指令,以防止出于优化目的而对内存访问进行重排序。
方法 | 说明 |
---|---|
barrier()
|
编译器级别的屏障。编译器不会将内存访问从该语句的一侧重新排序到另一侧。这对处理器实际执行生成的指令的顺序没有影响。 |
volatile 关键字也可以做到每次都从内存中重新读取变量的值,而不是用寄存器中暂存的值。
Linux内核中编译器级别的屏障(compiler barrier
)是 barrier()
,定义在 include/linux/compiler-gcc.h
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
这里的 memory
是告诉编译器,代码修改了内存中的内容,需要重新从内存中读取数据,而不是使用寄存器中暂存的数据。这条语句只会对编译器的行为产生约束,不会生成真正的指令,因此对CPU最终的指令执行没有任何影响。
强制性屏障
强制性屏障用于强制整个系统级别的内存一致性。常用于与映射的外设进行通信时。无论是什么体系结构,所有的强制性屏障都保证至少可以扩展到编译器级别的屏障。
方法 | 说明 |
---|---|
mb()
|
完整的系统内存屏障。mb() 之前的所有内存操作都执行完,才能执行mb() 之后的操作。这个顺序对系统中所有的总线主控都是可见的。它还确保从单个处理器到从属设备的访问顺序。
|
rmb()
|
与mb() 类似,但只保证读操作的访问顺序,也就是说,rmb() 之前所有的读操作将在rmb() 之后的任何读操作之前完成。
|
wmb()
|
与mb() 类似,但只保证写操作的访问顺序,也就是说,wmb() 之前所有的写操作将在wmb() 之后的任何写操作之前完成。
|
Linux内核中 mb()
,wmb()
, rwb()
的定义如下:
#define mb() dsb(sy)
#define rmb() dsb(ld)
#define wmb() dsb(st)
#define dsb(opt) asm volatile("dsb " #opt : : : "memory")
可见用的都是 dsb
指令,sy
的意思是 system
,ld
的意思是load
,st
的意思是store
。
SMP 条件屏障
SMP条件屏障用于保证SMP系统中不同核之间内存视图的一致性。当编译一个没有配置CONFIG_SMP
的Linux内核时,所有的SMP屏障都被转换成普通的编译器屏障。
注意:SMP屏障是强制性屏障的子集,不是超集(这是一个常见的误解)。SMP屏障不能取代强制性屏障,但是强制性屏障可以取代SMP屏障。
方法 | 说明 |
---|---|
smp_mb()
|
与mb() 类似,但是只能保证SMP系统中内核/处理器之间的顺序。smp_mb() 之前所有的内存访问对SMP系统中所有的内核都是可见的。
|
smp_rmb()
|
与smp_mb() 类似,但只保证读操作的访问顺序。
|
smp_wmb()
|
与smp_mb() 类似,但只保证写操作的访问顺序。
|
#define __smp_mb() dmb(ish) // ish: inner-shareable
#define __smp_rmb() __smp_mb()
#define __smp_wmb() dmb(ishst) // ishst: inner-shareable, store
#define dmb(opt) asm volatile("dmb " #opt : : : "memory")
隐式屏障
Linux内核中使用锁结构来充当隐式SMP屏障,与用户空间的pthread
同步操作相同。当使用这些来保护共享资源时,就不必使用显式屏障(以确保该资源的一致性)。但是,这并不能消除在与外部主机通信时对显式屏障的需要。
由于大量的设备驱动程序没有使用所需的屏障,因此当Linux内核使用CONFIG_ARM_DMA_BUFFERABLE
时,ARM体系结构的I/O访问器宏(readb()
, iowrite32()
等)将充当显式的内存屏障。这是在Linux-2.6.35
中增加的。
其他屏障
在 Linux 内核中还存在其他屏障,本文只涵盖了最常见的几种屏障。更多的信息请参考Linux内核文档。
屏障的开销
使用屏障的真正原因是为了防止编译器和硬件做了不安全的优化。此外,存在不同类型的屏障以便准确地描述需要强制执行的内存顺序。这意味着屏障会对系统性能产生负面影响。花费额外的时间来确定特定的情况下是否需要屏障,如果需要屏障,应该用哪个屏障,是非常值得的。
未来的文章
我的下一篇文章将介绍ARM体系结构中可用的屏障和操作。
内存访问顺序 - part2: 屏障及Linux kernel中屏障的使用相关推荐
- Linux kernel中常见的宏整理
0x00 宏的基本知识 // object-like #define 宏名 替换列表 换行符 //function-like #define 宏名 ([标识符列表]) 替换列表 换行符 替换列表和标识 ...
- Linux Kernel中AEP的现状和发展
阿里 石洋内核月谈Yesterday AEP简介 AEP是Intel推出的一种新型的非易失Optane Memory设备,又被称作Apache Pass,所以一般习惯称作AEP.在这之前也有类似的设备 ...
- linux kernel中的栈的介绍
目录 1.linux kernel中的中断irq的栈stack (1).arm32体系的irq的栈 (2).arm64体系的irq的栈 2.linux kernel中的栈stack (1).概念介绍: ...
- linux kernel中的进程栈
1.linux中的user mode的进程栈 在thread_info.h中,设置进程栈的大小为16k #define THREAD_SIZE 16384 #define THREAD_START_S ...
- Linux kernel 中模块化的平台驱动代码介绍
介绍 在linux kernel中通过module_platform_driver来实现模块化平台驱动.大量的设备驱动程序都基于该种方式来实现,使用频次非常的高,在linux kernel 5.4.1 ...
- Intel 计划在Linux kernel中引入 User Interrupts,效率是eventfd的10倍
文章目录 未来Eventfd的替代品 User Interrupts 详细介绍什么是 User Interrupts 底层是如何工作的? 内核管理相关的数据结构 User IPI 应用接口 具体的例子 ...
- linux内核 漏洞扫描,Linux kernel中存在15年的漏洞
SCSI 定义了并行I/O 总线和数据协议来连接硬盘驱动.打印机.扫描仪.光驱.测试设备.医疗设备等外部设备到本地计算机.近日,GRIMM在Linux kernel SCSI (Small Compu ...
- 关于Linux Kernel中的宏定义likely和unlikely
在Linux kernel的源代码中,经常能见到if(likely(x))或if(unlikely(x))之类的用法,其确切含义需要说明一下,以便更好的理解kernel的源代码. likely与unl ...
- linux kernel中的cmdline的详细介绍
cmdline 1.向linux kernel添加cmdline的四种方式 (1). 在dts中的bootargs中添加 (2).在BoardConfig中添加 (3).在uboot中添加 (4).在 ...
最新文章
- Judge Judy
- iphone-common-codes-ccteam源代码 CCUIAlertView.m
- 互联网1分钟 |1115
- 交叉编译openwrt php,构建 openwrt 交叉编译工具链
- C#连接Excel和Access(包括2003和2007版)方法总结
- 前端学习(3064):vue+element今日头条管理-展示频道处理
- 学习日记2、easyui treeGrid属性及时间
- 百倍性能的PL/SQL优化案例(r11笔记第13天)
- 学编程的人不能不看的好文章啊!!
- 虚拟化--062 vsphere workstation bios启动
- 阿里云开发笔记01——CuteFTP使用方法
- 查询vue版本时报错:“文件名、目录名或卷标语法不正确“
- 15/18位身份证号码验证的正则表达式总结
- 泰拉瑞亚 服务器linux,泰拉瑞亚Linux主机打造指南
- 一文搞定BP神经网络——从原理到应用(原理篇)
- CTS、CLS、CLR分别作何解释
- Github项目(克隆,上传)简单git命令流程使用记录
- 30个python的最佳实践,快去试试吧!
- python - BeautifulSoup教程
- MIC(最大信息系数)