본문 바로가기

Enginius/Linux

smp_wmb() 함수의 동작과 memory_barrier 개념

출처: http://lksas4-arm11.springnote.com/pages/899174

이백

최근 CPU들은 일련의 명령어들은 실행 순서를 변경하여, 자원을 효율적으로 사용하도록 하는 기능들을 지원한다고 합니다. 실행 순서의 변경은 모순된 결과를 초래하지 않는 범위 내에서 변경되도록 하는 것은 당연합니다. 이 경우 단순히 그 CPU 에서의 연산 결과에 모순이 없을 뿐만 아니라 다른 CPU에서 보았을 경우에도 연산 결과에 모순이 있으면 안됩니다. 실제 메모리 상의 데이터는 다른 CPU로 부터 참조나 갱신을 하기 위해 CPU간r의 동일한 데이터를 참조 할 때 그 데이터의 접근 순서를 제대로 지킬 필요가 있습니다. 이 메모리 접근 순서를 제어하기 위한 구조가 메모리 장벽-Memory Barrier 입니다. 예를 들어 메모리를 R,W,R,W,R,W 와 같은 순서로 참조되는 명령에서, CPU는 효율을 높이기 위해서, R,R,R,W,W,W 와 같이 명령을 재 배치 할 수 있는데, 이러한 Re-Ordering 를 막기 위해 Memory Barrier 를 사용합니다. 그리고, Memorry Barier 는 하드웨어적인 방법과 소프트웨어적인 방법이 있습니다. 소스를 좀 보다가 다음과 같은 코드를 보게 되었습니다.

          #if __LINUX_ARM_ARCH__ >= 7
          #define dmb() __asm__ __volatile__ ("dmb" : : : "memory")
          #elif defined(CONFIG_CPU_XSC3) || __LINUX_ARM_ARCH__ == 6
          #define dmb() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 5" \
                              : : "r" (0) : "memory")
          #else
          #define dmb() __asm__ __volatile__ ("" : : : "memory")
          #endif
          

arm V7 에는 새로운 명령 "dmb" 가 생겼나 봅니다. V6 에서는 저희가 보던 데이터 시트의 내용과 비슷한 코프로세서 명령을 사용합니다. 이는 하드웨어적인 방법인가 봅니다. 그리고 ("" : : : "memory") 은 소프트웨어적인 방법이라고 생각되는데요. 참.. 그리고 인라인 어셈블리에서 "cc" 는 condition 플래그가 변경된다는 것을 나타내는군요..


김태훈

최근에 리눅스 디바이스 드라이버쪽에 공부를 하고있는데 관련된 내용이 나오더군요. 작성된 코드의 순서를 바꾸는 주체가 컴파일러와 하드웨어 두가지가 있더군요. 드라이버에서 I/O 연산을 할때는 컴파일러 최적화와 하드웨어 재정렬을 하는것을 disable 시켜야 하는데 리눅스 커널에서 제공하는 관련된 API가 다음과 같은게 있습니다.

          #include <linux/kernel.h> 
          void barrier(void); 
          : 컴파일러 최적화를 막아줍니다. 단, 하드웨어 재정렬은 간섭하지 않습니다. 
          
          #include <asm/system.h> 
          void rmb(void); 
          void wmb(void); 
          void mb(void); 
          : 하드웨어 메모리 장벽을 삽입합니다. rmb()는 장벽이전에 read 연산을 모두 수행한 후에 뒤따라오는 read 연산을 수행하고, wmb()는 쓰기 연산에서 순서를 보장합니다. mb()는 읽고 쓰기 모두 순서를 보장합니다. 
          
          void smp_rmb(void); 
          void smp_wmb(void); 
          void smp_mb(void); 
          : SMP 버전입니다.
          

말책(리눅스 디바이스 드라이버) 9장에 있는 내용입니다. 이책에는 dmb() 함수에 대한 내용은 없네요.


이백

네.. 그렇군요.. 어느 김태훈님인지 모르겠지만, 잘 생긴 김태훈님이라 생각 됩니다. ^^

          // include/asm-x86_64/system.h 
          
          #ifdef CONFIG_SMP 
          #define smp_mb() mb() 
          #define smp_rmb() rmb() 
          #define smp_wmb() wmb() 
          #define smp_read_barrier_depends() do {} while(0) 
          #else 
          #define smp_mb() barrier() 
          #define smp_rmb() barrier() 
          #define smp_wmb() barrier() 
          #define smp_read_barrier_depends() do {} while(0) 
          #endif 
          
          /include/asm-arm/system.h 
          
          #ifndef CONFIG_SMP 
          #define mb() do { if (arch_is_coherent()) dmb(); else barrier(); } while (0) 
          #define rmb() do { if (arch_is_coherent()) dmb(); else barrier(); } while (0) 
          #define wmb() do { if (arch_is_coherent()) dmb(); else barrier(); } while (0) 
          #define smp_mb() barrier() 
          #define smp_rmb() barrier() 
          #define smp_wmb() barrier() 
          #else 
          #define mb() dmb() 
          #define rmb() dmb() 
          #define wmb() dmb() 
          #define smp_mb() dmb() 
          #define smp_rmb() dmb() 
          #define smp_wmb() dmb() 
          #endif
          

