LINUX内核内存屏障

Linux内核中的内存屏障是为了解决多CPU交互或CPU与设备交互时内存访问顺序不确定性的问题。内存屏障提供了不同类型的屏障,如写屏障、数据依赖屏障等,保证特定内存操作的顺序。文章介绍了屏障的种类、作用、使用场景,以及它们如何影响内存系统的顺序。同时,文章指出,不同的CPU架构有不同的内存模型,因此在编写跨平台的内核代码时,需要使用内存屏障来确保正确性。
摘要由CSDN通过智能技术生成

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处理器.

(*) 使用演示样例.

- 环型缓冲区.

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

================

内存訪问抽象模型

================

考虑例如以下抽象系统模型:

​ : :

​ : :

​ : :

​ +-------+ : +--------+ : +-------+

​ | | : | | : | |

​ | | : | | : | |

​ | 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操作(译注: 由于存在数据依赖).

操作设备

--------

对于一些设备, 其控制寄存器被映射到一组内存地址集合上, 而这些控制寄存器被訪问的顺

序是至关重要的. 如果, 一个以太网卡拥有一些内部寄存器, 通过一个地址port寄存器(A)

和一个数据port寄存器(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;

===============

什么是内存屏障?

===============

正如上面所说, 无关的内存操作会被按随机顺序有效的得到运行, 可是在CPU与CPU交互时或

CPU与IO设备交互时, 这可能会成为问题. 我们须要一些手段来干预编译器和CPU, 使其限制

指令顺序.

内存屏障就是这种干预手段. 他们能保证处于内存屏障两边的内存操作满足部分有序. (

译注: 这里"部分有序"的意思是, 内存屏障之前的操作都会先于屏障之后的操作, 可是假设

几个操作出如今屏障的同一边, 则不保证它们的顺序. 这一点下文将多次提到.)

这种强制措施是很重要的, 由于系统中的CPU和其它设备能够使用各种各样的策略来提

高性能, 包含对内存操作的乱序, 延迟和合并运行; 预取; 投机性的分支预測和各种缓存.

内存屏障用于禁用或抑制这些策略, 使代码可以清楚的控制多个CPU和/或设备的交互.

各式各样的内存屏障

------------------

内存屏障有四种基本类型:

(1) 写(STORE)内存屏障.

写内存屏障提供这种保证: 全部出如今屏障之前的STORE操作都将先于全部出如今屏

障之后的STORE操作被系统中的其它组件所感知.

写屏障仅保证针对STORE操作的部分有序; 不要求对LOAD操作产生影响.

随着时间的推移, 一个CPU提交的STORE操作序列将被存储系统所感知. 全部在写屏障

之前的STORE操作将先于全部在写屏障之后的STORE操作出如今被感知的序列中.

[!] 注意, 写屏障一般须要与读屏障或数据依赖屏障配对使用; 參阅"SMP内存屏障配

​ 对"章节. (译注: 由于写屏障仅仅保证自己提交的顺序, 而无法干预其它代码读内

​ 存的顺序. 所以配对使用非常重要. 其它类型的屏障亦是同理.)

(2) 数据依赖屏障.

数据依赖屏障是读屏障的弱化版本号. 如果有两个LOAD操作的场景, 当中第二个LOAD操

作的结果依赖于第一个操作(比方, 第一个LOAD获取地址, 而第二个LOAD使用该地址去

取数据), 数据依赖屏障确保在第一个LOAD获取的地址被用于訪问之前, 第二个LOAD的

目标内存已经更新.

(译注: 由于第二个LOAD要使用第一个LOAD的结果来作为LOAD的目标, 这里存在着数

据依赖. 由前面的"保证"章节可知, 第一个LOAD必然会在第二个LOAD之前运行, 不需

要使用读屏障来保证顺序, 仅仅须要使用数据依赖屏障来保证内存已刷新.)

数据依赖屏障仅保证针对相互依赖的LOAD操作的部分有序; 不要求对STORE操作,

独立的LOAD操作, 或重叠的LOAD操作产生影响.

正如(1)中所提到的, 在一个CPU看来, 系统中的其它CPU提交到内存系统的STORE操作

序列在某一时刻能够被其感知到. 而在该CPU上触发的数据依赖屏障将保证, 对于在屏

障之前发生的LOAD操作, 假设一个LOAD操作的目标被其它CPU的STORE操作所改动, 那

么在屏障完毕之时, 这个相应的STORE操作之前的全部STORE操作所产生的影响, 将被

数据依赖屏障之后运行的LOAD操作所感知.

參阅"内存屏障举例"章节所描写叙述的时序图.

[!] 注意, 对第一个LOAD的依赖的确是一个数据依赖而不是控制依赖. 而假设第二个

​ LOAD的地址依赖于第一个LOAD, 但并非通过实际载入的地址本身这种依赖条

​ 件, 那么这就是控制依赖, 须要一个完整的读屏障或更强的屏障. 參阅"控制依

​ 赖"相关章节.

[!] 注意, 数据依赖屏障一般要跟写屏障配对使用; 參阅"SMP内存屏障的配对使用"章

​ 节.

(3) 读(LOAD)内存屏障.

读屏障包括数据依赖屏障的功能, 而且保证全部出如今屏障之前的LOAD操作都将先于

