세마포어와 뮤텍스: 공유 자원 지키기
두 태스크가 같은 UART나 변수를 동시에 건드리면 데이터가 섞입니다. 뮤텍스로 잠그고, 세마포어로 신호를 줍니다.
문제
태스크 A가 UART로 "TEMP=25"를 보내는 도중 태스크 B가 끼어들어 "SPD=100"을 보내면 "TEMSPD=100P=25"가 나갑니다. 공유 자원은 한 번에 하나만 쓰게 해야 합니다.
뮤텍스: 열쇠 하나
xSemaphoreTake(uart_mutex, portMAX_DELAY);
uart_send(buf, len);
xSemaphoreGive(uart_mutex);
- 가진 태스크만 돌려줄 수 있습니다.
- 우선순위 상속이 들어 있어 우선순위 역전을 줄입니다.
- 인터럽트에서는 쓰지 않습니다.
세마포어: 신호
- 이진 세마포어: "일이 생겼다"는 신호. 인터럽트가 Give, 태스크가 Take하며 기다림
- 카운팅 세마포어: 자원이 N개일 때(버퍼 슬롯 N개) 남은 개수 관리
void USART1_IRQHandler(void) { // 인터럽트
BaseType_t woken = pdFALSE;
xSemaphoreGiveFromISR(rx_sem, &woken);
portYIELD_FROM_ISR(woken);
}
인터럽트에서는 반드시 FromISR 버전 함수를 씁니다.
데드락
A가 자원1을 잡고 자원2를 기다리고, B가 자원2를 잡고 자원1을 기다리면 둘 다 영원히 멈춥니다. 여러 자원을 잡을 때는 항상 같은 순서로 잡고, 잡고 있는 시간을 짧게 합니다. 아예 자원을 하나의 태스크가 소유하고 다른 태스크는 큐로 요청하게 만드는 것이 더 안전한 설계입니다.
무엇이 문제인가
두 태스크가 같은 UART로 문자열을 내보내면 글자가 섞입니다. 더 위험한 것은 이런 코드입니다.
counter = counter + 1; // 읽기 → 더하기 → 쓰기, 세 단계
태스크 A가 값을 읽은 직후 태스크 B로 바뀌어 B가 값을 올리면, 다시 돌아온 A는 예전 값에 1을 더해 써서 B의 증가분이 사라집니다. 이렇게 한 번에 한 태스크만 들어가야 하는 구간을 임계 구역이라고 합니다.
셋의 차이
| 뮤텍스 | 이진 세마포어 | 카운팅 세마포어 | |
|---|---|---|---|
| 용도 | 자원 보호 | 사건 알림 | 개수 세기 |
| 주인 | 있음 (잠근 태스크만 풀 수 있음) | 없음 | 없음 |
| 우선순위 상속 | 있음 | 없음 | 없음 |
| ISR에서 사용 | 불가 | 주기(give)만 가능 | 주기만 가능 |
자원을 지킬 때는 뮤텍스, 무슨 일이 일어났다고 알릴 때는 세마포어로 기억하면 됩니다.
3D 실험실여기까지 읽은 내용을 직접 돌려 볼 수 있습니다→뮤텍스로 자원 지키기
if (xSemaphoreTake(uart_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
uart_send(msg); // 짧게
xSemaphoreGive(uart_mutex);
} else {
err_count++; // 타임아웃도 처리
}
무한 대기(portMAX_DELAY) 대신 타임아웃을 주면, 어딘가에서 잠금을 풀지 않았을 때 시스템 전체가 멈추는 대신 오류로 드러납니다.
세마포어로 인터럽트에서 태스크 깨우기
void EXTI0_IRQHandler(void) {
BaseType_t woken = pdFALSE;
EXTI->PR = EXTI_PR_PR0;
xSemaphoreGiveFromISR(btn_sem, &woken);
portYIELD_FROM_ISR(woken); // 더 급한 태스크가 깨어났으면 바로 전환
}
void button_task(void *arg) {
for (;;) {
xSemaphoreTake(btn_sem, portMAX_DELAY); // 사건이 올 때까지 CPU를 쓰지 않고 대기
handle_button();
}
}
인터럽트 핸들러는 깃발만 올리고, 실제 처리는 태스크가 합니다. ISR 안에서는 반드시 FromISR로 끝나는 함수를 씁니다.
교착 상태
태스크 A가 뮤텍스 1을 잡고 2를 기다리는데, 태스크 B는 2를 잡고 1을 기다리면 둘 다 영원히 멈춥니다. 막는 방법은 단순합니다.
- 여러 뮤텍스가 필요하면 모든 태스크가 같은 순서로 잡습니다.
- 잠근 채로 다른 잠금을 기다리는 구간을 만들지 않습니다.
- 잠금 구간 안에서 지연 함수나 통신 대기를 하지 않습니다.
현업에서 자주 터지는 문제
- 뮤텍스 대신 이진 세마포어로 자원을 보호해 우선순위 역전이 발생
- 오류 경로에서
Give를 빠뜨려 다음부터 아무도 못 들어감 - 인터럽트와 태스크가 공유하는 자원을 뮤텍스로 보호하려 함(ISR에서는 뮤텍스를 쓸 수 없으므로 임계 구역이나 큐를 사용)
영상으로 이어 보기RC카 만들기 시리즈 6편대기업 임베디드 개발자 Lego RC카 메이킹 시작합니다. 【RC카 개발1】FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →