Semaphore

hyeony·2025년 8월 30일

RTOS

목록 보기
4/8

1. Binary Semaphores

가. Introduction to Binary Semaphores

1) What is Binary Semaphore?

이진 세마포어(Binary Semaphore)는 여러 용도로 사용될 수 있지만, 가장 흔한 활용 방식은 동기화이다. 특히, ISR(Interrupt Service Routine)Task의 동기화에 자주 사용된다.

즉, 특정 Interrupt가 발생할 때마다 세마포어를 이용해 Task를 깨워서 Task와 Interrupt를 효과적으로 동기화할 수 있다.

2) 지연된 Interrupt 처리

이진 세마포어를 사용하면 인터럽트 이벤트 처리의 대부분을 Task에서 수행할 수 있다. ISR에서는 아주 짧고 빠른 처리만 수행하고, 나머지는 세마포어를 통해 깨어난 Task에서 처리하게 된다. 이러한 기법을 지연된 인터럽트 처리(Deferred Interrupt Processing)라고 한다.

이렇게 하는 이유는, ISR은 가능한 한 짧고 빠르게 끝나야 한다. 만약 ISR에서 많은 시간을 소비하면 실시간 커널(real-time kernel)의 성능이 저하되기 때문이다.

따라서 보통 ISR은 Interrupt를 수신한 후, Task에 빠르게 알림만 보내고 실제 처리는 Task에서 수행하도록 한다. 대부분의 경우 이때 사용되는 것이 바로 이진 세마포어이다.

3) Task 간 동기화

보통 여러 개의 Tasks가 동시에 돌아가는데, 어떤 경우에는 두 Tasks가 같은 타이밍에 만나서 실행되기를 목적으로 할 수도 있다. 이때 세마포어가 사용될 수 있다.

① 두 개의 Tasks 동기화

  • Task A: 센서에서 데이터를 읽는 태스크
  • Task B: 읽은 데이터를 네트워크로 전송하는 태스크

이때, Task A가 데이터를 읽었다는 신호를 주기 전까지는 Task B가 실행되면 안 된다. Task A가 세마포어를 give()하여 신호를 주면, Task Btake()하여 그때 실행할 수 있다. 즉, 두 Tasks가 세마포어를 매개로 순서를 맞추는 것이다.

② 세 개 이상의 태스크 동기화

  • Task A: 카메라로 사진 촬영
  • Task B: 촬영한 사진을 압축
  • Task C: 압축한 사진을 저장

이 세 개 Tasks는 반드시 A → B → C 순서로 실행돼야 한다. 각 단계에서 세마포어를 give() / take()하도록 연결하면, 여러 Tasks가 동시에 경쟁하지 않고 차례대로 실행되도록 동기화할 수 있다.

③ 코드의 특정 부분만 동기화
때로는 전체 Task 실행을 동기화하는 게 아니라, 코드의 일부 구간만 동시에 실행되도록 해야 할 때가 있다.

예를 들어, Task ATask B는 서로 다른 일을 하지만, 둘 다 마지막에 같은 변수에 접근해야 한다고 하겠다. 변수에 동시에 접근하면 값이 꼬일 수 있으므로, 세마포어를 걸어서 같은 지점에서 만나도록 한다.

즉, 코드의 특정 위치에서 두 Tasks가 모두 기다리게 만들고, 세마포어가 풀리면 동시에 그 부분을 실행하게 하는 것이다.

4) 이진 세마포어 API 정리

① 세마포어 생성: xSemaphoreCreateBinary()
이 함수는 이진 세마포어를 생성한다. 인자는 없으며, 반환값으로 세마포어 핸들(handle)을 준다.

반환값이 NULL이라면, FreeRTOS가 세마포어 자료구조를 위한 힙 메모리를 확보하지 못한 경우이다.

② 세마포어 획득: xSemaphoreTake()
세마포어를 “획득(take)”하는 함수이다. 세마포어는 사용 가능할 때만 획득할 수 있다. 인자는 다음과 같이 두 가지이다.

  • 세마포어 핸들
  • xTicksToWait: 세마포어가 사용 불가능할 경우, 사용 가능해질 때까지 대기할 최대 시간

예를 들어, 두 Tasks가 하나의 세마포어를 공유한다고 가정하겠다. 한 Task가 세마포어를 획득하면, 다른 Task는 세마포어가 반환되기 전까지는 대기해야 한다.

