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

Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout

· Hawk · 4분 읽기

#한 줄 요약

“Deadlock은 네 조건이 동시에 성립할 때 발생합니다.” 그중 하나만 깨도 회피할 수 있습니다.

#Coffman의 네 조건 (1971)

조건의미어떻게 깰까
Mutual Exclusion자원이 한 task만 보유Mutex 대신 lock-free 사용
Hold and Wait자원 보유 중 다른 자원 요청Try-lock 또는 atomic acquire
No Preemption강제 회수 불가Timeout과 rollback
Circular WaitA→B→C→A 순환 대기Lock ordering

네 가지를 모두 만족할 때만 deadlock이 성립합니다. 하나라도 깨면 회피할 수 있습니다.

#전형적 시나리오: 2 Mutex 순환

SemaphoreHandle_t mtx_A, mtx_B;
void task1(void *p) {
xSemaphoreTake(mtx_A, portMAX_DELAY);
vTaskDelay(1); // 짧은 지연
xSemaphoreTake(mtx_B, portMAX_DELAY); // ← Task2가 잡고 있으면 deadlock
/* ... */
}
void task2(void *p) {
xSemaphoreTake(mtx_B, portMAX_DELAY);
vTaskDelay(1);
xSemaphoreTake(mtx_A, portMAX_DELAY); // ← Task1이 잡고 있으면 deadlock
}

Task1은 A를 잡은 채 B를 기다리고, Task2는 B를 잡은 채 A를 기다립니다. 두 task가 서로를 영원히 막아 시스템이 정지합니다.

#Wait-for Graph

T1 ──── waiting for ────→ T2
↑ │
│ holds
holds waiting for
│ ↓
mtx_A ←── waiting for ── mtx_B

Graph에 cycle이 있으면 곧 deadlock입니다. RTOS는 이 graph가 동적으로 만들어지므로 컴파일 타임에 확인할 수 없습니다.

#해결 1: Lock Ordering (가장 흔한 방법)

규칙은 간단합니다. 모든 mutex에 전역 순서를 부여하고, 항상 낮은 순서부터 take합니다.

/* Convention — 알파벳/주소/ID 기준 */
#define MTX_ORDER_A 0
#define MTX_ORDER_B 1
#define MTX_ORDER_C 2
void any_task(void) {
/* 항상 A → B → C 순서 */
xSemaphoreTake(mtx_A, ...);
xSemaphoreTake(mtx_B, ...);
xSemaphoreTake(mtx_C, ...);
/* ... */
xSemaphoreGive(mtx_C);
xSemaphoreGive(mtx_B);
xSemaphoreGive(mtx_A);
}

이 방식의 장점은 컴파일 타임에 도구나 문서로 검증할 수 있다는 점입니다. Linux 커널이나 DB 엔진에서도 표준 관행입니다.

#해결 2: Timeout으로 No Preemption 깨기

if (xSemaphoreTake(mtx_A, pdMS_TO_TICKS(100)) != pdPASS) {
log_warning("mtx_A acquire timeout — possible deadlock");
return ERROR_TIMEOUT;
}
if (xSemaphoreTake(mtx_B, pdMS_TO_TICKS(100)) != pdPASS) {
xSemaphoreGive(mtx_A); // 보유 자원 release
return ERROR_TIMEOUT;
}

감지와 복구를 한 번에 처리할 수 있습니다. 모든 lock acquisition에 유한 timeout을 지정하고, portMAX_DELAY 사용은 자제해야 합니다.

#해결 3: Try-Lock과 Rollback으로 Hold and Wait 깨기

if (!xSemaphoreTake(mtx_A, 0)) return EBUSY;
if (!xSemaphoreTake(mtx_B, 0)) {
xSemaphoreGive(mtx_A);
return EBUSY;
}
/* both acquired — work */
xSemaphoreGive(mtx_B);
xSemaphoreGive(mtx_A);

또는 all-or-nothing 패턴으로 작성합니다.

bool try_acquire_all(void) {
if (!xSemaphoreTake(mtx_A, 0)) return false;
if (!xSemaphoreTake(mtx_B, 0)) {
xSemaphoreGive(mtx_A);
return false;
}
return true;
}
/* Retry pattern with backoff */
while (!try_acquire_all()) {
vTaskDelay(pdMS_TO_TICKS(random_backoff()));
}

#해결 4: Hierarchical Locking (Linux lockdep 방식)

각 lock에 level을 부여합니다. 같은 level의 lock을 동시에 보유할 수 없고, 다른 level은 오름차순으로만 잡을 수 있습니다.

