본문 바로가기

FreeRTOS 공부하기 03 - 힙 메모리 관리

@밀양박씨!2026. 5. 2. 07:12

3.1 들어가기 전에

3.1.1 이 챕터를 이해하려면 알아야 할 것

이 챕터는 C 언어의 기본 개념을 알고 있다고 가정합니다. 구체적으로는 C 프로젝트가 컴파일되고 링킹되는 과정, 스택(Stack)과 힙(Heap)이 무엇인지, 그리고 표준 C 라이브러리의 malloc()과 free() 함수를 알고 있어야 합니다.


혹시 스택과 힙이 헷갈린다면 이렇게 이해하세요.

스택은 함수가 호출될 때 자동으로 생겼다가 함수가 끝나면 자동으로 사라지는 메모리입니다.

함수 안의 지역 변수들이 여기에 들어갑니다.

은 프로그래머가 malloc()으로 직접 필요할 때 잡고, free()로 직접 돌려주는 메모리입니다. 크기를 실행 중에 결정할 수 있어서 유연하지만, 관리를 직접 해야 합니다.



3.1.2 이 챕터에서 배우는 것

세 가지를 다룹니다.

 

FreeRTOS가 언제 RAM을 할당하는지,

FreeRTOS가 제공하는 다섯 가지 메모리 할당 방식(heap_1 ~ heap_5)이 각각 어떻게 동작하는지,

그리고 내 상황에 어떤 방식을 골라야 하는지입니다.

 

 

3.1.3 정적 할당 vs 동적 할당

FreeRTOS에서 태스크, 큐, 세마포어, 이벤트 그룹 같은 커널 객체를 만들 때 RAM이 필요합니다. 이 RAM을 확보하는 방법이 두 가지 있습니다.

 

동적 할당(Dynamic Allocation):

프로그램이 실행되는 중에 필요할 때 RAM을 할당합니다. 설계가 단순해지고 API 사용이 편하며 RAM을 효율적으로 씁니다. 단, 메모리 할당이 실패할 수 있고 단편화(fragmentation) 위험이 있습니다.

 

정적 할당(Static Allocation):

컴파일 타임에 미리 RAM을 확보합니다. 실행 결과가 예측 가능하고(deterministic), 할당 실패 처리가 필요 없으며, 단편화가 생기지 않습니다. 단, 설계 단계에서 필요한 메모리를 미리 전부 파악해야 합니다.

 

 

여기서 단편화 (fragmentation) 란, 힙 전체에 여유 메모리가 충분히 있는데도 연속된 빈 공간이 없어서 할당이 실패하는 현상입니다.

예를 들어 힙에 200바이트가 남아 있는데, 100바이트짜리 공간 두 개로 쪼개져 있다면 150바이트 할당 요청은 실패합니다.

c

onfigSUPPORT_STATIC_ALLOCATION을 1로 설정하면 정적 할당 API가 활성화되고, configSUPPORT_DYNAMIC_ALLOCATION을 1로 설정하거나 미정의 상태로 두면 동적 할당 API가 활성화됩니다.

두 가지를 동시에 활성화해도 됩니다.


3.1.4 왜 표준 malloc()을 그냥 쓰면 안 되나?

C 언어의 malloc()과 free()를 그냥 쓰면 안 되는 이유가 여러 가지 있습니다.

 

  • 소형 임베디드 시스템에서는 아예 제공되지 않는 경우가 있습니다.
  • 구현 코드 크기가 커서 소중한 코드 공간을 낭비합니다.
  •  스레드 안전(thread-safe)하지 않습니다. 즉, 두 태스크가 동시에 malloc()을 호출하면 메모리가 망가질 수 있습니다.
  • 비결정적(non-deterministic)입니다. 같은 크기를 요청해도 매번 걸리는 시간이 다릅니다. 실시간 시스템에서는 치명적입니다.
  •  단편화 문제가 있습니다.
  • 링커 설정이 복잡해집니다.
  • 힙이 다른 변수 영역과 겹쳐 디버그하기 어려운 오류를 일으킬 수 있습니다.

 

