[FreeRTOS 정리] 2. 임계영역 (Critical Section)

Embedded June·2021년 8월 15일
0

FreeRTOS

목록 보기
2/5

인프런 강의 링크
홍영기(가일스쿨) 교수님 블로그 링크

졸업프로젝트로 FreeRTOS 기반 상용 RTOS인 'ESP-IDF'를 활용한 프로젝트를 진행한 뒤, 인프런의 FreeRTOS 강의 수강을 통해 내용을 확실히 정리했습니다. 이해한 내용을 최종적으로 정리하며 기록을 남기고자 글을 올립니다.

저작권을 최대한 존중하기 위해 홍영기 교수님의 강의자료와 실습자료는 일부라도 절대 공유하지 않으며, FreeRTOS 공식 레퍼런스 문서를 기반으로 작성합니다.


4. 임계영역 (Critical Section)

4.1. Critical section 개요

IPC를 사용하다보면 필연적으로 공유자원을 사용하고 싶은 욕구가 생긴다.

.

  • Task A와 Task B는 buttonCnt라는 공유자원을 사용하고 있다.
  • buttonCnt++라는 단순한 한 줄 짜리 연산도 어셈블리어로 무려 5줄이나 차지하고 있다. 저 어셈블리 연산은 atomic 하지 않기 때문에 얼마든지 저 사이에 접근이 일어날 수 있다.
  • 연산 도중 특정 값이 바뀌어버리면, 우리가 원하는 대로 프로그램이 동작하지 않을 수 있다.
    • 예를 들어, task A에 buttonCnt == 100이면 특정 동작을 수행하도록 조건문을 만들었는데, buttonCnt++연산 도중 task B에 의해 한 번 더 증가해 buttonCnt = 101이 됐다고 하자. 그럼 task A의 특정 동작은 수행될 수가 없다.
    • 만일 저 영역을 critical section 보호처리 했다면, 5개의 어셈블리 연산은 모두 atomic함이 보장되므로 프로그램이 우리가 의도했던 대로 동작할 것이다.

∴ 따라서, critical section problem은 OS가 반드시 중요하게 다뤄야하는 문제다! Critical section에 대한 소유권은 반드시 단일 task가 독점하도록 보장해줘야 한다.

4.2. Critical Section Problem 해소방법

  1. 인터럽트 중단: SYSTICK interrupt를 막아서 preemption 및 context switching이 일어나지 않도록 만들어 해당 critical section에 대한 독점을 보장하는 방법. Critical section 진입 전에 인터럽트 비활성화하고, 수행 후에 인터럽트 활성화한다.
    • taskENTER_CRITICAL(), taskEXIT_CRITICAL() 이라는 커널 API를 제공한다. 이 API는 인터럽트보다 우선순위가 높다!!
  2. 스케줄링 중단: 마찬가지로 context switching이 일어나지 않도록 만든다. 무식한 방법이다. 애초에 FreeRTOS에서는 지원하지 않는다.
  3. 공유자원 또는 전역변수를 아예 사용하지 않는다: 믿기 어렵겠지만 좋은 해결방법이다. 알고리즘을 변형하거나 조금 더 생각한다면, 공유자원을 사용하지 않고도 서비스를 구현할 방법이 있다. 특정 HW 자원에 대한 접근을 특정 task에게 독점/제한 하는 방법이다.
  4. Semaphore/Mutex 사용: 가장 적합한 방법. Critical section에 대한 lock(p, wait, take) 연산과 unlock(v, signal, give) 연산을 제공해 독점권을 보장한다. 비록 그 자체로 오버헤드가 되겠지만, 안전한 프로그램을 만들 수 있다는 것이 훨씬 큰 장점이다.

4.3. 실습 03 - Critical section 보호 (1)

저작권을 존중하므로 전체 실습 소스코드는 공유하지 않는다.

FreeRTOS의 critical section 보호 방법 중 하나인 taskENTER_CRITICAL을 사용한다.

4.4. Semaphore

  • 다익스트라 알고리즘으로 유명한 Edsger Dijkstra가 만든 개념으로 공유자원에 대한 접근을 semaphore라는 잠금 기능을 통해 보호한다.
  • Semaphore는 동기화 및 공유자원 보호 그리고 이벤트 전달 용도로 사용한다.
  • 먼저 semaphore를 초기화한 뒤, 공유자원에 접근하기 위해서는 semaphore를 휙득해야 하며, 공유자원을 다 사용한 뒤에는 semaphore를 반환해야 한다.
    • 초기화: xSemaphoreCreateBinary(), xSemaphoreCreateCounting()
    • 휙득: p 연산, wait 연산, take 연산 이라고 부른다.
      xSemaphoreTake(), xSemaphoreTakeFromISR()
    • 반환: v 연산, signal 연산, give 연산 이라고 부른다.
      xSemaphoreGive(), xSemaphoreGiveFromISR()
    • 제거: vSemaphoreDelete()
  • Semaphore는 공유자원에 따라 두 가지 종류를 가진다.
    • Binary Semaphore: 01 값으로만 이뤄진 semaphore다. Critical section에 대해 p연산을 수행하면 semaphore가 0이 되서 다른 task의 접근을 막는다. 접근을 요청한 task는 blocked 상태가 되며 들어갈 때 까지 기다린다. (무한정 기다리는 건 아니고, take 함수에서 기다리는 시간을 설정할 수 있다.)
    • Counting Semaphore: 0부터 사용자가 설정한 수까지 값을 가지는 semaphore다. 예를 들어 공유자원이 10칸짜리 배열일 때, semaphore를 10으로 초기화하면, counting semaphore는 남은 공유자원의 개수를 의미한다. semaphore가 0이 되면 해당 배열이 가득 찼음을 의미하며 접근을 요청한 task는 blocked 상태가 되며 기다린다.

