=================
LINUX内核内存屏障
=================

By: David Howells <dhowells@redhat.com>
Paul E. McKenney <paulmck@linux.vnet.ibm.com>
译: kouu <kouucocu@126.com>

出处: Linux内核文档 -- Documentation/memory-barriers.txt

目录:

(*) 内存访问抽象模型.

- 操作设备.
- 保证.

(*) 什么是内存屏障?

- 各式各样的内存屏障.
- 关于内存屏障, 不能假定什么?
- 数据依赖屏障.
- 控制依赖.
- SMP内存屏障的配对使用.
- 内存屏障举例.
- 读内存屏障与内存预取.

(*) 内核中显式的内存屏障.

- 编译优化屏障.
- CPU内存屏障.
- MMIO写屏障.

(*) 内核中隐式的内存屏障.

- 锁相关函数.
- 禁止中断函数.
- 睡眠唤醒函数.
- 其他函数.

(*) 跨CPU的锁的屏障作用.

- 锁与内存访问.
- 锁与IO访问.

(*) 什么地方需要内存屏障?

- 处理器间交互.
- 原子操作.
- 访问设备.
- 中断.

(*) 内核中I/O屏障的作用.

(*) 最小限度有序的假想模型.

(*) CPU cache的影响.

- Cache一致性.
- Cache一致性与DMA.
- Cache一致性与MMIO.

(*) CPU所能做到的.

- 特别值得一提的Alpha处理器.

(*) 使用示例.

- 环型缓冲区.

(*) 引用.

================
内存访问抽象模型
================

考虑如下抽象系统模型:

: :
: :
: :
+-------+ : +--------+ : +-------+
| | : | | : | |
| | : | | : | |
| CPU 1 |<----->| 内存 |<----->| CPU 2 |
| | : | | : | |
| | : | | : | |
+-------+ : +--------+ : +-------+
^ : ^ : ^
| : | : |
| : | : |
| : v : |
| : +--------+ : |
| : | | : |
| : | | : |
+---------->| 设备 |<----------+
: | | :
: | | :
: +--------+ :
: :

假设每个CPU都分别运行着一个会触发内存访问操作的程序. 对于这样一个CPU, 其内存访问
顺序是非常松散的, 在保证程序上下文逻辑关系的前提下, CPU可以按它所喜欢的顺序来执
行内存操作. 类似的, 编译器也可以将它输出的指令安排成任何它喜欢的顺序, 只要保证不
影响程序表面的执行逻辑.

(译注:

内存屏障是为应付内存访问操作的乱序执行而生的. 那么, 内存访问为什么会乱序呢? 这里
先简要介绍一下:

现在的CPU一般采用流水线来执行指令. 一个指令的执行被分成: 取指, 译码, 访存, 执行,
写回, 等若干个阶段.

指令流水线并不是串行化的, 并不会因为一个耗时很长的指令在"执行"阶段呆很长时间, 而
导致后续的指令都卡在"执行"之前的阶段上.

相反, 流水线中的多个指令是可以同时处于一个阶段的, 只要CPU内部相应的处理部件未被
占满. 比如说CPU有一个加法器和一个除法器, 那么一条加法指令和一条除法指令就可能同
时处于"执行"阶段, 而两条加法指令在"执行"阶段就只能串行工作.

这样一来, 乱序可能就产生了. 比如一条加法指令出现在一条除法指令的后面, 但是由于除
法的执行时间很长, 在它执行完之前, 加法可能先执行完了. 再比如两条访存指令, 可能由
于第二条指令命中了cache(或其他原因)而导致它先于第一条指令完成.

一般情况下, 指令乱序并不是CPU在执行指令之前刻意去调整顺序. CPU总是顺序的去内存里
面取指令, 然后将其顺序的放入指令流水线. 但是指令执行时的各种条件, 指令与指令之间
的相互影响, 可能导致顺序放入流水线的指令, 最终乱序执行完成. 这就是所谓的"顺序流
入, 乱序流出".

指令流水线除了在资源不足的情况下会卡住之外(如前所述的一个加法器应付两条加法指令)
, 指令之间的相关性才是导致流水线阻塞的主要原因.

下文中也会多次提到, CPU的乱序执行并不是任意的乱序, 而必须保证上下文依赖逻辑的正
确性. 比如: a++; b=f(a); 由于b=f(a)这条指令依赖于第一条指令(a++)的执行结果, 所以
b=f(a)将在"执行"阶段之前被阻塞, 直到a++的执行结果被生成出来.

如果两条像这样有依赖关系的指令挨得很近, 后一条指令必定会因为等待前一条执行的结果
, 而在流水线中阻塞很久. 而编译器的乱序, 作为编译优化的一种手段, 则试图通过指令重
排将这样的两条指令拉开距离, 以至于后一条指令执行的时候前一条指令结果已经得到了,
那么也就不再需要阻塞等待了.

相比于CPU的乱序, 编译器的乱序才是真正对指令顺序做了调整. 但是编译器的乱序也必须
保证程序上下文的依赖逻辑.

由于指令执行存在这样的乱序, 那么自然, 由指令执行而引发的内存访问势必也可能乱序.
)