그래서 FreeRTOS는 malloc() 대신 pvPortMalloc()을, free() 대신 vPortFree()를 사용합니다. 이 두 함수는 FreeRTOS가 내부적으로 쓰는 동시에, 애플리케이션 코드에서도 직접 호출할 수 있습니다.

 

 

3.1.5 FreeRTOS의 접근 방식

FreeRTOS는 메모리 할당을 포터블 레이어(portable layer)의 일부로 취급합니다.

다시 말해, 메모리 할당 방식이 커널 핵심 코드에 고정되어 있지 않고, 하드웨어와 애플리케이션 특성에 맞게 교체할 수 있습니다.

FreeRTOS가 제공하는 다섯 가지 구현이 heap_1.c ~ heap_5.c이며, 모두 FreeRTOS/Source/portable/MemMang/ 디렉토리에 있습니다. 마음에 드는 것을 하나 골라서 프로젝트에 포함하면 됩니다. 직접 구현해서 사용해도 됩니다.

 

 

 

3.2 다섯 가지 메모리 할당 방식 상세 설명

3.2.1 Heap_1 — 가장 단순, 해제 불가

한 줄 요약: 한 번 할당하면 절대 해제하지 않는 시스템에 적합합니다.

소형 임베디드 시스템에서는 보통 스케줄러 시작 전에 태스크와 커널 객체를 전부 만들고, 이후 프로그램이 끝날 때까지 절대 삭제하지 않습니다. 이런 패턴에서는 복잡한 메모리 관리가 필요 없습니다.

Heap_1은 항상 결정적이며 메모리 단편화를 발생시키지 않습니다.

동작 방식: configTOTAL_HEAP_SIZE 크기의 uint8_t 배열을 하나 만들어 놓고, pvPortMalloc()이 호출될 때마다 이 배열의 앞부분부터 순서대로 잘라서 줍니다. vPortFree()는 아예 구현되어 있지 않습니다. 즉 한 번 쓰면 반납이 안 됩니다.

A는 작업을 생성하기 전의 배열을 보여줍니다. 배열 전체가 비어 있는 상태입니다.
B는 하나의 작업을 생성한 후의 배열을 보여줍니다.
C는 세 가지 작업을 생성한 후의 배열을 보여줍니다.

태스크 하나를 동적으로 생성할 때 pvPortMalloc()이 두 번 호출됩니다.

한 번은 TCB(Task Control Block — 태스크 상태를 저장하는 구조체)를 위해,

한 번은 태스크의 스택을 위해서입니다.

 

 

장점:

  • 구현이 매우 단순하고 코드 크기가 작습니다.
  • 항상 결정적(deterministic)입니다. 즉 실행 시간이 항상 일정합니다.
  • 절대로 단편화가 발생하지 않습니다.
  • 안전성이 중요한 시스템(항공, 의료)에서 동적 메모리를 금지하지만 heap_1은 허용하는 경우가 있습니다. 한 번 쓰고 반납하지 않으므로 사실상 정적 할당과 동일한 예측 가능성을 가지기 때문입니다.

단점:

  • 한 번 할당한 메모리를 절대 반납할 수 없습니다.
  • 태스크나 커널 객체를 삭제하는 애플리케이션에는 사용할 수 없습니다.

 

3.2.2 Heap_2 — 해제 가능하지만 구식(지금은 사용 비추)

한 줄 요약: heap_4로 대체되었으므로 새 프로젝트에는 쓰지 마세요. (하위 호환성을 위해 남아있습니다.)

 

 

heap_2도 configTOTAL_HEAP_SIZE 크기의 배열을 씁니다. heap_1과 다른 점은 vPortFree()가 구현되어 있어 메모리를 반납할 수 있다는 것입니다.

 

동작 방식 — 최적 적합(Best-Fit) 알고리즘: 요청한 크기에 가장 가까운 빈 블록을 찾아서 할당합니다.

