内存屏障(memory barrier)

内存屏障(memory barrier) (2008-08-01 11:43)
分类: linux内核


首先看看__set_current_state()和set_current_state()函数的区别
#define __set_current_state(state_value) \
do { current->state = (state_value); } while (0)

#define set_current_state(state_value) \
set_mb(current->state, (state_value))

#define set_mb(var, value) do { var = value; mb(); } while (0)
#define mb() __asm__ __volatile__ ("" : : : "memory")

1)set_mb(),mb(),barrier()函数追踪到底,就是__asm__ __volatile__("":::"memory"),而这行代码就是内存屏障。
2)__asm__用于指示编译器在此插入汇编语句
3)__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。
4)memory强制gcc编译器假设RAM所有内存单元均被汇编指令修改,这样cpu中的registers和cache中已缓存的内存单元中的数据将作废。cpu将不得不在需要的时候重新读取内存中的数据。这就阻止了cpu又将registers,cache中的数据用于去优化指令,而避免去访问内存。
5)"":::表示这是个空指令。barrier()不用在此插入一条串行化汇编指令。在后文将讨论什么叫串行化指令。
6)__asm__,__volatile__,memory在前面已经解释

linux/include/asm-i386/system.h定义:
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
7)lock前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu的一个内存屏障。
8)addl $0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock指令,在__asm__,__volatile__,memory的作用下,用作cpu的内存屏障。
9)set_task_state()带有一个memory barrier,set_task_state()肯定是安全的,但 __set_task_state()可能会快些。


关于barrier()宏实际上也是优化屏障:
#define barrier() __asm__ __volatile__("": : :"memory")
CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。      
             例1:
                1        int a = 5, b = 6;
                2        barrier();
                3        a = b;
       
            在line 3,GCC不会用存放b的寄存器给a赋值,而是重新读内存中的b值,赋值给a。
            
            
例2:
它在进程上下文中将一个元素插入一个单向链表:
new->next=i->next;
wmb();
i->next=new;
同时,如果不加锁地遍历这个单向链表。或者在遍历链表时已经可以看到new,或者new还不在该链表中。两个内存写
事件的顺序必须按照程序顺序进行。否则可能new的next指针将指向一个无效地址,就很可能出现 OOPS!

不论是gcc编译器的优化还是处理器本身采用的大量优化,如Write buffer, Lock-up free, Non- blocking reading, Register allocation, Dynamic scheduling, Multiple issues 等,都可能使得实际执行可能违反程序顺序,因此,引入内存屏障来保证事件的执行次序严格按程序顺序来执行。

使用内存屏障强加的严格的CPU内存事件次序,保证程序的执行看上去象是遵循顺序一致性模型。在当前的实现中,wmb() 实际上是一个空操作,这是因为目前Intel的CPU系列都遵循“处理机一致性”,所有的写操作是遵循程序顺序的,不会越过前面的读写操作。但是,由于 Intel CPU系列可能会在将来采用更弱的内存一致性模型并且其他体系结构可能采用其他放松的一致性模型,仍然在内核里必须适当地插入wmb()保证内存事件的正确次序。



在linux/include/asm-i386/alternative.h定义如下:
#define alternative(oldinstr, newinstr, feature) \
asm volatile ("661:\n\t" oldinstr "\n662:\n" \
".section .altinstructions,\"a\"\n" \
" .align 4\n" \
" .long 661b\n" /* label */ \
" .long 663f\n" /* new instruction */ \
" .byte %c0\n" /* feature bit */ \
" .byte 662b-661b\n" /* sourcelen */ \
" .byte 664f-663f\n" /* replacementlen */ \
".previous\n" \
".section .altinstr_replacement,\"ax\"\n" \
"663:\n\t" newinstr "\n664:\n" /* replacement */ \
".previous" :: "i" (feature) : "memory")


