Memory Barrier in SPDK/DPDK

멀티코어(SMP) 멀티쓰레딩의 경우 코드리뷰를 아무리 해도 Memory Barrier(MB) 관련 버그를 찾는 것이 불가능하기 때문에 CPU가 순서 없이 실행되는 것을 모르면 악몽이 될 것입니다. SPDK/DPDK에서는 MB를 어떻게 하고 있는지 살펴 봅니다.

Memory Barrier은 두 가지 범주로 나뉩니다.

  • 컴파일과 관련된 MB: 컴파일러에게 나를 최적화하지 말라고 지시합니다. 저는 필요없어요.
  • CPU 관련 MB: CPU에 순서대로 실행하지 말라고 지시합니다.

1. NVMeDirect의 Memory Barrier

NVMeDirect는 Linux 커널의 NVMe 드라이버(nvme.ko) 구현에 의존하므로 NVMeDirect는 자체 CPU 종속 MB를 구현할 필요가 없습니다.

/* nvmedirect/include/lib_nvmed.h */

38 #define COMPILER_BARRIER() asm volatile("" ::: "memory")

2. SPDK의 Memory Barrier

SPDK에서는 컴파일과 관련된 MB 뿐만 아니라 CPU와 관련된 MB도 구현되어 있다. 그러나 CPU관련 MB에서는 Read memory barrier가 구현되지 않는다.

/* 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")

3. DPDK의 Memory Barrier

DPDK에서는 x86, ARM 및 PowerPC의 세 가지 플랫폼을 지원하기 때문에 MB 구현이 조금 더 복잡합니다.

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의 Memory Barrier

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 언어의 키워드이며, 주요 목적은 컴파일러에게 최적화하지 않도록 지시하는 것입니다. 여기를 참조하십시오 .
  • mfence는 읽기 및 쓰기 Barrier(Memory)를 설정하기 위한 조립 명령입니다.
  • lfence는 읽기 Barrier(Load) 를 설정하기 위한 조립 지침입니다.
  • sfence는 쓰기 Barrier(Store)를 설정하기 위한 조립 지침입니다.

5.2 lock 명령 접두사

lock 명령 접두어는 Atomic 연산과 관련이 있습니다. CPU 칩에 리드 #HLOCK 핀이 있습니다. 어셈블리 언어 프로그램에서 명령어 앞에 “lock”(버스 잠금을 나타냄) 접두사가 붙으면, 컴파일된 머신 코드는 CPU가 이 명령어를 실행할 때 #HLOCK 핀의 레벨을 풀다운하고 이 명령어가 끝날 때까지 버스를 잠그게 합니다. 이러한 방식으로 동일한 버스의 다른 CPU는 버스를 통해 일시적으로 메모리에 액세스할 수 없으므로 다중 CPU 환경에서 이 명령의 Atomicity를 보장합니다.

5.3 CPU MB를 사용하는 근본 원인

SMP(Symmetrical Multiprocessor)에서 CPU는 멀티 코어이고 각 코어에는 자체 캐시가 있으며 읽기 및 쓰기 메모리는 먼저 캐시를 통과합니다. 그러나 메모리는 하나뿐이고 코어도 여러 개, 즉 메모리에는 동일한 데이터의 복사본이 하나만 있지만 여러 캐시 라인에 동시에 존재할 수 있습니다. 그렇다면 동기화는 어떻게 할까요? 대답은 Atomic 연산입니다.

원자성 연산의 전제는 상호배타입니다. 변수 X가 코어 1과 코어 2의 캐시 라인에 동시에 존재하는 경우, 코어 1이 X에서 “Atomic Add”를 수행하려면 먼저 변수 X를 독점해야 합니다. 즉, 캐시 라인에서 변수 X의 값이 만료되었음을 코어 2에게 알려주고, 나중에 X를 사용하고 싶을 때 최신 값을 얻어야 합니다. 이것은 lock과 매우 유사해 보이지만 lock이 사용되지 않습니다.

P.S. lock-free queue의 구현은 실제로 원자적 연산과 불가분의 관계입니다.) 따라서 Memory Barrier(mb, wmb, rmb)의 본질은 각 코어의 캐시 라인에서 통신하여 다음을 보장하는 것이라고 생각할 수 있습니다. CPU 메모리 데이터의 업데이트의 Atomicity를 보장합니다.

추가 정보