예를 들어 힙에 5바이트, 25바이트, 100바이트짜리 빈 블록이 있고 20바이트를 요청하면, 20바이트가 들어갈 수 있는 가장 작은 블록인 25바이트 블록을 선택합니다. 25바이트 블록을 20바이트와 5바이트로 쪼갠 뒤 20바이트를 반환하고, 남은 5바이트 블록은 다음 할당을 위해 보존합니다.

 

heap_2의 치명적 약점: 해제된 인접 블록들을 하나로 합치지(coalesce) 않습니다. 그 결과, 메모리를 반납해도 작은 조각들로 흩어진 채로 남아서 단편화가 심해질 수 있습니다.

단, 항상 같은 크기의 블록을 할당하고 해제하는 패턴이라면 단편화 문제가 생기지 않습니다.

 

heap_2는 비결정적(non-deterministic)이지만, 대부분의 표준 라이브러리 malloc()보다는 빠릅니다.

 


A는 세 개의 작업을 할당한 후의 배열을 보여줍니다. 배열의 맨 위에는 큰 빈 공간이 남아 있습니다.


B는 작업 하나를 삭제한 후의 배열을 보여줍니다. 배열 맨 위에 있는 큰 빈 블록은 그대로 남아 있습니다. 이제 삭제된 작업의 TCB와 스택이 있던 자리에 두 개의 작은 빈 블록이 생겼습니다.


C는 다른 작업을 생성한 후의 상황을 보여줍니다. 작업을 생성하면 API 함수 pvPortMalloc()내에서 두 번의 호출이 발생하는데 xTaskCreate(), 하나는 새 TCB를 할당하는 호출이고 다른 하나는 작업 스택을 할당하는 호출입니다. 
모든 TCB는 크기가 동일하므로 최적 적합 알고리즘은 삭제된 작업의 TCB를 저장했던 RAM 블록을 재사용하여 생성된 작업의 TCB를 저장합니다. 새로 생성된 작업에 할당된 스택의 크기가 이전에 삭제된 작업에 할당된 스택의 크기와 동일한 경우, 최적 적합 알고리즘은 삭제된 작업의 스택을 저장했던 RAM 블록을 재사용하여 생성된 작업의 스택을 저장합니다. 배열 상단에 있는 더 큰 할당되지 않은 블록은 그대로 유지됩니다.

 


malloc() Heap_2는 결정적이지는 않지만 대부분의 표준 라이브러리 구현보다 빠릅니다.




3.2.3 Heap_3 — 표준 malloc() 래퍼

한 줄 요약: 표준 malloc()/free()를 FreeRTOS에서 안전하게 쓸 수 있게 감싼 것입니다.

heap_3은 직접 메모리를 관리하지 않고, 시스템의 표준 malloc()과 free()를 그대로 사용합니다. 그 대신 이 함수들을 호출하는 동안 FreeRTOS 스케줄러를 일시적으로 중단(suspend)시켜서 스레드 안전성을 확보합니다.

특징:

  • configTOTAL_HEAP_SIZE가 아닌 링커 설정이 힙 크기를 결정합니다.
  • 스케줄러 중단을 통해 thread-safe를 보장합니다.
  • 표준 malloc()의 단점(비결정성, 단편화 등)을 그대로 가집니다.
  • 컴파일러/링커가 표준 malloc()을 제공하는 환경에서만 사용 가능합니다.

 

3.2.4 Heap_4 — 가장 많이 쓰이는 범용 방식 (강력 추천)

한 줄 요약: 단편화를 최소화하는 인접 블록 병합 기능이 있는 범용 방식으로, 대부분의 상황에서 최선의 선택입니다.

 

heap_4도 configTOTAL_HEAP_SIZE 크기의 배열을 사용합니다. heap_2와 달리 인접한 빈 블록을 하나로 합치는 병합(coalescence) 기능이 있어서 단편화 위험이 훨씬 줄어듭니다.

 

동작 방식

최초 적합(First-Fit) 알고리즘:

요청한 크기가 들어갈 수 있는 첫 번째 빈 블록을 찾아서 할당합니다.

