← Documents Documentation/atomic_t.txt GitHub 원문 ↗

Linux 6.18.37 · 동시성

atomic_t, atomic64_t, atomic_long_t

CPU 사이의 atomic RMW, memory ordering, cmpxchg와 LL/SC forward progress 조건을 설명합니다.

Source pathDocumentation/atomic_t.txt
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

Atomic type의 범위

atomic_t.txt:1-6

atomic_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에 놓인 커널 자료구조다.

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-75

atomic_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-94

Non-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-130

atomic_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-RMWunordered
반환값이 없는 RMWunordered
반환값이 있는 RMWfully 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-274

smp_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-315
int 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 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1
2 On atomic types (atomic_t atomic64_t and atomic_long_t).
3
4 The atomic type provides an interface to the architecture's means of atomic
5 RMW operations between CPUs (atomic operations on MMIO are not supported and
6 can lead to fatal traps on some platforms).
7
8 API
9 ---
10
11 The 'full' API consists of (atomic64_ and atomic_long_ prefixes omitted for
12 brevity):
13
14 Non-RMW ops:
15
16 atomic_read(), atomic_set()
17 atomic_read_acquire(), atomic_set_release()
18
19
20 RMW atomic operations:
21
22 Arithmetic:
23
24 atomic_{add,sub,inc,dec}()
25 atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}()
26 atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}()
27
28
29 Bitwise:
30
31 atomic_{and,or,xor,andnot}()
32 atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}()
33
34
35 Swap:
36
37 atomic_xchg{,_relaxed,_acquire,_release}()
38 atomic_cmpxchg{,_relaxed,_acquire,_release}()
39 atomic_try_cmpxchg{,_relaxed,_acquire,_release}()
40
41
42 Reference count (but please see refcount_t):
43
44 atomic_add_unless(), atomic_inc_not_zero()
45 atomic_sub_and_test(), atomic_dec_and_test()
46
47
48 Misc:
49
50 atomic_inc_and_test(), atomic_add_negative()
51 atomic_dec_unless_positive(), atomic_inc_unless_negative()
52
53
54 Barriers:
55
56 smp_mb__{before,after}_atomic()
57
58
59 TYPES (signed vs unsigned)
60 -----
61
62 While atomic_t, atomic_long_t and atomic64_t use int, long and s64
63 respectively (for hysterical raisins), the kernel uses -fno-strict-overflow
64 (which implies -fwrapv) and defines signed overflow to behave like
65 2s-complement.
66
67 Therefore, an explicitly unsigned variant of the atomic ops is strictly
68 unnecessary and we can simply cast, there is no UB.
69
70 There was a bug in UBSAN prior to GCC-8 that would generate UB warnings for
71 signed types.
72
73 With this we also conform to the C/C++ _Atomic behaviour and things like
74 P1236R1.
75
76
77 SEMANTICS
78 ---------
79
80 Non-RMW ops:
81
82 The non-RMW ops are (typically) regular LOADs and STOREs and are canonically
83 implemented using READ_ONCE(), WRITE_ONCE(), smp_load_acquire() and
84 smp_store_release() respectively. Therefore, if you find yourself only using
85 the Non-RMW operations of atomic_t, you do not in fact need atomic_t at all
86 and are doing it wrong.
87
88 A note for the implementation of atomic_set{}() is that it must not break the
89 atomicity of the RMW ops. That is:
90
91 C Atomic-RMW-ops-are-atomic-WRT-atomic_set
92
93 {
94 atomic_t v = ATOMIC_INIT(1);
95 }
96
97 P0(atomic_t *v)
98 {
99 (void)atomic_add_unless(v, 1, 0);
100 }
101
102 P1(atomic_t *v)
103 {
104 atomic_set(v, 0);
105 }
106
107 exists
108 (v=2)
109
110 In this case we would expect the atomic_set() from CPU1 to either happen
111 before the atomic_add_unless(), in which case that latter one would no-op, or
112 _after_ in which case we'd overwrite its result. In no case is "2" a valid
113 outcome.
114
115 This is typically true on 'normal' platforms, where a regular competing STORE
116 will invalidate a LL/SC or fail a CMPXCHG.
117
118 The obvious case where this is not so is when we need to implement atomic ops
119 with a lock:
120
121 CPU0 CPU1
122
123 atomic_add_unless(v, 1, 0);
124 lock();
125 ret = READ_ONCE(v->counter); // == 1
126 atomic_set(v, 0);
127 if (ret != u) WRITE_ONCE(v->counter, 0);
128 WRITE_ONCE(v->counter, ret + 1);
129 unlock();
130
131 the typical solution is to then implement atomic_set{}() with atomic_xchg().
132
133
134 RMW ops:
135
136 These come in various forms:
137
138 - plain operations without return value: atomic_{}()
139
140 - operations which return the modified value: atomic_{}_return()
141
142 these are limited to the arithmetic operations because those are
143 reversible. Bitops are irreversible and therefore the modified value
144 is of dubious utility.
145
146 - operations which return the original value: atomic_fetch_{}()
147
148 - swap operations: xchg(), cmpxchg() and try_cmpxchg()
149
150 - misc; the special purpose operations that are commonly used and would,
151 given the interface, normally be implemented using (try_)cmpxchg loops but
152 are time critical and can, (typically) on LL/SC architectures, be more
153 efficiently implemented.
154
155 All these operations are SMP atomic; that is, the operations (for a single
156 atomic variable) can be fully ordered and no intermediate state is lost or
157 visible.
158
159
160 ORDERING (go read memory-barriers.txt first)
161 --------
162
163 The rule of thumb:
164
165 - non-RMW operations are unordered;
166
167 - RMW operations that have no return value are unordered;
168
169 - RMW operations that have a return value are fully ordered;
170
171 - RMW operations that are conditional are unordered on FAILURE,
172 otherwise the above rules apply.
173
174 Except of course when a successful operation has an explicit ordering like:
175
176 {}_relaxed: unordered
177 {}_acquire: the R of the RMW (or atomic_read) is an ACQUIRE
178 {}_release: the W of the RMW (or atomic_set) is a RELEASE
179
180 Where 'unordered' is against other memory locations. Address dependencies are
181 not defeated. Conditional operations are still unordered on FAILURE.
182
183 Fully ordered primitives are ordered against everything prior and everything
184 subsequent. Therefore a fully ordered primitive is like having an smp_mb()
185 before and an smp_mb() after the primitive.
186
187
188 The barriers:
189
190 smp_mb__{before,after}_atomic()
191
192 only apply to the RMW atomic ops and can be used to augment/upgrade the
193 ordering inherent to the op. These barriers act almost like a full smp_mb():
194 smp_mb__before_atomic() orders all earlier accesses against the RMW op
195 itself and all accesses following it, and smp_mb__after_atomic() orders all
196 later accesses against the RMW op and all accesses preceding it. However,
197 accesses between the smp_mb__{before,after}_atomic() and the RMW op are not
198 ordered, so it is advisable to place the barrier right next to the RMW atomic
199 op whenever possible.
200
201 These helper barriers exist because architectures have varying implicit
202 ordering on their SMP atomic primitives. For example our TSO architectures
203 provide full ordered atomics and these barriers are no-ops.
204
205 NOTE: when the atomic RmW ops are fully ordered, they should also imply a
206 compiler barrier.
207
208 Thus:
209
210 atomic_fetch_add();
211
212 is equivalent to:
213
214 smp_mb__before_atomic();
215 atomic_fetch_add_relaxed();
216 smp_mb__after_atomic();
217
218 However the atomic_fetch_add() might be implemented more efficiently.
219
220 Further, while something like:
221
222 smp_mb__before_atomic();
223 atomic_dec(&X);
224
225 is a 'typical' RELEASE pattern, the barrier is strictly stronger than
226 a RELEASE because it orders preceding instructions against both the read
227 and write parts of the atomic_dec(), and against all following instructions
228 as well. Similarly, something like:
229
230 atomic_inc(&X);
231 smp_mb__after_atomic();
232
233 is an ACQUIRE pattern (though very much not typical), but again the barrier is
234 strictly stronger than ACQUIRE. As illustrated:
235
236 C Atomic-RMW+mb__after_atomic-is-stronger-than-acquire
237
238 {
239 }
240
241 P0(int *x, atomic_t *y)
242 {
243 r0 = READ_ONCE(*x);
244 smp_rmb();
245 r1 = atomic_read(y);
246 }
247
248 P1(int *x, atomic_t *y)
249 {
250 atomic_inc(y);
251 smp_mb__after_atomic();
252 WRITE_ONCE(*x, 1);
253 }
254
255 exists
256 (0:r0=1 /\ 0:r1=0)
257
258 This should not happen; but a hypothetical atomic_inc_acquire() --
259 (void)atomic_fetch_inc_acquire() for instance -- would allow the outcome,
260 because it would not order the W part of the RMW against the following
261 WRITE_ONCE. Thus:
262
263 P0 P1
264
265 t = LL.acq *y (0)
266 t++;
267 *x = 1;
268 r0 = *x (1)
269 RMB
270 r1 = *y (0)
271 SC *y, t;
272
273 is allowed.
274
275
276 CMPXCHG vs TRY_CMPXCHG
277 ----------------------
278
279 int atomic_cmpxchg(atomic_t *ptr, int old, int new);
280 bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new);
281
282 Both provide the same functionality, but try_cmpxchg() can lead to more
283 compact code. The functions relate like:
284
285 bool atomic_try_cmpxchg(atomic_t *ptr, int *oldp, int new)
286 {
287 int ret, old = *oldp;
288 ret = atomic_cmpxchg(ptr, old, new);
289 if (ret != old)
290 *oldp = ret;
291 return ret == old;
292 }
293
294 and:
295
296 int atomic_cmpxchg(atomic_t *ptr, int old, int new)
297 {
298 (void)atomic_try_cmpxchg(ptr, &old, new);
299 return old;
300 }
301
302 Usage:
303
304 old = atomic_read(&v); old = atomic_read(&v);
305 for (;;) { do {
306 new = func(old); new = func(old);
307 tmp = atomic_cmpxchg(&v, old, new); } while (!atomic_try_cmpxchg(&v, &old, new));
308 if (tmp == old)
309 break;
310 old = tmp;
311 }
312
313 NB. try_cmpxchg() also generates better code on some platforms (notably x86)
314 where the function more closely matches the hardware instruction.
315
316
317 FORWARD PROGRESS
318 ----------------
319
320 In general strong forward progress is expected of all unconditional atomic
321 operations -- those in the Arithmetic and Bitwise classes and xchg(). However
322 a fair amount of code also requires forward progress from the conditional
323 atomic operations.
324
325 Specifically 'simple' cmpxchg() loops are expected to not starve one another
326 indefinitely. However, this is not evident on LL/SC architectures, because
327 while an LL/SC architecture 'can/should/must' provide forward progress
328 guarantees between competing LL/SC sections, such a guarantee does not
329 transfer to cmpxchg() implemented using LL/SC. Consider:
330
331 old = atomic_read(&v);
332 do {
333 new = func(old);
334 } while (!atomic_try_cmpxchg(&v, &old, new));
335
336 which on LL/SC becomes something like:
337
338 old = atomic_read(&v);
339 do {
340 new = func(old);
341 } while (!({
342 volatile asm ("1: LL %[oldval], %[v]\n"
343 " CMP %[oldval], %[old]\n"
344 " BNE 2f\n"
345 " SC %[new], %[v]\n"
346 " BNE 1b\n"
347 "2:\n"
348 : [oldval] "=&r" (oldval), [v] "m" (v)
349 : [old] "r" (old), [new] "r" (new)
350 : "memory");
351 success = (oldval == old);
352 if (!success)
353 old = oldval;
354 success; }));
355
356 However, even the forward branch from the failed compare can cause the LL/SC
357 to fail on some architectures, let alone whatever the compiler makes of the C
358 loop body. As a result there is no guarantee what so ever the cacheline
359 containing @v will stay on the local CPU and progress is made.
360
361 Even native CAS architectures can fail to provide forward progress for their
362 primitive (See Sparc64 for an example).
363
364 Such implementations are strongly encouraged to add exponential backoff loops
365 to a failed CAS in order to ensure some progress. Affected architectures are
366 also strongly encouraged to inspect/audit the atomic fallbacks, refcount_t and
367 their locking primitives.
368

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

