内存屏障相关--barrier(),mb(),smp_mb(),乱序,内存一致性

转自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=14528823&id=4337007

  1.         内核中定义的内存屏障原语有:
  2.                 
  3.                 #define barrier() __asm__ __volatile__("": : :"memory")
  4.                 #define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
  5.                 #define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)

  6.         #ifdef CONFIG_SMP
  7.                 #define smp_mb()        mb()
  8.                 #define smp_rmb()        rmb()
  9.                 #define smp_wmb()        wmb()
  10.                 #define smp_read_barrier_depends()        read_barrier_depends()
  11.                 #define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
  12.         #else
  13.                 #define smp_mb()        barrier()
  14.                 #define smp_rmb()        barrier()
  15.                 #define smp_wmb()        barrier()
  16.                 #define smp_read_barrier_depends()        do { } while(0)
  17.                 #define set_mb(var, value) do { var = value; barrier(); } while (0)
  18.         #endif


  19.         1). smp_xxx()和xxx()的区别
  20.                 
  21.                 为了给其它CPU也提供相关的barrier宏。 例如x86的rmb()是用了lfence指令,但其它CPU不能用这个指令。


  22.         2). 关于barrier()宏,jkl大师是这么说的:

  23.                 CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在
  24.                 barrier()之后刷新寄存器对变量的分配。
  25.         
  26.             也就是说,barrier()宏只约束gcc编译器,不约束运行时的CPU行为。 举例:
  27.                 
  28.                 1        int a = 5, b = 6;
  29.                 2        barrier();
  30.                 3        a = b;
  31.         
  32.             在line 3,GCC不会用存放b的寄存器给a赋值,而是invalidate b的Cache line,重新读内存中的b值,赋值给a。


  33.         3). mb() vs. rmb() vs. wmb()

  34.             rmb()不允许读操作穿过内存屏障;wmb()不允许写操作穿过屏障;而mb()二者都不允许。

  35.             看IA32上wmb()的定义:
  36.             #ifdef CONFIG_X86_OOSTORE
  37.                     #define wmb() alternative("lock;addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM);
  38.             #else
  39.                     #define wmb() __asm__ __volatile__ ("": : :"memory");
  40.             #endif

  41.             Intel和AMD都没有在IA32 CPU中实现乱续写(Out-Of-Order Store),所以wmb()定义为空操作,不约束CPU行为;但
  42.             有些IA32 CPU厂商实现了OOO Store,所以就有了使用sfence的那个wmb()实现。
  43.         

  44.         4). 内存屏障的体系结构语义
  45.                 
  46.            4.1) 只有一个主体(CPU或DMA控制器)访问内存时,无论如何也不需要barrier;但如果有两个或更多主体访问内存,且
  47.                 其中有一个在观测另一个,就需要barrier了。

  48.            4.2)        IA32 CPU调用有lock前缀的指令,或者如xchg这样的指令,会导致其它的CPU也触发一定的动作来同步自己的Cache。
  49.                 CPU的#lock引脚链接到北桥芯片(North Bridge)的#lock引脚,当带lock前缀的执行执行时,北桥芯片会拉起#lock
  50.                 电平,从而锁住总线,直到该指令执行完毕再放开。  而总线加锁会自动invalidate所有CPU对 _该指令设计的内存_
  51.                 的Cache,因此barrier就能保证所有CPU的Cache一致性。

  52.            4.3) 接着解释。
  53.                 lock前缀(或cpuid、xchg等指令)使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。
  54.                 IA32在每个CPU内部实现了Snoopying(BUS-Watching)技术,监视着总线上是否发生了写内存操作(由某个CPU或DMA控
  55.                 制器发出的),只要发生了,就invalidate相关的Cache line。 因此,只要lock前缀导致本CPU写内存,就必将导致
  56.                 所有CPU去invalidate其相关的Cache line。

  57.                 两个地方可能除外:
  58.                         -> 如果采用write-through策略,则根本不存在缓存一致性问题(Linux对全部内存采用write-back策略);
  59.                         -> TLB也是Cache,但它的一致性(至少在IA32上)不能通过Snoopying技术解决,而是要发送
  60.                            INVALIDATE_TLB_VECTOR这个IPI给其它的CPU。
  61.         
  62.           4.4) 进一步解释,MESI协议
  63.                
  64.                M:        Modified,已修改
  65.                E:        Exclusive,排他
  66.                S:        Shared,共享
  67.                I:        Invalid,无效

  68.                IA32 的CPU实现了MESI协议来保证Cache coherence。 CPU的总线监测单元,始终监视着总线上所有的内存写操作,
  69.                以便随时调整自己的Cache状态。

  70.                     -> Modified。 本CPU写,则直接写到Cache,不产生总线事物;其它CPU写,则不涉及本CPU的Cache,其它CPU
  71.                                   读,则本CPU需要把Cache line中的数据提供给它,而不是让它去读内存。
  72.                     
  73.                     -> Exclusive。只有本CPU有该内存的Cache,而且和内存一致。 本CPU的写操作会导致转到Modified状态。

  74.                     -> Shared。   多个CPU都对该内存有Cache,而且内容一致。任何一个CPU写自己的这个Cache都必须通知其它
  75.                                   的CPU。

  76.                     -> Invalid。  一旦Cache line进入这个状态,CPU读数据就必须发出总线事物,从内存读。


  77.          5) 考虑到DMA
  78.                  
  79.                 5.1). Wirte through策略。 这种情形比较简单。

  80.                       -> 本CPU写内存,是write through的,因此无论什么时候DMA读内存,读到的都是正确数据。
  81.                       -> DMA写内存,如果DMA要写的内存被本CPU缓存了,那么必须Invalidate这个Cache line。下次CPU读它,就
  82.                          直接从内存读。
  83.                 
  84.                 5.2). Write back策略。 这种情形相当复杂。
  85.                  
  86.                       -> DMA读内存。被本CPU总线监视单元发现,而且本地Cache中有Modified数据,本CPU就截获DMA的内存读操作,
  87.                          把自己Cache Line中的数据返回给它。

  88.                       -> DMA写内存。而且所写的位置在本CPU的Cache中,这又分两种情况:
  89.                                       a@ Cache Line状态未被CPU修改过(即cache和内存一致),那么invalidate该cache line。
  90.                                  b@ Cache Line状态已经被修改过,又分2种情况:
  91.                                         
  92.                                         <1> DMA写操作会替换CPU Cache line所对应的整行内存数据,那么DMA写,CPU则invalidate
  93.                                             自己的Cache Line。
  94.                                         <2> DMA写操作只替换Cache Line对应的内存数据的一部分,那么CPU必须捕获DMA写操作的新
  95.                                             数据(即DMA想把它写入内存的),用来更新Cache Line的相关部分。