예를 들어 힙에 배열 순서대로 5바이트, 200바이트, 100바이트짜리 빈 블록이 있고 20바이트를 요청하면, 처음으로 20바이트가 들어갈 수 있는 200바이트 블록을 선택합니다. 200바이트 블록을 20바이트와 180바이트로 쪼갠 뒤 20바이트를 반환하고, 180바이트 블록은 보존합니다.

 

heap_4의 핵심 강점 — 블록 병합:

아래 시나리오로 이해해 보겠습니다.

A는 세 개의 작업을 생성한 후의 배열을 보여줍니다. 배열의 맨 위에는 큰 빈 공간이 남아 있습니다.

 B는 작업 하나를 삭제한 후의 배열을 보여줍니다. 배열 상단의 큰 빈 블록은 그대로 남아 있습니다. 삭제된 작업의 TCB와 스택이 있던 자리에 또 다른 빈 블록이 생겼습니다. heap_2 예제와 달리 heap_4에서는 삭제된 작업의 TCB와 스택을 각각 저장하던 두 개의 메모리 블록이 하나의 더 큰 빈 블록으로 병합됩니다.

C는 FreeRTOS 대기열을 생성한 후의 상황을 보여줍니다.

xQueCreate()는 pvPortMalloc()을 호출하여 대기열에서 사용되는 RAM을 할당합니다.

heap_4는 첫 번째 맞춤 알고리즘을 사용하므로 pvPortMalloc()은 대기열을 고정할 수 있을 만큼 큰 첫 번째 무료 RAM 블록에서 RAM을 할당합니다. 위 그림에서는 작업을 삭제하여 RAM을 해제했습니다.

대기열은 무료 블록의 모든 RAM을 소비하지 않으므로 블록은 두 개로 나뉘며, 사용되지 않은 부분은 나중에 pvPortMalloc()으로 호출할 수 있습니다.

D는 FreeRTOS API 함수를 간접적으로 호출하는 대신 애플리케이션 코드에서 직접 pvPortMalloc()을 호출한 후의 상황을 보여줍니다. 사용자가 할당한 블록은 큐에 할당된 메모리와 그 다음에 할당된 TCB에 할당된 메모리 사이의 블록인 첫 번째 자유 블록에 들어갈 수 있을 만큼 작았습니다. 작업을 삭제하여 확보한 메모리는 이제 세 개의 개별 블록으로 나뉘었습니다. 첫 번째 블록은 대기열을, 두 번째 블록은 사용자가 할당한 메모리를, 세 번째 블록은 자유롭게 유지됩니다.

E는 큐를 삭제한 후의 상황을 보여줍니다. 큐를 삭제하면 삭제된 큐에 할당된 메모리가 자동으로 해제됩니다. 이제 사용자가 할당한 블록 양쪽에 사용 가능한 메모리가 생깁니다.

F는 사용자 할당 메모리를 해제한 후의 상황을 보여줍니다. 사용자 할당 블록에서 이전에 사용했던 메모리는 양쪽의 사용 가능한 메모리와 합쳐져 더 큰 단일 사용 가능한 블록을 형성합니다.



이처럼 heap_4는 해제할 때마다 인접한 빈 블록들을 자동으로 합쳐서 큰 연속 블록을 유지합니다. 다양한 크기의 블록을 반복해서 할당하고 해제하는 애플리케이션에 특히 적합합니다.

heap_4도 비결정적이지만 대부분의 표준 라이브러리 malloc()보다 빠릅니다.


3.2.5 Heap_5 — 여러 RAM 영역을 하나의 힙으로

한 줄 요약: RAM이 메모리 맵에서 연속된 하나의 블록이 아닐 때 사용합니다.

 

heap_4는 하나의 배열만 힙으로 쓸 수 있지만, 어떤 하드웨어는 RAM이 메모리 맵에서 여러 군데 흩어져 있습니다. 예를 들어 내부 SRAM과 외부 SDRAM이 주소 공간에서 떨어져 있는 경우가 그렇습니다.

heap_5는 이런 여러 개의 분리된 RAM 영역을 하나의 힙으로 통합해서 관리할 수 있게 해줍니다. 할당 알고리즘 자체는 heap_4와 동일합니다.

 

 

