Skip to Content

이번 문서의 목표: 이 파일을 다 읽으면 RTOS에서 태스크 간·태스크-ISR 간 자원 공유와 데이터 전달에 세마포어·뮤텍스·메시지 큐 중 무엇을 골라야 하는지 판단하고, 컨텍스트 스위치의 비용을 사이클 단위로 계산할 수 있다.

왜 임베디드에서 동기화 도구를 다시 배워야 하는가

세마포어·뮤텍스라는 이름 자체는 독학사 2단계 운영체제 10편(임계 구역과 프로세스 동기화)에서 이미 배운 개념입니다. 그 편에서 배운 임계 구역의 3조건(상호 배제·진행·한정 대기), 세마포어의 P·V 연산, 뮤텍스의 소유권 개념은 임베디드 RTOS에서도 그대로 성립합니다. 다만 임베디드 시스템에는 범용 운영체제(GPOS)에 없는 제약이 하나 추가됩니다 — 인터럽트 서비스 루틴(Interrupt Service Routine, ISR)이 태스크와 자원을 주고받아야 한다는 점입니다.

08편에서 배운 것처럼 ISR은 매우 짧게, 빠르게 실행되어야 하고 절대 대기(block)하면 안 됩니다. 그런데 세마포어·뮤텍스·큐 같은 동기화 도구를 잘못 쓰면 ISR 안에서 실행이 멈춰버릴 수 있습니다. 이 편은 “일반 OS 동기화 지식 위에, ISR과 안전하게 상호작용하는 규칙”을 추가로 쌓는 데 초점을 맞춥니다.

쉽게 말하면: 일반 OS 동기화가 “여러 태스크가 사이좋게 자원을 나눠 쓰는 규칙”이었다면, RTOS 동기화는 여기에 “번개처럼 끼어드는 인터럽트와도 안전하게 데이터를 주고받는 규칙”이 하나 더 붙습니다.

1. 세마포어 — RTOS에서는 ISR과의 신호 전달에도 쓰인다

세마포어(semaphore)의 정의와 P·V(또는 take·give) 연산은 운영체제 10편과 동일합니다. RTOS에서 세마포어가 특히 중요한 이유는, ISR이 태스크에게 “일이 생겼다”는 신호를 보내는 표준적인 방법이기 때문입니다.

GPIO 인터럽트가 발생했을 때, ISR 안에서 무거운 처리(센서값 파싱, 화면 갱신 등)를 직접 하면 인터럽트 지연(06편)이 늘어나 다른 인터럽트를 놓칠 위험이 커집니다. 대신 ISR은 이진 세마포어(binary semaphore)를 “주기만”(give) 하고 즉시 리턴하며, 실제 무거운 처리는 그 세마포어를 기다리던(take) 일반 태스크가 맡습니다. 이 패턴을 인터럽트 지연 처리(deferred interrupt handling)라 부릅니다.