---------------
现在常说的SMP是共享总线结构,同时共享memory, memory可能是内存,也可能是cache。 

对于单CPU而言,CPU cache中的内容和memory中的内容的同步要注意:
1. 虽然CPU可能会更改执行顺序,但CPU更改后的指令在UP环境中是正确的。
2. CPU中的cache和memory的同步只需要考虑 DMA和CPU同时对memory访问导致的同步问题,这种问题要在编写驱动的时候使用合适的指令和mb来保证。也就是说,在UP中,只需要考虑cpu和dma的同步问题。

在SMP中,需要考虑CPU之间,CPU和dma之间的同步问题。

上面我们讨论的都是CPU之间的同步问题。
SMP是一个共享总线的结构,一般来说,存在两层总线, host总线和PCI总线或者其它IO总线。
host总线连接多个CPU和内存,host/PCI桥 (就是通常说的北桥)
PCI总线连接host/PCI桥和 PCI主从设备,及PCI/Isa桥。 就是通常说的南桥。

由此可见,PCI设备要将自己的register map到内存中,需要通过host/pci bridge, 要靠host/pci bridge访问host总线,然后到达内存。内存的映射和访问这些工作由bridge+dma完成。

而多个CPU要访问内存,也要通过host总线。

由上可见, 一个CPU或者DMA要访问内存,必须锁总线,总线是共享的。同样为了使得内存的修改能被其它设备知晓,必须用signal通知机制,某个设备修改了内 存,必须有监听总线的机制,然后通过某个signal通知到设备,如dma访问内存的时候,cpu监控总线, 用HIT和HITM通知cpu修改的内容命令cache, 所以相关cache要invalidate,一般是64bit。这个过程是一级一级cache往上走的过程。