암은 다 dmb 를 호출하도록 되어 있더군요.. 참 그리고, 한가지 궁금한 점이 있는데.. 컴파일러 최적화에서 volatile 키워드를 사용하는 것이랑 barrier 사용하는 것이랑 다른점은 무엇입니까? 몇년 지나면 암은 정복된다고 하더군요. 그래서 요즘 암 보험 드는 사람이 줄어 들지요.. ^^


백창우

volatile 키워드는 volatile 키워드로 지정된 주소 공간에 대해서 read operation이 발생할때 마다 매번 주소 공간에서 read 합니다. 보통 gcc는 최적화 과정에서 어떤 변수 또는 주소 공간에 대해서 이전에 그 주소 공간의 값을 레지스터로 읽어 왔고, 프로세서가 명시적으로 해당 변수의 값을 변경하지 않았다면 다시 주소 공간에서 읽어오지 않고 레지스터에 읽어온 값을 사용하게 되어 있습니다.

예를들면 r1이 callee saved register라고 가정할때

          volatile int mem; 
          a = mem; 
          delay(); 
          a = mem; 
          a = a + 1;
          

같은 경우에는

          r1 <= *(mem) 
          call delay 
          r1 <= *(mem) 
          r1 <= r1 + 1
          

이렇게 코드가 생성되는가 반해

          int mem; 
          a = mem; 
          delay(); 
          a = mem; 
          a = a + 1;
          

같은 경우에는

          r1 <= *(mem) 
          call delay 
          r1 <= r1 + 1
          

이렇게 코드가 생성됩니다. 보면 volatile로 지정된 mem 변수는 read operation이 발생할때마다 매번 값을 읽어온 반면 volatile로 지정되지 않은 mem 변수는 최초 한번만 읽어오는것을 확인할수 있을겁니다. 메모리 operation은 다른 operation 보다 많은 사이클이 들기 때문에 이렇게 하면 보다 사이클을 줄일수 있습니다. 하지만 memory mapped된 장치의 레지스터 같은 경우 프로세서가 명시적으로 write하지 않았다 하더라도 값이 장치에 의해 변경되게 됩니다. 이럴경우 volatile 키워드를 붙여주어야만 해당 주소 공간의 값을 매번 읽어와서 의도된 대로 동작하게 할수 있습니다. barrier() 같은 경우에 다음과 같은 매크로로 define 되어 있습니다.

          #define barrier() __asm__ __volatile__("": : :"memory")
          

보면 아무른 operation도 없는 inline assembly 코드입니다. 콜론(':')으로 구분된 "memory" 표기되어 있는 세번째 필드는 clobber 필드라고해서 해당 inline assembly가 수행된후 값이 변경되는 것들을 기입합니다. gcc는 clobber 필드에 기입된 내용을 토대로 다음 코드를 생성하면서 최적화를 수행하는데 일반적으로 값이 변경된 것들에 대해서는 어떤 값으로 변경된지에 대한 정보가 없기 때문에 최적화를 수행할 수 없습니다.

clobber 필드에 "memory"라고 표기 하는것은 모든 메모리 타입 저장 장치(모든 레지스터, 모든 프래그, 모든 메모리 등)의 값이 변경됨을 의미합니다. gcc는 이럴경우 __asm__ __volatile__("": : :"memory") 경계를 넘어가는 최적화 또는 instruction scheduling을 수행하지 않습니다. 때문에 __asm__ __volatile__("": : :"memory")를 사용하면 이전 코드의 수행 완료를 보장할수 있고 이후 코드가 __asm__ __volatile__("": : :"memory") 이전에 수행되는것을 방지 할수 있습니다.


이백

네 잘 알겠습니다. 차이가 명확하게 이해가 되는군요. 저희 암 스터디가 열심히 하고 있으니, 암을 정복 할 수 있을 것입니다. 걱정하지 마세요.. ^^


백창우

이전에 적다 말았는데, 암이 정복된다니 다행입니다. 제 생활 습관 때문에 걱정하고 있었는데 다행입니다. arm11 팀도 암을 정복하신다니 다행입니다. arm11 팀이 암을 정복하는거에 대해서는 전혀 걱정하지 않았답니다. =)


박은병

감사합니다~오호호호.... 하드웨어 차원에서 instruction을 재정렬 한다는 것이 있는줄 몰랐습니다.

          #if __LINUX_ARM_ARCH__ >= 7 
          #define dmb() __asm__ __volatile__ ("dmb" : : : "memory") 
          #elif defined(CONFIG_CPU_XSC3) || __LINUX_ARM_ARCH__ == 6 
          #define dmb() __asm__ __volatile__ ("mcr p15, 0, %0, c7, c10, 5" \ 
          : : "r" (0) : "memory") 
          #else 
          #define dmb() __asm__ __volatile__ ("" : : : "memory") 
          #endif
          