atomic_t 계열과 전체 API

1-57

Atomic type은 CPU 사이의 atomic read-modify-write(RMW) operation을 architecture가 제공하는 방식으로 사용할 interface다. MMIO에 대한 atomic operation은 지원하지 않으며 일부 platform에서는 fatal trap을 일으킬 수 있다.

아래 목록은 간결하게 쓰기 위해 atomic64_와 atomic_long_ prefix를 생략한 전체 API다.

분류API
Non-RMWatomic_read(), atomic_set(), atomic_read_acquire(), atomic_set_release()
Arithmetic RMWatomic_{add,sub,inc,dec}(), atomic_{add,sub,inc,dec}_return{,_relaxed,_acquire,_release}(), atomic_fetch_{add,sub,inc,dec}{,_relaxed,_acquire,_release}()
Bitwise RMWatomic_{and,or,xor,andnot}(), atomic_fetch_{and,or,xor,andnot}{,_relaxed,_acquire,_release}()
Swapatomic_xchg{,_relaxed,_acquire,_release}(), atomic_cmpxchg{,_relaxed,_acquire,_release}(), atomic_try_cmpxchg{,_relaxed,_acquire,_release}()
Reference countatomic_add_unless(), atomic_inc_not_zero(), atomic_sub_and_test(), atomic_dec_and_test(); 다만 refcount_t도 반드시 검토한다.
Miscatomic_inc_and_test(), atomic_add_negative(), atomic_dec_unless_positive(), atomic_inc_unless_negative()
Barriersmp_mb__before_atomic(), smp_mb__after_atomic()

Signed type과 overflow 의미

59-74

atomic_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-116

Non-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-131

Atomic operation을 lock으로 구현해야 하는 architecture에서는 단순 atomic_set()이 RMW atomicity를 깨뜨릴 수 있다. 원문의 두 CPU ASCII interleaving을 단계로 정리하면 다음과 같다.

순서CPU0: atomic_add_unless()CPU1: atomic_set()
1lock() 획득대기 또는 병행
2ret = READ_ONCE(v->counter), ret == 1
3lock 안에서 계산 계속WRITE_ONCE(v->counter, 0)
4ret != u이므로 WRITE_ONCE(v->counter, ret + 1), 즉 2 기록
5unlock()완료

이 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
_relaxedunordered
_acquireRMW의 read 부분 또는 atomic_read가 ACQUIRE
_releaseRMW의 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-273

smp_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-314
int 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-367

Arithmetic, 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도 함께 점검해야 한다.