Atomic 연산 (Atomic Operations)

Linux 커널 atomic_t, 메모리 배리어, memory ordering, cmpxchg, per-CPU 변수 종합 가이드.

관련 표준: C11/C++11 Memory Model (원자적 연산, acquire/release 시맨틱), LKMM (Linux Kernel Memory Model) — 커널 atomic 연산과 메모리 배리어의 이론적 기반입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

Atomic 연산 개요

Atomic 연산은 다른 CPU나 인터럽트에 의해 중간에 끼어들 수 없는 불가분(indivisible) 연산입니다. 단순한 카운터나 플래그에 대해 무거운 잠금 없이 안전한 동시 접근을 제공합니다.

atomic_t / atomic64_t

#include <linux/atomic.h>

atomic_t counter = ATOMIC_INIT(0);

atomic_set(&counter, 5);           /* counter = 5 */
int val = atomic_read(&counter);    /* val = 5 */

atomic_add(3, &counter);            /* counter += 3 */
atomic_sub(1, &counter);            /* counter -= 1 */
atomic_inc(&counter);               /* counter++ */
atomic_dec(&counter);               /* counter-- */

/* 반환값 있는 변형 */
int old = atomic_fetch_add(3, &counter);  /* old = counter; counter += 3 */
bool zero = atomic_dec_and_test(&counter); /* --counter == 0 ? */
bool neg = atomic_add_negative(-5, &counter);

/* Compare-and-swap */
int old = atomic_cmpxchg(&counter, expected, new_val);
int old = atomic_xchg(&counter, new_val);

비트 연산 (Bit Operations)

#include <linux/bitops.h>

unsigned long flags = 0;

set_bit(3, &flags);        /* 비트 3 설정 (atomic) */
clear_bit(3, &flags);      /* 비트 3 해제 (atomic) */
change_bit(3, &flags);     /* 비트 3 토글 (atomic) */

bool was_set = test_and_set_bit(3, &flags);
bool was_set = test_and_clear_bit(3, &flags);

/* Non-atomic 버전 (락 보호 하에 사용 시 더 빠름) */
__set_bit(3, &flags);
__clear_bit(3, &flags);

메모리 배리어 (Memory Barriers)

현대 CPU와 컴파일러는 성능 최적화를 위해 명령어 순서를 변경합니다. 메모리 배리어는 특정 지점에서 순서를 강제합니다.

메모리 배리어 유형 Full Barrier mb() / smp_mb() Read Barrier rmb() / smp_rmb() Write Barrier wmb() / smp_wmb() 이전 모든 읽기/쓰기 완료 후 이후 실행 이전 모든 읽기 완료 후 이후 읽기 이전 모든 쓰기 완료 후 이후 쓰기
리눅스 커널 메모리 배리어: smp_ 접두사는 SMP 시스템에서만 동작 (UP에서는 no-op)
/* 컴파일러 배리어 (CPU 재정렬은 방지하지 않음) */
barrier();

/* 메모리 배리어 (CPU + 컴파일러) */
smp_mb();    /* full memory barrier */
smp_rmb();   /* read memory barrier */
smp_wmb();   /* write memory barrier */

/* Acquire / Release semantics */
smp_load_acquire(&var);    /* 읽기 + acquire barrier */
smp_store_release(&var, val); /* 쓰기 + release barrier */

/* 예: producer-consumer pattern */
/* Producer */
data->field = value;
smp_store_release(&data_ready, 1);

/* Consumer */
if (smp_load_acquire(&data_ready))
    use(data->field);  /* guaranteed to see producer's write */

Per-CPU 변수

Per-CPU 변수는 각 CPU마다 독립적인 복사본을 가지므로, 동기화 없이 안전하게 접근할 수 있습니다 (선점만 비활성화하면 됩니다).

#include <linux/percpu.h>

/* 정적 per-CPU 변수 */
DEFINE_PER_CPU(int, my_counter);

/* 접근 */
get_cpu();  /* 선점 비활성화 + 현재 CPU ID */
this_cpu_inc(my_counter);
put_cpu();  /* 선점 재활성화 */

/* 또는 */
this_cpu_add(my_counter, 5);
int val = this_cpu_read(my_counter);

/* 전체 CPU 합산 */
int total = 0;
int cpu;
for_each_possible_cpu(cpu)
    total += per_cpu(my_counter, cpu);
💡

