在多核(SMP)多線程的情況下,如果不知道CPU亂序執行的話,將會是一場噩夢,因為無論怎么進行代碼Review也不可能發現跟內存屏障(MB)相關的Bug。內存屏障分為兩類:
- 跟編譯有關的內存屏障: 告訴編譯器,不要優化我,俺不需要
- 跟CPU有關的內存屏障: 告訴CPU, 不要亂序執行,謝謝
1. NVMeDirect中的內存屏障
/* nvmedirect/include/lib_nvmed.h */ 38 #define COMPILER_BARRIER() asm volatile("" ::: "memory")
由於NVMeDirect依賴於Linux內核的NVMe驅動(nvme.ko)實現,所以NVMeDirect並不需要實現它自己的與CPU相關的內存屏障。
2. SPDK中的內存屏障
/* 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為例,代碼實現如下:
- 與編譯相關的MB
/* 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)
- 與CPU相關的MB
/* 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(), 例如:
/* 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()到底是怎么回事。
$ 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()對應的匯編指令是
- 32位
4 __sync_synchronize(); 0x080483fa <+13>: lock orl $0x0,(%esp)
- 64位
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)有關的實現可歸納為:
- 與編譯有關的MB實現
#define XXX_compiler_barrier() asm volatile("" ::: "memory")
- 與CPU有關的MB實現
#define XXX_mb asm volatile("mfence" ::: "memory") #define XXX_rmb asm volatile("lfence" ::: "memory") #define XXX_wmb asm volatile("sfence" ::: "memory")
其中,
- volatile是C語言的關鍵字,主要目的是告訴編譯器不要做優化。 關於volatile的說明, 請參考這里。
- mfence是匯編指令,用於設定讀寫屏障(Memory)。有關mfence指令,請參考這里。
- lfence是匯編指令,用於設定讀屏障 (Load)。
- sfence也是匯編指令, 用於設定寫屏障 (Store)。
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中進行通信,保證內存數據的更新具有原子性。
擴展閱讀:
- Paper: Memory Barriers: a Hardware View for Software Hackers
- Paper: Mathematizing C++ Concurrency
- Wikipedia: https://en.wikipedia.org/wiki/Memory_barrier
- Blog: 巧奪天工的kfifo(修訂版)
- Blog: Linux 2.6內核中新的鎖機制--RCU
People seldom do what they believe in. They do what is convenient, then repent. | 人們很少做他們相信是對的事。他們做比較方便做的事,然后便會后悔。