锁与内存屏障

larmbr | 2014-11-14 | Tags: Linux内核, 同步机制

内存屏障是Linux内核编程或编写多处理器程序, 无法避免, 需要慎之又慎对待的棘手问题。幸好, 内核中提供的各种锁原语, 在提供同步/互斥的机制的同时, 也保证了对内存屏障的正确处理。

通用锁机制

  • 锁是为了保护可能被从并发的多个程序执行流访问同一块数据对象而引入的序列化手段。 并发的多个程序执行流可能有以下几种场景:

    1. 多个CPU情况。它们并发访问同一临界区。

    2. 只有一个CPU情况。此时只有一个执行流, 它访问数据对象C。 但是, 该执行流可以被中断, 比如来了一个异步网络数据包, 我们不妨称之为中断。处理这个数据包的中断例程, 如果也访问这个数据对象, 那么这个对象也变成临界区。此时场景等价于多CPU场景, 并发访问同一临界区。

    3. 有些设有DMA能力的设备, 能绕过CPU直接访问内存。所以, 这种场景下, 也能构成CPU, 设备并发访问同一临界区的情况。

为了避免这种竟争情况, 要引入一种序列化机制, 使得并发的访问变成序列化。这种序列化机制是一种凭证, 每参与者都必须先获取该凭证, 才能执行操作; 操作完毕, 要归还凭证。形象化地称该凭证为锁。所以,

    第一, 锁具有序列化的作用。
  • 有了序列化保证并未完事。在我们以锁的作比拟的例子中, 我们有一个隐含假设: 一个拿锁进临界区的参与者, 它见到的"作案现场" 必定是上一个进入者离开时的现场。这在现实中是必然的, 也是直观的。但在系统中, 这并不是必然的。主要有以下两种原因:

    1. 由于出于性能的考量, 在超线程, 流水线等技术持续压榨CPU的同时, CPU还会在合适的情况下采取乱序执行, 也就是, 真实的指令执行顺序并不一定等同程序编写时的程序顺序
    2. 此外, CPU的访问是存在层级的, 简言之, 即先cache, 再内存。 在多CPU的体系架构中, 每个CPU都有自己局部的内存, 当然它也可以访问别的CPU的局部内存。 这意味着, CPU访问离它近的内存快, 访问远的内存慢。 所以, 这会由于cache的刷新延迟而导致访问内存一致性的问题。 也就是内存被修改了, cache还没来得及刷新, CPU就来访问了, 此时访问的是一个无效的旧值。 也就是, 执行顺序并不一定等同于观察顺序

锁的实现还必须保证解决这两个问题, 所以,

   第二, 锁具有保持访存一致性的作用。

锁与内存屏障的关系

回到Linux内核中的锁机制, 不妨举Mutex为例, 以说明上文两点是如何保证的:

1 互斥机制。Metux是互斥锁。拿到该锁者, 独占访问, 称之为ACQUIRE操作; 访问完毕, 需释放锁, 称之为RELEASE操作。这种互斥保证了序列化。

2 ACQUIRERELEASE上附加保持访存一致性的语义:

ACQUIRE: 对于所有其它参与者来说, 在此操作后的所有读写操作必然发生在ACQUIRE这个动作之后。 前面半句状语从句很重要, 它保证执行顺序等同于观察顺序 。
RELEASE: 对于所有其它参与者来说, 在此操作前的所有读写操作必然发生在RELEASE这个动作之前。 前面半句状语从句很重要, 它保证执行顺序等同于观察顺序 。

注意, 这其中任意一个操作, 都只保证了一半的顺序:

对于ACQUIRE来说, 并没保证ACQUIRE前的读写操作不会发生在ACQUIRE动作之后。
对于RELEASE来说, 并没保证RELEASE后的读写操作不会发生在RELEASE动作之前。

但是, ACQUIRERELEASE配对起来使用后, 就有了完全顺序, 成为一个屏障性的保证, 术语叫memory barrier。 如下:

    |        CPU A                      CPU B
    |-----------------------   -----------------------
  时|
    |      ACQUIRE M
  间|
    |      <临界区>
  顺|
    |      RELEASE M   <---1
  序|                      2--->      ACQUIRE M
    |
    |                                 <临界区>
    |
    |                                 RELEASE M
    ↓

上图, CPU A, CPU B先后进入由Mutex M保护的临界区。

看标1, 2这两行。这两行, 一个RELEASE, 再一个ACQUIRE。实现了一个屏障。它保证 :

12 之前的读写, 不会穿越这个屏障,

12 之后的读写, 不会穿越这个屏障。

只要看下前面的ACQUIRERELEASE的语义, 就可证明。

同时, ACQUIRERELEASE的语义中"对于所有其它参与者来说"这一要求, 又保证了执行顺序等同于观察顺序。

因此, Mutex能实现序列化的同时, ACQUIRERELEASE的语义保证, 共同实现了访存一致性。

不同平台,内存顺序模型(memory ordering model)不同。X86采用的是很强的内存序模型,叫process consistency memory ordering。 也就是,它保证执行顺序等同于观察顺序。在处理器级别已经有cache coherence保证。对于弱模型平台,则必须在ACQUIRERELEASE的中显式实现。

* * * * * * 全文完 * * * * * *

分享到 --

本文内容遵从CC版权协议,转载请注明出自larmbr.com