Atomic Operation

<kernel v5.0>

Atomic Operation

Atomic Operation은 공유 자원에 대해 멀티 프로세서 및 멀티 스레드가 동시에 경쟁적으로 요청 시 조작(Operation)이 한 번에 하나씩 동작하여 그 중간에 끼어 들 수 없도록 한다.

 

아래 그림은 2개의 멀티 프로세서를 사용하는 시스템에서 Atomic Operation 적용 유무에 따른 결과를 보여준다. (처음 A 값은 100)

  • Atomic Operation을 사용하지 않은 경우
    • 각 CPU에서 명령들을 순서대로 처리하지 않고 거의 동시에 실행하면 공유된 자원의 결과 값이 예측되지 않는다.
  • Atomic Operation을 사용하는 경우
    • 아키텍처마다 많은 방법이 있는데 CAS(Compare-and-Swap) 방식 처럼 hardware가 어느 한 쪽을 delay하여 동기화를 하거나 또는 LL/SC(Load-Linked and Conditional-Store)) 방식처럼 hardware의 도움을 받아 동시 접근을 인지하여 어느 한 쪽을 실패 후 원복시키고, 다시 재 시도하는 방법등으로 atomic 함수들이 올바른 결과를 발생하게 작성되어 있다.

 

아키텍처별 Atomic Operation 구현 분류

Atomic Operation은 아키텍처마다 처리하는 명령과 방법이 조금씩 다르므로 정확한 처리 방법은 해당 아키텍처 소스를 참고하길 바라며, 이 글에서는 ARM 및 ARM64를 우선하여 설명한다.

  • 시스템 메모리에 있는 공유 변수를 증/감시키기 위해서는 보통 한 개의 명령으로 처리되지 않아 LL/SC 또는 CAS 방식을 사용하여 여러 개의 명령이 순차적으로 수행되어야 하는데 이를 보장하는 방법을 Atomic Operation이라고 하며 시스템에서 사용할 수 있는 가장 작은 단위의 동기화 기법이기도 하다.
  • critical section을 보호하는 각종 lock을 사용하지 않고 atomic operation을 사용하는 이유는 dead-lock 등이 없고 동기화 기법 중 가장 빠른 성능을 가지고 있다.

 

아키텍처들이 사용하는 큰 두 가지 유형

아키텍처들의 atomic 구현 방법은 세부적으로 모두 다르지만 크게 다음 두 가지 방법을 사용한다.

  • LL/SC(Load-Linked and Conditional-Store)
    • load 표식을 한 후 연산 후 동시에 다른 프로세스나 디바이스가 이 영역에 접근 여부를 hardware가 감지하여 없는 경우에만 store한다. 접근이 감지되면 다시 성공할 때까지 반복하는 방법을 사용한다. 단점으로 ARM의 경우 동시 접근한 atomic operation의 처리 순서는 가장 마지막에 접근한 atomic operation을 먼저 성공시킨다.
    • 사용 패턴
      • 1) load from memory
      • 2) 연산(inc/dec, …)
      • 3) store to memory
      • 4) 실패 시 반복
    • 적용 아키텍처
      • ARM
      • 68k
      • Alpha
      • ARM (ARMv6 ~ ARMv8)
      • MIPS
      • POWERPC
    • 참고: Exclusive loads and store | 문c
  • CAS(Compare-and-Swap)
    • hardware lock을 잡고 add 또는 exchange 후 unlock을 하는 방식이다. 언제나 한 번에 성공하므로 반복하지 않는다.
    • 사용 패턴
      • 1) lock
      • 2) 연산(add/exchange)
      • 3) unlock
    • 적용 아키텍처
      • IA64
      • SPARC
      • x64
      • x86
      • ARM (ARMv8.1~)

 

ARM 아키텍처에서의 구현

ARM & ARM64 시스템에 구현된 방법은 다음과 같다.

  • ARMv5 이하
    • 모두 UP(Uni Processor) 시스템용으로만 구현되어 있어 인터럽트를 막는 것으로 구현
  • ARMv6, ARMv7
    • UP로 커널을 빌드하여 사용 시 인터럽트를 막는 것으로 구현
    • SMP로 커널을 빌드하여 사용 시 LL/SC 방식으로 구현
  • ARMv8
    • UP/SMP 가리지 않고 LL/SC 방식으로 구현
  • ARMv8.1~
    • UP/SMP 가리지 않고 CAS 방식으로 구현

 

1) UP 시스템에서의 구현 – 인터럽트 disable 구현 방식
  • 해당 atomic operation 수행 중 태스크의 문맥교환이 일어나거나(태스크가 preemption) 인터럽트 루틴에서 해당 메모리에 대해 동시 처리되지 않도록 아예 인터럽트를 막아서 해결한다.
  • UP에서는 인터럽트만 막아도 태스크의 문맥교환이 일어나지 않으므로  인터럽트를 enable하기 전까지 atomic opeation을 보장하게 된다.

 

2) SMP 시스템에서의 구현 – LL/SC 구현 방식
  • UP 이외에도 SMP 시스템까지 동시에 처리 될 수 있도록 LL/SC(Load-Link/Store-Conditional)라는 테크닉을 사용하였다.
  • CPU에서 atomic operation을 수행하는 도중 인터럽트 또는 preemption 되는 것을 막지 않는다.
  • 여러 CPU에서 atomic operation이 동시에 수행되는 것도 막지 않는다.
  • 공유 메모리의 접근이 서로 중복되는 경우가 발생하면 해당 atomic operation을 취소하고 중복이 발생되지 않을 때 까지 retry하는 기법으로 atomic operation이 한 번에 하나만 수행되는 것을 보장한다.
  • 해당 공유 메모리의 접근이 중복되는 것을 알아채는 것은 하드웨어의 도움을 받아 처리한다.
    • ARMv6, ARMv7에서는 ldrex와 strex 명령의 쌍으로 처리할 수 있다.
    • ARMv8에서는 ldrx와 strx 명령의 쌍으로 처리할 수 있다. 그 외에 성능 극대화를 위해 배리어의 적용을 없앨 수 있게, 4 가지 타입의 오더링 명령셋이 추가되었다.
      • ARMv8: Cortex A53, A57, A72

 

다음 그림은 LL/SC 방식에서 동시에 요청된 atomic operation들이 처리되는 과정을 보여준다.

 

3) SMP 시스템에서의 구현 – CAS 구현 방식
  • ARMv8.1 이상에서는 LL/SC와는 다른 CAS 구현 방식을 사용하는 LSE(Large System Extension) atomic 명령이 채용되었다.
    • ARM 아키텍처도 CPU가 많아지면 LL/SC의 retry가 부담이 커지므로 CAS 방식도 지원하는 것으로 변경되었다.
    • ARMv8.2: Cortex A75, A76

 

아키텍처별 헤더 파일

Atomic operation은 기본 구현 헤더와 asm-generic 헤더 및 아키텍처에 따라 전용 구현이 준비되어 있다.
  • 기본 구현 헤더
    • include/linux/atomic.h
  • 아키텍처와 연결을 도와주기 위한 헤더
    •  include/asm-generic/atomic.h
      • 아키텍처와 연결되어 포팅을 도와주기 위한 코드
      • atomic.h는 커널 2.6.31에서 추가되었다.
      • 참고: asm-generic headers, v4 | LWN.net
  • 아키텍처별 헤더
    •  arch/arm/include/asm/atomic.h
      • 32bit ARM 시스템 전용 코드로 구성
      • asm-generic 보다 아키텍처 전용 코드 사용을 우선한다.
    • arch/arm64/include/asm/atomic.h (ARMv8.x 공통)
      • LL/SC 방식을 사용하는 64bit ARM 시스템 전용 코드로 구성
      • asm-generic 보다 아키텍처 전용 코드 사용을 우선한다.
    • arch/arm64/include/asm/atomic_ll_sc.h (ARMv8)
      • LL/SC 방식을 사용하는 ARMv8 아키텍처 전용 코드로 구성
    • arch/arm64/include/asm/atomic_lse.h (ARMv8.1~)
      • CAS 방식을 사용하는 ARMv8.1~ 아키텍처 전용 코드로 구성

 

