分类: C/C++
2012-01-22 22:29:04
如果有对某一变量上写锁, 就不能在不获得相应的锁时对其进行读取操作。 也就是说, 内存栅的作用在于保证内存操作的相对顺序, 但并不保证内存操作的严格时序。 换言之, 内存栅并不保证 CPU 将本地快取缓存或存储缓冲的内容刷写回内存, 而是在锁释放时确保其所保护的数据, 对于能看到刚释放的那个锁的 CPU 或设备可见。 持有内存栅的 CPU 可以在其快取缓存或存储缓冲中将数据保持其所希望的、 任意长的时间, 但如果其它 CPU 在同一数据元上执行原子操作, 则第一个 CPU 必须保证, 其所更新的数据值, 以及内存栅所要求的任何其它操作, 对第二个 CPU 可见。
例如, 假设在一简单模型中, 认为在主存 (或某一全局快取缓存) 中的数据是可见的, 当某一 CPU 上触发原子操作时, 其它 CPU 的存储缓冲和快取缓存就必须对同一快取缓存线上的全部写操作, 以及内存栅之后的全部未完成操作进行刷写。
这样一来, 在使用由原子操作保护的内存单元时就需要特别小心。 例如, 在实现 sleep mutex 时, 我们就必须使用 atomic_cmpset 而不是 atomic_set来打开 MTX_CONTESTED 位。 这样做的原因是, 我们需要把 mtx_lock 的值读到某个变量, 并据此进行决策。 然而, 我们读到的值可能是过时的, 也可能在我们进行决策的过程中发生变化。 因此, 当执行 atomic_set 时, 最终可能会对另一值进行置位, 而不是我们进行决策的那一个。 这就必须通过atomic_cmpset 来保证只有在我们的决策依据是最新的时, 才对相应的变量进行置位。
最后, 原子操作只允许一次更新或读一个内存单元。 需要原子地更新多个单元时, 就必须使用锁来代替它了。 例如, 如果需要更新两个相互关联的计数器时, 就必须使用锁, 而不是两次单独的原子操作了。
(freebsd系统手册)
2.实现
内核中定义的内存屏障原语有:
#define barrier() __asm__ __volatile__("": : :"memory")
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif
1). smp_xxx()和xxx()的区别
为了给其它CPU也提供相关的barrier宏。 例如x86的rmb()是用了lfence指令,但其它CPU不能用这个指令。
2). 关于barrier()宏,jkl大师是这么说的:
CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在
barrier()之后刷新寄存器对变量的分配。
也就是说,barrier()宏只约束gcc编译器,不约束运行时的CPU行为。 举例:
1 int a = 5, b = 6;
2 barrier();
3 a = b;
在line 3,GCC不会用存放b的寄存器给a赋值,而是invalidate b的Cache line,重新读内存中的b值,赋值给a。
3). mb() vs. rmb() vs. wmb()
rmb()不允许读操作穿过内存屏障;wmb()不允许写操作穿过屏障;而mb()二者都不允许。
看IA32上wmb()的定义:
#ifdef CONFIG_X86_OOSTORE
#define wmb() alternative("lock;addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM);
#else
#define wmb() __asm__ __volatile__ ("": : :"memory");
#endif
Intel和AMD都没有在IA32 CPU中实现乱序写(Out-Of-Order Store),所以wmb()定义为空操作,不约束CPU行为;但
有些IA32 CPU厂商实现了OOO Store,所以就有了使用sfence的那个wmb()实现。
4). 内存屏障的体系结构语义
4.1) 只有一个主体(CPU或DMA控制器)访问内存时,无论如何也不需要barrier;但如果有两个或更多主体访问内存,且
其中有一个在观测另一个,就需要barrier了。
4.2) IA32 CPU调用有lock前缀的指令,或者如xchg这样的指令,会导致其它的CPU也触发一定的动作来同步自己的Cache。
CPU的#lock引脚链接到北桥芯片(North Bridge)的#lock引脚,当带lock前缀的执行执行时,北桥芯片会拉起#lock
电平,从而锁住总线,直到该指令执行完毕再放开。 而总线加锁会自动invalidate所有CPU对 _该指令涉及的内存_
的Cache,因此barrier就能保证所有CPU的Cache一致性。
4.3) 接着解释。
lock前缀(或cpuid、xchg等指令)使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。
IA32在每个CPU内部实现了Snoopying(BUS-Watching)技术,监视着总线上是否发生了写内存操作(由某个CPU或DMA控
制器发出的),只要发生了,就invalidate相关的Cache line。 因此,只要lock前缀导致本CPU写内存,就必将导致
所有CPU去invalidate其相关的Cache line。
两个地方可能除外:
-> 如果采用write-through策略,则根本不存在缓存一致性问题(Linux对全部内存采用write-back策略);
-> TLB也是Cache,但它的一致性(至少在IA32上)不能通过Snoopying技术解决,而是要发送
INVALIDATE_TLB_VECTOR这个IPI给其它的CPU。
4.4) 进一步解释,MESI协议
包括IA32的许多体系结构的CPU,为了保证缓存一致性,实现了MESI协议。
M: Modified,已修改
E: Exclusive,排他
S: Shared,共享
I: Invalid,无效
IA32 的CPU实现了MESI协议来保证Cache coherence。 CPU的总线监测单元,始终监视着总线上所有的内存写操作,
以便随时调整自己的Cache状态。
-> Modified。 本CPU写,则直接写到Cache,不产生总线事物;其它CPU写,则不涉及本CPU的Cache,其它CPU
读,则本CPU需要把Cache line中的数据提供给它,而不是让它去读内存。
-> Exclusive。只有本CPU有该内存的Cache,而且和内存一致。 本CPU的写操作会导致转到Modified状态。
-> Shared。 多个CPU都对该内存有Cache,而且内容一致。任何一个CPU写自己的这个Cache都必须通知其它
的CPU。
-> Invalid。 一旦Cache line进入这个状态,CPU读数据就必须发出总线事物,从内存读。
5) 考虑到DMA
5.1). Wirte through策略。 这种情形比较简单。
-> 本CPU写内存,是write through的,因此无论什么时候DMA读内存,读到的都是正确数据。
-> DMA写内存,如果DMA要写的内存被本CPU缓存了,那么必须Invalidate这个Cache line。下次CPU读它,就
直接从内存读。
5.2). Write back策略。 这种情形相当复杂。
-> DMA读内存。被本CPU总线监视单元发现,而且本地Cache中有Modified数据,本CPU就截获DMA的内存读操作,
把自己Cache Line中的数据返回给它。
-> DMA写内存。而且所写的位置在本CPU的Cache中,这又分两种情况:
a@ Cache Line状态未被CPU修改过(即cache和内存一致),那么invalidate该cache line。
b@ Cache Line状态已经被修改过,又分2种情况:
<1> DMA写操作会替换CPU Cache line所对应的整行内存数据,那么DMA写,CPU则invalidate
自己的Cache Line。
<2> DMA写操作只替换Cache Line对应的内存数据的一部分,那么CPU必须捕获DMA写操作的新
数据(即DMA想把它写入内存的),用来更新Cache Line的相关部分。
Linux 2.6内核中新的锁机制--RCU