3.2.6 Heap_5 초기화 — vPortDefineHeapRegions()

heap_5는 사용하기 전에 반드시 vPortDefineHeapRegions()를 먼저 호출해야 합니다. 이것이 heap_5와 heap_4의 가장 큰 차이입니다. vPortDefineHeapRegions()를 호출하기 전에는 태스크, 큐, 세마포어 같은 커널 객체를 동적으로 생성할 수 없습니다.

void vPortDefineHeapRegions( const HeapRegion_t * const pxHeapRegions );

이 함수는 HeapRegion_t 구조체 배열을 받아서, 각 구조체가 정의하는 메모리 영역들을 합쳐 하나의 힙으로 만들어 줍니다.

typedef struct HeapRegion
{
    uint8_t *pucStartAddress;  // 메모리 영역의 시작 주소
    size_t   xSizeInBytes;     // 메모리 영역의 크기 (바이트)
} HeapRegion_t;

규칙:

  • 배열은 반드시 시작 주소 오름차순으로 정렬해야 합니다. 즉 주소가 가장 낮은 영역을 첫 번째 구조체로, 가장 높은 영역을 마지막 구조체로 넣어야 합니다.
  • 배열의 끝은 pucStartAddress가 NULL인 구조체로 표시합니다.

 

 

예시 상황 — RAM이 세 곳에 흩어진 하드웨어:

나쁜 방법 (비추천):

const HeapRegion_t xHeapRegions[] =
{
    { (uint8_t*)0x00010000, 64*1024 },  // RAM1 전체
    { (uint8_t*)0x00020000, 32*1024 },  // RAM2 전체
    { (uint8_t*)0x00030000, 32*1024 },  // RAM3 전체
    { NULL, 0 }
};

이 방법의 문제는 RAM1 전체를 힙에 줘버린다는 것입니다. 그런데 링커는 전역 변수, 정적 변수 등을 RAM1에 배치합니다. 결과적으로 힙과 변수가 같은 공간을 겹쳐 쓰게 되어 메모리가 망가집니다.

 

 

좋은 방법 (권장):

/* RAM1의 일부만 힙에 사용 — 나머지는 링커가 변수 배치에 씀 */
#define RAM1_HEAP_SIZE (30 * 1024)
static uint8_t ucHeap[RAM1_HEAP_SIZE];  // 링커가 RAM1 안에 배치

const HeapRegion_t xHeapRegions[] =
{
    { ucHeap,             RAM1_HEAP_SIZE },  // RAM1의 일부 (ucHeap 배열)
    { (uint8_t*)0x00020000, 32*1024 },       // RAM2 전체
    { (uint8_t*)0x00030000, 32*1024 },       // RAM3 전체
    { NULL, 0 }
};

int main(void)
{
    vPortDefineHeapRegions(xHeapRegions);  // 반드시 가장 먼저 호출!
    /* 이 이후에 태스크 생성 등 가능 */
}

ucHeap을 일반 변수로 선언하면 링커가 자동으로 RAM1 안에 배치하고, 나머지 RAM1 영역은 링커가 다른 변수들을 위해 씁니다. 이 방식의 장점은 네 가지입니다. 하드코딩된 주소가 필요 없고, 링커가 주소를 자동으로 결정하므로 빌드 환경이 바뀌어도 안전합니다. 힙과 변수가 절대 겹칠 수 없고, ucHeap이 너무 크면 링크 단계에서 오류가 납니다.

 

 

 

 

 

 

3.3 힙 관련 유틸리티 함수들

3.3.1 힙 시작 주소 지정하기

heap_1, heap_2, heap_4는 내부적으로 configTOTAL_HEAP_SIZE 크기의 배열을 자동으로 선언합니다. 하지만 성능을 위해 힙을 특정 주소(예: 빠른 내부 SRAM)에 위치시키고 싶을 때가 있습니다.

FreeRTOSConfig.h에서 configAPPLICATION_ALLOCATED_HEAP을 1로 설정하면, 자동 선언 대신 애플리케이션이 직접 ucHeap 배열을 선언하도록 바꿀 수 있습니다.