全部出如今屏障之后的LOAD操作被系统中的其它组件所感知.

读屏障仅保证针对LOAD操作的部分有序; 不要求对STORE操作产生影响.

读内存屏障隐含了数据依赖屏障, 因此能够用于替代它们.

[!] 注意, 读屏障一般要跟写屏障配对使用; 參阅"SMP内存屏障的配对使用"章节.

(4) 通用内存屏障.

通用内存屏障保证全部出如今屏障之前的LOAD和STORE操作都将先于全部出如今屏障

之后的LOAD和STORE操作被系统中的其它组件所感知.

通用内存屏障是针对LOAD和STORE操作的部分有序.

通用内存屏障隐含了读屏障和写屏障, 因此能够用于替代它们.

内存屏障还有两种隐式类型:

(5) LOCK操作.

它的作用相当于一个单向渗透屏障. 它保证全部出如今LOCK之后的内存操作都将在

LOCK操作被系统中的其它组件所感知之后才干发生.

出如今LOCK之前的内存操作可能在LOCK完毕之后才发生.

LOCK操作总是跟UNLOCK操作配对出现的.

(6) UNLOCK操作.

它的作用也相当于一个单向渗透屏障. 它保证全部出如今UNLOCK之前的内存操作都将

在UNLOCK操作被系统中的其它组件所感知之前发生.

出如今UNLOCK之后的内存操作可能在UNLOCK完毕之前就发生了.

须要保证LOCK和UNLOCK操作严格依照相互影响的正确顺序出现.

(译注: LOCK和UNLOCK的这样的单向屏障作用, 确保临界区内的訪存操作不能跑到临界区

外, 否则就起不到"保护"作用了.)

使用LOCK和UNLOCK之后, 一般就不再须要其它内存屏障了(可是注意"MMIO写屏障"章节

中所提到的例外).

仅仅有在存在多CPU交互或CPU与设备交互的情况下才可能须要用到内存屏障. 假设能够确保某

段代码中不存在这种交互, 那么这段代码就不须要使用内存屏障. (译注: CPU乱序运行指

令, 相同会导致寄存器的存取顺序被打乱, 可是为什么不须要寄存器屏障呢?

就是由于寄存

器是CPU私有的, 不存在跟其它CPU或设备的交互.)

注意, 对于前面提到的最低限度保证. 不同的体系结构可能提供很多其它的保证, 可是在特定体

系结构的代码之外, 不能依赖于这些额外的保证.

关于内存屏障, 不能假定什么?

---------------------------

Linux内核的内存屏障不保证以下这些事情:

(*) 在内存屏障之前出现的内存訪问不保证在内存屏障指令完毕之前完毕; 内存屏障相当

于在该CPU的訪问队列中画一条线, 使得相关訪存类型的请求不能相互跨越. (译注:

用于实现内存屏障的指令, 其本身并不作为參考对象, 其两边的訪存操作才被当作參

考对象. 所以屏障指令运行完毕并不表示出如今屏障之前的訪存操作已经完毕. 而如

果屏障之后的某一个訪存操作已经完毕, 则屏障之前的全部訪存操作必然都已经完毕

了.)

(*) 在一个CPU上运行的内存屏障不保证会直接影响其它系统中的CPU或硬件设备. 仅仅会间

接影响到第二个CPU感知第一个CPU产生訪存效果的顺序, 只是请看下一点:

(*) 不能保证一个CPU可以按顺序看到还有一个CPU的訪存效果, 即使还有一个CPU使用了内存屏

障, 除非这个CPU也使用了与之配对的内存屏障(參阅"SMP内存屏障的配对使用"章节).

(*) 不保证一些与CPU相关的硬件不会乱序訪存. CPU cache一致性机构会在CPU之间传播内

存屏障所带来的间接影响, 可是可能不是按顺序的.

​ [*] 很多其它关于总线主控DMA和一致性的问题请參阅:

​ Documentation/PCI/pci.txt

​ Documentation/PCI/PCI-DMA-mapping.txt

​ Documentation/DMA-API.txt

数据依赖屏障

------------

数据依赖屏障的使用需求有点微妙, 并不总是非常明显就能看出须要他们. 为了说明这一点,

考虑例如以下的操作序列:

​ CPU 1 CPU 2

​ =============== ===============

​ { A == 1, B == 2, C = 3, P == &A, Q == &C }

​ B = 4;

​ <写屏障>

​ P = &B

​ Q = P;

​ D = *Q;

这里有明显的数据依赖, 在序列运行完之后, Q的值一定是&A和&B之中的一个, 也就是:

​ (Q == &A) 那么 (D == 1)

​ (Q == &B) 那么 (D == 4)

可是! CPU 2可能在看到P被更新之后, 才看到B被更新, 这就导致以下的情况:

​ (Q == &B) 且 (D == 2) ?

?

?

?

尽管这看起来似乎是一个一致性错误或逻辑关系错误, 但事实上不是, 而且在一些真实的CPU

中就能看到这种行为(就比方DEC Alpha).

为了解决问题, 必须在取地址和取数据之间插入一个数据依赖或更强的屏障:

​ CPU 1 CPU 2

​ =============== ===============

​ { A == 1, B == 2, C = 3, P == &A, Q == &C }

​ B = 4;

​ <写屏障>

​ P = &B<

  • 27
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值