在上面的图示中, 一个CPU执行内存操作所产生的影响, 一直要到该操作穿越该CPU与系统中
其他部分的界面(见图中的虚线)之后, 才能被其他部分所感知.

举例来说, 考虑如下的操作序列:

CPU 1 CPU 2
=============== ===============
{ A == 1; B == 2 }
A = 3; x = A;
B = 4; y = B;

这一组访问指令在内存系统(见上图的中间部分)上生效的顺序, 可以有24种不同的组合:

STORE A=3, STORE B=4, x=LOAD A->3, y=LOAD B->4
STORE A=3, STORE B=4, y=LOAD B->4, x=LOAD A->3
STORE A=3, x=LOAD A->3, STORE B=4, y=LOAD B->4
STORE A=3, x=LOAD A->3, y=LOAD B->2, STORE B=4
STORE A=3, y=LOAD B->2, STORE B=4, x=LOAD A->3
STORE A=3, y=LOAD B->2, x=LOAD A->3, STORE B=4
STORE B=4, STORE A=3, x=LOAD A->3, y=LOAD B->4
STORE B=4, ...
...

然后这就产生四种不同组合的结果值:

x == 1, y == 2
x == 1, y == 4
x == 3, y == 2
x == 3, y == 4

甚至于, 一个CPU在内存系统上提交的STORE操作还可能不会以相同的顺序被其他CPU所执行
的LOAD操作所感知.

进一步举例说明, 考虑如下的操作序列:

CPU 1 CPU 2
=============== ===============
{ A == 1, B == 2, C = 3, P == &A, Q == &C }
B = 4; Q = P;
P = &B D = *Q;

这里有一处明显的数据依赖, 因为在CPU2上, LOAD到D里面的值依赖于从P获取到的地址. 在
操作序列的最后, 下面的几种结果都是有可能出现的:

(Q == &A) 且 (D == 1)
(Q == &B) 且 (D == 2)
(Q == &B) 且 (D == 4)

注意, CPU2决不会将C的值LOAD到D, 因为CPU保证在将P的值装载到Q之后才会执行对*Q的
LOAD操作(译注: 因为存在数据依赖).

操作设备
--------

对于一些设备, 其控制寄存器被映射到一组内存地址集合上, 而这些控制寄存器被访问的顺
序是至关重要的. 假设, 一个以太网卡拥有一些内部寄存器, 通过一个地址端口寄存器(A)
和一个数据端口寄存器(D)来访问它们. 要读取编号为5的内部寄存器, 可能使用如下代码:
*A = 5;
x = *D;

但是这可能会表现为以下两个序列之一(译注: 因为从程序表面看, A和D是不存在依赖的):

STORE *A = 5, x = LOAD *D
x = LOAD *D, STORE *A = 5

其中的第二种几乎肯定会导致错误, 因为它在读取寄存器之后才设置寄存器的编号.

保证
----

对于一个CPU, 它最低限度会提供如下的保证:

(*) 对于一个CPU, 在它上面出现的有上下文依赖关系的内存访问将被按顺序执行. 这意味
着:

Q = P; D = *Q;

CPU会顺序执行以下访存:

Q = LOAD P, D = LOAD *Q

并且总是按这样的顺序.

(*) 对于一个CPU, 重叠的LOAD和STORE操作将被按顺序执行. 这意味着:

a = *X; *X = b;

CPU只会按以下顺序执行访存:

a = LOAD *X, STORE *X = b

同样, 对于:

*X = c; d = *X;

CPU只会按以下顺序执行访存:

STORE *X = c, d = LOAD *X

(如果LOAD和STORE的目标指向同一块内存地址, 则认为是重叠).

还有一些事情是必须被假定或者必须不被假定的:

(*) 必须不能假定无关的LOAD和STORE会按给定的顺序被执行. 这意味着:

X = *A; Y = *B; *D = Z;

可能会得到如下几种执行序列之一:

X = LOAD *A, Y = LOAD *B, STORE *D = Z
X = LOAD *A, STORE *D = Z, Y = LOAD *B
Y = LOAD *B, X = LOAD *A, STORE *D = Z
Y = LOAD *B, STORE *D = Z, X = LOAD *A
STORE *D = Z, X = LOAD *A, Y = LOAD *B
STORE *D = Z, Y = LOAD *B, X = LOAD *A

(*) 必须假定重叠内存访问可能被合并或丢弃. 这意味着:

X = *A; Y = *(A + 4);

可能会得到如下几种执行序列之一:

X = LOAD *A; Y = LOAD *(A + 4);
Y = LOAD *(A + 4); X = LOAD *A;
{X, Y} = LOAD {*A, *(A + 4) };

同样, 对于:

*A = X; Y = *A;

可能会得到如下几种执行序列之一:

STORE *A = X; Y = LOAD *A;
STORE *A = Y = X;

