그라운드 임베디드 EN 유튜브 구독

노트 / RTOS

펌웨어RTOS4 / 8

세마포어와 뮤텍스: 공유 자원 지키기

두 태스크가 같은 UART나 변수를 동시에 건드리면 데이터가 섞입니다. 뮤텍스로 잠그고, 세마포어로 신호를 줍니다.

GROUND EMBEDDED · 정리 노트RTOS 04세마포어와 뮤텍스 1xSemaphoreTake(uart_mutex, portMAX_DELA… 2uart_send(buf, len); 3xSemaphoreGive(uart_mutex);

문제

태스크 A가 UART로 "TEMP=25"를 보내는 도중 태스크 B가 끼어들어 "SPD=100"을 보내면 "TEMSPD=100P=25"가 나갑니다. 공유 자원은 한 번에 하나만 쓰게 해야 합니다.

뮤텍스: 열쇠 하나

A가 UART 사용B가 기다림B가 UART 사용
뮤텍스. 태스크 B는 A가 열쇠를 돌려줄 때까지 기다렸다가 UART를 씁니다. 두 문자열이 섞이지 않습니다.
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을 기다림
데드락. 서로 상대가 가진 것을 기다립니다. 잠그는 순서를 항상 같게 하면 생기지 않습니다.

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에서는 뮤텍스를 쓸 수 없으므로 임계 구역이나 큐를 사용)
3D 실험실에서 직접 돌려 보기값을 바꾸며 이 노트의 원리를 확인합니다→영상으로 이어 보기RC카 만들기 시리즈 6편대기업 임베디드 개발자 Lego RC카 메이킹 시작합니다. 【RC카 개발1】FREE PDF무료 전자책 3권 받기이 주제를 한 권으로 이어서 읽고 싶다면. BLDC 모터 제어, 직렬통신, 센서를 책 분량으로 묶었습니다. 이메일만 남기면 바로 받습니다.무료로 받기 →