Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
#한 줄 요약
“Mutex = Mutual Exclusion + Owner” — semaphore 위에 얹힌 owner 추적이 차이의 전부입니다.
#왜 Mutex가 필요한가
// 두 task가 같은 자원 (예: SPI bus) 접근void task_a(void *arg) { while (1) { spi_write(buf_a, 64); // 64 byte 전송 — 도중 preempt 시? }}
void task_b(void *arg) { while (1) { spi_write(buf_b, 64); // ← task_a 도중 끼어들면 데이터 섞임 }}Mutual Exclusion이 필요합니다. 한 task의 SPI 트랜잭션이 끝날 때까지 다른 task는 대기해야 합니다.
SemaphoreHandle_t spi_mutex = xSemaphoreCreateMutex();
void task_a(void *arg) { while (1) { xSemaphoreTake(spi_mutex, portMAX_DELAY); spi_write(buf_a, 64); xSemaphoreGive(spi_mutex); }}#Mutex vs Semaphore — 본질적 차이
| Mutex | Binary Semaphore | |
|---|---|---|
| Counter | 0 또는 1 | 0 또는 1 |
| Owner 추적 | ✓ 누가 lock 했는지 기록 | ✗ |
| Unlock 권한 | Owner만 | 누구나 |
| Priority Inheritance | ✓ 지원 | ✗ |
| Recursive (nested lock) | ✓ (option) | ✗ |
| ISR에서 | ✗ (owner 없음) | ✓ (give만) |
| 사용 목적 | Mutual exclusion | Signal |
핵심은 owner 추적입니다. Mutex는 누가 잠갔는지 압니다. 이 정보가 priority inheritance, recursive, unlock 권한의 토대가 됩니다.
#Owner 추적 — 내부 구조
typedef struct { int locked; // 0=free, 1=locked Task_t *owner; // lock 보유 task int recursion_count; // 같은 task의 재진입 횟수 int original_priority; // owner의 원래 priority (PI 시 복원용) List_t wait_list;} Mutex_t;xSemaphoreGive() 호출 시 호출 task가 owner인지 확인합니다. 아니면 error를 내거나 무시합니다.
#Recursive Mutex
같은 task가 여러 번 lock 할 수 있습니다. lock한 횟수만큼 unlock 해야 풀립니다.
SemaphoreHandle_t mtx = xSemaphoreCreateRecursiveMutex();
void inner(void) { xSemaphoreTakeRecursive(mtx, portMAX_DELAY); /* ... */ xSemaphoreGiveRecursive(mtx);}
void outer(void *arg) { xSemaphoreTakeRecursive(mtx, portMAX_DELAY); // count=1 inner(); // count=2 → 1 xSemaphoreGiveRecursive(mtx); // count=0 → unlock}💡 Recursive mutex는 코드 구조가 나쁘다는 신호일 때가 많습니다. 가능하면 비재귀로 설계하는 것이 좋습니다.
#Priority Inheritance — Mars Pathfinder
#시나리오 (1997 NASA)
T_high (priority 5) ←─ semaphore wait (T_low가 보유)T_med (priority 3) ←─ 실행 중 — T_low를 preemptT_low (priority 1) ←─ semaphore 보유, preempted
→ T_high가 *영원히 대기* (T_med이 끝나야 T_low 실행)화성 탐사선이 지속적으로 reset 됐던 원인이 바로 이것이었습니다.
#해결 — Priority Inheritance
Mutex 보유자가 대기자의 priority를 일시적으로 상속받습니다.
T_high (priority 5) ←─ mutex wait → T_low의 priority를 5로 boostT_low (priority 5 임시) ←─ 실행 가능 → mutex 해제T_high 진행FreeRTOS에서는 configUSE_MUTEX_PI = 1이 기본값입니다.
#알고리즘
void mutex_take(Mutex_t *m, TickType_t timeout) { portENTER_CRITICAL(); if (m->locked == 0) { m->locked = 1; m->owner = current_task; m->original_priority = current_task->priority; portEXIT_CRITICAL(); return; } // PI — owner의 priority를 자신과 같게 올림 if (current_task->priority > m->owner->priority) { m->owner->priority = current_task->priority; rebalance_ready_lists(m->owner); } add_to_wait_list(m->wait_list, current_task); portEXIT_CRITICAL(); block(timeout);}
void mutex_give(Mutex_t *m) { portENTER_CRITICAL(); if (current_task != m->owner) { portEXIT_CRITICAL(); return ERROR_NOT_OWNER; } current_task->priority = m->original_priority; // 복원 if (!list_empty(m->wait_list)) { Task_t *next = pop_highest(m->wait_list); m->owner = next; m->original_priority = next->priority; wake(next); } else { m->locked = 0; m->owner = NULL; } portEXIT_CRITICAL();}#Priority Ceiling Protocol (PCP) — PI의 대안
각 mutex에 priority ceiling을 정적으로 할당합니다. 이 값은 해당 mutex를 lock 할 수 있는 가장 높은 priority입니다.
// Mutex의 ceiling = 5 (T_high의 priority)T_low가 mutex lock 시 → 즉시 priority를 5로 boostT_med (priority 3)은 *시작도 못 함*장점은 PI보다 단순하고 deadlock을 방지한다는 점입니다. 단점은 priority를 미리 알아야 한다는 점입니다(정적).
VxWorks와 일부 RTOS가 채택했고, FreeRTOS는 PI만 지원합니다.
#Deadlock — Mutex 사용의 어둠
#예 — Lock Ordering 위반
// Task AxSemaphoreTake(mutex_X, ...);xSemaphoreTake(mutex_Y, ...);
// Task BxSemaphoreTake(mutex_Y, ...);xSemaphoreTake(mutex_X, ...);
// A는 X 보유 후 Y 기다림, B는 Y 보유 후 X 기다림 → 영원해결책은 항상 같은 순서로 lock 하는 것입니다. 글로벌 lock order를 정해두고 모든 코드가 이를 준수해야 합니다.
#Timeout 사용
if (xSemaphoreTake(mtx, pdMS_TO_TICKS(100)) != pdTRUE) { log_warning("mutex timeout — possible deadlock"); return ERROR;}portMAX_DELAY 대신 유한 timeout을 쓰면 deadlock을 감지하고 복구할 수 있습니다.
#Mutex Hold Time — 짧게
// 나쁨xSemaphoreTake(mtx, portMAX_DELAY);log_data(); // 5 ms (printf 등)xSemaphoreGive(mtx);
// 좋음 — 자원 접근만 protectxSemaphoreTake(mtx, portMAX_DELAY);data = shared_resource;xSemaphoreGive(mtx);log_data(); // mutex 밖에서Mutex hold time이 길수록 다른 task가 더 오래 대기합니다. µs 단위로 유지해야 합니다.
#ISR에서 Mutex 사용 불가
void some_ISR(void) { xSemaphoreTakeFromISR(mtx, ...); // ✗ 컴파일 에러 또는 crash}ISR은 owner가 될 task가 없으므로 mutex 의미 자체가 성립하지 않습니다. ISR과 task 사이의 signal에는 semaphore를 씁니다.
#자주 하는 실수
⚠️ Owner 확인 없이 give
Non-owner가 give를 시도하면 error가 발생합니다. 코드 검토 시 take/give를 한 함수에 묶거나 RAII pattern을 활용합니다.
⚠️ PI 비활성으로 critical 자원 보호
configUSE_MUTEX_PI = 0이면 Mars Pathfinder가 재현될 수 있습니다. 기본값이 enabled이므로 그대로 두는 것이 좋습니다.
⚠️ Recursive mutex 남용
비재귀 mutex로 충분한 코드까지 recursive로 만드는 경우가 많습니다. 재진입이 정말 필요한 경우에만 써야 합니다.
⚠️ Mutex hold 중 long blocking call
다른 mutex take나 queue receive(timeout=infinite)를 하면 cascading wait가 발생합니다.
#정리
- Mutex는 Mutual Exclusion + Owner 구조입니다.
- Owner 추적이 Priority Inheritance의 토대입니다.
- Recursive mutex는 코드 구조의 경고 신호일 때가 많으므로, 가능하면 비재귀로 설계합니다.
- Mars Pathfinder 사례는 PI 미적용 시 발생할 수 있는 임베디드 사고를 보여 줍니다.
- ISR에서는 사용할 수 없습니다(owner가 없으므로).
- Hold time은 짧게, lock order는 일관되게 유지합니다.
다음 편에서는 큐와 메시지 패싱으로 Producer-Consumer와 Ring Buffer를 다룹니다.
#관련 항목
Practical RTOS Internals · 9 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
관련 글
Priority Inheritance 구현 — Inherit·Disinherit·Chain
FreeRTOS PI 코드 분석 — vTaskPriorityInherit, vTaskPriorityDisinherit, chain handling.
Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
Mutex = Semaphore + pxMutexHolder + uxBasePriority. Recursive variant는 lock-count.
동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
공유 자원 보호의 3가지 도구로 interrupt disable, spinlock, mutex가 있습니다. 언제 어느 것을 쓰는지 정리합니다.
이 글을 참조하는 글 (6)
- Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded— Practical RTOS Internals
- Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지— Practical RTOS Internals
- 큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미— Practical RTOS Internals
- Semaphore 개념 분해 — Counting·Binary·P/V 연산— Practical RTOS Internals
- Spinlock vs Mutex 결정 가이드 — Context Switch·Hold Time— Modern Embedded Recipes
- RTOS Mutex 활용 — Recursive·Priority Inheritance 적용— Modern Embedded Recipes