typedef struct {
SemaphoreHandle_t sem;
int level;
const char *name;
} hier_mutex_t;
bool hier_take(hier_mutex_t *m, TickType_t timeout) {
/* TLS 또는 TCB 확장에 현재 task의 보유 level set 보관 */
for (each held lock h) {
if (h.level >= m->level) {
panic("Lock order violation: hold %s (L%d), taking %s (L%d)",
h.name, h.level, m->name, m->level);
}
}
xSemaphoreTake(m->sem, timeout);
record_held(m);
return true;
}

Runtime에서 lock 순서를 검증하는 방식입니다. Linux lockdep이 이 기법으로 실제 deadlock이 발생하기 전에 위반을 잡아냅니다.

#Reader-Writer Lock Deadlock

rwlock_t rw;
read_lock(&rw);
/* ... */
write_lock(&rw); // ✗ Recursive read-then-write

같은 task가 read lock을 보유한 상태에서 write lock을 요청하면 자기 자신을 기다리는 deadlock에 빠집니다. recursive variant나 lock upgrade API로 해결합니다.

#ABA 변형: Priority Inversion 결합

Low task L: mtx 보유, M에 의해 선점
Med task M: CPU 점유 (mtx 안 씀)
High task H: mtx 대기 → L 못 풀고 M이 CPU → unbounded wait

엄밀히 deadlock은 아니지만 효과는 비슷합니다. Priority Inheritance로 해결합니다 (3-05 참고).

#검출 도구

#FreeRTOS Trace + Sysview (Segger)

/* configUSE_TRACE_FACILITY = 1 */
TaskStatus_t arr[20];
uxTaskGetSystemState(arr, 20, NULL);
for (int i = 0; i < N; i++) {
if (arr[i].eCurrentState == eBlocked) {
printf("Task %s blocked on %p\n",
arr[i].pcTaskName, arr[i].xEventListItem);
}
}

모든 task가 blocked 상태이고 tick도 진행되지 않으면 deadlock을 의심합니다.

#Static Analysis (Coverity, Klocwork)

함수 호출 그래프와 lock acquire 순서를 함께 분석해 lock order inconsistency를 경고합니다.

#Runtime Watchdog

void wd_task(void *p) {
for (;;) {
vTaskDelay(pdMS_TO_TICKS(1000));
if (system_alive_flag == 0) {
log_panic("Possible deadlock — resetting");
HAL_NVIC_SystemReset();
}
system_alive_flag = 0;
}
}
void main_task(void *p) {
for (;;) {
do_work();
system_alive_flag = 1;
}
}

Lock acquisition timeout과 watchdog reset의 조합은 최후의 보루입니다.

#Livelock: Deadlock의 사촌

while (try_take_all() == false) {
release_all();
vTaskDelay(1); // 양보
}

Task A와 Task B가 동시에 release하고 다시 retry하는 패턴이 반복되면 영원히 진전이 없습니다. random backoff로 해결합니다.

vTaskDelay(pdMS_TO_TICKS(rand() % 10));

#자주 하는 실수

⚠️ portMAX_DELAY를 남용하는 경우

xSemaphoreTake(mtx, portMAX_DELAY); // ✗ deadlock 감지 불가

Production code에는 반드시 유한한 timeout을 지정해야 합니다. Debug build에서만 무한 대기를 허용합니다.

⚠️ Recursive lock을 잘못 사용하는 경우

xSemaphoreTake(mtx, ...);
/* ... */
xSemaphoreTake(mtx, ...); // ✗ Self-deadlock (mutex 아닌 binary semaphore)

같은 task가 mutex를 두 번 잡는 패턴이 필요하면 recursive variant를 명시적으로 사용해야 합니다.

⚠️ ISR과 task가 lock을 공유하는 경우

xSemaphoreTake(mtx, ...);
some_isr_disable_func();
/* ISR 발생 → 같은 mtx 시도 → ✗ */

ISR과 task 간 동기화에는 mutex가 아니라 queue, event group, semaphore를 사용합니다.

⚠️ Lock 보유 중 long-blocking API를 호출하는 경우

xSemaphoreTake(mtx_A, ...);
xQueueReceive(q, &item, portMAX_DELAY); // ✗ mtx_A 보유 중 무한 대기

Lock hold time은 짧게 유지하고, blocking call 전에는 반드시 release합니다.

#정리

  • Deadlock은 Coffman의 네 조건이 동시에 성립할 때 발생합니다.
  • Lock ordering이 가장 실용적인 해결책이며 모든 lock에 전역 순서를 부여합니다.
  • Timeout과 rollback으로 감지하고 복구할 수 있습니다.
  • Hierarchical lock으로 runtime에 lock 순서를 검증할 수 있습니다.
  • 모든 production lock에는 유한 timeout과 watchdog을 함께 두어야 합니다.

다음 part에서는 memory management를 다룹니다. heap, stack, MPU 순서로 살펴봅니다.

#관련 항목

Practical RTOS Internals · 32 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