Per-CPU 변수는 캐시라인 바운싱을 완전히 제거하므로 카운터에 매우 효율적입니다. 네트워크 패킷 카운터, 통계 수집 등에 널리 사용됩니다.

참조 카운팅 (refcount_t)

#include <linux/refcount.h>

refcount_t ref = REFCOUNT_INIT(1);

refcount_inc(&ref);                  /* 증가 (0→1 변환 시 WARN) */
bool last = refcount_dec_and_test(&ref); /* 0이면 true */

/* atomic_t 대신 refcount_t를 사용하면 use-after-free, */
/* overflow/underflow 등의 버그를 조기에 탐지합니다. */

atomic_long_t와 atomic64_t

아키텍처 독립적인 타입으로, 32비트와 64비트 시스템에서 일관된 동작을 보장합니다:

/* atomic_long_t: sizeof(long)에 따라 32/64비트 */
atomic_long_t counter = ATOMIC_LONG_INIT(0);
atomic_long_inc(&counter);

/* atomic64_t: 항상 64비트 (32비트 시스템에서도) */
atomic64_t big_counter = ATOMIC64_INIT(0);
atomic64_add(1000000, &big_counter);
s64 val = atomic64_read(&big_counter);

Lock-free 패턴 (cmpxchg)

cmpxchg는 compare-and-swap 연산으로, lock-free 알고리즘의 핵심 프리미티브입니다:

/* cmpxchg(ptr, old, new) → 실제 이전 값 반환 */
/* 반환 값 == old 이면 교환 성공 */

/* Lock-free 카운터 증가 패턴 */
int old, new;
do {
    old = atomic_read(&counter);
    new = old + 1;
} while (atomic_cmpxchg(&counter, old, new) != old);

/* Lock-free 연결 리스트 삽입 (스택) */
struct node *new_node = kmalloc(sizeof(*new_node), GFP_ATOMIC);
do {
    new_node->next = READ_ONCE(stack_top);
} while (cmpxchg(&stack_top, new_node->next, new_node) != new_node->next);

/* try_cmpxchg: 더 효율적인 변형 (x86에서 1 instruction) */
int old = atomic_read(&counter);
while (!atomic_try_cmpxchg(&counter, &old, old + 1))
    ;  /* old가 자동으로 업데이트됨 */

메모리 순서 의미론 (Ordering Semantics)

atomic 연산에는 메모리 순서 변형이 있습니다:

접미사순서 보장설명
(없음)Fully ordered완전한 순서 보장 (가장 무거움)
_relaxedNone원자성만 보장, 순서 없음 (가장 가벼움)
_acquireAcquire이후 읽기/쓰기가 이 연산 전으로 이동 불가
_releaseRelease이전 읽기/쓰기가 이 연산 후로 이동 불가
/* 성능이 중요한 경우: relaxed 사용 */
atomic_inc_return_relaxed(&counter);  /* 순서 보장 불필요 시 */

/* 잠금 획득 패턴: acquire */
while (atomic_cmpxchg_acquire(&lock, 0, 1) != 0)
    cpu_relax();

/* 잠금 해제 패턴: release */
atomic_set_release(&lock, 0);

/* x86에서는 TSO(Total Store Order) 때문에 */
/* _acquire/_release가 거의 무비용 */
/* ARM/RISC-V에서는 barrier 명령어 필요 → 성능 차이 큼 */

local_t (Per-CPU atomic)

local_t는 Per-CPU 변수에 대한 최적화된 atomic 연산입니다. 다른 CPU의 동시 접근은 없지만 인터럽트로부터 보호가 필요할 때 사용합니다:

#include <asm/local.h>

DEFINE_PER_CPU(local_t, my_counter);

/* 같은 CPU 내에서만 수정 → SMP 동기화 불필요 */
local_inc(this_cpu_ptr(&my_counter));

/* x86에서 local_inc는 lock 접두사 없는 inc → 매우 빠름 */
/* atomic_inc는 lock inc (캐시라인 잠금) → 느림 */

WRITE_ONCE / READ_ONCE

컴파일러의 최적화(load/store 분할, 중복 접근, 재정렬)를 방지합니다:

/* 다른 CPU/인터럽트와 공유하는 변수 접근 시 */
WRITE_ONCE(shared_flag, 1);   /* 단일 store 보장 */
int v = READ_ONCE(shared_flag); /* 단일 load 보장 */

