在多核(SMP)多線程的狀況下,若是不知道CPU亂序執行的話,將會是一場噩夢,由於不管怎麼進行代碼Review也不可能發現跟內存屏障(MB)相關的Bug。內存屏障分爲兩類:html
1. NVMeDirect中的內存屏障linux
/* nvmedirect/include/lib_nvmed.h */ 38 #define COMPILER_BARRIER() asm volatile("" ::: "memory")
因爲NVMeDirect依賴於Linux內核的NVMe驅動(nvme.ko)實現,因此NVMeDirect並不須要實現它本身的與CPU相關的內存屏障。多線程
2. SPDK中的內存屏障dom
/* src/spdk-17.07.1/include/spdk/barrier.h */ 47 /** Compiler memory barrier */ 48 #define spdk_compiler_barrier() __asm volatile("" ::: "memory") 49 50 /** Write memory barrier */ 51 #define spdk_wmb() __asm volatile("sfence" ::: "memory") 52 53 /** Full read/write memory barrier */ 54 #define spdk_mb() __asm volatile("mfence" ::: "memory")
在SPDK中,不只實現了與編譯相關的內存屏障,還實現了與CPU有關的內存屏障。 可是, 在與CPU有關的MB中, 讀內存屏障(Read memory barrier)並無實現。函數
3. DPDK中的內存屏障優化
在DPDK中,內存屏障的實現要複雜一點,由於支持x86, ARM和PowerPC三種平臺。 以x86爲例,代碼實現以下:atom
/* src/dpdk-17.08/lib/librte_eal/common/include/generic/rte_atomic.h */ 132 /** 133 * Compiler barrier. 134 * 135 * Guarantees that operation reordering does not occur at compile time 136 * for operations directly before and after the barrier. 137 */ 138 #define rte_compiler_barrier() do { \ 139 asm volatile ("" : : : "memory"); \ 140 } while(0)
/* src/dpdk-17.08/lib/librte_eal/common/include/arch/x86/rte_atomic.h */ 52 #define rte_mb() _mm_mfence() 54 #define rte_wmb() _mm_sfence() 56 #define rte_rmb() _mm_lfence() 58 #define rte_smp_mb() rte_mb() 60 #define rte_smp_wmb() rte_compiler_barrier() 62 #define rte_smp_rmb() rte_compiler_barrier() 64 #define rte_io_mb() rte_mb() 66 #define rte_io_wmb() rte_compiler_barrier() 68 #define rte_io_rmb() rte_compiler_barrier()
另外,DPDK在對ARM32的MB支持中,使用了gcc的內嵌函數__sync_synchronize(), 例如:spa
/* src/dpdk-17.08/lib/librte_eal/common/include/arch/arm/rte_atomic_32.h */ 52 #define rte_mb() __sync_synchronize() 60 #define rte_wmb() do { asm volatile ("dmb st" : : : "memory"); } while (0) 68 #define rte_rmb() __sync_synchronize()
因而,讓咱們反彙編看看gcc的__sync_synchronize()究竟是怎麼回事。.net
$ cat -n foo.c 1 int main(int argc, char *argv[]) 2 { 3 int n = 0x1; 4 __sync_synchronize(); 5 return ++n; 6 } $ gcc -g -Wall -m32 -o foo foo.c $ gdb foo ...<snip>... (gdb) disas /m main Dump of assembler code for function main: 2 { 0x080483ed <+0>: push %ebp 0x080483ee <+1>: mov %esp,%ebp 0x080483f0 <+3>: sub $0x10,%esp 3 int n = 0x1; 0x080483f3 <+6>: movl $0x1,-0x4(%ebp) 4 __sync_synchronize(); 0x080483fa <+13>: lock orl $0x0,(%esp) 5 return ++n; 0x080483ff <+18>: addl $0x1,-0x4(%ebp) 0x08048403 <+22>: mov -0x4(%ebp),%eax 6 } 0x08048406 <+25>: leave 0x08048407 <+26>: ret End of assembler dump. $ gcc -g -Wall -m64 -o foo foo.c $ gdb foo ...<snip>... (gdb) disas /m main Dump of assembler code for function main: 2 { 0x00000000004004d6 <+0>: push %rbp 0x00000000004004d7 <+1>: mov %rsp,%rbp 0x00000000004004da <+4>: mov %edi,-0x14(%rbp) 0x00000000004004dd <+7>: mov %rsi,-0x20(%rbp) 3 int n = 0x1; 0x00000000004004e1 <+11>: movl $0x1,-0x4(%rbp) 4 __sync_synchronize(); 0x00000000004004e8 <+18>: mfence 5 return ++n; 0x00000000004004eb <+21>: addl $0x1,-0x4(%rbp) 0x00000000004004ef <+25>: mov -0x4(%rbp),%eax 6 } 0x00000000004004f2 <+28>: pop %rbp 0x00000000004004f3 <+29>: retq End of assembler dump.
由於沒有ARM平臺,就在x86上分別進行32位和64位的編譯,因而發現__sync_synchronize()對應的彙編指令是線程
4 __sync_synchronize(); 0x080483fa <+13>: lock orl $0x0,(%esp)
4 __sync_synchronize(); 0x00000000004004e8 <+18>: mfence
關於lock指令前綴和mfence指令,後面再講。
4. Linux內核中的內存屏障
Linux內核支持不少種平臺,這裏僅以x86爲例:
/* linux-4.11.3/arch/x86/include/asm/barrier.h */ 13 #ifdef CONFIG_X86_32 14 #define mb() asm volatile(ALTERNATIVE("lock; addl $0,0(%%esp)", "mfence", \ 15 X86_FEATURE_XMM2) ::: "memory", "cc") 16 #define rmb() asm volatile(ALTERNATIVE("lock; addl $0,0(%%esp)", "lfence", \ 17 X86_FEATURE_XMM2) ::: "memory", "cc") 18 #define wmb() asm volatile(ALTERNATIVE("lock; addl $0,0(%%esp)", "sfence", \ 19 X86_FEATURE_XMM2) ::: "memory", "cc") 20 #else 21 #define mb() asm volatile("mfence" ::: "memory") 22 #define rmb() asm volatile("lfence" ::: "memory") 23 #define wmb() asm volatile("sfence" ::: "memory") 24 #endif
5. 總結
5.1 在x86_64平臺上實現內存屏障(MB)
從NVMeDirect到SPDK, 再到DPDK和Linux內核, 咱們能夠得出在x86_64平臺上,與內存屏障(MB)有關的實現可概括爲:
#define XXX_compiler_barrier() asm volatile("" ::: "memory")
#define XXX_mb asm volatile("mfence" ::: "memory") #define XXX_rmb asm volatile("lfence" ::: "memory") #define XXX_wmb asm volatile("sfence" ::: "memory")
其中,
5.2 lock指令前綴
lock指令前綴與原子操做有關。對於Lock指令前綴的總線鎖,早期CPU芯片上有一條引線#HLOCK pin, 若是彙編語言的程序中在一條指令前面加上前綴"lock"(表示鎖總線),通過彙編之後的機器碼就使CPU在執行這條指令的時候把#HLOCK pin的電平拉低,持續到這條指令結束時放開,從而把總線鎖住,這樣同一總線上的別的CPU就暫時不能經過總線訪問內存了,保證了這條指令在多CPU環境中的原子性。
5.3 使用CPU內存屏障的根本緣由
在SMP(對稱多處理器)中,CPU是多核的,每一個核有本身的cache,讀寫內存都先經過cache。然而內存只有一個,核有多個,也就是說,同一份數據在內存中只有一份,但卻可能同時存在於多個cache line中。那麼,如何進行同步? 答案就是原子操做,注意原子操做的前提是獨佔。假如一個變量X同時存在於核1和核2的cache line中,那麼當核1想要對X進行"原子加(atomic add)"的時候必須先獨佔這個變量X,也就是告訴核2變量X的值在你的cache line已經失效了,之後想要操做X的時候到哥哥我這裏來取最新的值。這看起來很是像鎖,可是沒有用到鎖。(P.S.: 無鎖隊列的實現其實都離不開原子操做) 所以,咱們能夠這麼認爲,內存屏障(mb, wmb, rmb)的本質是用來在CPU各個核的cache line中進行通訊,保證內存數據的更新具備原子性。
擴展閱讀:
People seldom do what they believe in. They do what is convenient, then repent. | 人們不多作他們相信是對的事。他們作比較方便作的事,而後便會後悔。