缓存一致性
顺序一致性
内存屏障
多处理器间同步
有了SMP之后,线程就开始同时在多个处理器上运行。只要是线程就有通信和同步的要求。幸好SMP系统是共享内存的,也就是所有处理器看到的内存内容都一样,虽然有独立的L1 cache,但还是由硬件完成了缓存一致性处理的问题。那不同处理器上的线程要访问同一数据,需要临界区,需要同步。靠什么同步?之前在UP系统中,我们上靠信号量,下靠关中断和读修改写指令。现在在SMP系统中,关中断已经废了,虽然为了同步同一处理器上的线程还是需要的,但只靠它已经不行了。读修改写指令?也不行了。在你指令中读操作完成写操作还没进行时,就可能有另外的处理器进行了读操作或者写操作。缓存一致性协议是先进,但还没有先进到预测这条读操作是哪种指令发出来的。所以x86又发明了带lock前缀的指令。在此指令执行时,会将所有包含指令中读写地址的cache line失效,并锁定内存总线。这样别的处理器要想对同样的地址或者同一个cache line上的地址读写,既无法从cache中进行(cache中相关line已经失效了),也无法从内存总线上进行(整个内存总线都锁了),终于达到了原子性执行的目的。当然,从P6处理器开始,如果带lock前缀指令 要访问的地址本来就在cache中,就无需锁内存总线,也能完成原子性操作了(虽然我怀疑这是因为加了多处理器内部公共的L2 cache的缘故)。
内存屏障的实现
- #ifdef CONFIG_SMP
- #define smp_mb() mb()
- #define smp_rmb() rmb()
- #define smp_wmb() wmb()
- #else
- #define smp_mb() barrier()
- #define smp_rmb() barrier()
- #define smp_wmb() barrier()
- #endif
CONFIG_SMP就是用来支持多处理器的。如果是UP(uniprocessor)系统,就会翻译成barrier()。
- #define barrier() __asm__ __volatile__("": : :"memory")
- #ifdef CONFIG_X86_32
- /*
- * Some non-Intel clones support out of order store. wmb() ceases to be a
- * nop for these.
- */
- #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 wmb() alternative("lock; addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM)
- #else
- #define mb() asm volatile("mfence":::"memory")
- #define rmb() asm volatile("lfence":::"memory")
- #define wmb() asm volatile("sfence" ::: "memory")
- #endif
可以看到,如果是64位x86,肯定有mfence、lfence和sfence三条指令,而32位的x86系统则不一定,所以需要进一步查看cpu是否支持这三条新的指令,不行则用加锁的方式来增加内存屏障。
SFENCE,LFENCE,MFENCE指令提供了高效的方式来保证读写内存的排序,这种操作发生在产生弱排序数据的程序和读取这个数据的程序之间。
SFENCE——串行化发生在SFENCE指令之前的写操作但是不影响读操作。
LFENCE——串行化发生在SFENCE指令之前的读操作但是不影响写操作。
MFENCE——串行化发生在MFENCE指令之前的读写操作。
sfence:在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence:在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence:在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。
至于带lock的内存操作,会在锁内存总线之前,就把之前的读写操作结束,功能相当于mfence,当然执行效率上要差一些。
说起来,现在写点底层代码真不容易,既要注意SMP问题,又要注意cpu乱序读写问题,还要注意cache问题,还有设备DMA问题,等等。