SW atomic operation vs HW atomic operation 지원

1) S/W 접근 방법 (ARMv5 까지 사용)

  • 인터럽트를 막음으로 atomic operation을 수행 중 다른 태스크가 preemption 되지 않게 한다.
  • ARMv5까지는 UP(Uni Processor) 시스템이므로 현재 CPU의 인터럽트만 막아도 atomic operation이 성립한다.
  • 참고로 SDRAM에 존재하는 한 변수를 증감하려 할 때 cpu clock은 캐시 상태(hit/miss)에 따라 수 cycle ~ 수십 cycle이 소요된다.
  • 매크로로 만들어진 아래 함수를 설명하면…
    • 현재 CPU의 인터럽트 상태를 보관 하고 disable하여 인터럽트 호출을 막는다.
    • atomic operation에 필요한 add/sub 또는 xchg 연산등을 수행한다.
    • 다시 인터럽트 상태를 원래대로 돌려놓는다.
static inline void atomic_add(int i, atomic_t *v)
{
        unsigned long flags;                
        raw_local_irq_save(flags);
        v->counter += i;
        raw_local_irq_restore(flags);
}

 

2) LL/SC – H/W 접근 방법 (ARMv6 ~ ARMv8까지 사용)

  • ldrex/strex 사용: UP에서의 멀티스레드에서의 preemption 이외에도 SMP 시스템에서는 메모리 access가 여러 개의 CPU에서 동시 처리될 수 있기 때문에 데이터에 대해 Atomic operation(load – operation – store, 읽고 연산하고 기록) 수행 시 다른 CPU가 끼어들면 실패를 감지할 수 있어야 한다. 따라서 ARM 아키텍처에서는 이를 위해 ldr과 str 대신 별도로 ldrex와 strex 명령을 사용하여 처리하게 하였다.
  • pld(pldw) 사용:
    • ldrex와 strex 사이에 처리되어야 하는 메모리가 캐시 미스되어 ldrex 부터 strex 까지의 처리에 CPU clock의 지연을 일으키므로 이를 막기 위해 사용하려는 메모리를 ldrex로 로드하기 전에 미리 캐시에 사전 로드하는 방법을 사용하기 위해 ARM 아키텍처 전용 명령인 pld(pldw) 명령을 사용하여 지정된 메모리의 데이터가 캐시에 없는 경우 이를 캐시에 미리로드(linefill)하라고 지시한다.
    • 처음 사용 시 critical section을 최소화 시키는 것과 유사한 효과를 보이므로 strex에서 실패할 확률을 줄여준다.
  • Out of order execution 및 Out of order access memory 기능을 가지고 있는 ARM 아키텍처를 위해 atomic_xxx_return() 및 atomic_cmpxchg_relaxed() 명령의 경우 smp_mb() 베리어를 추가 사용한다.
  • “Qo” memory constraints

아래는 실제 매크로 코드들을 풀어썻다.

static inline void atomic_add(int i, atomic_t *v)
{
        unsigned long tmp;
        int result;
        prefetchw(&v->counter);
        __asm__ __volatile__(
"1:     ldrex   %0, [%3]\n"
"       add     %0, %0, %4\n"
"       strex   %1, %0, [%3]\n"
"       teq     %1, #0\n"
"       bne     1b"
        : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
        : "r" (&v->counter), "Ir" (i)
        : "cc");                           /* clobbers */
}

 

3) CAS – H/W 접근 방법 (ARMv8.1 부터 사용)

아래는 실제 매크로 코드들을 풀어썻다.

static inline void atomic_add(int i, atomic_t *v)
{
        register int w0 asm ("w0") = i;
        register atomic_t *x1 asm ("x1") = v;

        asm volatile(
"       stadd %w[i], %[v]\n"
        : [i] "+r" (w0), [v] "+Q" (v->counter)
        : "r" (x1)
        : "x16", "x17", "x30");		/* clobbers */
}
  • ll/sc 방식과는 다르게 prefetch 및 반복문이 없는 것을 확인할 수 있다.
  • stadd 명령은 atomic add 명령으로 32비트의 w레지스터 또는 64비트의 x레지스터를 다룬다. 또한 메모리 오더링을 사용하지 않는다.
  • asm 명령 내부에서 x16, x17, x30 레지스터가 변경될 수 있음을 컴파일러에 명기
    • x16, x17 레지스터는 long 분기 명령이 필요할 때 컴파일러가 임의로 사용할 수 있는 IP 레지스터이다.
      • IP 레지스터 도움없이 분기 명령으로 jump할 수 있는 범위는 제한되어 있다.
    • x30 레지스터는 리턴 주소를 담는 lr 레지스터이다.
    • 참고: arm64: lse: deal with clobbered IP registers after branch via PLT

 

Atomic operation 명령

리눅스 커널이 제공하는 Atomic Operation  명령들을 알아보자. (v=32bit 카운터 값을 가진 atomic_t 구조체 주소)

 

RMW vs non-RMW

atomic API들은 크게 다음과 같이 분류할 수 있다.

  • RMW(Read-Modify-Write)
    • 종류
      • 산술연산(add, sub, inc, dec)
      • 비트조작(xor, andnot, and)
      • 교체(xchg, …)
      • 기타(add_unless, …)
    • 특징
      • 결과 값이 없는 RMW 명령들은 weaked(out-of) 오더를 가질 수 있다.
        • 예) atomic_add()
      • 결과 값이 있는 RMW 명령들은 strong(in, full) 오더를 가진다.
        • 예) atomic_add_return()
  • non-RWM
    • 종류
      • read, set
    • 특징
      • non-RMW 명령들의 특징은 weaked(out-of) 메모리 오더를 가질 수 있다.

 

1) 기본 명령

커널 v4.3-rc1부터 atomic_or() 명령이외에 atomic_xor(), atomic_and() 명령들도 추가되었다.

 

  • atomic_add(i, &v)
    • v값을 읽은 후 i만큼 증가시키고 저장한다.
      • *v += i
  • atomic_sub(i, &v)
    • v값을 읽은 후 i만큼 감소시키고 저장한다.
      • *v -= i
  • atomic_and(i, &v)
    • v값을 읽은 후 i값을 and 연산한 후 저장한다.
      • *v &= i
  • atomic_andnot(i, &v)
    • v값을 읽은 후 i값을 andnot 연산한 후 저장한다.
      • *v &= ~i
  • atomic_or(i, &v)
    • v값을 읽은 후 i값을 or 연산한 후 저장한다.
      • *v |= i
  • atomic_xor(i, &v)
    • v값을 읽은 후 i값을 xor 연산한 후 저장한다.
      • *v ^= i

 

아래 그림은 단순하게 atomic_t 변수 100에 10을 더해 110이 되어 저장하는 과정을 보여준다.

 

