Atomic 연산 (Atomic Operations)
Linux 커널 atomic_t, 메모리 배리어, memory ordering, cmpxchg, per-CPU 변수 종합 가이드.
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와 컴파일러는 성능 최적화를 위해 명령어 순서를 변경합니다. 메모리 배리어는 특정 지점에서 순서를 강제합니다.
/* 컴파일러 배리어 (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 | 완전한 순서 보장 (가장 무거움) |
_relaxed | None | 원자성만 보장, 순서 없음 (가장 가벼움) |
_acquire | Acquire | 이후 읽기/쓰기가 이 연산 전으로 이동 불가 |
_release | Release | 이전 읽기/쓰기가 이 연산 후로 이동 불가 |
/* 성능이 중요한 경우: 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에서는 실제 배리어 명령어 발행