③ 세마포어 반환: xSemaphoreGive()
태스크가 세마포어 사용을 마쳤을 때 호출한다. 인자는 하나, 세마포어 핸들뿐이다. 세마포어를 반환하면 다른 Task가 다시 이를 획득할 수 있게 된다.

④ 인터럽트에서 세마포어 반환: xSemaphoreGiveFromISR()
ISR 내부에서는 일반 xSemaphoreGive()를 사용할 수 없다. 대신 반드시 xSemaphoreGiveFromISR()를 사용해야 한다. 인자는 다음과 같이 두 가지이다.

  • 세마포어 핸들
  • HigherPriorityTaskWoken: ISR에서 세마포어를 주었을 때, 더 높은 우선순위를 가진 태스크가 깨어났는지를 나타낸다.

나. Coding: Creating Binary Semaphores

#include <Arduino_FreeRTOS.h>
#include "semphr.h"

#define  RED      6
#define  YELLOW   7
#define  BLUE     8

typedef int TaskProfiler;

TaskProfiler RedTaskProfiler;
TaskProfiler BlueTaskProfiler;
TaskProfiler  YellowTaskProfiler;

SemaphoreHandle_t xBinarySemaphore;

void setup()
{
  Serial.begin(9600);
  
  xBinarySemaphore = xSemaphoreCreateBinary();

  xTaskCreate(redLedControllerTask, "Red Led Task",100,NULL,1,NULL);
  xTaskCreate(blueLedControllerTask, "Blue Led Task", 100,NULL,1,NULL);
  xTaskCreate(yellowLedControllerTask,"Yellow Led Task", 100,NULL,1,NULL);
 
}

void redLedControllerTask(void *pvParameters)
{
  pinMode(RED,OUTPUT);
  
  xSemaphoreGive(xBinarySemaphore);
  while(1)
  {
   xSemaphoreTake(xBinarySemaphore,portMAX_DELAY);
   Serial.println("This is RED Task");
   xSemaphoreGive(xBinarySemaphore);
   vTaskDelay(1);
  }
}
void blueLedControllerTask(void *pvParameters)
{
  pinMode(BLUE,OUTPUT);
  while(1)
  {
    xSemaphoreTake(xBinarySemaphore,portMAX_DELAY);
    Serial.println("This is BLUE Task");
    xSemaphoreGive(xBinarySemaphore);
    vTaskDelay(1);
  }
}
void yellowLedControllerTask(void *pvParameters)
{
  pinMode(YELLOW,OUTPUT);
  while(1)
  {
    xSemaphoreTake(xBinarySemaphore,portMAX_DELAY);
    Serial.println("This is YELLOW Task");
    xSemaphoreGive(xBinarySemaphore);
    vTaskDelay(1);
  }
}

void loop(){}

1) 코드 설명

① 세마포어 생성

xBinarySemaphore = xSemaphoreCreateBinary();

이진 세마포어를 생성한다. 생성 직후에는 “빈 상태”이므로 take()에 성공할 수 없다.

② Task 생성

xTaskCreate(redLedControllerTask, "Red Led Task",100,NULL,1,NULL);

총 세 개의 Tasks를 동일한 우선순위(1)로 생성한다. 각 Task는 RED, BLUE, YELLOW에 해당한다.

③ 첫 번째 신호 주기

xSemaphoreGive(xBinarySemaphore);

RED Task가 실행되자마자 give()를 호출한다. 이로써 세마포어가 “사용 가능” 상태가 되고, 다른 Task가 take() 할 수 있게 된다.

④ 임계구역 보호

xSemaphoreTake(xBinarySemaphore,portMAX_DELAY);
Serial.println("This is RED Task");
xSemaphoreGive(xBinarySemaphore);

임계구역(여기서는 Serial.println)에 진입하기 전에 반드시 take() 한다. 다른 Task는 세마포어가 반환되기 전까지 기다려야 한다. 출력 후에는 give()해서 다른 Task가 진입할 수 있게 한다.

vTaskDelay(1)
각 태스크가 take()printlngive()를 끝낸 후 1틱 동안 지연한다. 덕분에 스케줄러가 다른 Task에게 CPU를 넘기고, 결과적으로 세 Tasks가 번갈아가며 실행된다.