1.alternative()宏用于在不同的cpu上优化指令。oldinstr为旧指令,newinstr为新指令,feature为cpu特征位。
2.oldinstr的长度必须>=newinstr的长度。不够将填充空操作符。

注:这是源代码注释,具体我也看不懂

在linux/include/asm-i386/system.h定义如下:
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)

#define read_barrier_depends() do { } while(0)

#ifdef CONFIG_X86_OOSTORE
/* Actually there are no OOO store capable CPUs for now that do SSE,but make it already an possibility. */
-->OOO:Out of Order,乱序执行。
-->SSE:SSE是英特尔提出的即MMX之后新一代(当然是几年前了)CPU指令集,最早应用在PIII系列CPU上。
本小段内核注释意即:乱序存储的cpu还没有问世,故CONFIG_X86_OOSTORE也就仍未定义的,wmb()当为后面空宏(在__volatile__作用下,阻止编译器重排顺序优化)。

#define wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
#else
#define wmb() __asm__ __volatile__ ("": : :"memory")
#endif

1.lock前缀表示将后面这句汇编语句:"addl $0,0(%%esp)"作为cpu的一个内存屏障。addl $0,0(%%esp)表示将数值0加到esp寄存器中,而该寄存器指向栈顶的内存单元。加上一个0,esp寄存器的数值依然不变。即这是一条无用的汇编指令。在此利用这条无价值的汇编指令来配合lock指令,用作cpu的内存屏障。

2.mfence保证系统在后面的memory访问之前,先前的memory访问都已经结束。这是mfence是X86cpu家族中的新指令。

3.新旧指令对比:
以前的源代码:
#define mb() __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")
__asm__用于指示编译器在此插入汇编语句
__volatile__用于告诉编译器,严禁将此处的汇编语句与其它的语句重组合优化。即:原原本本按原来的样子处理这这里的汇编。

现在的源代码:
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
两者比较:
比起以前的源代码来少了__asm__和__volatile__。增加了alternative()宏和mfence指令。


而SFENCE指令(在Pentium III中引入)和LFENCE,MFENCE指令(在Pentium 4和Intel Xeon处理器中引入)提供了某些特殊类型内存操作的排序和串行化功能。sfence,lfence,mfence指令是在后继的cpu中新出现的的指令。

SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。
SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。
LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。
MFENCE——串行化发生在MFENCE指令之前的读写操作。
注意:SFENCE,LFENCE,MFENCE指令提供了比CPUID指令更灵活有效的控制内存排序的方式。

sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

其实这里是用mfence新指令来替换老的指令串:__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")。
mfence的执行效果就等效于 __asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")的执行效果。只不过,__asm__ __volatile__ ("lock; addl $0,0(%%esp)": : :"memory")是在以前的cpu平台上所设计的,借助于编译器__asm__,__volatile__,lock这些指令来实现内存屏障。而在 Pentium 4和Intel Xeon 处理器中由于已经引入了mfence指令,无须再用这一套指令,直接调用这一条指令即ok。而alternative()宏就是用于这个优化指令的替换,用新的指令来替换老的指令串。


这些函数在已编译的指令流中插入硬件内存屏障;具体的插入方法是平台相关的。rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行之前完成。wmb 保证写操作不会乱序,mb 指令保证了两者都不会。这些函数都是 barrier函数的超集。
内存屏障出现因为编译器或现在的处理器常会自作聪明地对指令序列进行一些处理,比如数据缓存,读写指令乱序执行等等。如果优化对象是普通内存,那么一般会提升性能而且不会产生逻辑错误。但如果对 I/O操作进行类似优化很可能造成致命错误。所以要使用内存屏障,以强制该语句前后的指令以正确的次序完成。其实在指令序列中放一个wmb的效果是使得指令执行到该处时,把所有缓存的数据写到该写的地方,同时使得wmb前面的写指令一定会在wmb的写指令之前执行。

posted @ 2012-03-29 13:10  Lunaa  阅读(1294)  评论(0编辑  收藏  举报