낮은 우선순위 태스크가 높은 태스크가 기다리는 자원을 갖고 있으면 그 태스크의 우선순위를 일시적으로 올려 주는 기법(priority inheritance).
동작
-
초기: H(우선순위 1) M(2) L(3)
-
① L이 뮤텍스 획득 (우선순위 3으로 실행)
-
② H가 뮤텍스 요청 → L이 갖고 있음
-
③ L의 우선순위를 3 → 1로 승격 ★ 상속
-
④ M(2)이 준비돼도 L(1)을 선점하지 못한다
-
⑤ L이 임계 구역을 끝내고 뮤텍스 해제
-
⑥ L의 우선순위를 3으로 복원
-
⑦ H가 뮤텍스를 얻어 실행
"기다리게 만드는 놈을 끌어올려 빨리 끝내게 한다" 가 발상이다.
왜 뮤텍스만 지원하나 — 소유권
- 뮤텍스 — : 잠근 태스크가 명확하다 (소유자)
- "누구의 우선순위를 올려야 하는지" 알 수 있다
세마포어 : 소유자가 없다. 아무나 give 할 수 있다
- 올려 줄 대상을 특정할 수 없다
소유권이 있어야 상속이 가능하다. 이것이 "상호 배제에는 세마포어가 아니라 뮤텍스를 써야 한다"는 조언의 기술적 근거다.
FreeRTOS에서
// 뮤텍스 — 우선순위 상속 O
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
xSemaphoreTake(mtx, portMAX_DELAY);
// 임계 구역
xSemaphoreGive(mtx);
// 이진 세마포어 — 상속 X
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
configUSE_MUTEXES = 1이어야 뮤텍스를 쓸 수 있다.
한계와 주의점
① 여전히 제한된 역전은 남는다
H의 최악 대기 시간 = L의 임계 구역 실행 시간
상속은 M의 개입을 막을 뿐 L이 일을 마치는 시간은 줄이지 못한다. → 임계 구역을 짧게 유지하는 것이 여전히 가장 중요하다.
② 연쇄 블로킹 (chained blocking)
H가 뮤텍스 A와 B를 순서대로 필요로 하고 L1이 A를, L2가 B를 갖고 있으면 H는 두 번 기다린다 → 대기 시간이 누적된다
③ 데드락은 막지 못한다
T1: A 잠금 → B 요청
T2: B 잠금 → A 요청
우선순위와 무관한 순환 대기다. 잠그는 순서를 통일해야 한다.
④ 중첩 상속의 복잡도
여러 단계로 상속이 전파되면 우선순위 계산이 복잡해지고, 구현에 따라 오버헤드가 늘어난다.
우선순위 상한과의 비교
| 우선순위 상속 | 우선순위 상한 | |
|---|---|---|
| 시점 | 경쟁이 발생했을 때 올린다 | 잡는 즉시 올린다 |
| 사전 분석 | 불필요 | 필요 (상한값을 정해야) |
| 불필요한 승격 | 없다 | 있다 (경쟁이 없어도 올라감) |
| 데드락 방지 | ✗ | ○ |
| 연쇄 블로킹 | 발생 | 방지 |
| 구현 | 간단 | 복잡 |
상한 프로토콜은 데드락까지 막는다 — 모든 태스크가 상한 이상으로 올라가야 자원을 잡으므로 순환 대기가 성립하지 않는다.
실무에서는 상속이 훨씬 널리 쓰인다. 사전 분석 부담이 없고 대부분의 경우 충분하기 때문이다. 안전 인증이 필요한 시스템에서는 상한을 쓰기도 한다.
확인하는 법
// 디버거나 로그로 태스크의 현재 우선순위를 확인
UBaseType_t p = uxTaskPriorityGet(handle);
상속 중에는 원래보다 높은 값이 보인다. "왜 이 태스크의 우선순위가 다르지?"라고 당황하지 않으려면 이 동작을 알고 있어야 한다.
설계 원칙
- ① 우선순위 차이가 큰 태스크끼리 자원을 공유하지 않는다 ← 최선
- ② 공유해야 하면 뮤텍스를 쓴다 (상속 활성)
- ③ 임계 구역을 최소화한다
- ④ 여러 뮤텍스를 잡을 때는 항상 같은 순서로
- ⑤ 최악 실행 시간 분석에 "제한된 역전"을 포함한다