← System Programming DUJINLABS.COM

Thread / Synchronization · Linux userspace / kernel ABI

C atomic memory order와 futex

atomic value 자체의 원자성과 주변 데이터의 가시성을 구분하고 acquire/release publish, futex sleep을 연결합니다.

Series
29 / 37
Build
cc -std=c17 -Wall -Wextra -O2 -pthread atomic_publish.c -o atomic_publish
Run
./atomic_publish
Kernel
Linux 6.18.37 LTS

atomic store를 썼다는 사실만으로 다른 데이터도 안전하게 보이는가?

atomic operation은 해당 atomic object의 찢어진 read/write와 data race를 막지만 주변 non-atomic data의 순서를 자동으로 정하지 않는다. producer가 payload를 쓴 뒤 release store로 ready를 publish하고 consumer가 acquire load로 ready를 관찰해야 이전 payload write가 보인다.

futex는 userspace atomic word 값이 예상과 같을 때만 kernel wait queue에서 잠들게 한다. uncontended lock은 syscall 없이 끝나고, contention 때만 wait/wake로 들어간다.

구조 그림

그림 1. userspace atomic word와 kernel futex wait queue

Producer CPU

  • payload stores
  • release store ready=1
  • futex wake

Shared cache line

  • payload[4]
  • atomic ready
  • modification order

Consumer CPU

  • acquire load
  • payload reads
  • CAS/spin fast path

Kernel futex bucket

  • key=(mm,address)
  • waiter list
  • timeout/signal/wake

정확성은 atomic state가 담당하고 kernel은 contention 때 잠드는 장소만 제공한다. FUTEX_WAIT는 잠들기 직전 값을 다시 검사한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
payload write 일반 memory
release store ready=1 publish
acquire load ready 관찰
payload read happens-before
futex wait/wake 필요할 때 sleep

memory ordering과 thread scheduling을 분리한다. acquire/release는 값의 가시성 순서를 만들고 futex는 기다리는 동안 CPU를 양보한다.

그림 3. 커널 내부에서 지나가는 주요 지점
user atomic fast path/CAS
FUTEX_WAIT value 재검사
hash bucket waiter enqueue
FUTEX_WAKE matching key
scheduler task runnable

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
kernel/futex/waitwake.c futex_wait_setup(), futex_wait_queue() expected value 재검사와 lost wakeup 방지
kernel/futex/waitwake.c futex_wake() 같은 futex key waiter를 runnable로 전환
kernel/futex/core.c get_futex_key() private virtual address와 shared backing을 key로 변환

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 -pthread atomic_publish.c -o atomic_publish
01#include <pthread.h>
02#include <stdatomic.h>
03#include <stdio.h>
04
05struct message {
06    int payload[4];
07    atomic_int ready;
08};
09
10static void *producer(void *argument)
11{
12    struct message *message = argument;
13    message->payload[0] = 10;
14    message->payload[1] = 20;
15    message->payload[2] = 30;
16    message->payload[3] = 40;
17    atomic_store_explicit(&message->ready, 1, memory_order_release);
18    return NULL;
19}
20
21int main(void)
22{
23    struct message message = { .payload = {0}, .ready = ATOMIC_VAR_INIT(0) };
24    pthread_t thread;
25    pthread_create(&thread, NULL, producer, &message);
26    while (atomic_load_explicit(&message.ready, memory_order_acquire) == 0)
27        ;
28    printf("%d %d %d %d\n", message.payload[0], message.payload[1],
29           message.payload[2], message.payload[3]);
30    pthread_join(thread, NULL);
31    return 0;
32}

코드 조각별 설명

실제 코드 6행int payload[4]

payload는 non-atomic이지만 producer만 publish 전에 쓰고 consumer는 acquire 뒤 읽어 happens-before가 성립하므로 data race가 없다.

실제 코드 7행atomic_int ready

publish flag만 atomic object로 둔다. lock-free 여부는 implementation과 type/alignment에 따라 확인한다.

실제 코드 17행memory_order_release

앞선 payload store가 ready=1 이후로 재배치되지 않게 하고 acquire reader에 공개한다.

실제 코드 26행memory_order_acquire

1을 읽은 뒤 payload load가 ready load 앞으로 이동하지 않게 하며 같은 value를 publish한 release와 synchronize한다.

실제 코드 26행== 0)

예제는 busy wait라 짧은 대기 외에는 비효율적이다. atomic_wait/C++ 또는 futex/condition variable로 sleep을 결합한다.

세부 동작

01

relaxed도 atomic이지만 publish는 아니다

memory_order_relaxed load/store는 ready 자체의 modification order는 유지하지만 payload 접근과 순서를 만들지 않는다. counter 통계처럼 다른 data와 불변 조건이 없을 때 적합하다.

x86에서 우연히 동작하는 테스트를 C memory model의 portable 보장으로 착각하지 않는다.

02

compare-exchange에는 성공·실패 order가 있다

CAS 성공은 lock ownership publish/acquire를 만들 수 있지만 실패 path는 write를 하지 않으므로 release order를 사용할 수 없다. expected 값이 갱신되는 semantics도 loop 설계에 포함한다.

ABA, object lifetime, reclamation은 atomic pointer CAS 하나로 해결되지 않는다.

03

futex wait는 조건을 한 번 더 확인한다

userspace가 lock이 잠긴 것을 본 뒤 syscall에 들어가기 전 unlock/wake가 발생할 수 있다. FUTEX_WAIT는 kernel에서 현재 word가 expected와 같은지 다시 검사하고 다르면 EAGAIN으로 잠들지 않는다.

정확성은 userspace atomic state machine이 담당하고 futex는 blocking 최적화다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
atomic object공유 구조체 수명 동안 존재하고 모든 concurrent 접근이 memory model을 지켜야 한다modification order, alignment
payloadrelease 전 producer가 쓰고 matching acquire 뒤 consumer가 읽는다ownership, happens-before
futex waitercontention 시 syscall에서 잠들고 wake/timeout/signal에서 제거된다key, expected value, priority

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
다른 architecture에서 값이 깨짐relaxed publish 또는 data raceTSan과 memory-order review
CPU 100%무제한 spin loopwait duration과 futex/condvar 전환
missed wakeupstate 변경과 futex expected protocol 불일치CAS/state diagram과 syscall 반환

직접 확인

  1. release/acquire를 relaxed로 바꾼 variant를 ARM 장비와 ThreadSanitizer 관점에서 분석하되 잘못된 코드를 배포하지 않는다.
  2. spin 횟수 뒤 condition variable로 전환하는 adaptive wait를 구현해 latency와 CPU 사용을 비교한다.
  3. raw futex WAIT/WAKE wrapper를 만들고 expected value가 이미 바뀌었을 때 EAGAIN을 확인한다.
실행./atomic_publish
추적strace -f -e trace=futex,clone ./atomic_publish

원문