为了防止dma中的数据cache在CPU中,大家一般采用申明为volatile的方法,这种方法会导致效率不高,CPU每次必须lock 总线,访问内存才能获得相应的内容。

上面介绍的都是硬件相关的东西。

软件上,代码执行顺序的更改可能被编译器和CPU更改。
为了保证访问内存代码按照指定顺序执行,必须使用smp_*mb*()宏。


在单CPU中,smp_*mb*()只是一个compiler barrier,仅仅是防止编译器错误地优化访问内存代码:
#define barrier() __asm__ __volatile__("": : :"memory")
volatile告诉编译器,这段代码不能忽略, "memory" 是编译器的clobber,告诉编译器,
1. 内存信息已经修改,在这条指令后面的寄存器的值必须从内存中重新获取
2. 代码的先后顺序必须按照原有的产生汇编代码

在SMP中,smp_*mb*()是一个hardware barrier和compiler barrier的组合
#define smp_mb()        mb()
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
alternative()用来保持CPU指令兼容,在P4以前的CPU没有mfence指令,用lock; addl $0,0(%%esp)指令。alternative()包含"memory" clobber,所以包含compiler barrier功能。
lock的作用是发出lock信号,占用host总线,同时其它的CPU会监听总线。invalidate cache中的相应内容,一般每项64bit。

在哪些情况下需要使用memory barrier,参考:
http://www.linuxjournal.com/article/8211
http://www.linuxjournal.com/article/8212
和附件的文档


关于I/O DMA和CPU的memory barrier问题,欢迎大家继续讨论。

还有一个问题,为什么Linux发行版本中的glibc库没有分UP和SMP版本? 正常来说,已经编译成二进制的系统库应该UP和SMP不兼容啊,因为锁的实现等都要靠mb.

-------------
在SMP系统上,出现内存乱序的根本原因可能有以下几个:

1. 现代CPU并行执行指令,导致了内存的写入或者读入顺序的不可确定性。

2. 各个CPU内部的数据指令缓冲及各个CPU Cache之间的一致性问题。

因此前面提到的几条原则可以这么理解:

  1. A given CPU always perceives its own memory operations as occurring in program order. That is, memory-reordering issues arise only when a CPU is observing other CPUs' memory operations.
  
  单处理器系统出现的乱序CPU自己可以解决,只有SMP的系统上才会要求内核程序员考虑处理内存乱序。原因就是上面的两点。

  2. An operation is reordered with a store only if the operation accesses a different location than does the store.

  如果乱序的指令包含了store,那么必然其它操作访问的内存单元与这个store访问的内存单元无关。

  3. Aligned simple loads and stores are atomic.

  对已经对齐的数据进行简单的load和store操作是原子的。意味着非对齐的数据的load或者store可能会对其它CPU而言,存在乱序可能。

  4. Linux-kernel synchronization primitives contain any needed memory barriers, which is a good reason to use these primitives.

  任何操作系统的同步原语中都包含了必须的memory barriers指令,前面我给的Solaris也不例外。
----------
在CPU硬件上,Memory 和 cache 的一致性,cache的一致性靠CPU自身的硬件机制保证,我们只需要使用一些指令,如lock和mfence等。
lock指令完成的事情其实是产生一个lock signal,至于这个操作是不是拉高到北桥的引脚,没有查到。lock指令在P6后的CPU有一些优化,可能不会锁host总线。

如果要锁host总线,如访问内存,在两个CPU的系统中,MRM占用总线, LRM用PHIT和PHITM来监听是否命中本CPU中的cache,命中通知MRM,invalidate相关cache. 
mfence指令是P4才有的,开销比lock小

这就是为什么smp中,smp_mb() == mb(), UP中 smp_mb() == barrier()。 barrier()只是compiler barrier,gcc 将barrier后的寄存器访问改成内存访问,以保证一致性。
-----
关于snoopying和SMP上的缓存一致性:

IA32的每个CPU都要实现MESI协议(M:Modified;E:Exclusive;S:Shared;I:Invalid)

CPU的总线监测单元始终监视着总线上所有的内存写操作, 以便调整自己的Cache状态。
-------
这要看你站在什么视角审视内存屏障:
如果仅仅从内核程序员视角触发,不需要上升到Memory Consistency相关理论以及协议的高度,只要了解处理器可能进行的乱序操作就可以编写出正确的代码:
以IA32/Intel64为例子,最近Intel终于发布了自己体系结构的Memory Ordering白皮书,之前Intel并未明确自己处理器的的Memory Consistency Model。
这个白皮书可以从Intel的网站下载,其中明确的将自己的体系结构定义为TLO+CC(Total Lock Ordering + Casual Consistency),
即完全锁按序+因果一致性,其主要特点如下:
1. Loads are not reordered with other loads.
2. Stores are not reordered with other stores.
3. Stores are not reordered with older loads.
4. Loads may be reordered with older stores to different locations but not with older
stores to the same location.

5. In a multiprocessor system, memory ordering obeys causality (memory ordering
respects transitive visibility). (因果一致性)
6. In a multiprocessor system, stores to the same location have a total order.(符合coherence语义)
7. In a multiprocessor system, locked instructions have a total order.(与8一起构成TLO)
8. Loads and stores are not reordered with locked instructions.

要注意, TLO+CC仅仅针对WB类型的,对于WC/WC+类型,一致性由具体实现决定,一般较WB类型要弱.
详细可以参考Intel手册关于MTRR/PAT/Cache Control/TLB的描述.

如果想要了解详细的内容大家可以下载该白皮书以及看下面这个Intel工程师的视频讲解:
http://www.youtube.com/watch?v=WUfvvFD5tAA
如果要从系统结构审视这个问题,那就如你所说:相当复杂的问题,关系到SMP中最核心的内存系统:
仍以IA32/Intel64为例子,如果想深入研究IA32内存屏障指令的实现 (MFENCE/SFENCE),大家可以参考Intel相关的专利:
http://www.freepatentsonline.com/6651151.html
如果想大概了解一下理论,我也推荐一个tutorial:
<<Shared Memory Consistency Model: A Turtoial>>
如果想研究理论,那就是学海无涯苦做舟了。
其中由不少中国人写的论文:Kai Li关于SVM的论文,开启了DSM时代,另外我们龙芯的胡总设计师也发表过多篇相关领域的论文。
------------
UP情况下
乱序,并非是全乱执行,它只是对于没有依赖性的指令乱序执行。
在我上面举的这个例子中,a=i就不会在i++之前执行,因为两条指令之间有依赖,称为WAW依赖(write after write )。同样,还有RAW、WAR依赖。

所以preempt_disable中对抢占计数器加是个安全的操作,和这个计数器有关联的指令不会被乱序执行,只需要防止编译器把相关指令提前即可,用barrier足够。

那么什么时候要防止乱序呢?通常在一个块内存,既对CPU可见,又对设备可见时。举个例子:
一个结构体
  1. struct dev
  2. {
  3.     int enable;
  4.     void *ptr;
  5. }dev;
复制代码

这个结构体所处的内存,设备和CPU都可以看到。正确操作设备的顺序是先给ptr指针赋值,在对enable写1启用设备。那么,下面的代码反应了这个过程:
  1. dev.ptr = buffer;
  2. dev.enable = 1;
复制代码

这里的两个写操作是没有相关性的。所以CPU可以乱序执行它们。这就造成了一个情况,ptr还没赋值之前,enable就已经写1了。那么设备可能在ptr为非法值时启动执行。我们要防止这种情况,就要用内存屏障。如下:
  1. dev.ptr = buffer;
  2. wmb();
  3. dev.enable = 1;
-----------
内存屏障解决的是load/store的乱序问题,不是cache一致性问题,cache一致性问题由cache snooping协议保证,intel x86的规范是MESI
-----------

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页