4.5. 실습 04 - Critical section 보호 (2)

1. Binary semaphore에 대한 실습

2. Counting semaphore에 대한 실습

  • STM32F103RB의 B1 버튼을 클릭할 때마다 몇 번 클릭됐는지 출력한다. Clicked[]
  • 버튼을 클릭할 때마다 무거운 연산이 수행되며 give() 연산으로 semaphore 값 증가한다.
  • semaphore가 생기면, take() 중인 task가 blocked에서 running이 되며 몇 번째 semaphore이고 현재 semaphore의 값이 무엇인지 출력한다. Sem번호(semaphore 값)
  • 버튼을 굉장히 빠른 속도로 눌러보자. 버튼에 대한 인터럽트가 task보다 우선순위가 높고, 연산이 무겁기 때문에 Clicked[12], [13], [14]가 먼저 출력되는 것을 확인할 수 있다. 그럼 이제 semaphore가 0, 1, 2로 늘었을 것이고, task는 연속해서 3번 take() 연산을 수행할 수 있다.
  • Task는 semaphore에 대한 정보를 연달아서 3번 출력하는 것을 확인할 수 있다. 즉, critical section에 대한 진입 회수가 semaphore의 값과 동일하다는 것을 확인할 수 있다.

(P.s. ISR에서 무거운 연산을 처리할 경우 버튼에 대한 인터럽트가 온전히 실행되지 않을 수 있다. 실제로 버튼을 굉장히 빠르게 5번 눌렀지만, 4번(11, 12, 13, 14)만 인식이 됐다. ISR은 최대한 작게 만들어야 다른 인터럽트나 task에 악영향을 최소한으로 만들 수 있다. ISR이 무겁고 속도가 느릴 수 밖에 없다면 뒤에서 배울 deferred interrupt processing 이라는 좋은 방법을 사용해서 해결할 수 있다.)

4.5. Mutex

Mutex는 binary semaphore과 거의 같은 기능을 수행하지만, 중대한 차이가 하나 있다.
바로 우선순위 역전 현상을 예방해줄 메커니즘을 포함하고 있다는 점이다.

Priority Inversion (우선순위 역전)

Semaphore는 critical section 및 공유자원에 대한 독점권을 보장하는 훌륭한 동기화 수단이지만, 치명적인 문제인 ‘우선순위 역전 현상’을 유발할 수 있다.

  • 우선순위 역전 현상은, 우선순위가 높은 task가 되려 우선순위가 낮은 task의 수행이 끝날 때까지 기다리는 상황을 말한다. Semaphore를 사용할 경우 이런 문제가 발생할 수 있다.
  1. Task1이 실행되고 take() 연산을 수행해 critical section이 lock이 걸린다.
  2. HPT인 task2가 생성되면 task1을 preempt하고 context switching이 발생한다.
  3. Task2도 take() 연산을 수행한다. 이때 semaphore가 0이므로 task2는 blocked state로 진입한다.
  4. 다시 task1이 실행된다.
  5. Task3이 생성되면 task1을 preempt하고 context switching이 발생한다.
  6. Task3은 실행을 완료하고 delete된다.
  7. 다시 task1이 실행되고, critical section을 수행한 뒤 give() 연산으로 semaphore를 반환한다.
  8. 반환되자마자 task2가 실행된 뒤 완료하고 delete된다.
  • Semaphore 때문에 task1이 give()하기 전까지 task2는 계속 기다리는 상황이며 심지어 우선순위가 낮은 task3가 먼저 실행되는 어이없는 상황이 발생한다.

우선순위 상속 (Priority Inheritance)

우선순위 역전 현상을 해결하는 방법으로 FreeRTOS는 우선순위 상속(inheritance)을 사용한다.

  • 동일한 semaphore를 요청한 우선순위가 높은 task의 우선순위를 상속받아 우선순위가 높게 만들어 위 그림과 같이 문제를 해결한다.
  • 이때 task1은 give() 하자마자 다시 원래 우선순위로 돌아간다.
  • 상속 방법은 역전 현상을 완전히 해결할 수는 없지만, 역전 현상의 빈도를 상당히 낮추고, 발생하더라도 적정한 time bound 내에 해결되도록 유도할 수 있는 좋은 방법이다.

profile
임베디드 시스템 공학자를 지망하는 컴퓨터공학+전자공학 복수전공 학부생입니다. 타인의 피드백을 수용하고 숙고하고 대응하며 자극과 반응 사이의 간격을 늘리며 스스로 반응을 컨트롤 할 수 있는 주도적인 사람이 되는 것이 저의 20대의 목표입니다.

0개의 댓글