// GCC에서 특정 섹션에 힙 배치
uint8_t ucHeap[configTOTAL_HEAP_SIZE] __attribute__((section(".my_heap")));

// IAR에서 절대 주소에 힙 배치
uint8_t ucHeap[configTOTAL_HEAP_SIZE] @ 0x20000000;

이후 링커 스크립트에서 .my_heap 섹션을 원하는 메모리 위치에 배치하면 됩니다.

 

 

3.3.2 xPortGetFreeHeapSize() — 현재 남은 힙 크기 확인

size_t xPortGetFreeHeapSize( void );

호출 시점에 힙에서 할당되지 않은 바이트 수를 반환합니다. 이것으로 힙 여유 공간을 모니터링할 수 있습니다.

주의할 점은 총 여유 공간만 알려줄 뿐, 단편화 상태(가장 큰 연속 빈 블록이 얼마인지)는 알려주지 않는다는 것입니다. heap_3에서는 구현되어 있지 않습니다.

실용 예시: 디버깅 중에 주기적으로 이 값을 출력하면 힙이 점점 소진되는지 감지할 수 있습니다.

 

3.3.3 xPortGetMinimumEverFreeHeapSize() — 역대 최솟값 확인

size_t xPortGetMinimumEverFreeHeapSize( void );

FreeRTOS 앱이 시작된 이후 힙의 여유 공간이 최소였던 순간의 값을 반환합니다. heap_4와 heap_5에서만 구현되어 있습니다.

이 값의 활용법이 매우 실용적입니다. 예를 들어 이 함수가 200을 반환한다면, 앱 실행 중 어느 순간 힙이 200바이트밖에 남지 않은 적이 있다는 뜻입니다. 아슬아슬한 것을 알 수 있습니다.

반대로, 가장 힙을 많이 쓰는 코드 구간을 실행한 후 이 함수가 2000을 반환한다면, configTOTAL_HEAP_SIZE를 최대 2000바이트 줄여도 안전합니다. 이렇게 힙 크기를 최적화하는 데 씁니다.

 

 

3.3.4 vPortGetHeapStats() — 힙 상세 통계

heap_4와 heap_5에서만 사용 가능합니다.

void vPortGetHeapStats( HeapStats_t *xHeapStats );

HeapStats_t 구조체에 아래 정보를 채워줍니다.

 

 

xAvailableHeapSpaceInBytes 현재 총 여유 바이트 수 (모든 빈 블록의 합)
xSizeOfLargestFreeBlockInBytes 현재 가장 큰 빈 블록의 크기
xSizeOfSmallestFreeBlockInBytes 현재 가장 작은 빈 블록의 크기
xNumberOfFreeBlocks 현재 빈 블록 개수
xMinimumEverFreeBytesRemaining 역대 최소 여유 바이트 수
xNumberOfSuccessfulAllocations 성공한 pvPortMalloc() 호출 횟수
xNumberOfSuccessfulFrees 성공한 vPortFree() 호출 횟수

 

xAvailableHeapSpaceInBytes가 크더라도 xSizeOfLargestFreeBlockInBytes가 작다면 단편화가 심한 상태입니다. 이렇게 두 값을 비교하면 단편화 정도를 파악할 수 있습니다.

 

 

 

3.3.5 태스크별 힙 사용 통계 수집

FreeRTOS는 traceMALLOC와 traceFREE라는 트레이스 매크로를 제공합니다. 이 매크로를 FreeRTOSConfig.h에 정의하면, pvPortMalloc()과 vPortFree()가 호출될 때마다 내가 원하는 코드가 자동으로 실행됩니다.

책에서 제공하는 예시 구현은 두 개의 테이블을 유지합니다.

할당 테이블: 각 할당에 대해 어떤 태스크가 할당했는지, 크기는 얼마인지, 포인터는 무엇인지를 기록합니다.

태스크별 테이블: 각 태스크가 현재 얼마나 메모리를 들고 있는지, 역대 최대치가 얼마였는지를 추적합니다.

