요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
API 분류
atomic_t.txt:8-57아래 목록에서는 간결하게 쓰기 위해 atomic64_와 atomic_long_ 접두사를 생략한다. 실제 API에는 자료형에 맞는 접두사가 붙는다.
Non-RMW operation
atomic_read(), atomic_set()
atomic_read_acquire(), atomic_set_release()
값을 읽거나 쓰기만 한다. read-modify-write가 아니며 보통 READ_ONCE(), WRITE_ONCE(), acquire load, release store로 구현된다.
산술 RMW operation
atomic_{add,sub,inc,dec}()
atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}()
atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}()
return 계열은 연산 뒤의 수정된 값을 반환하고, fetch 계열은 연산 전의 값을 반환한다. add, sub, inc, dec는 역연산이 가능하므로 두 형태를 모두 제공한다.
Bitwise RMW operation
atomic_{and,or,xor,andnot}()
atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}()
bitwise operation은 수정된 값에서 원래 값을 역산할 수 없으므로 modified value를 돌려주는 *_return 계열은 제공하지 않는다. 원래 값이 필요하면 atomic_fetch_*를 사용한다.
Swap과 compare-and-swap
atomic_xchg{,_relaxed,_acquire,_release}()
atomic_cmpxchg{,_relaxed,_acquire,_release}()
atomic_try_cmpxchg{,_relaxed,_acquire,_release}()
Reference count 성격의 operation
atomic_add_unless(), atomic_inc_not_zero()
atomic_sub_and_test(), atomic_dec_and_test()
객체 lifetime을 관리하는 reference count라면 overflow와 use-after-free 방어가 들어간 refcount_t를 먼저 검토해야 한다.
그 밖의 조건부 operation과 barrier
atomic_inc_and_test(), atomic_add_negative()
atomic_dec_unless_positive(), atomic_inc_unless_negative()
smp_mb__before_atomic()
smp_mb__after_atomic()
Signed type과 overflow
atomic_t.txt:59-75atomic_t, atomic_long_t, atomic64_t의 내부 값은 각각 int, long, s64다. 역사적인 이유로 signed type을 사용하지만, 커널은 -fno-strict-overflow를 사용하며 이는 -fwrapv를 포함한다.
따라서 signed overflow는 2의 보수 wraparound로 정의된다. unsigned 전용 atomic API를 별도로 둘 필요가 없고, 필요한 경우 cast해도 C의 undefined behavior가 되지 않는다.
GCC 8 이전 UBSAN에는 signed type에서 잘못된 undefined-behavior warning을 만들던 문제가 있었다. 현재 규칙은 C/C++ _Atomic의 동작과 P1236R1 같은 정리 방향에도 부합한다.
Non-RMW operation의 의미
atomic_t.txt:77-94Non-RMW operation은 일반적으로 regular load와 store다. atomic_read(), atomic_set(), atomic_read_acquire(), atomic_set_release()는 각각 READ_ONCE(), WRITE_ONCE(), smp_load_acquire(), smp_store_release()로 구현되는 것이 정석이다.
atomic_t를 사용하면서 non-RMW API만 호출한다면 atomic_t가 필요하지 않다. 그 경우에는 READ_ONCE()/WRITE_ONCE()와 필요한 ordering을 명시하는 편이 자료구조의 실제 동시성 규칙을 더 정확히 드러낸다.
atomic_set()이 RMW atomicity를 깨뜨리면 안 되는 이유
atomic_t.txt:89-130atomic_set()은 단순 store처럼 보이지만 같은 객체에 대한 atomic RMW와 경쟁할 수 있다. 이때 RMW가 중간 상태를 만들어 내도록 구현해서는 안 된다.
C Atomic-RMW-ops-are-atomic-WRT-atomic_set
{
atomic_t v = ATOMIC_INIT(1);
}
P0(atomic_t *v)
{
(void)atomic_add_unless(v, 1, 0);
}
P1(atomic_t *v)
{
atomic_set(v, 0);
}
exists (v=2)
CPU 1의 atomic_set(v, 0)이 먼저 끝나면 CPU 0의 atomic_add_unless()는 조건이 맞지 않아 아무 일도 하지 않는다. atomic_add_unless()가 먼저 끝나면 CPU 1의 store가 그 결과를 0으로 덮는다. 어느 순서에서도 최종값 2는 나올 수 없다.
일반적인 architecture에서는 경쟁하는 regular store가 LL/SC reservation을 무효화하거나 CMPXCHG를 실패시키므로 이 조건을 만족한다.
문제는 atomic RMW를 lock으로 흉내 내는 구현이다. RMW가 lock 안에서 1을 읽은 뒤, 다른 CPU의 atomic_set()이 0을 쓰고, 첫 CPU가 오래된 값 1에 1을 더해 2를 쓰면 금지된 결과가 생긴다. 이런 구현에서는 atomic_set()도 atomic_xchg()로 구현해 같은 직렬화 규칙에 참여시켜야 한다.
RMW operation의 반환 형태
atomic_t.txt:132-158| 형태 | 반환값과 용도 |
|---|---|
| atomic_*() | 반환값 없이 대상 값만 변경 |
| atomic_*_return() | 변경된 뒤의 값을 반환 |
| atomic_fetch_*() | 변경되기 전의 값을 반환 |
| xchg/cmpxchg/try_cmpxchg | 교환 또는 조건부 교환 |
| special-purpose operation | 일반적인 cmpxchg loop를 architecture가 더 효율적으로 구현 |
이 operation들은 모두 SMP atomic이다. 하나의 atomic variable에 대한 operation에는 전체 순서를 세울 수 있고, 중간 상태가 사라지거나 다른 CPU에 노출되지 않는다.
Memory ordering 기본 규칙
atomic_t.txt:160-188이 절을 읽기 전에 Documentation/memory-barriers.txt의 load/store ordering, ACQUIRE, RELEASE, full barrier 개념을 먼저 확인하는 것이 좋다.
| Operation | 기본 ordering |
|---|---|
| non-RMW | unordered |
| 반환값이 없는 RMW | unordered |
| 반환값이 있는 RMW | fully ordered |
| 조건부 RMW가 실패한 경우 | unordered |
성공한 operation에 _relaxed가 붙으면 다른 memory location에 대해 unordered다. _acquire는 RMW의 read 부분을 ACQUIRE로 만들고, _release는 write 부분을 RELEASE로 만든다. conditional operation은 실패하면 여전히 unordered다.
unordered라도 address dependency를 없애지는 않는다. fully ordered primitive는 앞의 모든 access와 뒤의 모든 access 사이에 순서를 세우므로, 개념상 operation 앞뒤에 smp_mb()가 하나씩 있는 것과 같다.
smp_mb__before_atomic()과 smp_mb__after_atomic()
atomic_t.txt:190-232두 helper barrier는 RMW atomic operation에만 적용한다. operation이 원래 제공하는 ordering을 더 강하게 만들 때 사용한다.
smp_mb__before_atomic()은 그보다 앞선 모든 access를 RMW 자체와 그 뒤의 access보다 먼저 오게 한다. smp_mb__after_atomic()은 뒤의 모든 access를 RMW 자체와 그 앞의 access보다 나중에 오게 한다.
다만 barrier와 RMW 사이에 다른 access를 끼워 넣으면 그 access까지 기대한 방식으로 정렬되지 않는다. 가능한 한 barrier를 대상 atomic operation 바로 옆에 둬야 한다.
architecture마다 SMP atomic instruction이 암묵적으로 제공하는 ordering이 다르기 때문에 helper가 필요하다. TSO architecture의 fully ordered atomic에서는 helper가 no-op일 수 있다. fully ordered atomic RMW는 compiler barrier도 포함해야 한다.
atomic_fetch_add();
/* ordering 관점에서 다음과 동등하다. */
smp_mb__before_atomic();
atomic_fetch_add_relaxed();
smp_mb__after_atomic();
동등한 의미라도 architecture는 atomic_fetch_add()를 더 효율적으로 구현할 수 있으므로, 불필요하게 세 operation으로 풀어 쓰지 않는다.
Helper barrier는 단순 ACQUIRE·RELEASE보다 강하다
atomic_t.txt:214-274smp_mb__before_atomic(); atomic_dec(&X);는 흔히 RELEASE 모양으로 쓰이지만 RELEASE보다 강하다. 앞선 instruction을 atomic_dec()의 read와 write 양쪽, 그리고 뒤따르는 모든 instruction보다 앞에 둔다.
atomic_inc(&X); smp_mb__after_atomic();도 ACQUIRE보다 강하다. ACQUIRE는 RMW의 read 부분 뒤에 오는 access만 제한하지만, after helper는 RMW의 write 부분과 뒤의 access 사이에도 순서를 세운다.
P0(int *x, atomic_t *y)
{
r0 = READ_ONCE(*x);
smp_rmb();
r1 = atomic_read(y);
}
P1(int *x, atomic_t *y)
{
atomic_inc(y);
smp_mb__after_atomic();
WRITE_ONCE(*x, 1);
}
exists (0:r0=1 /\ 0:r1=0)
위 결과는 허용되지 않아야 한다. 가상의 atomic_inc_acquire()는 RMW write와 뒤의 WRITE_ONCE(*x, 1) 사이를 정렬하지 않으므로 같은 결과를 허용할 수 있다. 이것이 after helper를 단순 ACQUIRE로 치환할 수 없는 이유다.
CMPXCHG와 TRY_CMPXCHG
atomic_t.txt:276-315int atomic_cmpxchg(atomic_t *ptr, int old, int new);
bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new);
두 함수의 기능은 같다. cmpxchg는 실제로 관측한 old value를 반환한다. try_cmpxchg는 성공 여부를 bool로 반환하고, 실패하면 *oldp를 실제 관측값으로 갱신한다.
old = atomic_read(&v);
do {
new = func(old);
} while (!atomic_try_cmpxchg(&v, &old, new));
실패할 때 old가 자동으로 최신 관측값으로 바뀌므로 별도의 tmp와 비교, 대입이 필요 없다. 특히 x86에서는 try_cmpxchg 형태가 hardware instruction의 operand와 더 잘 맞아 더 작은 코드를 만들 수 있다.
Forward progress
atomic_t.txt:317-368산술·bitwise operation과 xchg 같은 unconditional atomic operation에는 일반적으로 강한 forward progress가 기대된다. 커널의 상당한 코드는 conditional atomic operation에도 일정 수준의 progress를 요구한다.
특히 단순한 cmpxchg loop들이 서로를 영원히 굶기지 않아야 한다. 하지만 LL/SC architecture에서 이 보장은 자동으로 따라오지 않는다. architecture가 경쟁하는 LL/SC section 자체의 progress를 보장하더라도, C loop 전체를 포함하는 cmpxchg 구현까지 같은 보장이 확장되지는 않는다.
failed compare 뒤의 forward branch만으로도 LL/SC reservation이 실패하는 architecture가 있고, compiler가 loop body에 만든 instruction은 reservation 유지 가능성을 더 낮춘다. 그 결과 v가 들어 있는 cache line이 local CPU에 머물고 loop가 진전된다는 보장이 사라진다.
native CAS architecture도 primitive의 forward progress를 보장하지 못할 수 있으며 Sparc64가 그 예다.
영향을 받는 구현은 CAS 실패 뒤 exponential backoff를 넣어 progress 가능성을 높이는 것이 강하게 권장된다. architecture maintainer는 generic atomic fallback, refcount_t와 locking primitive도 함께 점검해야 한다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
On atomic types (atomic_t atomic64_t and atomic_long_t).
The atomic type provides an interface to the architecture's means of atomic
RMW operations between CPUs (atomic operations on MMIO are not supported and
can lead to fatal traps on some platforms).
API
---
The 'full' API consists of (atomic64_ and atomic_long_ prefixes omitted for
brevity):
Non-RMW ops:
atomic_read(), atomic_set()
atomic_read_acquire(), atomic_set_release()
RMW atomic operations:
Arithmetic:
atomic_{add,sub,inc,dec}()
atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}()
atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}()
Bitwise:
atomic_{and,or,xor,andnot}()
atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}()
Swap:
atomic_xchg{,_relaxed,_acquire,_release}()
atomic_cmpxchg{,_relaxed,_acquire,_release}()
atomic_try_cmpxchg{,_relaxed,_acquire,_release}()
Reference count (but please see refcount_t):
atomic_add_unless(), atomic_inc_not_zero()
atomic_sub_and_test(), atomic_dec_and_test()
Misc:
atomic_inc_and_test(), atomic_add_negative()
atomic_dec_unless_positive(), atomic_inc_unless_negative()
Barriers:
smp_mb__{before,after}_atomic()
TYPES (signed vs unsigned)
-----
While atomic_t, atomic_long_t and atomic64_t use int, long and s64
respectively (for hysterical raisins), the kernel uses -fno-strict-overflow
(which implies -fwrapv) and defines signed overflow to behave like
2s-complement.
Therefore, an explicitly unsigned variant of the atomic ops is strictly
unnecessary and we can simply cast, there is no UB.
There was a bug in UBSAN prior to GCC-8 that would generate UB warnings for
signed types.
With this we also conform to the C/C++ _Atomic behaviour and things like
P1236R1.
SEMANTICS
---------
Non-RMW ops:
The non-RMW ops are (typically) regular LOADs and STOREs and are canonically
implemented using READ_ONCE(), WRITE_ONCE(), smp_load_acquire() and
smp_store_release() respectively. Therefore, if you find yourself only using
the Non-RMW operations of atomic_t, you do not in fact need atomic_t at all
and are doing it wrong.
A note for the implementation of atomic_set{}() is that it must not break the
atomicity of the RMW ops. That is:
C Atomic-RMW-ops-are-atomic-WRT-atomic_set
{
atomic_t v = ATOMIC_INIT(1);
}
P0(atomic_t *v)
{
(void)atomic_add_unless(v, 1, 0);
}
P1(atomic_t *v)
{
atomic_set(v, 0);
}
exists
(v=2)
In this case we would expect the atomic_set() from CPU1 to either happen
before the atomic_add_unless(), in which case that latter one would no-op, or
_after_ in which case we'd overwrite its result. In no case is "2" a valid
outcome.
This is typically true on 'normal' platforms, where a regular competing STORE
will invalidate a LL/SC or fail a CMPXCHG.
The obvious case where this is not so is when we need to implement atomic ops
with a lock:
CPU0 CPU1
atomic_add_unless(v, 1, 0);
lock();
ret = READ_ONCE(v->counter); // == 1
atomic_set(v, 0);
if (ret != u) WRITE_ONCE(v->counter, 0);
WRITE_ONCE(v->counter, ret + 1);
unlock();
the typical solution is to then implement atomic_set{}() with atomic_xchg().
RMW ops:
These come in various forms:
- plain operations without return value: atomic_{}()
- operations which return the modified value: atomic_{}_return()
these are limited to the arithmetic operations because those are
reversible. Bitops are irreversible and therefore the modified value
is of dubious utility.
- operations which return the original value: atomic_fetch_{}()
- swap operations: xchg(), cmpxchg() and try_cmpxchg()
- misc; the special purpose operations that are commonly used and would,
given the interface, normally be implemented using (try_)cmpxchg loops but
are time critical and can, (typically) on LL/SC architectures, be more
efficiently implemented.
All these operations are SMP atomic; that is, the operations (for a single
atomic variable) can be fully ordered and no intermediate state is lost or
visible.
ORDERING (go read memory-barriers.txt first)
--------
The rule of thumb:
- non-RMW operations are unordered;
- RMW operations that have no return value are unordered;
- RMW operations that have a return value are fully ordered;
- RMW operations that are conditional are unordered on FAILURE,
otherwise the above rules apply.
Except of course when a successful operation has an explicit ordering like:
{}_relaxed: unordered
{}_acquire: the R of the RMW (or atomic_read) is an ACQUIRE
{}_release: the W of the RMW (or atomic_set) is a RELEASE
Where 'unordered' is against other memory locations. Address dependencies are
not defeated. Conditional operations are still unordered on FAILURE.
Fully ordered primitives are ordered against everything prior and everything
subsequent. Therefore a fully ordered primitive is like having an smp_mb()
before and an smp_mb() after the primitive.
The barriers:
smp_mb__{before,after}_atomic()
only apply to the RMW atomic ops and can be used to augment/upgrade the
ordering inherent to the op. These barriers act almost like a full smp_mb():
smp_mb__before_atomic() orders all earlier accesses against the RMW op
itself and all accesses following it, and smp_mb__after_atomic() orders all
later accesses against the RMW op and all accesses preceding it. However,
accesses between the smp_mb__{before,after}_atomic() and the RMW op are not
ordered, so it is advisable to place the barrier right next to the RMW atomic
op whenever possible.
These helper barriers exist because architectures have varying implicit
ordering on their SMP atomic primitives. For example our TSO architectures
provide full ordered atomics and these barriers are no-ops.
NOTE: when the atomic RmW ops are fully ordered, they should also imply a
compiler barrier.
Thus:
atomic_fetch_add();
is equivalent to:
smp_mb__before_atomic();
atomic_fetch_add_relaxed();
smp_mb__after_atomic();
However the atomic_fetch_add() might be implemented more efficiently.
Further, while something like:
smp_mb__before_atomic();
atomic_dec(&X);
is a 'typical' RELEASE pattern, the barrier is strictly stronger than
a RELEASE because it orders preceding instructions against both the read
and write parts of the atomic_dec(), and against all following instructions
as well. Similarly, something like:
atomic_inc(&X);
smp_mb__after_atomic();
is an ACQUIRE pattern (though very much not typical), but again the barrier is
strictly stronger than ACQUIRE. As illustrated:
C Atomic-RMW+mb__after_atomic-is-stronger-than-acquire
{
}
P0(int *x, atomic_t *y)
{
r0 = READ_ONCE(*x);
smp_rmb();
r1 = atomic_read(y);
}
P1(int *x, atomic_t *y)
{
atomic_inc(y);
smp_mb__after_atomic();
WRITE_ONCE(*x, 1);
}
exists
(0:r0=1 /\ 0:r1=0)
This should not happen; but a hypothetical atomic_inc_acquire() --
(void)atomic_fetch_inc_acquire() for instance -- would allow the outcome,
because it would not order the W part of the RMW against the following
WRITE_ONCE. Thus:
P0 P1
t = LL.acq *y (0)
t++;
*x = 1;
r0 = *x (1)
RMB
r1 = *y (0)
SC *y, t;
is allowed.
CMPXCHG vs TRY_CMPXCHG
----------------------
int atomic_cmpxchg(atomic_t *ptr, int old, int new);
bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new);
Both provide the same functionality, but try_cmpxchg() can lead to more
compact code. The functions relate like:
bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new)
{
int ret, old = *oldp;
ret = atomic_cmpxchg(ptr, old, new);
if (ret != old)
*oldp = ret;
return ret == old;
}
and:
int atomic_cmpxchg(atomic_t *ptr, int old, int new)
{
(void)atomic_try_cmpxchg(ptr, &old, new);
return old;
}
Usage:
old = atomic_read(&v); old = atomic_read(&v);
for (;;) { do {
new = func(old); new = func(old);
tmp = atomic_cmpxchg(&v, old, new); } while (!atomic_try_cmpxchg(&v, &old, new));
if (tmp == old)
break;
old = tmp;
}
NB. try_cmpxchg() also generates better code on some platforms (notably x86)
where the function more closely matches the hardware instruction.
FORWARD PROGRESS
----------------
In general strong forward progress is expected of all unconditional atomic
operations -- those in the Arithmetic and Bitwise classes and xchg(). However
a fair amount of code also requires forward progress from the conditional
atomic operations.
Specifically 'simple' cmpxchg() loops are expected to not starve one another
indefinitely. However, this is not evident on LL/SC architectures, because
while an LL/SC architecture 'can/should/must' provide forward progress
guarantees between competing LL/SC sections, such a guarantee does not
transfer to cmpxchg() implemented using LL/SC. Consider:
old = atomic_read(&v);
do {
new = func(old);
} while (!atomic_try_cmpxchg(&v, &old, new));
which on LL/SC becomes something like:
old = atomic_read(&v);
do {
new = func(old);
} while (!({
volatile asm ("1: LL %[oldval], %[v]\n"
" CMP %[oldval], %[old]\n"
" BNE 2f\n"
" SC %[new], %[v]\n"
" BNE 1b\n"
"2:\n"
: [oldval] "=&r" (oldval), [v] "m" (v)
: [old] "r" (old), [new] "r" (new)
: "memory");
success = (oldval == old);
if (!success)
old = oldval;
success; }));
However, even the forward branch from the failed compare can cause the LL/SC
to fail on some architectures, let alone whatever the compiler makes of the C
loop body. As a result there is no guarantee what so ever the cacheline
containing @v will stay on the local CPU and progress is made.
Even native CAS architectures can fail to provide forward progress for their
primitive (See Sparc64 for an example).
Such implementations are strongly encouraged to add exponential backoff loops
to a failed CAS in order to ensure some progress. Affected architectures are
also strongly encouraged to inspect/audit the atomic fallbacks, refcount_t and
their locking primitives.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
atomic_t 계열과 전체 API
1-57Atomic type은 CPU 사이의 atomic read-modify-write(RMW) operation을 architecture가 제공하는 방식으로 사용할 interface다. MMIO에 대한 atomic operation은 지원하지 않으며 일부 platform에서는 fatal trap을 일으킬 수 있다.
아래 목록은 간결하게 쓰기 위해 atomic64_와 atomic_long_ prefix를 생략한 전체 API다.
| 분류 | API |
|---|---|
| Non-RMW | atomic_read(), atomic_set(), atomic_read_acquire(), atomic_set_release() |
| Arithmetic RMW | atomic_{add,sub,inc,dec}(), atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}(), atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}() |
| Bitwise RMW | atomic_{and,or,xor,andnot}(), atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}() |
| Swap | atomic_xchg{,_relaxed,_acquire,_release}(), atomic_cmpxchg{,_relaxed,_acquire,_release}(), atomic_try_cmpxchg{,_relaxed,_acquire,_release}() |
| Reference count | atomic_add_unless(), atomic_inc_not_zero(), atomic_sub_and_test(), atomic_dec_and_test(); 다만 refcount_t도 반드시 검토한다. |
| Misc | atomic_inc_and_test(), atomic_add_negative(), atomic_dec_unless_positive(), atomic_inc_unless_negative() |
| Barrier | smp_mb__before_atomic(), smp_mb__after_atomic() |
Signed type과 overflow 의미
59-74atomic_t, atomic_long_t, atomic64_t는 각각 int, long, s64를 사용한다. 원문은 역사적 이유를 “hysterical raisins”라는 말장난으로 표현한다. Kernel은 -fno-strict-overflow를 사용하며 이는 -fwrapv를 내포하므로 signed overflow가 2의 보수 wrapping처럼 동작한다고 정의한다.
따라서 별도의 unsigned atomic operation variant는 엄밀히 필요하지 않다. 단순히 cast해도 undefined behavior가 없다. GCC 8 이전 UBSAN에는 signed type에 잘못된 UB warning을 생성하는 bug가 있었다.
이 정의는 C/C++ _Atomic behavior와 P1236R1 같은 제안에도 부합한다.
Non-RMW semantics와 atomic_set()의 의무
77-116Non-RMW operation은 일반적으로 regular LOAD와 STORE다. 각각 READ_ONCE(), WRITE_ONCE(), smp_load_acquire(), smp_store_release()로 구현한다. atomic_t에서 non-RMW operation만 사용한다면 실제로 atomic_t가 필요 없으며 설계가 잘못된 것이다.
atomic_set() 구현은 RMW operation의 atomicity를 깨뜨려서는 안 된다. 원문은 다음 memory-model litmus test로 조건을 표현한다.
C Atomic-RMW-ops-are-atomic-WRT-atomic_set
{
atomic_t v = ATOMIC_INIT(1);
}
P0(atomic_t *v)
{
(void)atomic_add_unless(v, 1, 0);
}
P1(atomic_t *v)
{
atomic_set(v, 0);
}
exists
(v=2)
CPU1의 atomic_set()이 atomic_add_unless()보다 먼저 실행되면 후자는 아무 일도 하지 않는다. 뒤에 실행되면 atomic_add_unless() 결과를 0으로 덮어쓴다. 어느 순서에서도 최종값 2는 유효하지 않다.
일반적인 platform에서는 경쟁하는 regular STORE가 LL/SC reservation을 무효화하거나 CMPXCHG를 실패시키므로 이 조건이 자연스럽게 성립한다.
Lock 기반 atomic 구현에서 생기는 race
118-131Atomic operation을 lock으로 구현해야 하는 architecture에서는 단순 atomic_set()이 RMW atomicity를 깨뜨릴 수 있다. 원문의 두 CPU ASCII interleaving을 단계로 정리하면 다음과 같다.
| 순서 | CPU0: atomic_add_unless() | CPU1: atomic_set() |
|---|---|---|
| 1 | lock() 획득 | 대기 또는 병행 |
| 2 | ret = READ_ONCE(v->counter), ret == 1 | |
| 3 | lock 안에서 계산 계속 | WRITE_ONCE(v->counter, 0) |
| 4 | ret != u이므로 WRITE_ONCE(v->counter, ret + 1), 즉 2 기록 | |
| 5 | unlock() | 완료 |
이 interleaving은 유효하지 않아야 할 2를 만든다. 일반적인 해결책은 atomic_set()도 atomic_xchg()로 구현하여 같은 atomic protocol에 참여시키는 것이다.
RMW operation의 형태
134-157| 형태 | 의미 |
|---|---|
| atomic_{}() | Return value가 없는 plain operation |
| atomic_{}_return() | 변경된 값을 return한다. 되돌릴 수 있는 arithmetic operation에만 있다. Bit operation은 비가역적이어서 변경값의 유용성이 불분명하다. |
| atomic_fetch_{}() | 변경 전 원래 값을 return한다. |
| xchg(), cmpxchg(), try_cmpxchg() | Swap 및 conditional swap operation |
| 특수 목적 operation | 보통 (try_)cmpxchg loop로 구현할 수 있지만 time-critical하고, LL/SC architecture에서는 더 효율적으로 구현할 수 있어 별도 API를 둔다. |
이 operation은 모두 SMP atomic이다. 하나의 atomic variable에 대한 operation을 완전히 순서화할 수 있고 중간 state가 유실되거나 외부에 보이지 않는다.
Memory ordering 기본 규칙
160-185이 절을 읽기 전에 Documentation/memory-barriers.txt를 먼저 읽어야 한다.
- Non-RMW operation은 unordered다.
- Return value가 없는 RMW operation은 unordered다.
- Return value가 있는 RMW operation은 fully ordered다.
- Conditional RMW operation은 실패 시 unordered이며 성공 시에는 위 규칙을 따른다.
| suffix | 성공한 operation의 ordering |
|---|---|
| _relaxed | unordered |
| _acquire | RMW의 read 부분 또는 atomic_read가 ACQUIRE |
| _release | RMW의 write 부분 또는 atomic_set이 RELEASE |
여기서 unordered는 다른 memory location에 대한 관계를 말한다. Address dependency까지 무효화되지는 않는다. Conditional operation은 suffix와 관계없이 실패 시 여전히 unordered다.
Fully ordered primitive는 앞의 모든 operation과 뒤의 모든 operation에 대해 순서화된다. 즉 primitive 앞뒤에 각각 smp_mb()가 있는 것처럼 동작한다.
smp_mb__before_atomic()과 smp_mb__after_atomic()
188-218두 helper barrier는 RMW atomic operation에만 적용하며 operation 자체의 ordering을 보강하거나 강화한다.
smp_mb__before_atomic()은 앞선 모든 access를 RMW operation 자체와 그 뒤의 모든 access에 대해 순서화한다. smp_mb__after_atomic()은 뒤의 모든 access를 RMW operation과 그 앞의 모든 access에 대해 순서화한다.
다만 barrier와 RMW operation 사이에 놓인 access는 순서화되지 않으므로 가능한 한 barrier를 RMW 바로 옆에 둔다.
Architecture마다 SMP atomic primitive에 내재된 ordering이 다르기 때문에 helper가 존재한다. TSO architecture는 fully ordered atomic을 제공하므로 이 barrier가 no-op이다. Fully ordered atomic RMW는 compiler barrier도 내포해야 한다.
따라서 다음 두 형태는 동등하지만 architecture는 첫 번째를 더 효율적으로 구현할 수 있다.
atomic_fetch_add();
/* equivalent ordering */
smp_mb__before_atomic();
atomic_fetch_add_relaxed();
smp_mb__after_atomic();
Atomic helper barrier가 ACQUIRE/RELEASE보다 강한 이유
220-273smp_mb__before_atomic(); atomic_dec(&X);는 전형적인 RELEASE pattern이지만 엄밀히는 RELEASE보다 강하다. 앞선 instruction을 atomic_dec()의 read와 write 양쪽 및 모든 뒤 instruction에 대해 순서화하기 때문이다.
atomic_inc(&X); smp_mb__after_atomic();는 흔하지 않은 ACQUIRE pattern이지만 마찬가지로 일반 ACQUIRE보다 강하다. 다음 litmus test의 결과는 허용되어서는 안 된다.
C Atomic-RMW+mb__after_atomic-is-stronger-than-acquire
{
}
P0(int *x, atomic_t *y)
{
r0 = READ_ONCE(*x);
smp_rmb();
r1 = atomic_read(y);
}
P1(int *x, atomic_t *y)
{
atomic_inc(y);
smp_mb__after_atomic();
WRITE_ONCE(*x, 1);
}
exists
(0:r0=1 /\ 0:r1=0)
가상의 atomic_inc_acquire(), 예를 들어 (void)atomic_fetch_inc_acquire()라면 이 결과를 허용한다. ACQUIRE는 RMW의 write 부분과 뒤의 WRITE_ONCE를 순서화하지 않기 때문이다. 원문은 다음 LL/SC 실행을 허용 가능한 예로 든다.
P0 P1
t = LL.acq *y (0)
t++;
*x = 1;
r0 = *x (1)
RMB
r1 = *y (0)
SC *y, t;
cmpxchg와 try_cmpxchg 비교
276-314int atomic_cmpxchg(atomic_t *ptr, int old, int new);
bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new);
두 function은 같은 기능을 제공하지만 try_cmpxchg()가 더 간결한 code를 만들 수 있다. 의미 관계는 다음 구현으로 표현할 수 있다.
bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new)
{
int ret, old = *oldp;
ret = atomic_cmpxchg(ptr, old, new);
if (ret != old)
*oldp = ret;
return ret == old;
}
int atomic_cmpxchg(atomic_t *ptr, int old, int new)
{
(void)atomic_try_cmpxchg(ptr, &old, new);
return old;
}
일반 cmpxchg loop와 try_cmpxchg loop의 대응은 다음과 같다.
/* cmpxchg */
old = atomic_read(&v);
for (;;) {
new = func(old);
tmp = atomic_cmpxchg(&v, old, new);
if (tmp == old)
break;
old = tmp;
}
/* try_cmpxchg */
old = atomic_read(&v);
do {
new = func(old);
} while (!atomic_try_cmpxchg(&v, &old, new));
try_cmpxchg()는 hardware instruction과 function 형태가 더 잘 맞는 x86 같은 일부 platform에서 더 좋은 machine code를 생성한다.
Forward progress 보장
317-367Arithmetic, bitwise class와 xchg() 같은 unconditional atomic operation에는 일반적으로 강한 forward progress를 기대한다. 많은 code는 conditional atomic operation에도 progress를 요구한다.
특히 단순한 cmpxchg loop끼리는 서로를 무한히 starvation시키지 않아야 한다. 하지만 LL/SC architecture에서는 이 조건이 자명하지 않다. Competing LL/SC section 사이의 forward progress 보장이 LL/SC로 구현한 cmpxchg() 전체 C loop까지 자동으로 이어지지 않기 때문이다.
old = atomic_read(&v);
do {
new = func(old);
} while (!atomic_try_cmpxchg(&v, &old, new));
LL/SC에서는 위 loop가 대략 다음처럼 확장될 수 있다.
old = atomic_read(&v);
do {
new = func(old);
} while (!({
volatile asm ("1: LL %[oldval], %[v]\n"
" CMP %[oldval], %[old]\n"
" BNE 2f\n"
" SC %[new], %[v]\n"
" BNE 1b\n"
"2:\n"
: [oldval] "=&r" (oldval), [v] "m" (v)
: [old] "r" (old), [new] "r" (new)
: "memory");
success = (oldval == old);
if (!success)
old = oldval;
success; }));
일부 architecture에서는 compare 실패 뒤의 forward branch만으로도 LL/SC가 실패할 수 있고 compiler가 C loop body를 변환한 결과는 더 큰 영향을 줄 수 있다. 따라서 v가 있는 cacheline이 local CPU에 남아 실제 progress가 이루어진다는 보장이 없다.
Native CAS architecture도 primitive의 forward progress를 보장하지 못할 수 있으며 Sparc64가 한 예다. 이런 구현은 실패한 CAS에 exponential backoff loop를 추가하여 progress를 보장하는 것이 강하게 권장된다. 영향받는 architecture는 atomic fallback, refcount_t, locking primitive도 함께 점검해야 한다.
Atomic type의 범위
atomic_t.txt:1-6atomic_t, atomic64_t, atomic_long_t는 여러 CPU가 같은 memory location에 read-modify-write를 수행할 때 architecture가 제공하는 atomic operation을 공통 API로 노출한다.
이 API를 MMIO register에 사용하면 안 된다. MMIO에 대한 atomic operation은 지원되지 않으며, 일부 platform에서는 fatal trap이 발생할 수 있다. atomic_t의 대상은 normal memory에 놓인 커널 자료구조다.