2) 실행 결과

시리얼 모니터에는 다음과 같이 출력된다. 세 Tasks가 세마포어를 통해 교대로 실행되고 있음을 확인할 수 있다.

This is RED Task
This is BLUE Task
This is YELLOW Task
This is RED Task
This is BLUE Task
This is YELLOW Task
...

2. Counting Semaphore

가. Introduction to Counting Semaphore

1) What is Counting Semaphore?

Counting Semaphore을 사용하려면 FreeRTOS 설정 파일에서 Counting Semaphore 기능을 활성화해야 한다. 이를 위해 configUSE_COUNTING_SEMAPHORES 값을 1로 설정하면 된다. Counting Semaphore는 일반적으로 다음과 같이 두 가지 용도로 사용된다.

① Counting Event
Event가 발생할 때마다 Event Handler는 Semaphore를 give()한다. 그때마다 Semaphore의 카운트 값은 1씩 증가한다.

한편, Task는 Event를 처리할 때마다 Semaphore를 take()한다. 그때마다 Semaphore의 카운트 값은 1씩 감소한다.

즉, 카운트 값 = 발생한 이벤트 개수 - 처리된 이벤트 개수가 된다.

Event Counting 목적으로 Semaphore을 사용할 때는, Semaphore을 초기값 0으로 설정해야 한다.

② Resource Manangement
카운트 값은 현재 사용 가능한 자원의 개수를 의미한다. Task가 자원을 사용하려면 먼저 Semaphore를 take()해야 하며, 이때 카운트 값은 1 감소한다. 카운트 값이 0이 되면, 더 이상 가용 자원이 없다는 뜻이다.

Task가 자원 사용을 마치면 Semaphore를 give()하여 자원을 반환하고, 카운트 값은 1 증가한다.

자원 관리 목적으로 사용하는 Counting Semaphore는 보통 초기 카운트 값 = 사용 가능한 자원의 개수로 생성한다.

2) 카운팅 세마포어 관련 API

가) xSemaphoreCreateCounting()

Counting Semaphore를 생성할 때 사용하는 함수이다. 이 함수는 다음과 같이 두 개의 인자를 받는다.

uxMaxCount
Semaphore가 가질 수 있는 최대 카운트 값이다.

  • Event Counting에 사용할 경우: Semaphore가 세어야 하는 최대 이벤트 개수

  • Resource Management에 사용할 경우: 사용 가능한 자원의 총 개수

uxInitialCount
Semaphore 생성 직후의 초기 카운트 값이다.

  • Event Counting에 사용할 경우: 0으로 설정(아직 발생한 Event가 없으므로)

  • Resource Management에 사용할 경우: uxMaxCount와 같은 값으로 설정(처음에는 모든 자원이 사용 가능하므로)

나. Code Implementation

#include <Arduino_FreeRTOS.h>
#include "semphr.h"

#define  RED      6
#define  YELLOW   7
#define  BLUE     8

SemaphoreHandle_t xCountingSemaphore

void setup()
{
  pinMode(RED, OUTPUT);
  pinMode(YELLOW, OUTPUT);
  pintMode(BLUE, OUTPUT);

  Serial.begin(9600);
  xCountingSemaphore = xSemaphoreCreateCounting(1, 0);

  xTaskCreate(redLEDControllerTask, "RED Task", 100, NULL, 1, NULL);
  xTaskCreate(blueLEDControllerTask, "BLUE Task", 100, NULL, 1, NULL);
  xTaskCreate(yellowLEDControllerTask, "YELLOW Task", 100, NULL, 1, NULL);
}

void redLEDControllerTask(void *pvParameters)
{
  while(1)
  {
      xSemaphoreTake(xCountingSemaphore, portMAX_DELAY);
      digitalWrite(RED, digitalRead(RED)^1);
      Serial.println(" RED Task ");
      xSemaphoreGive(xCountingSemaphore);
      vTaskDelay(1);
  }
}

void blueLEDControllerTask(void *pvParameters)
{
  while(1)
  {
      xSemaphoreTake(xCountingSemaphore, portMAX_DELAY);
      digitalWrite(BLUE, digitalRead(BLUE)^1);
      Serial.println(" BLUE Task ");
      xSemaphoreGive(xCountingSemaphore);
      vTaskDelay(1);
  }
}