/* 없으면 컴파일러가 이렇게 변환할 수 있음: */
/* shared_flag = 0; shared_flag = 1; (2회 store) */
/* 또는 register에 캐싱하여 재로드 안 함 */

atomic_t 심화 — 내부 구현과 아키텍처 차이

atomic_t 연산은 아키텍처마다 구현이 다릅니다. x86은 LOCK 접두사로 버스 락/캐시 락을 사용하고, ARM은 LDREX/STREX(ARMv7) 또는 LDXR/STXR(ARMv8) LL/SC(Load-Linked/Store-Conditional)를 사용합니다.

/* x86 atomic_add 구현 (arch/x86/include/asm/atomic.h) */
static __always_inline void arch_atomic_add(int i, atomic_t *v)
{
    asm volatile(LOCK_PREFIX "addl %1,%0"
                 : "+m"(v->counter) : "ir"(i) : "memory");
}

/* ARM64 atomic_add 구현 (LL/SC 방식) */
static inline void arch_atomic_add(int i, atomic_t *v)
{
    unsigned long tmp;
    int result;
    asm volatile(
    "1: ldxr   %w0, %2\n"
    "   add    %w0, %w0, %w3\n"
    "   stxr   %w1, %w0, %2\n"
    "   cbnz   %w1, 1b"          /* stxr 실패 시 재시도 */
    : "=&r"(result), "=&r"(tmp), "+Q"(v->counter)
    : "Ir"(i));
}

/* ARM64 LSE (Large System Extensions) — 하드웨어 atomic */
/* ARMv8.1+: stadd, ldadd 등 하드웨어 atomic 명령어 */
static inline void arch_atomic_add(int i, atomic_t *v)
{
    asm volatile(
    "   stadd  %w[i], %[v]\n"
    : [v] "+Q"(v->counter)
    : [i] "r"(i)
    : "memory");
}

atomic 연산의 메모리 순서 보장

연산 변형순서 보장예시사용 시나리오
기본 (fully ordered)전후 배리어 포함atomic_add_return()동기화 지점, 락 구현
_relaxed순서 보장 없음atomic_add_return_relaxed()통계 카운터, 성능 우선
_acquire이후 접근이 재배열 안 됨atomic_add_return_acquire()락 획득, 데이터 읽기 전
_release이전 접근이 재배열 안 됨atomic_add_return_release()락 해제, 데이터 쓰기 후
/* 성능이 중요한 카운터: relaxed 사용 */
atomic_inc(&stats->packets);  /* fully ordered — 불필요하게 느림 */
atomic_add_return_relaxed(1, &stats->packets);  /* 순서 불필요 → 빠름 */

/* 락 구현 패턴: acquire/release */
/* 락 획득: 이후 코드가 앞으로 재배열되면 안 됨 */
while (atomic_cmpxchg_acquire(&lock, 0, 1) != 0)
    cpu_relax();

/* 크리티컬 섹션 */
shared_data = new_value;

/* 락 해제: 이전 코드가 뒤로 재배열되면 안 됨 */
atomic_set_release(&lock, 0);

refcount_t 심화 — atomic_t와의 차이

/* refcount_t는 참조 카운팅 전용으로 atomic_t보다 안전 */
/* 차이점: */
/* 1. 0 → 1 전환 방지 (use-after-free 방지) */
/* 2. 오버플로/언더플로 감지 (REFCOUNT_FULL 시) */
/* 3. 포화(saturation) 동작으로 corruption 방지 */

struct my_obj {
    refcount_t ref;
    struct kref kref;  /* 또는 kref 사용 */
};

/* refcount_t API */
refcount_set(&obj->ref, 1);             /* 초기값 설정 */
refcount_inc(&obj->ref);                /* 증가 (0에서 증가 시 WARN) */
bool last = refcount_dec_and_test(&obj->ref);  /* 감소 후 0이면 true */
if (last) kfree(obj);

/* 안전한 증가: 0이 아닐 때만 */
if (!refcount_inc_not_zero(&obj->ref))
    return NULL;  /* 이미 해제된 객체 */

/* kref: refcount_t의 고수준 래퍼 */
kref_init(&obj->kref);
kref_get(&obj->kref);
kref_put(&obj->kref, my_obj_release);  /* 0 도달 시 release 콜백 */

static void my_obj_release(struct kref *kref)
{
    struct my_obj *obj = container_of(kref, struct my_obj, kref);
    kfree(obj);
}

