동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
#한 줄 요약
“공유 데이터엔 동기화” — Race condition은 간헐 버그의 1위입니다. 짧고 정확한 보호가 핵심입니다.
#Race Condition — 가장 까다로운 버그
// 두 task가 같은 counter 증가volatile uint32_t counter = 0;
void task_a(void *arg) { while (1) { counter++; vTaskDelay(1); }}
void task_b(void *arg) { while (1) { counter++; vTaskDelay(1); }}counter++이 single instruction 같지만, 어셈블리로 보면:
LDR r0, [counter] ; (1) readADD r0, r0, #1 ; (2) incrementSTR r0, [counter] ; (3) write3 단계 사이에 preempt 되면 다른 task가 같은 값을 read 하면서 증가분 하나를 잃습니다.
시점 →Task A: read=5 . . . . . . . . . . . . . . . . . . increment=6, write=6Task B: . . . . . . read=5 . . . increment=6, write=6 . . . . . .결과: counter = 6 (7이 맞음)#Critical Section — 보호 구간
“이 코드 블록은 원자적으로 실행한다”는 목표를 달성하기 위한 3가지 도구를 살펴봅니다.
#1. Interrupt Disable
가장 강력합니다. ISR도 막습니다. 짧게(수십 µs) 유지해야 합니다.
__disable_irq();counter++;__enable_irq();
// 또는 FreeRTOS APItaskENTER_CRITICAL();counter++;taskEXIT_CRITICAL();#장단점
- ✓ ISR과도 안전합니다
- ✓ Spin·context switch가 없습니다
- ✗ ISR latency가 늘어 실시간성이 떨어집니다
- ✗ Long work는 금지입니다
#2. Spinlock (SMP)
여러 코어에서 동작합니다. busy-wait 방식입니다.
spin_lock(&lock);shared_data = value;spin_unlock(&lock);#장단점
- ✓ 짧은 critical section에 효율적입니다
- ✓ Context switch가 없어 latency가 결정적입니다
- ✗ SMP에서만 의미가 있고, 단일 코어에서는 무의미합니다
- ✗ Hold time이 길어지면 다른 코어가 spin 합니다
#3. Mutex (Task 간)
Blocking 방식입니다. 대기 task가 Blocked 상태로 전환됩니다.
xSemaphoreTake(mutex, portMAX_DELAY);shared_data = value;xSemaphoreGive(mutex);#장단점
- ✓ Long critical section도 가능합니다
- ✓ Priority inheritance를 지원합니다 (Mars Pathfinder 해결)
- ✗ Context switch overhead가 있습니다
- ✗ ISR에서는 사용할 수 없습니다
#선택 기준
| 상황 | 도구 |
|---|---|
| ISR과 task 공유, ≤ 10 µs | Interrupt disable |
| Task 간 공유, > 100 µs work | Mutex |
| SMP 짧은 작업 | Spinlock |
| Lock-free 가능 | atomic API |
#Atomic Operations
CPU가 원자성을 보장하는 명령입니다. 짧고 빠릅니다.
#include <stdatomic.h>
atomic_int counter = 0;atomic_fetch_add(&counter, 1); // counter++ atomicARMv7+ ldrex/strex (Load-Exclusive·Store-Exclusive):
loop: LDREX r0, [counter] ADD r0, r0, #1 STREX r1, r0, [counter] CMP r1, #0 ; STREX 성공? BNE loop ; 실패 시 재시도CAS (Compare-And-Swap) 변형도 있습니다. Lock-free 자료구조의 토대입니다.
#Memory Ordering — Reordering 함정
ARM·RISC-V는 relaxed memory model을 따르므로 컴파일러와 CPU가 명령 순서를 바꿀 수 있습니다.
// Producerdata = 42; // (1)ready = 1; // (2)
// Consumerwhile (!ready); // (3)use(data); // (4) — 42 받는 보장 없음!(1)과 (2)의 write order가 바뀌면 (4)에서 garbage를 읽게 됩니다. 해결책은 memory barrier입니다.
data = 42;__sync_synchronize(); // 또는 std::atomic_thread_fenceready = 1;ARM은 DMB ST (Data Memory Barrier, Store)를 사용하고, x86은 기본적으로 strong order를 가집니다.
#False Sharing — Cache 함정
struct { int counter_a; // CPU 0이 자주 쓰기 int counter_b; // CPU 1이 자주 쓰기} shared; // 같은 cache line 64 byteCPU 0이 counter_a에 write 하면 cache line 전체가 CPU 0의 cache로 들어옵니다. CPU 1이 counter_b에 write 하면 line이 CPU 1로 이동합니다. 코어 간 ping-pong이 발생합니다. 해결책은 padding입니다.
struct { alignas(64) int counter_a; alignas(64) int counter_b;};#Critical Section 길이 — 권장
| 작업 | 추정 시간 | 적합 도구 |
|---|---|---|
| 변수 1개 update | 50 ns | atomic (또는 IRQ disable) |
| 구조체 update (수십 byte) | 1 µs | IRQ disable 또는 mutex |
| 1 KB 데이터 copy | 10 µs | mutex |
| File I/O, network | ms | mutex (절대 IRQ disable 금지) |
IRQ disable은 최대 50 µs까지가 안전합니다. 그 이상 길어지면 ISR이 막혀 interrupt loss 위험이 있습니다.
#Volatile — 동기화 ≠
volatile은 컴파일러 최적화 방지 역할만 합니다. Atomic도 아니고 memory order 보장도 아닙니다.
volatile int counter = 0;counter++; // 여전히 3-instruction → race conditionvolatile은 MMIO register access(HW)나 interrupt-shared flag 같은 single-byte flag에만 씁니다.
#FreeRTOS API 요약
| API | 효과 |
|---|---|
taskENTER_CRITICAL() | IRQ mask (BASEPRI) + scheduler suspend |
taskEXIT_CRITICAL() | 복원 |
taskENTER_CRITICAL_FROM_ISR() | ISR 내 critical section |
vTaskSuspendAll() | Scheduler 정지 (IRQ는 활성) |
xTaskResumeAll() | 복원 |
portDISABLE_INTERRUPTS() | IRQ 완전 mask |
#자주 하는 실수
⚠️
volatile로 race condition 해결 시도
위에서 설명했듯이 atomic API 또는 critical section이 필요합니다.
⚠️ Critical section 안에서 long work
100 ms 작업을 critical section 안에 넣으면 그 동안 모든 ISR과 task가 막힙니다. 짧게 유지해야 합니다.
⚠️ Memory barrier 누락
ARM·RISC-V relaxed model에서는 write order가 보장되지 않습니다. 멀티코어·DMA와 공유할 때는 barrier가 필수입니다.
⚠️ Mutex를 ISR에서
ISR에서 mutex take를 시도하면 crash 합니다. Semaphore Give(signal)만 가능합니다.
#정리
- Race condition은 공유 데이터, 동시 접근, 동기화 없음이 합쳐질 때 발생합니다.
- 보호 도구 3종은 IRQ disable, Spinlock(SMP), Mutex입니다.
- 짧은 작업은 IRQ disable, 긴 작업은 mutex가 적합합니다.
- Atomic API가 lock-free의 토대입니다.
volatile, atomic, memory barrier는 셋 다 별개의 개념입니다.
다음 편에서는 Semaphore 개념으로 Counting과 Binary, 사용 패턴을 다룹니다.
#관련 항목
Practical RTOS Internals · 7 of 53
- 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
- 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
- 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
- 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
- 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
- 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
- 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
- 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
- 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
- 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
- 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
- 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
- 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
- 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
- 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
- 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
- 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
- 18RISC-V Context Switch 분석 — ECALL·mret·CSR
- 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
- 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
- 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
- 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
- 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
- 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
- 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
- 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
- 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
- 28Priority Ceiling Protocol — Immediate vs Original 비교
- 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
- 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
- 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
- 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
- 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
- 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
- 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
- 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
- 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
- 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
- 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
- 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
- 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
- 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
- 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
- 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
- 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
- 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
- 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
- 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
- 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
- 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
- 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
- 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
- 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
관련 글
Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
FreeRTOS Queue 코드 — pcWriteTo·pcReadFrom·uxMessagesWaiting + xTasksWaitingToSend/Receive.
Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
Mutex = Semaphore + pxMutexHolder + uxBasePriority. Recursive variant는 lock-count.
Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
3 가지 구현 — cpsid/BASEPRI mask, taskENTER_CRITICAL, SMP spinlock. Hold time이 latency 결정.