2) Return 추가 명령

  • atomic_add_return(i, &v)
    • v값을 읽은 후 i만큼 증가시키고 저장한다. 또한 증가시켜 저장한 값을 반환한다.
      • *v += i
  • atomic_sub_return(i, &v)
    • v값을 읽은 후 i만큼 감소시키고 저장한다. 또한 감소시켜 저장한 값을 반환한다.
      • *v -= i

 

아래 그림은 atomic_t 변수 100에 10을 더해 110이 되어 저장하는 것과, 연산 후의 값을 결과 값으로 반환하는 것을 보여준다.

 

3) Fetch 추가 명령

커널 v4.3-rc1부터 _set, _clear 명령대신 andnot 명령을 사용하게 하였고, 커널 v4.8-rc에서 삭제하였다.

 

  • atomic_fetch_add(i, &v)
    • v값을 읽은 후 i만큼 증가시키고 저장한다. 그리고 연산 전의 v 값을 반환한다.
      • *v += i
  • atomic_fetch_sub(i, &v)
    • v값을 읽은 후 i만큼 증가시키고 저장한다. 그리고 연산 전의 v 값을 반환한다.
      • *v -= i
  • atomic_fetch_and(i, &v)
    • v값을 읽은 후 i값을 and 연산한 후 저장한다. 그리고 연산 전의 v 값을 반환한다.
      • *v &= i
  • atomic_fetch_andnot(i, &v)
    • v값을 읽은 후 i값을 andnot 연산한 후 저장한다. 그리고 연산 전의 v 값을 반환한다.
      • *v &= ~i
  • atomic_fetch_or(i, &v)
  • atomic_fetch_xor(i, &v)
    • v값을 읽은 후 i값을 xor 연산한 후 저장한다. 그리고 연산 전의 v 값을 반환한다.
      • *v ^= i

 

아래 그림은 atomic_t 변수 100에 10을 더해 110이 되어 저장하는 것과, 연산 전의 값을 결과 값으로 반환하는 것을 보여준다.

 

4) Exchange 명령

  • atomic_xchg(&v, new)
    • v값을 읽은 후 new 값으로 교체한 후 다시 저장한다. 그리고 교체 전의 v 값을 반환한다.
      • *v <- new
  • atomic_cmpxchg(&v, old, new)
    • v값을 읽어 old와 같은 경우에만 new 값으로 교체한 후 다시 저장한다. 그리고 교체 전의 v 값을 반환한다.
      • *v <- new (if v == old)

 

아래 그림은 atomic_t 변수를 10으로 교체하여 저장하는 것과, 교체 전 값을 결과 값으로 반환하는 것을 보여준다.

 

아래 그림은 atomic_t 변수가 지정된 값(100)과 같은 경우에만 20으로 교체하여 저장하는 것과, 교체 전 값을 결과 값으로 반환하는 것을 보여준다.

 

  • atomic_try_cmpxchg(&v, &val, new)

 

5) read & set 명령

non-RMW 타입의 atomic 명령들은 다음과 같다.

  • atomic_read(&v)
    • v 주소 값을 읽어온다.
  • atomic_set(&v, i)
    • v 주소 값에 i를 저장한다.
      • *v = i

 

arch/arm/include/asm/atomic.h – for ARM

/*
 * On ARM, ordinary assignment (str instruction) doesn't clear the local
 * strex/ldrex monitor on some implementations. The reason we can use it for
 * atomic_set() is the clrex or dummy strex done on every exception return.
 */
#define atomic_read(v)  READ_ONCE((v)->counter)
#define atomic_set(v,i) WRITE_ONCE(((v)->counter), (i))

READ_ONCE()와 WRITE_ONCE() 매크로 함수는 컴파일러 배리어 역할을 수행한다.

  • ARM 및 ARM64 시스템에서는 atomic_read() 구현 시 1개의 instruction으로 처리가능하고, weaked(out-of) 오더로 처리해도 되므로 아키텍처 배리어는 사용할 필요없다.

 

6) 기타 명령

  • atomic_inc(&v)
    • v값을 읽은 후 1 증가시키고 저장한다.
      • *v++
  • atomic_dec(&v)
    • v값을 읽은 후 1 감소시키고 저장한다.
      • *v–
  • atomic_inc_and_test(&v)
    • v값을 읽은 후 1 증가시키고 저장한다. 또한 증가시킨 값이 0이면 true(1)를 반환하고, 그 외의 값이면 false(0)를 반환한다.
      • *v–
  • atomic_dec_and_test(&v)
    • v값을 읽은 후 1 감소시키고 저장한다. 또한 감소시킨 값이 0이면 true(1)를 반환한고, 그 외의 값이면 false(0)를 반환한다.
      • *v–
  • atomic_inc_return(&v)
    • v값을 읽은 후 1 증가시키고 저장한다. 또한 증가시켜 저장한 값을 반환한다.
      • *v++
  • atomic_dec_return(&v)
    • v값을 읽은 후 1 감소시키고 저장한다. 또한 감소시켜 저장한 값을 반환한다.
      • *v–
  • atomic_add_negative(&v)
    • v 값을 읽은 후 i만큼 증가시키고 저장한다. 또한 증가시켜 저장한 값이 음수(-)이면 true(1)를 반환한고, 그 외의 값이면 false(0)을 반환한다.
      • *v += i
  • atomic_add_unless(&v, i, u)
    • v 값을 읽은 후 u 값과 다른 경우에 한해 i 값을 더해 저장한다. 결과 값이 u 값과 다른 경우 true(1)를 반환한다.

 

아래 그림은 atomic v값이 비교 값(140)이 아닌 경우에만 20을 더하여 저장하는 것을 보여준다. 결과 값이 비교 값(140)과 다른 경우 1을 반환한다.

 

  •  atomic_inc_not_zero(&v)
    • v 값을 읽은 후 0이 아니면 1을 증가시키고 저장한다. 결과 값이 0이면 0을 반환하고, 그 외 1을 반환한다.
  • atomic_inc_not_zero_hint(&v, hint)
    • hint 값에 따라 가 다음과 같은 동작을 한다.
      • hint = 0인 경우
        • atomic_inc_not_zero()와 같이 동작하고,
      • hint가 1인 경우
        • 0이 아닌 경우 exchange
    • 네트워크 스택에서 atomic_inc_not_zero()를 많이 사용하는데, 이 함수는 read/modify/write를 단계를 거친다. 이러한 단계를 거치지 않고 atomic하게 exchange를 할 수 있는 아키텍처를 위해 hint를 제공하는 API를 추가하였다.
      • hint가 제공되는 경우 CAS 방식의 atomic operation을 지원하는 아키텍처의 경우 캐시 코히런스 프로토콜(예: x86의 MESI, arm의 MOESI)의 공유 상태 진입 없이 처리가능하다.
      • 아키텍처 specific한 함수를 include/linux/atomic.h으로 만들었는데 아직 호응은 별로 없다.
        • 현재 /net/atm/pppoatm.c에만 적용되어 있다.
      • 참고: atomic: add atomic_inc_not_zero_hint()
    • 드디어 아키텍처를 가리는 이 함수가 커널 v4.19-rc1에서 제거되었다.
  • atomic_dec_if_positive(&v)

 

arch/arm/include/asm/atomic.h – for ARM

#define atomic_xchg(v, new)      (xchg(&((v)->counter), new))
#define atomic_inc(v)            atomic_add(1, v)
#define atomic_dec(v)            atomic_sub(1, v)
#define atomic_inc_and_test(v)   (atomic_add_return(1, v) == 0)
#define atomic_dec_and_test(v)   (atomic_sub_return(1, v) == 0)
#define atomic_inc_return(v)     (atomic_add_return(1, v))
#define atomic_dec_return(v)     (atomic_sub_return(1, v))
#define atomic_add_negative(i,v) (atomic_add_return(i, v) < 0)

 