void yellowLEDControllerTask(void *pvParameters)
{
  while(1)
  {
      xSemaphoreTake(xCountingSemaphore, portMAX_DELAY);
      digitalWrite(YELLOW, digitalRead(YELLOW)^1);
      Serial.println(" YELLOW Task ");
      xSemaphoreGive(xCountingSemaphore);
      vTaskDelay(1);
  }
}

void loop() {}

1) 코드 설명

① Counting Semaphore 생성

xCountingSemaphore = xSemaphoreCreateCounting(1, 0);
  • 첫 번째 인자 1: 세마포어가 가질 수 있는 최대 카운트 값
  • 두 번째 인자 0: 초기 카운트 값
  • 즉, 최대 1개의 자원만 허용하고, 초기에는 사용 불가능(0) 상태로 시작

이 구조는 사실상 Mutex처럼 동작하게 된다.(동시에 하나의 Task만 임계 구역에 진입 가능)

② Task 생성

xTaskCreate(redLEDControllerTask, "RED Task", 100, NULL, 1, NULL);
  • 세 개의 태스크를 생성한다: RED, BLUE, YELLOW
  • 모두 동일한 우선순위(1)를 가진다.
  • 각 태스크는 자신의 LED 핀을 toggle하고, Serial 모니터에 실행 사실을 출력

③ 임계 구역 보호

xSemaphoreTake(xCountingSemaphore, portMAX_DELAY);
digitalWrite(RED, digitalRead(RED)^1);
Serial.println(" RED Task ");
xSemaphoreGive(xCountingSemaphore);
  • 각 Task는 Semaphore를 먼저 획득(take)해야만 LED를 toggle할 수 있음
  • 임계 구역(LED 제어 + Serial 출력)이 끝나면 Semaphore를 반환(give)한다.
  • 이렇게 하면 세 Tasks가 동시에 같은 자원에 접근하지 못하게 된다.

④ vTaskDelay(1)

vTaskDelay(1);
  • Task가 임계 구역을 빠져나온 후 잠시 양보한다.
  • 다른 Task가 실행될 수 있도록 CPU를 넘겨주는 역할을 한다.
  • 결과적으로 세 Tasks가 교대로 실행된다.

2) 실행 결과

시리얼 모니터에는 다음과 같은 출력이 반복된다.

 RED Task 
 BLUE Task 
 YELLOW Task 
 RED Task 
 BLUE Task 
 YELLOW Task 
 ...

그리고 아두이노 보드에 연결된 RED, BLUE, YELLOW LED 핀은 번갈아 깜박인다.

3. 참고 개념

① 우선순위 역전(Priority Inversion)
우선순위 역전은 우선순위가 높은 Task가 우선순위가 낮은 Task를 기다려야 하는 상황에서 발생한다. 이 경우, 높은 우선순위 Task는 낮은 우선순위 Task가 끝날 때까지 기다리게 되며, 사실상 낮은 우선순위 Task의 우선순위를 따라가게 된다. 즉, 우선순위가 뒤바뀌는 현상이므로 이를 우선순위 역전이라고 부른다.

② 교착 상태(Deadlock)
교착 상태는 두 개 이상의 Tasks가 서로의 자원을 기다리느라 아무도 진행하지 못하는 상황을 말한다.

예를 들어, Task A가 자원 1을 가지고 자원 2를 기다리고 있고, Task B가 자원 2를 가지고 자원 1을 기다린다면 둘 다 멈춰버린다.

이런 상황은 Semaphore 사용 시 흔히 발생할 수 있다. 모든 Task가 Semaphore를 기다리고 있지만, 누구도 먼저 Semaphore를 반환하지 않는다면 아무도 실행을 진행할 수 없게 된다.

③ 우선순위 상속(Priority Inheritance)
우선순위 상속은 자원을 보유한 Task의 우선순위를, 그 자원을 기다리고 있는 가장 높은 우선순위 Task의 수준까지 일시적으로 끌어올리는 방법이다.

이 기법을 사용하면 앞서 살펴본 우선순위 역전 문제를 방지할 수 있다.

<참고 자료>
https://www.udemy.com/course/arduino-freertos/

profile
Chung-Ang Univ. EEE.

0개의 댓글