FreeRTOS Semaphore, Mutex, Priority Inversion
Semaphore와 Mutex는 둘 다 동기화 메커니즘이지만 목적이 다르다. Semaphore는 이벤트 신호 전달, Mutex는 공유 자원 보호에 사용한다.
Binary Semaphore
“이벤트가 발생했다”를 알리는 용도다. 값은 0 또는 1이다.
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
// 생성 직후 값은 0 (비어 있음)
// 대기 측 (태스크)
if (xSemaphoreTake(sem, portMAX_DELAY) == pdTRUE) {
// 이벤트 발생 후 실행
}
// 신호 측 (ISR 또는 다른 태스크)
xSemaphoreGive(sem);
// ISR에서는: xSemaphoreGiveFromISR(sem, &woken);
ISR에서 태스크로 이벤트를 알릴 때 가장 흔히 쓴다. Queue와의 차이: 데이터 없이 “발생 여부”만 전달할 때 Semaphore가 적합하다.
주의: Give가 Take보다 먼저 여러 번 호출되면
Binary Semaphore의 값은 최대 1이다. xSemaphoreGive를 3번 호출해도 1 상태를 유지한다. 이후 xSemaphoreTake는 1번만 성공한다. 이벤트 횟수를 세야 하면 Counting Semaphore를 사용한다.
Counting Semaphore
최대 N개까지 카운트하는 Semaphore다.
// 최대 카운트 10, 초기값 0
SemaphoreHandle_t csem = xSemaphoreCreateCounting(10, 0);
// Give 할 때마다 카운트 +1 (최대값까지)
xSemaphoreGive(csem); // 1
xSemaphoreGive(csem); // 2
// Take 할 때마다 카운트 -1 (0이면 블로킹)
xSemaphoreTake(csem, portMAX_DELAY); // 1
xSemaphoreTake(csem, portMAX_DELAY); // 0
사용 사례:
- 이벤트 카운팅: DMA 전송 완료를 카운트. 처리 태스크가 하나씩 소비
- 리소스 풀 관리: 초기값 N으로 생성. Take로 리소스 획득, Give로 반환
Mutex
공유 자원에 대한 **배타적 접근(mutual exclusion)**을 보장한다.
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
// 생성 직후 값은 1 (잠금 해제 상태)
void task_a(void* param) {
while (1) {
if (xSemaphoreTake(mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// 공유 자원 사용
shared_buffer[0] = 'A';
xSemaphoreGive(mutex);
}
}
}
void task_b(void* param) {
while (1) {
if (xSemaphoreTake(mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// 공유 자원 사용
shared_buffer[0] = 'B';
xSemaphoreGive(mutex);
}
}
}
Mutex vs Binary Semaphore
API가 같아 보이지만 내부 동작이 다르다.
| Binary Semaphore | Mutex | |
|---|---|---|
| 목적 | 이벤트 신호 전달 | 공유 자원 보호 |
| 소유권 | 없음. 아무나 Give 가능 | 있음. Take한 태스크만 Give 가능 |
| Priority Inheritance | 없음 | 있음 |
| ISR에서 Give | 가능 | 불가 |
| 재귀적 잠금 | 불가 | xSemaphoreCreateRecursiveMutex로 가능 |
소유권이 핵심 차이다. Mutex는 “이 태스크가 잠금을 보유하고 있다”는 정보를 추적한다. 이 정보가 Priority Inheritance를 가능하게 한다.
Priority Inversion
Priority Inversion은 높은 우선순위 태스크가 낮은 우선순위 태스크에 의해 간접적으로 블로킹되는 현상이다.
우선순위 높음: Task H
우선순위 중간: Task M
우선순위 낮음: Task L
1. Task L이 Mutex를 획득하고 공유 자원 사용 중
2. Task H가 Ready → 선점 → Mutex Take 시도 → Blocked (L이 보유 중)
3. Task M이 Ready → Task L을 선점 → Task M 실행
4. Task H는 Task M이 끝나고, Task L이 실행되어 Mutex를 반환할 때까지 대기
Task H가 Task M보다 우선순위가 높은데, Task M 때문에 대기한다. 우선순위가 역전된 것이다.
시간 →
Task H: ████████░░░░░░░░░░░░░░░██████
실행 Blocked (L의 Mutex 대기) 실행
Task M: ░░░░░░░░████████████░░░░░░░░░
실행 (H보다 늦게 끝남)
Task L: ████░░░░░░░░░░░░░░░░████░░░░░
Mutex 선점당함 Mutex Give
Take → H 재개
문제는 Task M이 끼어드는 것이다. Task M이 없으면 Task L이 바로 Mutex를 반환하겠지만, Task M이 Task L을 선점하므로 Task H의 대기 시간이 무한정 늘어날 수 있다.
Priority Inheritance
FreeRTOS의 Mutex는 Priority Inheritance를 자동으로 수행한다.
1. Task L이 Mutex를 획득
2. Task H가 Mutex Take 시도 → Blocked
3. FreeRTOS: Task L의 우선순위를 Task H와 같은 수준으로 일시 상승
4. Task M이 Ready → 하지만 Task L이 이제 더 높은 우선순위 → Task L 계속 실행
5. Task L이 Mutex Give → 우선순위 원래대로 복원 → Task H 즉시 실행
Task L이 Mutex를 보유하는 동안만 일시적으로 우선순위가 올라간다. Mutex를 반환하면 원래 우선순위로 돌아간다.
Binary Semaphore는 소유권이 없으므로 Priority Inheritance를 수행할 수 없다. 공유 자원 보호에 Binary Semaphore 대신 Mutex를 사용해야 하는 이유다.
Priority Inheritance의 한계
Priority Inheritance는 unbounded priority inversion을 방지하지만, priority inversion 자체를 완전히 없애지는 않는다. Task H는 여전히 Task L이 Mutex를 반환할 때까지 대기해야 한다. 대기 시간이 Task L의 임계 구간(critical section) 길이로 제한될 뿐이다.
따라서 Mutex를 보유한 상태에서의 실행 시간을 최소화하는 것이 중요하다.
// 나쁜 예: Mutex 내에서 긴 작업
xSemaphoreTake(mutex, portMAX_DELAY);
read_sensor(); // 10ms
process_data(); // 50ms
update_shared_state(); // 즉시
xSemaphoreGive(mutex);
// 좋은 예: 공유 자원 접근만 Mutex로 보호
read_sensor();
process_data();
xSemaphoreTake(mutex, portMAX_DELAY);
update_shared_state(); // 즉시
xSemaphoreGive(mutex);
메모
xSemaphoreCreateMutex로 만든 Mutex만 Priority Inheritance가 동작한다.xSemaphoreCreateBinary는 동작하지 않는다- Mutex를 ISR에서 Give/Take하면 안 된다. 소유권 추적과 Priority Inheritance가 ISR 컨텍스트에서는 의미가 없다
xSemaphoreCreateRecursiveMutex는 같은 태스크가 여러 번 Take할 수 있다. 같은 횟수만큼 Give해야 해제된다. 재귀 함수에서 Mutex를 사용할 때 필요하다- Deadlock 주의: 태스크 A가 Mutex 1을 잡고 Mutex 2를 기다리고, 태스크 B가 Mutex 2를 잡고 Mutex 1을 기다리면 양쪽 모두 영원히 블로킹된다. 항상 같은 순서로 Mutex를 획득해야 한다
- FreeRTOS의 Priority Inheritance는 1단계만 지원한다. A→B→C처럼 체인이 연결되면 중간 단계의 상속이 누락될 수 있다. 이 경우 Priority Ceiling 프로토콜이 필요하지만, FreeRTOS는 이를 기본 지원하지 않는다