arch/arm64/include/asm/atomic.h – for ARM64

#define atomic_xchg(v, new)      xchg(&((v)->counter), (new))
#define atomic_inc(v)            atomic_add(1, (v))
#define atomic_dec(v)            atomic_sub(1, (v))
#define atomic_inc_and_test(v)   (atomic_inc_return(1, v) == 0)
#define atomic_dec_and_test(v)   (atomic_dec_return(1, v) == 0)
#define atomic_inc_return(v)     atomic_add_return(1, (v))
#define atomic_dec_return(v)     atomic_sub_return(1, (v))
#define atomic_add_negative(i,v) (atomic_add_return((i), (v)) < 0)

 

 

Barrier 포함 atomic 명령

성능 향상을 위해 대부분의 아키텍처들이 메모리에 대한 접근 순서를 요구한 순서대로 하지 않고, 때에 따라서는 변경하여 처리한다. atomic_add_return() 타입같은 경우 명령 자체가 순차 처리를 강제하게 되어 있다. 더 극대화된 성능 처리를 위해 이러한 오더링 타입을 변경하도록 atomic operation들도 이에 대응하는 명령들을 아래와 같이 지원한다.

 

relaxed, …

커널 v4.3-rc부터Atomic 오퍼레이션 동작 시 메모리 오더링에 대한 옵션을 사용할 수 있도록 3가지 접미사(_acquire, _release, _relaxed)를 가진 명령들을 포함하였다. 현재 대부분의 아키텍처들은 모든 명령들이 full 오더링 개런티로 동작되고있다. 그러나 일부 아키텍처들은 성능 향상을 위해 일부 함수에 대해 오더링 옵션들을 아래 의도대로 정확히 적용시키고 있다.

  • 무접미사
    • Atomic 오퍼레이션시 full ordering 개런티를 한다. (최저 성능)
    • 순차 처리하도록 강제한다.
      • ARMv8: dmb ish 추가 수행, stxr 대신 xtlxr 사용
  •  *_acquire
    • acquire 이후의 읽기 및 쓰기 조작 전에 이 atomic operation이 완료되는 것을 보장하게 강제한다.
      • ARMv8: ldxr 대신 ldaxr 사용
    • 사용 사례:
      • queued_read_lock() – atomic_add_return_acquire()
      • queued_write_lock() – atomic_cmpxchg_acquire()
      • queued_spin_lock() – atomic_cmpxchg_acquire()
      • osq_lock() – osq_wait_next() – atomic_cmpxchg_acquire()
  • *_release
    • release 이전의 모든 읽기 및 쓰기 작업이 완료된 후 이 atomic operation이 시작하는 것을 보장하게 강제한다.
      • ARMv8: stxr 대신 stlxr 사용
    • 사용 사례:
      • queued_read_unlock()- atomic_sub_return_release()
      • queued_write_unlock()- atomic_cmpxchg_release()
      • queued_spin_unlock() – atomic_cmpxchg_release()
      • osq_unlock() – atomic_cmpxchg_release()
  • *_relaxed
    • Atomic 오퍼레이션 시 ordering 개런티를 하지 않아도 될 때 사용한다. (최고 성능)
      • 강제하지 않기 때문에 아키텍처가 ordering을 하든 안하든 관여하지 않는다.
    • 사용 사례:
      • queued_spin_lock_slowpath() – atomic_cmpxchg_relaxed()

 

아래 그림은 atomic 함수와 배리어 관련 접미사가 붙었을 때 배리어가 동작하는 과정을 보여준다.

  • barrier cost가 높기 때문에 성능을 극대화하려면 *_relaxed를 사용하여 atomic operation 위 아래로 barrier 없이 동작할 수 있는 코드를 사용할 수 있는지 여부를 판단한 후에 적용해야 한다.

 

before | after atomic

여러 개의 atomic opeartion 전/후로 smp_mb()를 수행하는 API들을 없애고, 두 개의 API를 추가하여 사용한다.

 

  • smp_mb__before_atomic()
    • Atomic operation 전에 smp_mb()를 수행한다.
  • smp_mb__after_atomic()
    • Atomic operation 전에 smp_mb()를 수행한다.

 

read/set
  • atomic_read_acquire(&v)
    • 단방향 배리어와 함께 v 주소 값을 읽어온다.
    • 이 매크로 함수는 smp_load_acquire() 배리어 함수를 호출한다.
    • 거의 대부분의 코드들은 atomic_read_acquire() atomic 함수를 사용하지 않고 smp_load_acquire() 배리어 함수를 직접 사용한다.
    • atomic_read_acquire() 사용 사례:
      • include/linux/qrwlock.h – rspin_until_writer_unlock()
  • atomic_set_release(&v, i)
    • 단방향 배리어와 함께 v 주소 값에 i 값을 대입한다.
    • smp_store_release() 배리어 함수를 호출한다.
    • 거의 대부분의 코드들은 atomic_set_release() atomic 함수를 사용하지 않고 smp_store_release() 배리어 함수를 직접 사용한다.
    • atomic_set_release() 사용 사례:
      • kernel/jump_label.c – static_key_slow_inc_cpuslocked()

 

 

32bit용 ATOMIC_OPS() 매크로 함수

  • atomic64 접두사로 시작하는 명령을 만드는 64bit 레지스터 값을 다루는 ATOMIC64_OPS() 매크로 함수의 분석은 생략한다.

 

ARMv6 & ARMv7

add & sub 용 ATOMIC_OPS()

arch/arm/include/asm/atomic.h

#define ATOMIC_OPS(op, c_op, asm_op)                                    \
        ATOMIC_OP(op, c_op, asm_op)                                     \
        ATOMIC_OP_RETURN(op, c_op, asm_op)                              \
        ATOMIC_FETCH_OP(op, c_op, asm_op)

ATOMIC_OPS(add, +=, add)
ATOMIC_OPS(sub, -=, sub)

addsub에 대한 atomic 함수를 만드는 ATOMIC_OPS() 매크로는 다음과 같은 추가 매크로를 호출하여 각각의 명령을 생성한다.

  • 32비트 ARM 아키텍처에서는 return 및 fetch를 포함하는 atomic operation 조차 배리어가 없는 명령으로만 제공된다.
  • ATOMIC_OP()
    • atomic_add()
  • ATOMIC_OP_RETURN()
    • atomic_add_return_relaxed()
  • ATOMIC_FETCH_OP()
    • atomic_fetch_add_relaxed()

 

and, andnot, or 및 xor용 ATOMIC_OPS()

arch/arm/include/asm/atomic.h

#undef ATOMIC_OPS
#define ATOMIC_OPS(op, c_op, asm_op)                                    \
        ATOMIC_OP(op, c_op, asm_op)                                     \
        ATOMIC_FETCH_OP(op, c_op, asm_op)

ATOMIC_OPS(and, &=, and)
ATOMIC_OPS(andnot, &= ~, bic)
ATOMIC_OPS(or,  |=, orr)
ATOMIC_OPS(xor, ^=, eor)

and, andnot, or, xor에 대한 atomic 함수를 만드는 ATOMIC_OPS 매크로는 다시 재정의되어 다음과 같은 추가 매크로를 호출하여 각각의 명령을 생성한다.

  • ATOMIC_OP()
    • atomic_and()
  • ATOMIC_FETCH_OP()
    • atomic_fetch_and_relaxed()

 