[译] LINUX内核内存屏障相关推荐

  1. LINUX内核内存屏障

    ================= LINUX内核内存屏障 ================= By: David Howells <dhowells@redhat.com> Paul E ...

  2. linux内核内存屏障,从硬件引申出内存屏障,带你深入了解Linux内核RCU

    本文简介 本文从硬件的角度引申出内存屏障,这不是内存屏障的详尽手册,但是相关知识对于理解RCU有所帮助.这不是一篇单独的文章,这是<谢宝友:深入理解Linux RCU>系列的第2篇,前序文 ...

  3. 【Linux 内核 内存管理】优化内存屏障 ③ ( 编译器屏障 | 禁止 / 开启内核抢占 与 方法保护临界区 | preempt_disable 禁止内核抢占源码 | 开启内核抢占源码 )

    文章目录 一.禁止 / 开启内核抢占 与 方法保护临界区 二.编译器优化屏障 三.preempt_disable 禁止内核抢占 源码 四.preempt_enable 开启内核抢占 源码 一.禁止 / ...

  4. 【Linux 内核 内存管理】RCU 机制 ② ( RCU 机制适用场景 | RCU 机制特点 | 使用 RCU 机制保护链表 )

    文章目录 一.RCU 机制适用场景 二.RCU 机制特点 三.使用 RCU 机制保护链表 一.RCU 机制适用场景 在上一篇博客 [Linux 内核 内存管理]RCU 机制 ① ( RCU 机制简介 ...

  5. 【Linux 内核 内存管理】虚拟地址空间布局架构 ③ ( 内存描述符 mm_struct 结构体成员分析 | mmap | mm_rb | task_size | pgd | mm_users )

    文章目录 一.mm_struct 结构体成员分析 1.mmap 成员 2.mm_rb 成员 3.get_unmapped_area 函数指针 4.task_size 成员 5.pgd 成员 6.mm_ ...

  6. 【Linux 内核 内存管理】内存管理架构 ④ ( 内存分配系统调用过程 | 用户层 malloc free | 系统调用层 brk mmap | 内核层 kmalloc | 内存管理流程 )

    文章目录 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) 二.内存管理流程 一.内存分配系统调用过程 ( 用户层 | 系统调用 | 内核层 ) " 堆内存 " ...

  7. 【Linux 内核 内存管理】内存管理架构 ② ( 用户空间内存管理 | malloc | ptmalloc | 内核空间内存管理 | sys_brk | sys_mmap | sys_munmap)

    文章目录 一.用户空间内存管理 ( malloc / free / ptmalloc / jemalloc / tcmalloc ) 二.内核空间内存管理 1.内核内存管理系统调用 ( sys_brk ...

  8. pae扩展内存 linux,浅析linux内核内存管理之PAE

    浅析linux内核内存管理之PAE 早期Intel处理器从80386到Pentium使用32位物理地址,理论上,这样可以访问4GB的RAM.然而,大型服务器需要大于4GB的RAM来同时运行数以千计的进 ...

  9. Linux内核内存管理(3):kmemcheck介绍

    Linux内核内存管理 kmemcheck介绍 rtoax 2021年3月 在英文原文基础上,针对中文译文增加5.10.13内核源码相关内容. 5.10.13不存在kmemcheck的概念,取代的是k ...

最新文章

  1. Mobileye 自动驾驶策略(一)
  2. 腾讯“科学探索奖”提名报名正式启动
  3. decfloat转换成oracle种类型,Oracle GoldenGate 19.1新特性
  4. 皮一皮:直男的世界不可揣测!
  5. 定时任务--mysql数据库备份
  6. 碎片化学习前端资料分享~
  7. 【交互】【随机】Lost Root(CF1061F)
  8. 95-140-122-源码-transform-算子reduce
  9. 2018-10-15
  10. [管理员手册](五)Ubuntu desktop 20.04系统安装显卡驱动NVIDIA cuda pytorch向日葵sunlogin安装
  11. 数字图像处理实验一 直方图均衡化与直方图规定化
  12. Vue之跨域调用网易云音乐API及QQ音乐API
  13. 农历阳历转换 java_Java编程实例:Java版农历和阳历转换源码
  14. HTML文件mhl,比HDMI更强!MHL与HDMI技术解析
  15. WebMvcConfigurerAdapter已被废弃的解决方法
  16. 被Z世代称为B站张子枫,她有一种天然的治愈力。
  17. Java程序员的薪资取决于工作年限还是技术?
  18. 爬虫漫游指南:浏览器指纹
  19. 微机原理(8086基本知识点)
  20. 班级优化大师显示网络或服务器异常,希沃班级优化大师常见问题总汇

热门文章

  1. IIS搭建站点错误系列
  2. 5个很好用的.net 分析工具
  3. regsvr32.exe进程注册dll文件
  4. HttpModule 介绍(转)
  5. P2P之UDP穿透NAT的原理与实现
  6. 大数据之-Hadoop3.x_MapReduce_MapJoin案例需求分析---大数据之hadoop3.x工作笔记0133
  7. Android学习笔记---15_采用Pull解析器解析和生成XML内容
  8. java 数字的进制转换
  9. 二叉树 BinaryTree (先序、中序、后序遍历 节点查找、插入、删除 完整类) Java数据结构与算法
  10. linux查usb版本,怎么查看 ubuntu usb版本