이것을 활용하면 "어느 태스크가 메모리를 많이 쓰는가", "메모리 누수(할당하고 해제하지 않는 것)가 있는가"를 진단할 수 있습니다. 실제 프로덕션보다는 개발·디버깅 단계에서 유용합니다.

 

 

3.3.6 Malloc 실패 훅 함수

pvPortMalloc()이 메모리를 할당하지 못하면 NULL을 반환합니다. 이때 자동으로 호출되는 콜백 함수를 등록할 수 있습니다. FreeRTOSConfig.h에서 configUSE_MALLOC_FAILED_HOOK을 1로 설정하면 됩니다.

void vApplicationMallocFailedHook( void );

이 함수를 애플리케이션에서 직접 구현해야 합니다. 만약 이 훅이 FreeRTOS API 함수 내부에서 호출되었다면(예: xTaskCreate() 중 메모리 부족), 해당 커널 객체는 생성되지 않습니다.

많은 FreeRTOS 데모는 할당 실패를 치명적 오류로 처리하지만, 실제 제품에서는 더 우아하게 복구하는 로직을 구현하는 것이 바람직합니다. 예를 들어 실패를 로그에 기록하고, 중요도가 낮은 태스크를 삭제해서 메모리를 확보하는 방식을 쓸 수 있습니다.

 

 

3.3.7 태스크 스택을 빠른 메모리에 배치하기

스택은 읽고 쓰는 빈도가 매우 높습니다. 그래서 가능하면 빠른 내부 SRAM에 스택을 배치하고, 나머지 힙은 좀 더 느린 외부 메모리를 써도 되는 경우가 있습니다.

이를 위해 FreeRTOS는 pvPortMallocStack()과 vPortFreeStack() 매크로를 제공합니다. 이 매크로들은 기본적으로 pvPortMalloc()과 vPortFree()로 연결되어 있습니다. 스택용 별도 메모리 할당자를 만들고 싶으면 이 매크로를 재정의하면 됩니다.

// 애플리케이션에서 빠른 메모리 할당 함수를 따로 구현
void *pvMallocFastMemory( size_t xWantedSize );
void vPortFreeFastMemory( void *pvBlockToFree );

// FreeRTOSConfig.h에 아래를 추가해서 스택 할당자를 교체
#define pvPortMallocStack( x )  pvMallocFastMemory( x )
#define vPortFreeStack( x )     vPortFreeFastMemory( x )

 

이렇게 하면 태스크 스택만 빠른 메모리에서 가져오고, 다른 객체(큐, 세마포어 등)는 일반 힙에서 할당합니다.

 

3.4 정적 메모리 할당 사용하기

동적 할당의 단점(비결정성, 단편화, 실패 가능성)을 피하고 싶다면 정적 할당을 선택할 수 있습니다.

정적 할당의 장점:

  • 필요한 메모리가 컴파일 타임에 전부 확정됩니다.
  • 모든 메모리 동작이 결정적(deterministic)입니다.

추가되는 책임:

  • 커널 내부 메모리(Idle 태스크, Timer 태스크용)를 사용자가 직접 제공해야 합니다.
  • 정적으로 선언하는 모든 변수가 적절한 스코프(scope)에 있어야 합니다. 함수 안에 선언하면 함수가 끝날 때 사라지므로, 반드시 static으로 선언하거나 전역 변수로 두어야 합니다.

 

 

3.4.1 정적 할당 활성화

FreeRTOSConfig.h에서 configSUPPORT_STATIC_ALLOCATION을 1로 설정하면 됩니다. 그러면 아래와 같은 Static 버전 API가 활성화됩니다.

  • xTaskCreateStatic() — 태스크를 정적 메모리로 생성
  • xQueueGenericCreateStatic() — 큐를 정적 메모리로 생성
  • xEventGroupCreateStatic() — 이벤트 그룹을 정적 메모리로 생성
  • xTimerCreateStatic() — 타이머를 정적 메모리로 생성
  • xStreamBufferGenericCreateStatic() — 스트림 버퍼를 정적 메모리로 생성

 

 

