Posted on Jan 1, 1

memory berriers in Linux kernel

C 语言的代码在编译和运行的过程中,会发生许多和代码不同的行为,这包括了编译器的指令重排优化和 CPU 的乱序执行,以及 CPU 的 cacheline。这些行为在一般情况下(比如单线程程序),不会对代码的正确性造成影响,但是

cacheline

cacheline 有时会对程序正确性的影响,参考这篇对 memory barrier 的介绍文章中的例子,可能看到,多线程情况下有些时候可能会出现这样的问题:

  1. CPU 0 先设置 A = 0,再设置 B = 0
  2. CPU 1 读取 B 发现已被设置为 0,此时读取 A,A 的值却并非为 0

也就是 cacheline 可能造成变量的更新无法按照时间顺序在多个 CPU 上生效。解决这个问题就需要 memory barrier。

但是大多数情况下,硬件可以正确处理 cacheline 在多 CPU 和与主存的数据同步问题。此时主要会造成的则是性能问题,没有良好实现的代码可能会造成 cache miss 的概率高到无法接受而导致性能降低。

编译器指令重排

插入 memory barrier 可以形成一个栅栏,避免编译器对特定部分的指令重排。许多编译器都提供了特定的扩展(比如一些宏),这种 memory barrier 就不需要存在与最后生成的可执行程序中了。

CPU 乱序执行的 memory order

  • READ_ONCE 和 WRITE_ONCE 宏:使用 volatile 等关键字,让编译器和 CPU 真正意义上只读/写某个内存/寄存器一次。因为编译器和 CPU 可能做出非常花哨的优化,在某些情况下多次读取一个内存位置。在一般情况下不会造成问题,但是如果我们是对物理设备端口/设备映射的内存进行读写操作,多次读写就可能造成问题。

内存屏障(memory fence)主要用来解决 CPU 的乱序执行可能导致的内存访问问题。内存访问即 store 和 load 操作,比如 x86 架构中的 mov 指令。Linux 内核对 CPU 有几个基本的假设

  • 同一个 CPU 内的有依赖的内存访问操作是有序的 即 Q = READ_ONCE(P); D = READ_ONCE(*Q) 会且仅会发生如下的内存操作:Q = LOAD P, D = LOAD *Q

CPU 足够聪明,可以自己分析我们的代码中的依赖,并且避免乱序执行依赖的内存操作。所以对于单线程的代码,我们并不需要使用内存屏障。内存屏障主要用在多线程的同步上。想象如下的两个线程

A = 0, B = 0
thread 1.     thread 2.
A = 1         if (A == 1) {
B = 2           assert(B == 2);
              }

由于 A 和 B 之间并没有