[FreeRTOS 정리] 1. FreeRTOS와 Task

Embedded June·2021년 8월 14일
2

FreeRTOS

목록 보기
1/5

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

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

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


FreeRTOS 정리

1. RTOS와 FreeRTOS

1.1. RTOS의 특징

  1. Realtime: 애플리케이션의 수행 마감 시간을 정확히(혹은 조금 융통성있는 정도로) 정할 수 있음.
    • Soft realtime: Task를 가능한 한 빠르게 수행하지만, 반드시 정해진 수행시간을 지킬 필요는 없음.
      예) 커피 자판기의 ‘동전 인식’ 기능은 500ms내에 수행해야 하지만, 550ms 소요됐다고 별 일 없음.
    • Hard realtime: Task마다 정해진 수행시간을 반드시 만족해야 함.
      예) 자동차의 제동 관련 ECU 제어 기능은 반드시 최대 500ms 내에 수행해야 한다. 550ms 소요될 경우, 운전자나 보행자의 안전이 위험할 수 있기 때문임.
  2. Scalability: 환경설정을 통해 사용자가 원하는 기능을 추가/제거해서 크기를 조절할 수 있음.
  3. Preemptive: 현재 실행하는 task보다 우선도가 높은 task가 나타날 경우, 선점을 허용함.
  4. Multitasking: 여러 task를 (마치) 동시에 실행할 수 있음.
  5. Robustness: RTOS는 CPU의 utilization을 극대화할 수 있는 수단임.

1.2. FreeRTOS 소개

  • 2003년 Richard Barry가 만든 ANSI C 기반 RTOS임.
  • 아마존 AWS IoT 서비스 확장을 위해 2017년 11월에 인수함.
  • 200개 이상의 폭넓은 MCU를 지원하며 포팅을 위한 다양한 컴파일러와 예제 제공.
  • MIT 라이센스로 조건없는 상업 목적 사용 가능.
  • Priority based preemptive scheduling을 사용하며, round-robin도 함께 지원한다.
  • 환경설정에 따라 binary image는 4KB ~ 9KB를 가질 정도로 굉장히 light-weight이며 높은 신뢰성과 안정성을 자랑한다.

2. 네이밍 규칙 (Naming rules)

FreeRTOS의 코드는 다음과 같은 네이밍 규칙을 사용한다.

.

FreeRTOS에서 변수명을 결정할 때 접두어는 아래와 같이 결정한다.

  • c: char 타입을 의미한다.
  • s: int16_t 타입 (short)을 의미한다.
  • i: int32_t타입 (int)을 의미한다.
  • x: BaseType_t 타입을 의미한다. 상당히 자주 사용되는데, 구조체나 인스턴스 핸들 등 일반적인 타입을 제외하면 대부분 x다.
  • u: unsigned를 의미한다.
  • p: 포인터 변수를 의미한다.

FreeRTOS에서 함수명을 결정할 때 접두어는 아래와 같이 결정한다.

  • v: void 반환값이 없는 함수를 의미한다.
  • x: 변수명의 접두어인 x와 같은 의미다. BaseType_t를 반환하는 함수다.
  • pv: void* 타입을 반환하는 함수다.
  • prv: private 함수를 의미한다. 대표적으로 아무런 task도 실행되지 않을 때 실행되는 idle task가 호출하는 callback 함수 (hook)가 이 접두어를 사용하고 있다.

FreeRTOS에서 매크로명은 그 매크로가 선언된 소스파일의 이름을 따라 결정된다.
예를 들어 무한정 기다림을 의미하는 portMAX_DELAYportable.h라는 커널 소스에 선언돼있다.
문서에 따르면 semaphore 관련 API는 거의 매크로로 구성돼있지만, 함수명 네이밍 규칙을 따라간다고 명시돼있다.


3. 태스크 (Task)

FreeRTOS에서 하나의 task는 하나의 스레드(thread)를 의미한다.

3.1. Task의 특징과 생김새

  • 각 task에는 우선순위를 할당하며 숫자가 높을수록 큰 우선순위를 의미한다.

    • 우선순위는 0부터 configMAX_PRIORITIES까지 할당이 가능하다.
    • 우선순위가 높은 task는 낮은 task를 선점(preemption)할 수 있고, 이때 context switching이 발생한다.
    • 동일한 우선순위 사이에서는 round robin을 사용한다.
  • Task는 return value가 없으며 (void*) 타입으로 여러 자료형을 매개변수로 받을 수 있다.

  • Task는 일회용 task주기적 task 2가지 종류가 있다.

    • 일회용 task는 기능을 한 번 수행한 뒤 마지막에 스스로를 삭제한다. (꼭 해줘야 함.)
    void 태스크이름( void* pvParameters ) {
        /* 어쩌고 저쩌고 */
        vTaskDelete(NULL);	// 꼭 해줘야 함.
    }
  • 주기적 task는 무한 loop가 들어있다. Delay 또는 suspend 함수 등으로 task의 state가 blocked(waiting)로 변하지 않는 이상 계속해서 기능을 빠르게 반복 실행한다.
    void 태스크이름( void* pvParameters ) {
        while(true) {
            /* 어쩌고 저쩌고 */
        }
    }
  • 각 task마다 local stack 공간이 할당된다. 이 공간은 메모리의 .bss영역 또는 .heap 영역에 들어간다.

    • Stack 메모리는 정적 또는 동적으로 할당이 가능하다.
    • 후술하겠지만, stack 메모리 할당은 5가지 방법으로 사용자가 환경설정 할 수 있으며, 메모리 단편화(fragmentation)를 최대한 막으며 메모리를 할당한다.
    • configSUPPORT_DYNAMIC_ALLOCATIONconfigSUPPORT_STATIC_ALLOCATION 값을 어떻게 설정하느냐에 따라, 그리고 task 생성 함수를 xTaskCreatexTaskCreateStatic중 어느 것을 사용하느냐에 따라 할당 방법이 달라진다.