ATOMIC_OP()

arch/arm/include/asm/atomic.h

/*
 * ARMv6 UP and SMP safe atomic ops.  We use load exclusive and
 * store exclusive to ensure that these are atomic.  We may loop
 * to ensure that the update happens.
 */

#define ATOMIC_OP(op, c_op, asm_op)                                     \
static inline void atomic_##op(int i, atomic_t *v)                      \
{                                                                       \
        unsigned long tmp;                                              \
        int result;                                                     \
                                                                        \
        prefetchw(&v->counter);                                         \
        __asm__ __volatile__("@ atomic_" #op "\n"                       \
"1:     ldrex   %0, [%3]\n"                                             \
"       " #asm_op "     %0, %0, %4\n"                                   \
"       strex   %1, %0, [%3]\n"                                         \
"       teq     %1, #0\n"                                               \
"       bne     1b"                                                     \
        : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)               \
        : "r" (&v->counter), "Ir" (i)                                   \
        : "cc");                                                        \
} 
#define ATOMIC_OPS(op, c_op, asm_op)                                    \
        ATOMIC_OP(op, c_op, asm_op)                                     \
        ATOMIC_OP_RETURN(op, c_op, asm_op)                              \
        ATOMIC_FETCH_OP(op, c_op, asm_op)

 

v 주소의 32비트 값을 읽은 후 i 값을 연산(add, sub, and, andnot, or 및 xor)시켜 저장한다. 이를 atomic 하게 처리한다.

  • atomic operation이 실패하여 재반복 하는 case
    • 현재 프로세스가 v 주소에 접근하는 중에 다른 프로세스가 v 라인값이 저장된 캐시 라인에 접근하는 경우 실패

 

ATOMIC_OP_RETURN()

arch/arm/include/asm/atomic.h

#define ATOMIC_OP_RETURN(op, c_op, asm_op)                              \
static inline int atomic_##op##_return(int i, atomic_t *v)              \
{                                                                       \
        unsigned long tmp;                                              \
        int result;                                                     \
                                                                        \
        smp_mb();                                                       \
        prefetchw(&v->counter);                                         \
                                                                        \
        __asm__ __volatile__("@ atomic_" #op "_return\n"                \
"1:     ldrex   %0, [%3]\n"                                             \
"       " #asm_op "     %0, %0, %4\n"                                   \
"       strex   %1, %0, [%3]\n"                                         \
"       teq     %1, #0\n"                                               \
"       bne     1b"                                                     \
        : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)               \
        : "r" (&v->counter), "Ir" (i)                                   \
        : "cc");                                                        \
                                                                        \
        smp_mb();                                                       \
                                                                        \
        return result;                                                  \
}

v 주소의 32비트값을 읽은 후 i 값을 연산(add, sub, and, andnot, or 및 xor)시킨 후 저장한다. 이를 atomic 하게 처리하는 것은 동일하지만 결과 값으로 연산시켜 저장한 값을 그대로 반환한다.

 

ATOMIC_FETCH_OP()

arch/arm/include/asm/atomic.h

#define ATOMIC_FETCH_OP(op, c_op, asm_op)                               \
static inline int atomic_fetch_##op##_relaxed(int i, atomic_t *v)       \
{                                                                       \
        unsigned long tmp;                                              \
        int result, val;                                                \
                                                                        \
        prefetchw(&v->counter);                                         \
                                                                        \
        __asm__ __volatile__("@ atomic_fetch_" #op "\n"                 \
"1:     ldrex   %0, [%4]\n"                                             \
"       " #asm_op "     %1, %0, %5\n"                                   \
"       strex   %2, %1, [%4]\n"                                         \
"       teq     %2, #0\n"                                               \
"       bne     1b"                                                     \
        : "=&r" (result), "=&r" (val), "=&r" (tmp), "+Qo" (v->counter)  \
        : "r" (&v->counter), "Ir" (i)                                   \
        : "cc");                                                        \
                                                                        \
        return result;                                                  \
}

v 주소의 32비트값을 읽은 후 i 값을 연산(add, sub, and, andnot, or 및 xor)시키고 저장한다. 이를 atomic 하게 처리하는 것은 동일하지만 결과 값으로 연산전에 fetch한 v값을 반환한다.

 

ARMv8

64bit ARMv8 아케턱처에서도 32비트에서 atomic assembly 명령인 ldrex/strex 명령이 사용하는 LL/SC(Load excLusive/Store excLusive) 방식과 동일한 방식을 사용한다. ARMv8에서는 ldxr과 stxr 명령을 사용한다.

  • ARMv8 아키텍처에서는 return 및 fetch를 포함하는 atomic operation들은 모두 4가지 타입의 배리어 관련 명령들도 제공한다.

 

add & sub 용 ATOMIC_OPS()

arch/arm64/include/asm/atomic_ll_sc.h

#define ATOMIC_OPS(...)                                                 \
        ATOMIC_OP(__VA_ARGS__)                                          \
        ATOMIC_OP_RETURN(        , dmb ish,  , l, "memory", __VA_ARGS__)\
        ATOMIC_OP_RETURN(_relaxed,        ,  ,  ,         , __VA_ARGS__)\
        ATOMIC_OP_RETURN(_acquire,        , a,  , "memory", __VA_ARGS__)\
        ATOMIC_OP_RETURN(_release,        ,  , l, "memory", __VA_ARGS__)\
        ATOMIC_FETCH_OP (        , dmb ish,  , l, "memory", __VA_ARGS__)\
        ATOMIC_FETCH_OP (_relaxed,        ,  ,  ,         , __VA_ARGS__)\
        ATOMIC_FETCH_OP (_acquire,        , a,  , "memory", __VA_ARGS__)\
        ATOMIC_FETCH_OP (_release,        ,  , l, "memory", __VA_ARGS__)

ATOMIC_OPS(add, add)
ATOMIC_OPS(sub, sub)

add 또는 sub가 있는 ATOMIC_OPS() 매크로는 다음과 같이 여러 개의 매크로를 사용하여 각각 지원하는 명령을 만들어낸다.

  • ATOMIC_OP()
    • atomic_add()
  •  ATOMIC_OP_RETURN()
    • atomic_add_return()
    • atomic_add_return_relaxed()
    • atomic_add_return_acquire()
    • atomic_add_return_release()
  • ATOMIC_FETCH_OP()
    • atomic_fetch_add()
    • atomic_fetch_add_relaxed()
    • atomic_fetch_add_acquire()
    • atomic_fetch_add_release()

 

and, andnot, or 및 xor용 ATOMIC_OPS()

arch/arm64/include/asm/atomic_ll_sc.h

#undef ATOMIC_OPS
#define ATOMIC_OPS(...)                                                 \
        ATOMIC_OP(__VA_ARGS__)                                          \
        ATOMIC_FETCH_OP (        , dmb ish,  , l, "memory", __VA_ARGS__)\
        ATOMIC_FETCH_OP (_relaxed,        ,  ,  ,         , __VA_ARGS__)\
        ATOMIC_FETCH_OP (_acquire,        , a,  , "memory", __VA_ARGS__)\
        ATOMIC_FETCH_OP (_release,        ,  , l, "memory", __VA_ARGS__)

ATOMIC_OPS(and, and)
ATOMIC_OPS(andnot, bic)
ATOMIC_OPS(or, orr)
ATOMIC_OPS(xor, eor)