refcount_t 주의사항:

  • atomic_t를 참조 카운팅에 사용하지 마십시오 — refcount_t가 use-after-free와 오버플로를 방지합니다
  • refcount_inc()은 0에서 호출하면 WARN 출력. 반드시 refcount_inc_not_zero() 사용
  • RCU와 결합 시: rcu_read_lock() 내에서 refcount_inc_not_zero()로 안전하게 참조 획득
  • CONFIG_REFCOUNT_FULL=y (커널 4.x) 또는 기본 (5.x+)으로 모든 검사 활성화

READ_ONCE / WRITE_ONCE 심화

/* include/asm-generic/rwonce.h */
/* 목적: 컴파일러가 메모리 접근을 최적화하지 못하도록 방지 */
/* 1. 접근 분할(tearing) 방지: 단일 load/store 보장 */
/* 2. 접근 병합(merging) 방지: 여러 접근을 하나로 합치지 않음 */
/* 3. 접근 삽입(invention) 방지: 없는 접근을 만들지 않음 */
/* 4. volatile 의미론을 정확한 지점에만 적용 */

/* BAD: 컴파일러가 최적화할 수 있음 */
while (flag == 0)  /* 컴파일러: "flag는 안 변해" → 무한 루프 */
    cpu_relax();

/* GOOD: 매번 메모리에서 읽기 강제 */
while (READ_ONCE(flag) == 0)
    cpu_relax();

/* BAD: 컴파일러가 쓰기를 재배열하거나 제거할 수 있음 */
flag = 1;   /* 다른 CPU에서 관찰 불가능할 수 있음 */

/* GOOD: 단일 store 보장 */
WRITE_ONCE(flag, 1);

/* 주의: READ_ONCE/WRITE_ONCE는 CPU 간 순서를 보장하지 않음 */
/* CPU 간 가시성이 필요하면 메모리 배리어도 함께 사용 */
WRITE_ONCE(data, new_value);
smp_wmb();                    /* 쓰기 배리어 */
WRITE_ONCE(data_ready, 1);

/* 읽는 쪽 */
if (READ_ONCE(data_ready)) {
    smp_rmb();                /* 읽기 배리어 */
    val = READ_ONCE(data);     /* 최신 data 보장 */
}

메모리 배리어 심화

배리어범위목적비용
mb()모든 CPU + 디바이스전체 순서 보장매우 높음
rmb()모든 CPU + 디바이스읽기 순서 보장높음
wmb()모든 CPU + 디바이스쓰기 순서 보장높음
smp_mb()CPU 간전체 순서 (SMP만)중간 (UP에서는 barrier())
smp_rmb()CPU 간읽기 순서 (SMP만)낮음~중간
smp_wmb()CPU 간쓰기 순서 (SMP만)낮음
smp_store_release()CPU 간 (단방향)이전 접근 완료 후 store낮음
smp_load_acquire()CPU 간 (단방향)load 후에만 이후 접근낮음
barrier()컴파일러만컴파일러 재배열 방지없음 (런타임 비용 0)
dma_wmb()DMA 디바이스DMA 디스크립터 쓰기 순서아키텍처 의존
dma_rmb()DMA 디바이스DMA 디스크립터 읽기 순서아키텍처 의존
/* 권장 패턴: smp_store_release / smp_load_acquire 쌍 */
/* smp_wmb/smp_rmb보다 의도가 명확하고 실수 가능성 낮음 */

/* 생산자 (CPU 0) */
buffer[idx] = data;
smp_store_release(&producer_idx, idx + 1);
/* buffer 쓰기가 producer_idx 갱신보다 먼저 보장 */

/* 소비자 (CPU 1) */
unsigned int idx = smp_load_acquire(&producer_idx);
/* idx 읽은 후의 buffer 접근이 재배열 안 됨 */
val = buffer[idx - 1];  /* 올바른 데이터 보장 */
💡

배리어 선택 가이드:

  • CPU 간 데이터 공유: smp_store_release() / smp_load_acquire() 쌍 우선 사용
  • DMA 디스크립터: dma_wmb() / dma_rmb()
  • MMIO 레지스터: mb() / rmb() / wmb() (디바이스 포함 필요)
  • 컴파일러 최적화 방지만: barrier() (가장 가벼움)
  • x86에서 smp_rmb()barrier()와 동일 (x86은 읽기 순서 자동 보장). ARM에서는 실제 배리어 명령어 발행