3.2. Task 관련 API

6가지 API가 주로 사용되며 앞 3개가 중요하고, 뒤 3개는 잘 안 쓴다.

  1. xTaskCreate()

    .

    • pvTaskCode는 task의 기능이 선언된 함수의 함수포인터를 말한다.
    • pcName은 디버깅 용도로 사용하는 문자열이며 task의 이름을 말한다.
    • usStackDepth는 task마다 할당되는 stack 메모리를 말한다. 단위는 WORD이며 우리가 사용하는 ARM Cortex-M보드에서는 1WORD == 4Byte라는 점을 기억하자.
    • pvParameters는 task 함수로 전달할 매개변수를 말한다. 없다면 NULL 쓰면 된다. 전달할 매개변수를 (void*) 타입으로 캐스팅한 뒤 여기다가 넣어주면 task 함수에서 사용할 수 있다.
    • pxCreatedTask는 task를 제어하기 위한 TaskHandle_t 타입 핸들을 말한다. Task의 우선순위를 바꾸거나, task를 멈추거나 등 task에 대한 설정은 모두 이 핸들을 통해서 이뤄진다.
    • `xTaskCreate((TaskFunction_t)foo, “task”, 64, NULL, 10, handle);``
  2. vTaskDelay()

    .

    • Task의 상태를 running에서 blocked(waiting)으로 변경하는 함수다. 설정된 시간 xTicksToDelay동안 해당 task는 blocked task가 되며, 다음 우선순위를 가진 task가 실행된다.
    • 확장성 및 이식성 좋은 코드를 만들기 위해서 tick단위 시간을 사용하는 것 보다 pdMS_TO_TICKS() 매크로를 사용해서 우리에게 편한 ms(밀리초) 단위를 tick으로 변환해서 사용하는 것이 좋다.
      • 왜냐하면, 사용하는 보드와 사용자 환경설정 값에 따라 1 Tick이 의미하는 시간이 다르기 때문이다.
    • 문서에서도 그렇게 권장하고 있다.
  3. vTaskDelayUntil()

    • .

    • vTaskDelay()와 똑같은 기능을 수행하지만, 작동 방식이 조금 다르다.

    • vTaskDelay()는 호출 시점부터 지정된 시간만큼 blocked 되는 반면,
      vTaskDelayUntil()은 호출 시점과 관계 없이 목표 절대 시간 주기에 맞춰 blocked 된다.

  4. vTaskSuspend()vTaskResume()

    • Delay 함수는 task를 일정 시간동안 blocked state로 만들지만, suspend 함수는 task를 기약없이 blocked state로 만든다.
    • Suspend 된 task는 resume 함수를 사용할 때 까지 계속 blocked state가 된다.
    • Task의 우선순위를 바꿀 때 task를 일단 blocked state로 만들 필요가 있다. 이때 사용한다.
    • 괄호 안에는 task의 handle이 들어간다는 점을 잊지말자.
  5. vTaskPrioritySet()

    • Task의 우선순위를 바꾼다.

3.3. Example 01 :: Task 실습

전체 소스코드는 저작권을 고려해 올리지 않는다.

실습 내용은 다음과 같다.

  • 알파벳 ‘a’를 1초마다 한 번씩 출력하는 우선순위 10의 task1을 만든다.
    알파벳 ‘b’를 1초마다 한 번씩 출력하는 우선순위 9의 task2를 만든다.
    • xTaskCreate((TaskFunction_t)Task1, "Task1", 128, NULL, 10, &xHandle1);
    • xTaskCreate((TaskFunction_t)Task2, "Task2", 128, (void*)Param, 9, &xHandle2 );
  • 다양한 데이터를 담고있는 구조체를 만들고 task2의 parameter로 넘겨준다.
  • 중간에 task1의 우선순위를 8로 낮춘다.
    • vTaskSuspend(xHandle1);
    • vTaskPrioritySet(xHandle1, 8);
    • vTaskResume(xHandle1);
  • task1과 task2의 vTaskDelay()를 주석처리 해보면서 두 task가 어떻게 작동하는지 확인한다.
    • .
      • Task1, 2 모두 vTaskDelay()를 주석처리 하지 않았을 때 결과다.
      • Task2의 우선순위가 task1보다 높기 때문에 먼저 b를 출력하고 1초간 blocked 된다.
      • 이제 task1이 실행될 수 있으며 a를 출력하고 1초간 blocked 된다.
      • 아무것도 실행되지 않으므로 idle task가 실행될 것이다.
      • Blocked state에서 벗어난 task2가 b를 출력하고 1초간 blocked 되는 과정이 반복된다.
    • .
      • Task 1의 vTaskDelay()를 주석했을 때 결과다.
      • 먼저 task2가 시작되고, vTaskDelay()를 만난 뒤 blocked 되면 task1이 시작된다.
      • Task1은 빠른 속도로 a를 출력한다.
      • 1초 이후 task2가 task1을 선점한 뒤 b를 1회 출력한 뒤 다시 blocked 된다.
        (위 사진에는 안나와있지만, 한참 뒤에 b가 한 번 찍힌다.)
      • Task1은 다시 빠른 속도로 a를 출력하는 과정이 반복된다.
    • .
      • Task 2의 vTaskDelay()를 주석했을 때 결과다.
      • 먼저 task2가 시작되며 blocked 될 일이 없기 때문에 task1은 영원히 실행되지 않는다.
    • .
      • Task 1, 2 모두 vTaskDelay()를 주석했을 때 결과다. (위 사진과 같은 결과)
      • 먼저 task2가 시작되며 blocked 될 일이 없기 때문에 task1은 영원히 실행되지 않는다.

이번 실습을 통해 다음 세 가지를 배울 수 있었다.

  1. FreeRTOS에서 멀티테스킹이 작동하는 방식 - Priority와 preemption.
  2. Task를 생성하고 우선순위를 변경하며 관리하는 방법.
  3. 실행흐름을 올바르게 인식하지 못하면 task의 priority 설정을 잘못해 starvation을 유발할 수 있다.

3.4. Stack Overflow 검사

우리는 앞서 각 task마다 지정된 크기의 local stack을 갖고있다고 배웠다. 만일 task 내에서 해당 stack size를 넘는 데이터를 선언하고 연산을 수행한다면 stack overflow가 발생할 것이다. 이 치명적인 fault를 어떻게 발견할 수 있을까? FreeRTOS는 stack overflow를 발견하기 위한 기초적인 두 가지 알고리즘을 제공한다.

FreeRTOSConfig.h 헤더파일 안에는 configCHECK_FOR_STACK_OVERFLOW라는 상수가 정의돼있다. 이 상수는 0, 1, 2 세 가지 값을 가질 수 있다. 0일 경우 stack overflow를 컨트롤러가 따로 검사하지는 않는다.

1번 방법

  • Stack 하위 빈 영역을 ‘0’으로 모두 초기화 한 뒤 context switching이 발생할 때 ‘0’ 패턴이 얼마나 남았는지를 통해 stack overflow를 검사하는 방법이다.
  • 간단한 방법이지만, context switching 때만 검사하기 때문에 stack overflow를 발견했을 때 이게 언제 어디서 발생했는지는 알 수 없다.

2번 방법

  • Stack 하위 20Byte를 인식하기 편리한 정해진 패턴으로 초기화 한 뒤 이 패턴이 조금이라도 변경될 경우 stack overflow가 발생했다고 판단하는 방법이다.
  • 1번 방법에 비해 속도는 느리지만 stack overflow가 발생하는 시점과 위치를 정확하게 알 수 있다. 하지만, 2번 방법도 모든 stack overflow를 찾지는 못한다.

3.5. 실습 02 - Stack overflow

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

  • Task2의 local stack size는 128WORD = 512B이다.
  • Task2에 550B 짜리, 1024B 짜리 char형 배열을 선언하고 memset 함수를 이용해서 초기화하는 함수를 이용해서 stack overflow를 유도한다.
  • Stack overflow 검사 2번 방법을 사용했음에도 불구하고 실행 결과 stack overflow가 발생하지 않고 정상적으로 실행되는 모습을 확인할 수 있다.
  • 사실 이게 더 위험한거다!! 문제가 발생했음에도 assertion이 걸리지 않은 이 상황이 훨씬 위험하다. 후속조치를 취할 수도 없고 문제가 발생했다는 사실조차 알 수 없기 때문이다.

  • 반면에 800B 짜리 배열로 다시 돌렸을 때는 stack overflow를 정상적으로 발견한 것을 확인할 수 있다. 따라서 FreeRTOS에서 제공하는 stack overflow 검사 알고리즘도 완전히 신뢰할 수 없다는 것을 배울 수 있다.
  • 결국 stack overflow를 막기 위한 최선의 방법은 시큐어 코딩 및 개발자의 경험치인 것이다.
profile
임베디드 시스템 공학자를 지망하는 컴퓨터공학+전자공학 복수전공 학부생입니다. 타인의 피드백을 수용하고 숙고하고 대응하며 자극과 반응 사이의 간격을 늘리며 스스로 반응을 컨트롤 할 수 있는 주도적인 사람이 되는 것이 저의 20대의 목표입니다.

0개의 댓글