이백님이 올려주신것처럼 dmb명령이나 mcr p15, 0, %0, c7, c10, 5 명령이 있는것을 보니 하드웨어적으로 명령어를 재정렬해주는 기능이 있나 보네요. 이것을 방지하는게 이런 명령인것 같고 그리고 조건부 컴파일 상 v6이전 버전에는 하드웨어적인 재정렬이 없는것으로 추측할 수 있겠네요.

그럼 대체 하드웨어적인 재정렬은 어떻게 이루어질까요 물론 하드웨어 설계관점에서 접근해야 하겠지만 컴파일러가 재정렬하는것만큼의 수준의 재정렬을 수행하기는 어려워 보이는데...명령어를 읽어서 디코딩 하는 순간에 어떤 작업이 일어날 것 같은데 궁금하네용..


백창우

          ftp://ftp.cs.wisc.edu/sohi/papers/1995/ieee-proc.superscalar.pdf
          


mm

Memory Barrier.......

음...이게 예전에 AMP 기반의 two core를 할 때 봤던 적이 있는 것 같습니다. 결론적으로 이야기 하자면, Memory Barrier라는 것이 Memory의 Protectioin을 거는 것입니다. 일단 두 개의 프로세서가 하나의 메모리 영역에 접근한다고 했을 때, 접근시에 동기화가 필요합니다. 프로세서 A에서 값을 기록했다가 프로세서 B에서 값을 읽거나 할 경우에 프로텍션을 걸어줌으로써, 해당되는 프로세서가 값을 읽지 못하도록 만들 수 있습니다.

메모리를 관리해 주는 유닛은 MPU와 MCU가 있는데, 요즘은 MCU가 일반화 되어 있지만, 과거에는 MPU를 썼다고 합니다. Kernel 부팅에 대한 어셈블리에서 도메인에 대한 부분은 간단히 봤었는데, MCU에서 Memory Protection을 걸어줄 때, 도메인을 사용합니다. 자세한 내용은 ARM System Developer's Guide를 보시면 나와 있습니다..^.-

volatile 지시어도 Memory Barrier의 일종이라고 보실 수 있습니다.


박은병

ㅋ실시간 리플..^^ 제 생각에는 Memory Barrier, 메모리 접근 동기화와 (MPU,MCU)는 셋이 좀 다른 문제인것 같습니다. 일단 Memory Barrier는 위에서 여러분들이 말씀하신 것처럼 컴파일러 최적화와 하드웨어 instruction 재정렬 관련문제를 의미한것 같구 메모리 접근 동기화와는 조금 다른 문제가 아닐까 싶은데요

          메모리 접근 동기화라고 하면 또 소프트웨어적인것과 하드웨어적인 것으로 나눌수 있을것 같습니다. 소프트웨어적인것이라하면 일반적인 lock같은 류를 들 수 있을 것 같고 하드웨어적인 것이라면 말그대로 cpu간에 동시에 메모리를 접근할 수 없으므로 이에대한 어떤 메커니즘이라고 생각합니다. 또한 arm11이상의 아키텍쳐에서는 원자적인 메모리접근을 지원하는 하드웨어적 명령이 있는듯 하네요 다른아키텍쳐들도 지원되는걸로 알고 있습니다.
          

음..아직 잘 모르겠지만 SMP환경이라면 각각의 CPU간의 메모리 접근의 허가를 허락해주는 어떤 하드웨어적인 요소(?)가 있을거라고 생각하구요..AMP환경이라면 이것이 어떻게 이루어질지 궁금하군요...잘 모르지만..NUMA구조와도 관련이 있을 것 같고 아직 현대 OS에서 해결해야할 과제라고도 어디서 주워 들은 것 같은데..

마지막으로 MPU,MCU는 MPU(Memory protection unit)과 MCU혹은MMU(virtual memory+MPU)정도로 보시면 될거 같습니다.


박은병

앗..그리구 위의 자료 감솨감솨~~^^... 저도 언젠간 언어의 장벽을 극복할 수 있는 날이 오리라 굳게 믿습니다...


김태훈

이런 유익한 게시물들은 중복이긴 하지만 위키에도 정리 하시면 좋을것 같네요. 스터디를 오래하다보면 게시판에 있는 자료는 찾기가 힘이 듭니다.(묻혀버리죠;;)


mm

Kernel Document의 memory-barriers.txt 파일 참조하세요.. :-) 궁극적으로는 이백님이 말씀하신 용도로 사용하는 것이 맞는 것 같습니다. 허나 접근하시는 방법이 너무 어려워서 잘 이해가 안 가서 조금 찾아봤습니다.... :-)