and 또는 andnot, or 및 xor가 있는 ATOMIC_OPS() 매크로는 다음과 같이 여러 개의 매크로를 사용하여 각각 지원하는 명령을 만들어낸다.

  • ATOMIC_OP()
    • atomic_and()
  • ATOMIC_FETCH_OP()
    • atomic_fetch_and()
    • atomic_fetch_and_relaxed()
    • atomic_fetch_and_acquire()
    • atomic_fetch_and_release()

 

ATOMIC_OP() – for ARMv8

arch/arm64/include/asm/atomic_ll_sc.h

/*
 * AArch64 UP and SMP safe atomic ops.  We use load exclusive and
 * store exclusive to ensure that these are atomic.  We may loop
 * to ensure that the update happens.
 *
 * NOTE: these functions do *not* follow the PCS and must explicitly
 * save any clobbered registers other than x0 (regardless of return
 * value).  This is achieved through -fcall-saved-* compiler flags for
 * this file, which unfortunately don't work on a per-function basis
 * (the optimize attribute silently ignores these options).
 */
#define ATOMIC_OP(op, asm_op)                                           \
__LL_SC_INLINE void                                                     \
__LL_SC_PREFIX(atomic_##op(int i, atomic_t *v))                         \
{                                                                       \
        unsigned long tmp;                                              \
        int result;                                                     \
                                                                        \
        asm volatile("// atomic_" #op "\n"                              \
"       prfm    pstl1strm, %2\n"                                        \
"1:     ldxr    %w0, %2\n"                                              \
"       " #asm_op "     %w0, %w0, %w3\n"                                \
"       stxr    %w1, %w0, %2\n"                                         \
"       cbnz    %w1, 1b"                                                \
        : "=&r" (result), "=&r" (tmp), "+Q" (v->counter)                \
        : "Ir" (i));                                                    \
}                                                                       \
__LL_SC_EXPORT(atomic_##op);

ldxr 및 stxr 명령을 사용하여 성공리에 변경이 된 경우에만 함수를 빠져나간다.

  • prefetch destination word를 위해 prfm 명령을 사용한다.
  • ldxr/stxr 명령은 load/store exclusive register 명령이다.
    • 뒤에 w레지스터가 오는 경우 32bit general 레지스터를 처리
    • 뒤에 x레지스터가 오는 경우 64bit general 레지스터를 처리

 

ATOMIC_OP_RETURN() – for ARMv8

arch/arm64/include/asm/atomic_ll_sc.h

#define ATOMIC_OP_RETURN(name, mb, acq, rel, cl, op, asm_op)            \
__LL_SC_INLINE int                                                      \
__LL_SC_PREFIX(atomic_##op##_return##name(int i, atomic_t *v))          \
{                                                                       \
        unsigned long tmp;                                              \
        int result;                                                     \
                                                                        \
        asm volatile("// atomic_" #op "_return" #name "\n"              \
"       prfm    pstl1strm, %2\n"                                        \
"1:     ld" #acq "xr    %w0, %2\n"                                      \
"       " #asm_op "     %w0, %w0, %w3\n"                                \
"       st" #rel "xr    %w1, %w0, %2\n"                                 \
"       cbnz    %w1, 1b\n"                                              \
"       " #mb                                                           \
        : "=&r" (result), "=&r" (tmp), "+Q" (v->counter)                \
        : "Ir" (i)                                                      \
        : cl);                                                          \
                                                                        \
        return result;                                                  \
}                                                                       \
__LL_SC_EXPORT(atomic_##op##_return##name);

ldxr 및 stxr 명령을 사용하여 성공리에 변경이 된 경우에만 함수를 빠져나간다. 결과 값으로 변경 후의 값을 반환한다.

  • _acquire 접미사를 사용하는 경우 배리어 적용을 위해 “memory” 클로버를 사용하고, ldxr 대신 ldaxr 명령을 사용한다.
    • ldaxr은 load-acquire exclusive register 명령이다.
  • _release 접미사를 사용하는 경우 배리어 적용을 위해 “memory” 클로버를 사용하고, stxr 대신 stlxr 명령을 사용한다.
    • stlxr은 store-release exclusive register 명령이다.
  • 접미사가 없는 경우 위의 두 배리어를 적용한다.
  • _relaxed 접미사를 사용하는 경우 “memory” 클로버도 사용하지 않고, 어떠한 배리어도 적용하지 않는다.

 

ATOMIC_FETCH_OP() – for ARMv8

arch/arm64/include/asm/atomic_ll_sc.h

#define ATOMIC_FETCH_OP(name, mb, acq, rel, cl, op, asm_op)             \
__LL_SC_INLINE int                                                      \
__LL_SC_PREFIX(atomic_fetch_##op##name(int i, atomic_t *v))             \
{                                                                       \
        unsigned long tmp;                                              \
        int val, result;                                                \
                                                                        \
        asm volatile("// atomic_fetch_" #op #name "\n"                  \
"       prfm    pstl1strm, %3\n"                                        \
"1:     ld" #acq "xr    %w0, %3\n"                                      \
"       " #asm_op "     %w1, %w0, %w4\n"                                \
"       st" #rel "xr    %w2, %w1, %3\n"                                 \
"       cbnz    %w2, 1b\n"                                              \
"       " #mb                                                           \
        : "=&r" (result), "=&r" (val), "=&r" (tmp), "+Q" (v->counter)   \
        : "Ir" (i)                                                      \
        : cl);                                                          \
                                                                        \
        return result;                                                  \
}                                                                       \
__LL_SC_EXPORT(atomic_fetch_##op##name);

ldxr 및 stxr 명령을 사용하여 성공리에 변경이 된 경우에만 함수를 빠져나간다. 결과 값으로 변경 전 값을 반환한다.

  • 위의 return 함수들과 동일한 방식으로 4가지 타입의 배리어 명령을 지원한다.

 

ARMv8.1~

64bit ARMv8.1 아케턱처부터 CAS(Compare-and-Swap) atomic 방식으로 변경되었다.

  • CONFIG_ARM64_LSE_ATOMICS 커널 옵션
    • ARMv8.1에서는 Large System Extension을 위해 atomic 명령을 더 개선하였다. 이의 사용 유무를 제어하는 옵션이다.

 

ATOMIC_OP() – for ARMv8.1

다음 매크로를 통해 add, andnot, or 및 xor 명령을 지원한다.

  • atomic_add()

arch/arm64/include/asm/atomic_lse.h

#define ATOMIC_OP(op, asm_op)                                           \
static inline void atomic_##op(int i, atomic_t *v)                      \
{                                                                       \
        register int w0 asm ("w0") = i;                                 \
        register atomic_t *x1 asm ("x1") = v;                           \
                                                                        \
        asm volatile(ARM64_LSE_ATOMIC_INSN(__LL_SC_ATOMIC(op),          \
"       " #asm_op "     %w[i], %[v]\n")                                 \
        : [i] "+r" (w0), [v] "+Q" (v->counter)                          \
        : "r" (x1)                                                      \
        : __LL_SC_CLOBBERS);                                            \
}

ATOMIC_OP(andnot, stclr)
ATOMIC_OP(or, stset)
ATOMIC_OP(xor, steor)
ATOMIC_OP(add, stadd)
  • ARM64_LSE_ATOMIC_INSN(old, new)
    • LSE가 지원되지 않으면 old를 사용하고, 지원하는 경우 new를 사용한다.
  • __LL_SC_ATOMIC()
    • llsc 방식 atomic 명령
  • 사용하는 lse atomic 명령은 모두 메모리 오더링을 사용하지 않는다.
    • stclr
    • stset
    • steor
    • stadd
  • __LL_SC_CLOBBERS
    • x16, x17, x30 레지스터

 

ATOMIC_FETCH_OP() – for ARMv8.1

다음 매크로를 통해 add, andnot, or 및 xor 명령을 지원한다.

  • atomic_fetch_add()
  • atomic_fetch_add_relaxed()
  • atomic_fetch_add_acquire()
  • atomic_fetch_add_release()

arch/arm64/include/asm/atomic_lse.h

#define ATOMIC_FETCH_OP(name, mb, op, asm_op, cl...)                    \
static inline int atomic_fetch_##op##name(int i, atomic_t *v)           \
{                                                                       \
        register int w0 asm ("w0") = i;                                 \
        register atomic_t *x1 asm ("x1") = v;                           \
                                                                        \
        asm volatile(ARM64_LSE_ATOMIC_INSN(                             \
        /* LL/SC */                                                     \
        __LL_SC_ATOMIC(fetch_##op##name),                               \
        /* LSE atomics */                                               \
"       " #asm_op #mb " %w[i], %w[i], %[v]")                            \
        : [i] "+r" (w0), [v] "+Q" (v->counter)                          \
        : "r" (x1)                                                      \
        : __LL_SC_CLOBBERS, ##cl);                                      \
                                                                        \
        return w0;                                                      \
}

#define ATOMIC_FETCH_OPS(op, asm_op)                                    \
        ATOMIC_FETCH_OP(_relaxed,   , op, asm_op)                       \
        ATOMIC_FETCH_OP(_acquire,  a, op, asm_op, "memory")             \
        ATOMIC_FETCH_OP(_release,  l, op, asm_op, "memory")             \
        ATOMIC_FETCH_OP(        , al, op, asm_op, "memory")

ATOMIC_FETCH_OPS(andnot, ldclr)
ATOMIC_FETCH_OPS(or, ldset)
ATOMIC_FETCH_OPS(xor, ldeor)
ATOMIC_FETCH_OPS(add, ldadd)
  • 사용하는 lse atomic은 4가지 메모리 오더링 명령을 지원한다.
    • andnot
      • ldclr, ldclra, ldclrl, ldclral
    • or
      • ldset, ldseta, ldsetl, ldsetal
    • xor
      • ldeor, ldeora, ldeorl, ldeoral
    • add
      • ldadd, ldadda, ldaddl, ldaddal

 

ATOMIC_ADD_RETURN() – for ARMv8.1

다음 매크로를 통해 add 명령을 지원한다.

  • atomic_add_return()
  • atomic_add_return_relaxed()
  • atomic_add_return_acquire()
  • atomic_add_return_release()

arch/arm64/include/asm/atomic_lse.h

#define ATOMIC_OP_ADD_RETURN(name, mb, cl...)                           \
static inline int atomic_add_return##name(int i, atomic_t *v)           \
{                                                                       \
        register int w0 asm ("w0") = i;                                 \
        register atomic_t *x1 asm ("x1") = v;                           \
                                                                        \
        asm volatile(ARM64_LSE_ATOMIC_INSN(                             \
        /* LL/SC */                                                     \
        __LL_SC_ATOMIC(add_return##name)                                \
        __nops(1),                                                      \
        /* LSE atomics */                                               \
        "       ldadd" #mb "    %w[i], w30, %[v]\n"                     \
        "       add     %w[i], %w[i], w30")                             \
        : [i] "+r" (w0), [v] "+Q" (v->counter)                          \
        : "r" (x1)                                                      \
        : __LL_SC_CLOBBERS, ##cl);                                      \
                                                                        \
        return w0;                                                      \
}

ATOMIC_OP_ADD_RETURN(_relaxed,   )
ATOMIC_OP_ADD_RETURN(_acquire,  a, "memory")
ATOMIC_OP_ADD_RETURN(_release,  l, "memory")
ATOMIC_OP_ADD_RETURN(        , al, "memory")

사용하는 lse atomic은 4가지 메모리 오더링 명령을 지원한다.

  • add
    • ldadd, ldadda, ldaddl, ldaddal

 

atomic_and() – for ARMv8.1

arch/arm64/include/asm/atomic_lse.h

static inline void atomic_and(int i, atomic_t *v)
{
        register int w0 asm ("w0") = i;
        register atomic_t *x1 asm ("x1") = v;

        asm volatile(ARM64_LSE_ATOMIC_INSN(
        /* LL/SC */
        __LL_SC_ATOMIC(and)
        __nops(1),
        /* LSE atomics */
        "       mvn     %w[i], %w[i]\n"
        "       stclr   %w[i], %[v]")
        : [i] "+r" (w0), [v] "+Q" (v->counter)
        : "r" (x1)
        : __LL_SC_CLOBBERS);
}

and 연산 atomic 함수는 매크로를 사용하지 않고 별도로 작성되어 있다.

 

ATOMIC_FETCH_OP_AND() – for ARMv8.1

다음 매크로를 통해 and 명령을 지원한다.

  • atomic_fetch_and()
  • atomic_fetch_and_relaxed()
  • atomic_fetch_and_acquire()
  • atomic_fetch_and_release()

arch/arm64/include/asm/atomic_lse.h

#define ATOMIC_FETCH_OP_AND(name, mb, cl...)                            \
static inline int atomic_fetch_and##name(int i, atomic_t *v)            \
{                                                                       \
        register int w0 asm ("w0") = i;                                 \
        register atomic_t *x1 asm ("x1") = v;                           \
                                                                        \
        asm volatile(ARM64_LSE_ATOMIC_INSN(                             \
        /* LL/SC */                                                     \
        __LL_SC_ATOMIC(fetch_and##name)                                 \
        __nops(1),                                                      \
        /* LSE atomics */                                               \
        "       mvn     %w[i], %w[i]\n"                                 \
        "       ldclr" #mb "    %w[i], %w[i], %[v]")                    \
        : [i] "+r" (w0), [v] "+Q" (v->counter)                          \
        : "r" (x1)                                                      \
        : __LL_SC_CLOBBERS, ##cl);                                      \
                                                                        \
        return w0;                                                      \
}

ATOMIC_FETCH_OP_AND(_relaxed,   )
ATOMIC_FETCH_OP_AND(_acquire,  a, "memory")
ATOMIC_FETCH_OP_AND(_release,  l, "memory")
ATOMIC_FETCH_OP_AND(        , al, "memory")

 

atomic_sub() – for ARMv8.1

arch/arm64/include/asm/atomic_lse.h

static inline void atomic_sub(int i, atomic_t *v)
{
        register int w0 asm ("w0") = i;
        register atomic_t *x1 asm ("x1") = v;

        asm volatile(ARM64_LSE_ATOMIC_INSN(
        /* LL/SC */
        __LL_SC_ATOMIC(sub)
        __nops(1),
        /* LSE atomics */
        "       neg     %w[i], %w[i]\n"
        "       stadd   %w[i], %[v]")
        : [i] "+r" (w0), [v] "+Q" (v->counter)
        : "r" (x1)
        : __LL_SC_CLOBBERS);
}

sub 연산 atomic 함수는 매크로를 사용하지 않고 별도로 작성되어 있다.

 

ATOMIC_OP_SUB_RETURN() – for ARMv8.1

다음 매크로를 통해 sub 명령을 지원한다.

  • atomic_sub_return()
  • atomic_sub_return_relaxed()
  • atomic_sub_return_acquire()
  • atomic_sub_return_release()

arch/arm64/include/asm/atomic_lse.h

#define ATOMIC_OP_SUB_RETURN(name, mb, cl...)                           \
static inline int atomic_sub_return##name(int i, atomic_t *v)           \
{                                                                       \
        register int w0 asm ("w0") = i;                                 \
        register atomic_t *x1 asm ("x1") = v;                           \
                                                                        \
        asm volatile(ARM64_LSE_ATOMIC_INSN(                             \
        /* LL/SC */                                                     \
        __LL_SC_ATOMIC(sub_return##name)                                \
        __nops(2),                                                      \
        /* LSE atomics */                                               \
        "       neg     %w[i], %w[i]\n"                                 \
        "       ldadd" #mb "    %w[i], w30, %[v]\n"                     \
        "       add     %w[i], %w[i], w30")                             \
        : [i] "+r" (w0), [v] "+Q" (v->counter)                          \
        : "r" (x1)                                                      \
        : __LL_SC_CLOBBERS , ##cl);                                     \
                                                                        \
        return w0;                                                      \
}

ATOMIC_OP_SUB_RETURN(_relaxed,   )
ATOMIC_OP_SUB_RETURN(_acquire,  a, "memory")
ATOMIC_OP_SUB_RETURN(_release,  l, "memory")
ATOMIC_OP_SUB_RETURN(        , al, "memory")

사용하는 lse atomic은 add 명령을 사용하여 sub를 구현하였고, 4가지 메모리 오더링 명령을 지원한다.

  • ldadd
    • ldadd, ldadda, ldaddl, ldaddal

 

ATOMIC_FETCH_OP_SUB() – for ARMv8.1

다음 매크로를 통해 sub 명령을 지원한다.

  • atomic_fetch_sub()
  • atomic_fetch_sub_relaxed()
  • atomic_fetch_sub_acquire()
  • atomic_fetch_sub_release()

arch/arm64/include/asm/atomic_lse.h

#define ATOMIC_FETCH_OP_SUB(name, mb, cl...)                            \
static inline int atomic_fetch_sub##name(int i, atomic_t *v)            \
{                                                                       \
        register int w0 asm ("w0") = i;                                 \
        register atomic_t *x1 asm ("x1") = v;                           \
                                                                        \
        asm volatile(ARM64_LSE_ATOMIC_INSN(                             \
        /* LL/SC */                                                     \
        __LL_SC_ATOMIC(fetch_sub##name)                                 \
        __nops(1),                                                      \
        /* LSE atomics */                                               \
        "       neg     %w[i], %w[i]\n"                                 \
        "       ldadd" #mb "    %w[i], %w[i], %[v]")                    \
        : [i] "+r" (w0), [v] "+Q" (v->counter)                          \
        : "r" (x1)                                                      \
        : __LL_SC_CLOBBERS, ##cl);                                      \
                                                                        \
        return w0;                                                      \
}

ATOMIC_FETCH_OP_SUB(_relaxed,   )
ATOMIC_FETCH_OP_SUB(_acquire,  a, "memory")
ATOMIC_FETCH_OP_SUB(_release,  l, "memory")
ATOMIC_FETCH_OP_SUB(        , al, "memory")

 

 

Atomic Exchange

ARMv8 아키텍처는 추후로 미룬다.

 

atomic_xchg()

arch/arm/include/asm/atomic.h

#define atomic_xchg(v, new) (xchg(&((v)->counter), new))
  • v값을 읽은 후 new 값으로 교체한 후 다시 저장한다. 그리고 교체 전의 v 값을 반환한다.
    • *v <- new

 

xchg()

arch/arm/include/asm/cmpxchg.h

#define xchg(ptr,x) \
    ((__typeof__(*(ptr)))__xchg((unsigned long)(x),(ptr),sizeof(*(ptr))))
__xchg()
arch/arm/include/asm/cmpxchg.h
static inline unsigned long __xchg(unsigned long x, volatile void *ptr, int size)
{
        extern void __bad_xchg(volatile void *, int);
        unsigned long ret;
#ifdef swp_is_buggy
        unsigned long flags;
#endif
#if __LINUX_ARM_ARCH__ >= 6
        unsigned int tmp;
#endif

        prefetchw((const void *)ptr);

        switch (size) {
#if __LINUX_ARM_ARCH__ >= 6
#ifndef CONFIG_CPU_V6 /* MIN ARCH >= V6K */
        case 1:
                asm volatile("@ __xchg1\n"
                "1:     ldrexb  %0, [%3]\n"
                "       strexb  %1, %2, [%3]\n"
                "       teq     %1, #0\n"
                "       bne     1b"
                        : "=&r" (ret), "=&r" (tmp)
                        : "r" (x), "r" (ptr)
                        : "memory", "cc");
                break;
        case 2:
                asm volatile("@ __xchg2\n"
                "1:     ldrexh  %0, [%3]\n"
                "       strexh  %1, %2, [%3]\n"
                "       teq     %1, #0\n"
                "       bne     1b"
                        : "=&r" (ret), "=&r" (tmp)
                        : "r" (x), "r" (ptr)
                        : "memory", "cc");
                break;
#endif
        case 4:
                asm volatile("@ __xchg4\n"
                "1:     ldrex   %0, [%3]\n"
                "       strex   %1, %2, [%3]\n"
                "       teq     %1, #0\n"
                "       bne     1b"
                        : "=&r" (ret), "=&r" (tmp)
                        : "r" (x), "r" (ptr)
                        : "memory", "cc");
                break;
#elif defined(swp_is_buggy)
(...생략...)
#else
(...생략...)
#endif
        default:
                /* Cause a link-time error, the xchg() size is not supported */
                __bad_xchg(ptr, size), ret = 0;
                break;
        }

        return ret;
}

 

atomic_cmpxchg()

mutex(optimistic spin lock), futex, qrwlock 등에서 사용하는 함수이다.

아래 소스도 ARMv6 이상 SMP 시스템용 코드이다.

  • 주요 어셈블리 코드는 다음과 같다.
    • ldrex   oldval <- [&ptr->counter]
    • mov     res, #0
    • teq     oldval, old
    • strexeq res, new, [&ptr->counter]
  • ptr->counter 값이 old와 같은 경우에만 new 값을 기록한다.
  • smp_mb()
    • 동기화를 위해 결과 값이 store buffer를 통해 메모리에 기록되는데 완전히 기록이 완료될 때까지 기다리고 oldval 값을 리턴한다.

arch/arm/include/asm/atomic.h

static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new)
{
        int oldval;
        unsigned long res;

        smp_mb();
        prefetchw(&ptr->counter);

        do {
                __asm__ __volatile__("@ atomic_cmpxchg\n"
                "ldrex  %1, [%3]\n"
                "mov    %0, #0\n"
                "teq    %1, %4\n"
                "strexeq %0, %5, [%3]\n"
                    : "=&r" (res), "=&r" (oldval), "+Qo" (ptr->counter)
                    : "r" (&ptr->counter), "Ir" (old), "r" (new)
                    : "cc");
        } while (res);

        smp_mb();

        return oldval;
}

ptr값을 읽어 old와 같은 경우에만 new 값으로 교체한 후 다시 저장한다. 그리고 교체 전의 ptr 값을 반환한다.

  • *ptr <- new (if *ptr == old)

 

atomic 구조체 타입

atomic_t

include/linux/types.h

typedef struct {
        int counter;
} atomic_t;
  • 32bit counter 변수 하나로만 구성되어 있다.

 

atomic64_t

include/linux/types.h

#ifdef CONFIG_64BIT
typedef struct {
        long counter;
} atomic64_t;
#endif
  • 64bit counter 변수 하나로만 구성되어 있다.

 

참고

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다