/* ISR: 아주 짧게, 세마포어만 주고 즉시 리턴 */ void GPIO_IRQHandler(void) { clear_interrupt_flag(); /* 인터럽트 플래그 해제 */ xSemaphoreGiveFromISR(dataReadySem, &xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } /* 태스크: 세마포어를 기다렸다가 무거운 처리 수행 */ void SensorTask(void *pv) { for (;;) { xSemaphoreTake(dataReadySem, portMAX_DELAY); /* 신호 올 때까지 대기 */ process_sensor_data(); /* 무거운 처리는 여기서 */ } }

여기서 함수 이름이 xSemaphoreGiveFromISR처럼 FromISR이 붙은 별도 버전이라는 점이 시험에서 자주 나오는 포인트입니다. ISR 안에서는 절대 대기할 수 없으므로, 일반 xSemaphoreGive가 아니라 대기하지 않고 즉시 반환하는 ISR 전용 API를 써야 합니다.

자주 틀리는 점: “ISR 안에서도 일반 세마포어 함수(예: xSemaphoreTake)를 그대로 호출해도 된다”는 생각은 틀렸습니다. 일반 take 함수는 대기 상태로 전환될 수 있는데, ISR은 컨텍스트 자체가 태스크가 아니므로 대기 상태로 전환될 수 없습니다. ISR에서는 반드시 FromISR 접미사가 붙은, 논블로킹으로 설계된 전용 API만 써야 합니다.

2. 뮤텍스 — 태스크 간 자원 보호에는 세마포어보다 뮤텍스

운영체제 10편에서 배운 것처럼 뮤텍스(mutex)는 “잠근 태스크만 풀 수 있다”는 소유권(ownership) 개념이 있는 상호 배제 전용 락입니다. RTOS에서 두 태스크가 같은 공유 자원(예: I2C 버스, 전역 변수)에 접근할 때는 이진 세마포어가 아니라 뮤텍스를 쓰는 것이 원칙입니다. 이유는 두 가지입니다.

  1. 소유권 확인: 뮤텍스는 락을 건 태스크만 풀 수 있어, 실수로 다른 태스크가 남의 락을 풀어버리는 논리 오류를 막을 수 있습니다. 세마포어는 소유권이 없어 이런 실수를 막지 못합니다.
  2. 우선순위 상속: 대부분의 RTOS(FreeRTOS 포함)는 뮤텍스에 한해 우선순위 상속(priority inheritance) 프로토콜을 지원합니다. 이 내용은 16편에서 시나리오와 함께 전 과정을 다룹니다. 이진 세마포어에는 이 기능이 없습니다.

자주 틀리는 점: “뮤텍스는 ISR에서 자원 보호에 써도 된다”는 생각은 틀렸습니다. 뮤텍스는 소유권 개념 때문에 반드시 그것을 잠근 주체(태스크)가 풀어야 하는데, ISR은 태스크가 아니므로 뮤텍스를 잠그거나 풀 수 없습니다. ISR과 관련된 동기화에는 세마포어(주로 이진 세마포어)를 쓰고, 태스크끼리의 자원 보호에는 뮤텍스를 쓴다는 구분이 시험의 핵심 포인트입니다.

항목이진 세마포어뮤텍스
소유권없음(누구든 give 가능)있음(잠근 태스크만 unlock 가능)
ISR에서 사용가능(FromISR 버전으로 give)불가능(태스크 개념이 없는 ISR은 소유할 수 없음)
우선순위 상속지원하지 않음지원함(16편)
주된 용도ISR → 태스크 신호 전달, 이벤트 알림태스크 간 공유 자원(전역 변수, 버스, 하드웨어 레지스터) 보호

3. 메시지 큐 — 신호가 아니라 “데이터 자체”를 주고받기

세마포어는 “일이 생겼다/안 생겼다”라는 신호 하나만 전달할 뿐, 데이터 자체를 옮기지 못합니다. 여러 바이트로 이루어진 실제 데이터(센서 측정값, 통신 프레임 등)를 태스크와 태스크 사이, 또는 ISR과 태스크 사이에서 안전하게 주고받으려면 메시지 큐(message queue)를 씁니다.

메시지 큐는 정해진 개수의 슬롯(항목을 저장하는 칸)을 가진 원형 버퍼로, 한쪽(생산자, producer)이 데이터를 넣고 다른 쪽(소비자, consumer)이 데이터를 꺼내는 생산자-소비자 패턴(producer-consumer pattern)의 표준 구현입니다.

QueueHandle_t sensorQueue; /* 큐 핸들 */ /* 생산자 태스크: 센서값을 큐에 넣는다 */ void ProducerTask(void *pv) { int32_t value; for (;;) { value = read_sensor(); /* 큐가 가득 차면 최대 100틱까지 대기, 그래도 못 넣으면 실패 */ if (xQueueSend(sensorQueue, &value, pdMS_TO_TICKS(100)) != pdPASS) { log_queue_full_error(); } } } /* 소비자 태스크: 큐에서 값을 꺼내 처리한다 */ void ConsumerTask(void *pv) { int32_t value; for (;;) { if (xQueueReceive(sensorQueue, &value, portMAX_DELAY) == pdPASS) { handle_value(value); } } }

메시지 큐의 동작을 슬롯이 3개인 경우로 표에 추적해 보겠습니다.

순서동작큐 상태(슬롯 3개)결과
1생산자가 값 A를 xQueueSend[A]즉시 성공(빈 슬롯 있음)
2생산자가 값 B를 xQueueSend[A, B]즉시 성공
3생산자가 값 C를 xQueueSend[A, B, C]즉시 성공(슬롯 가득 참)
4생산자가 값 D를 xQueueSend(대기 시간 100틱 지정)[A, B, C]큐가 가득 차 대기 상태로 전환
5소비자가 xQueueReceive 호출[B, C]A를 꺼내고, 대기 중이던 생산자가 깨어남
6대기하던 생산자의 D가 큐에 들어감[B, C, D]성공

결과 해석: 큐가 가득 찬 상태에서 생산자가 값을 넣으려 하면 지정한 시간만큼 대기하고, 그사이 소비자가 하나를 꺼내면 즉시 자리가 생겨 대기하던 생산자가 이어서 넣을 수 있습니다. 이 자동 대기·재개 동작 덕분에, 생산자와 소비자의 실행 속도가 서로 다르더라도(센서는 빠르게 값을 만들고 처리 태스크는 느리게 소비하는 경우 등) 프로그래머가 직접 세마포어와 버퍼를 조합해 만들 필요 없이 큐 하나로 안전하게 처리할 수 있습니다.

자주 틀리는 점: “메시지 큐는 ISR에서 못 쓴다”는 생각은 틀렸습니다. 세마포어와 마찬가지로 xQueueSendFromISR, xQueueReceiveFromISR처럼 논블로킹 ISR 전용 버전이 제공되어, ISR에서도 큐에 데이터를 넣거나 꺼낼 수 있습니다. 다만 이 경우도 일반 버전이 아니라 반드시 FromISR 버전을 써야 한다는 원칙은 동일합니다.

4. 컨텍스트 스위치 — 동기화 도구를 쓸 때마다 드는 숨은 비용

컨텍스트 스위치(context switch, 문맥 교환)는 06편·08편에서 다룬 개념으로, CPU가 실행하던 태스크를 멈추고 다른 태스크로 넘어갈 때 현재 태스크의 실행 상태(레지스터 값, 프로그램 카운터(PC), 스택 포인터(SP) 등)를 저장하고, 다음 태스크의 저장된 상태를 복원하는 과정입니다.

세마포어를 주고받거나(give/take), 큐에 데이터를 넣고 꺼내는 동작은 그 자체로는 짧지만, 그 결과로 더 높은 우선순위의 태스크가 깨어나면 즉시 컨텍스트 스위치가 일어납니다. 이 컨텍스트 스위치 자체의 비용이 동기화 설계에서 반드시 고려해야 할 오버헤드입니다.

컨텍스트 스위치 한 번에 드는 시간을 계산해 보겠습니다. Cortex-M 계열 MCU가 클록 주파수 100MHz로 동작하고, 컨텍스트 스위치 한 번에 레지스터 저장·복원과 스케줄러 판단을 합쳐 약 200 CPU 사이클이 걸린다고 가정합니다.

한 사이클의 시간=1100×106Hz=10ns\text{한 사이클의 시간} = \frac{1}{100 \times 10^6 \text{Hz}} = 10 \text{ns} 컨텍스트 스위치 1회 시간=200×10ns=2000ns=2μs\text{컨텍스트 스위치 1회 시간} = 200 \times 10\text{ns} = 2000\text{ns} = 2\mu s

이 태스크가 1ms(1000μs)마다 세마포어를 통해 다른 태스크를 깨운다면, 컨텍스트 스위치가 그 주기마다 최소 2회(대기 태스크가 깨어날 때, 원래 태스크로 돌아올 때) 일어난다고 가정할 때 오버헤드 비율은 다음과 같습니다.

오버헤드 비율=2×2μs1000μs×100=0.4%\text{오버헤드 비율} = \frac{2 \times 2\mu s}{1000\mu s} \times 100 = 0.4\%

결과 해석: 이 예제에서는 오버헤드가 전체 주기의 0.4퍼센트로 크지 않지만, 만약 태스크가 100μs마다 반복하고 그때마다 여러 번 동기화를 일으킨다면 같은 2μs짜리 컨텍스트 스위치도 전체 시간의 상당 부분을 차지하게 됩니다. 동기화 도구(세마포어·뮤텍스·큐)를 지나치게 잦은 주기로 사용하면, 실제 하려던 일보다 컨텍스트 스위치 자체에 CPU 시간을 더 많이 쓰게 될 수 있다는 것이 이 계산이 보여주는 설계 교훈입니다.

자주 틀리는 점: “동기화 도구를 쓰는 것 자체는 공짜(비용 없음)“라고 생각하면 안 됩니다. P·V 연산이나 큐 송수신 코드 자체의 실행 시간은 매우 짧지만, 그로 인해 유발되는 컨텍스트 스위치의 비용까지 함께 계산해야 정확한 응답 시간·인터럽트 지연 분석(06편, 13편)이 가능합니다.

핵심 정리

  • 세마포어·뮤텍스의 기본 정의는 운영체제 10편과 같지만, RTOS에서는 ISR과 안전하게 상호작용하는 것이 추가 요구사항이다.
  • ISR은 절대 대기할 수 없으므로, 세마포어와 큐 모두 FromISR이 붙은 논블로킹 전용 API로만 다뤄야 한다.
  • 뮤텍스는 소유권이 있어 잠근 태스크만 풀 수 있고 우선순위 상속을 지원하지만, 이 소유권 개념 때문에 ISR에서는 쓸 수 없다. ISR과의 신호 전달에는 이진 세마포어를 쓴다.
  • 메시지 큐는 신호만 전달하는 세마포어와 달리 실제 데이터를 슬롯 단위로 옮기며, 생산자-소비자 패턴을 자동으로 처리한다.
  • 동기화 도구를 사용할 때마다 컨텍스트 스위치가 일어날 수 있고, 그 비용은 사이클 수와 클록 주파수로 계산할 수 있으며, 너무 잦은 동기화는 오버헤드가 전체 실행 시간에서 차지하는 비중을 키운다.

마무리 복습

문제 14지선다
RTOS의 ISR(인터럽트 서비스 루틴)에서 세마포어를 다룰 때 지켜야 할 규칙으로 옳은 것은?
문제 24지선다
뮤텍스(mutex)를 ISR에서 잠그거나 풀 수 없는 근본적인 이유는?
문제 34지선다
이진 세마포어와 뮤텍스의 차이에 대한 설명으로 옳지 않은 것은?
문제 44지선다
슬롯이 2개인 메시지 큐에 이미 값 두 개가 채워진 상태에서 생산자가 xQueueSend를 대기 시간 없이(즉시 실패 허용) 호출하면 어떻게 되는가?
문제 54지선다
컨텍스트 스위치(context switch)에서 저장·복원되는 대상으로 가장 옳은 것은?
문제 64지선다
클록 주파수 100MHz인 MCU에서 컨텍스트 스위치 한 번에 200 CPU 사이클이 걸린다면, 그 시간은?
문제 74지선다
동기화 도구 사용과 컨텍스트 스위치 오버헤드의 관계에 대한 설명으로 옳은 것은?

참고 자료

Last updated on