3.4.2 커널 내부 정적 메모리 제공

정적 할당이 활성화되면, FreeRTOS가 자동으로 생성하는 두 가지 내부 태스크(Idle 태스크, Timer 태스크)도 정적 메모리로 운영됩니다. 이 메모리는 사용자가 직접 함수를 구현해서 제공해야 합니다.

 

3.4.2.1 vApplicationGetTimerTaskMemory() — 타이머 태스크 메모리 제공

configUSE_TIMERS가 1이고 정적 할당이 활성화된 경우, 커널이 이 함수를 호출해서 타이머 태스크의 TCB와 스택에 쓸 메모리를 요청합니다.

void vApplicationGetTimerTaskMemory( StaticTask_t **ppxTimerTaskTCBBuffer,
                                     StackType_t  **ppxTimerTaskStackBuffer,
                                     uint32_t     *pulTimerTaskStackSize )
{
    // 반드시 static 으로 선언! 함수가 끝나도 메모리가 살아있어야 합니다.
    static StaticTask_t xTimerTaskTCB;
    static StackType_t  uxTimerTaskStack[ configMINIMAL_STACK_SIZE ];

    *ppxTimerTaskTCBBuffer   = &xTimerTaskTCB;         // TCB 버퍼 전달
    *ppxTimerTaskStackBuffer = uxTimerTaskStack;        // 스택 버퍼 전달
    *pulTimerTaskStackSize   = configMINIMAL_STACK_SIZE; // 스택 크기 전달
}

중요: static으로 선언하는 이유는, 함수가 반환된 후에도 커널이 계속 이 메모리를 사용하기 때문입니다. static 없이 선언하면 함수가 끝날 때 스택에서 사라져 버립니다.

타이머 태스크는 시스템 전체에 하나만 있으므로, 이렇게 static 지역 변수로 선언하는 것이 가장 깔끔한 방법입니다.

 

 

3.4.2.2 vApplicationGetIdleTaskMemory() — Idle 태스크 메모리 제공

Idle 태스크는 모든 태스크가 대기 상태일 때 실행됩니다. 약간의 내부 정리 작업을 하고, vTaskIdleHook()이 활성화된 경우 이를 호출합니다. SMP(멀티코어) 시스템에서는 나머지 코어용 Idle 태스크가 추가로 있지만, 이것들은 내부적으로 정적 할당됩니다.

void vApplicationGetIdleTaskMemory( StaticTask_t **ppxIdleTaskTCBBuffer,
                                    StackType_t  **ppxIdleTaskStackBuffer,
                                    uint32_t     *pulIdleTaskStackSize )
{
    // 마찬가지로 반드시 static!
    static StaticTask_t xIdleTaskTCB;
    static StackType_t  uxIdleTaskStack[ configMINIMAL_STACK_SIZE ];

    *ppxIdleTaskTCBBuffer   = &xIdleTaskTCB;
    *ppxIdleTaskStackBuffer = uxIdleTaskStack;
    *pulIdleTaskStackSize   = configMINIMAL_STACK_SIZE;
}

 

 

전체 핵심 요약

다섯 가지 방식 한눈 비교:

  heap_1 heap_2  heap_3 heap_4 heap_5
해제 가능
단편화 방지 ✅ (완벽) ⚠️ 취약 표준 의존 ✅ (병합) ✅ (병합)
결정적
여러 RAM 영역
초기화 필요
추천 여부 단순 시스템 ❌ (구식) 상황에 따라 일반 추천 분산 RAM

 

어떤 방식을 골라야 하나?

태스크를 한 번 만들고 절대 삭제하지 않는 단순한 시스템이라면 heap_1을 씁니다. RAM이 메모리 맵에서 여러 군데 흩어져 있다면 heap_5를 씁니다. 표준 malloc()을 반드시 써야 하는 환경이라면 heap_3을 씁니다. 그 외의 대부분의 경우에는 heap_4가 최선의 선택입니다.

 

밀양박씨!
@밀양박씨! :: 박씨의 개발블로그

lovebotw049 님의 블로그 입니다.

목차