본문으로 건너뛰기
Practical RTOS Internals · 8/53

Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance

· Hawk · 4분 읽기

#한 줄 요약

“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 — 본질적 차이

MutexBinary Semaphore
Counter0 또는 10 또는 1
Owner 추적✓ 누가 lock 했는지 기록
Unlock 권한Owner만누구나
Priority Inheritance✓ 지원
Recursive (nested lock)✓ (option)
ISR에서✗ (owner 없음)✓ (give만)
사용 목적Mutual exclusionSignal

핵심은 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

Priority Inversion (without/with PI) timeline

#시나리오 (1997 NASA)

T_high (priority 5) ←─ semaphore wait (T_low가 보유)
T_med (priority 3) ←─ 실행 중 — T_low를 preempt
T_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로 boost
T_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로 boost
T_med (priority 3)*시작도 못 함*

장점은 PI보다 단순하고 deadlock을 방지한다는 점입니다. 단점은 priority를 미리 알아야 한다는 점입니다(정적).

VxWorks와 일부 RTOS가 채택했고, FreeRTOS는 PI만 지원합니다.

#Deadlock — Mutex 사용의 어둠

#예 — Lock Ordering 위반

// Task A
xSemaphoreTake(mutex_X, ...);
xSemaphoreTake(mutex_Y, ...);
// Task B
xSemaphoreTake(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);
// 좋음 — 자원 접근만 protect
xSemaphoreTake(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

  1. 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
  2. 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
  3. 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
  4. 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
  5. 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
  6. 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
  7. 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
  8. 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
  9. 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
  10. 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
  11. 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
  12. 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
  13. 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
  14. 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
  15. 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
  16. 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
  17. 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
  18. 18RISC-V Context Switch 분석 — ECALL·mret·CSR
  19. 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
  20. 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
  21. 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
  22. 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
  23. 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
  24. 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
  25. 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
  26. 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
  27. 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
  28. 28Priority Ceiling Protocol — Immediate vs Original 비교
  29. 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
  30. 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
  31. 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
  32. 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
  33. 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
  34. 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
  35. 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
  36. 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
  37. 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
  38. 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
  39. 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
  40. 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
  41. 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
  42. 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
  43. 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
  44. 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
  45. 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
  46. 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
  47. 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
  48. 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
  49. 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
  50. 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
  51. 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
  52. 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
  53. 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL