Static Allocation — 컴파일 타임으로 동적 위험 제거하기
#한 줄 요약
“Static allocation은 컴파일 타임에 모든 메모리를 결정합니다.” — safety-critical의 황금 표준입니다.
#어떤 문제를 푸는가
지금까지 본 heap_1~5와 TLSF는 어떻게 더 잘 alloc할 것인가에 대한 답이었습니다. 하지만 자동차 ECU나 항공기 비행 제어처럼 동적 할당 자체를 금지하는 도메인이 있습니다.
이유는 단순합니다. 동적 할당은 실패할 수 있고, 변동 시간을 갖고, fragmentation 누적 위험이 있고, 분석이 어렵습니다. safety-critical 인증은 이 모든 항목을 증명해야 합니다. 차라리 동적 할당을 완전히 빼면 증명할 항목 자체가 사라집니다.
MISRA C
Dir 4.12, DO-178C Level A/B, ISO 26262 ASIL-D 모두 dynamic allocation 회피를 명시합니다. KSLV-II 누리호 비행 컴퓨터, BMW iX ECU, Boeing 787 일부 모듈이 malloc 한 호출도 없는 시스템으로 출하됩니다. 이번 편은 그 패턴을 정리합니다.#FreeRTOS Static API
FreeRTOS는 거의 모든 객체 생성 API에 *Static variant를 제공합니다. 사용자가 storage buffer와 control block buffer를 직접 제공하고, RTOS는 그 자리에 placement-new 형태로 객체를 구성합니다.
/* Task */static StaticTask_t sensor_tcb;static StackType_t sensor_stack[2048];
TaskHandle_t h = xTaskCreateStatic( sensor_task_fn, /* entry */ "sensor", /* name */ 2048, /* stack depth in words */ NULL, /* parameter */ 5, /* priority */ sensor_stack, /* stack buffer */ &sensor_tcb); /* TCB buffer */
/* Queue */static StaticQueue_t cmd_q_buf;static uint8_t cmd_q_storage[10 * sizeof(cmd_t)];QueueHandle_t q = xQueueCreateStatic(10, sizeof(cmd_t), cmd_q_storage, &cmd_q_buf);
/* Semaphore */static StaticSemaphore_t data_sem_buf;SemaphoreHandle_t s = xSemaphoreCreateBinaryStatic(&data_sem_buf);
/* Mutex */static StaticSemaphore_t bus_mtx_buf;SemaphoreHandle_t m = xSemaphoreCreateMutexStatic(&bus_mtx_buf);
/* Software Timer */static StaticTimer_t timer_buf;TimerHandle_t t = xTimerCreateStatic("hb", pdMS_TO_TICKS(100), pdTRUE, NULL, hb_cb, &timer_buf);
/* Event group */static StaticEventGroup_t eg_buf;EventGroupHandle_t eg = xEventGroupCreateStatic(&eg_buf);모든 storage가 BSS 또는 data section에 들어갑니다. heap은 한 byte도 안 씁니다. 빌드 시 arm-none-eabi-size 출력만 봐도 최종 RAM 사용량이 결정되어 있습니다.
#define configSUPPORT_DYNAMIC_ALLOCATION 0#define configSUPPORT_STATIC_ALLOCATION 1configSUPPORT_DYNAMIC_ALLOCATION = 0으로 두면 pvPortMalloc 자체가 링크되지 않습니다. 누군가 실수로 xTaskCreate(static 아닌 버전)를 호출하면 link error가 즉시 발생합니다. 컴파일 단계에서 동적 할당이 한 곳도 없음을 보장하는 셈입니다.
#Application-Provided 메모리 훅
configSUPPORT_STATIC_ALLOCATION = 1이면 idle task와 timer task를 application이 직접 제공한 buffer에 만들어야 합니다. FreeRTOS는 두 개의 weak 함수를 정의하지 않으므로 사용자가 채워 줍니다.
void vApplicationGetIdleTaskMemory( StaticTask_t **ppxTCB, StackType_t **ppxStack, uint32_t *pulStackSize) { static StaticTask_t tcb; static StackType_t stack[configMINIMAL_STACK_SIZE]; *ppxTCB = &tcb; *ppxStack = stack; *pulStackSize = configMINIMAL_STACK_SIZE;}
void vApplicationGetTimerTaskMemory( StaticTask_t **ppxTCB, StackType_t **ppxStack, uint32_t *pulStackSize) { static StaticTask_t tcb; static StackType_t stack[configTIMER_TASK_STACK_DEPTH]; *ppxTCB = &tcb; *ppxStack = stack; *pulStackSize = configTIMER_TASK_STACK_DEPTH;}두 훅을 정의하지 않으면 link error로 잡혀 부팅 자체가 안 됩니다. 동적 할당을 완전히 끊었다는 증거가 빌드 단계에서 확보됩니다.
#Zephyr 매크로 패턴
Zephyr RTOS는 같은 사상을 매크로로 더 깔끔하게 표현합니다.
K_THREAD_DEFINE(sensor_tid, 2048, sensor_thread_fn, NULL, NULL, NULL, 5, 0, 0);
K_SEM_DEFINE(data_sem, 0, 1);
K_MUTEX_DEFINE(bus_mtx);
K_MSGQ_DEFINE(cmd_q, sizeof(cmd_t), 10, 4);
K_TIMER_DEFINE(heartbeat, hb_handler, NULL);이 매크로들은 컴파일 타임에 객체와 storage를 정의하고 부팅 시 자동 초기화 테이블에 등록합니다. application 코드에는 xTaskCreateStatic 호출조차 없습니다. 모두 link time에 결정됩니다.
#Linker Section으로 메모리 영역 분리
STM32H7이나 i.MX RT처럼 DTCM, SRAM, 외부 SDRAM이 함께 있는 SoC에서는 어떤 객체를 어디에 둘지를 직접 지정합니다.
__attribute__((section(".dtcm"))) static StaticTask_t critical_tcb;__attribute__((section(".dtcm"))) static StackType_t critical_stack[1024];__attribute__((section(".sdram"))) static uint8_t frame_buffer[1024 * 1024];__attribute__((section(".sram"))) static StaticQueue_t cmd_q_buf;빠른 응답이 필요한 control task의 stack은 *DTCM(0-wait)*으로, 큰 frame buffer는 외부 SDRAM으로 보냅니다. linker script가 이 section을 메모리 영역에 매핑합니다.
SECTIONS{ .dtcm (NOLOAD) : { *(.dtcm) } > DTCM .sram (NOLOAD) : { *(.sram) } > SRAM .sdram (NOLOAD) : { *(.sdram) } > SDRAM}NOLOAD로 잡으면 image에는 안 들어가고 부팅 시에만 BSS처럼 0으로 채워집니다. 메모리 배치가 컴파일·링크 시점에 완전히 결정됩니다.
#메모리 footprint 분석
부팅 후의 RAM 사용량을 빌드 산출물에서 직접 읽을 수 있다는 것이 static allocation의 큰 장점입니다.
$ arm-none-eabi-size firmware.elf text data bss dec hex filename 98432 1024 131072 230528 38480 firmware.elf
$ arm-none-eabi-nm --size-sort firmware.elf | tail -2020000400 00000800 b sensor_stack20000c00 00000800 b control_stack20001400 00000400 b cmd_q_storage....map 파일을 보면 모든 static 객체의 정확한 주소와 크기가 적혀 있습니다. 메모리 회계가 완전히 닫혀 있습니다. 동적 할당이 있으면 런타임 누적 결과를 측정해야만 알 수 있는 정보입니다.
#Stack Size 계산 — -fstack-usage
각 task의 stack 크기는 추측이 아니라 계산으로 정합니다. GCC의 -fstack-usage 옵션이 함수별 stack 사용량을 파일로 떨어뜨립니다.
$ gcc -fstack-usage -c source.c$ cat source.susource.c:42:6:task_entry 128 staticsource.c:55:6:process 256 staticsource.c:78:6:compute 512 staticsource.c:92:6:log_event 64 staticcall graph를 따라 worst path를 합산합니다.
| 항목 | Byte |
|---|---|
task_entry(128) → process(256) → compute(512) | 896 |
| ISR worst case stack | +64 |
| context switch overhead | +64 |
| safety margin (25%) | +256 |
| total | 1280 → round up to 2048 |
운영 중에는 uxTaskGetStackHighWaterMark로 실제 최대 사용량을 측정합니다. 정적 분석값과 실측값이 2배 이상 차이나면 둘 중 하나가 틀린 것이므로 재검토합니다.
UBaseType_t high_water = uxTaskGetStackHighWaterMark(task);/* high_water = 남아 있던 가장 작은 stack word 수 */#C++ 객체의 static 할당
C++ 전역 객체는 컴파일 타임에 storage가 잡히고, 부팅 시 constructor가 호출됩니다. embedded toolchain은 _init_array에 constructor 포인터를 모아 두고 Reset_Handler가 순차 호출합니다.
class SensorController {public: void run();private: int state_; Filter filter_;};
/* 전역 — BSS + constructor */static SensorController controller;
static StaticTask_t task_tcb;static StackType_t task_stack[2048];
extern "C" void task_entry(void *p) { static_cast<SensorController*>(p)->run();}
int main() { xTaskCreateStatic(task_entry, "ctrl", 2048, &controller, 5, task_stack, &task_tcb); vTaskStartScheduler();}static 또는 namespace-level 객체는 모두 static allocation입니다. 런타임에 new를 호출하지 않는 한 heap은 깨끗합니다.
#안전 표준과의 정합성
- MISRA C Dir 4.12 — “Dynamic memory allocation shall not be used.”
- DO-178C Level A/B — “Dynamic memory allocation in flight software shall be avoided unless thoroughly justified, analyzed, and verified.”
- ISO 26262 ASIL-D — “Dynamic memory allocation should be avoided in safety-critical execution paths.”
- CERT C MEM30-C, MEM31-C, MEM34-C — “Various memory management rules that static allocation makes trivially satisfied.”
static allocation은 이 모든 규칙을 공짜로 만족시킵니다. 동적 할당을 써서 같은 규칙을 만족시키려면 WCET 분석, fragmentation 증명, OOM 경로 검증, robustness testing이 모두 필요합니다. 비용 차이는 수십 인-월이 됩니다.
#자동차 ECU 사례
/* Brake controller — fully static */static StaticTask_t brake_tcb, sensor_tcb, comm_tcb;static StackType_t brake_stack[2048], sensor_stack[1024], comm_stack[1024];
static StaticQueue_t cmd_q_buf;static uint8_t cmd_q_storage[16 * sizeof(brake_cmd_t)];
static StaticSemaphore_t data_mtx_buf;static StaticEventGroup_t status_eg_buf;
int main(void) { /* 전부 static — heap 호출 0회 */ xTaskCreateStatic(brake_task, "brake", 2048, NULL, 6, brake_stack, &brake_tcb); xTaskCreateStatic(sensor_task, "sensor", 1024, NULL, 5, sensor_stack, &sensor_tcb); xTaskCreateStatic(comm_task, "comm", 1024, NULL, 4, comm_stack, &comm_tcb);
vTaskStartScheduler(); for (;;); /* unreachable */}arm-none-eabi-nm 결과가 모든 RAM 사용처를 망라합니다. 메모리 인증 문서에 그 표를 그대로 첨부할 수 있습니다.
#정적 할당의 한계
만능은 아닙니다. peak 메모리가 항상 점유된다는 단점이 있습니다. 한 task가 가끔만 큰 buffer를 쓰더라도 항상 그 크기를 잡고 있어야 합니다. 평균 사용량과 peak 사용량의 비율이 수십 배 차이나는 시스템에서는 static이 비효율적입니다.
이 경계가 모호한 경우 static 기반 + 작은 memory pool로 절충합니다. RTOS 객체와 task stack은 static으로 두고, 동적 buffer는 4-05의 fixed-size pool로 처리합니다. pool도 본질적으로는 컴파일 타임에 잡힌 storage 위에서 도므로 정적 분석이 가능합니다.
#자주 보는 함정과 안티패턴
⚠️ Stack size를 감으로 정함
xTaskCreateStatic(..., 512, ...) 같이 적당한 숫자를 던지면 언젠가 overflow합니다. -fstack-usage + watermark 측정을 모든 task에 적용합니다. 4-06편에서 더 자세히 다룹니다.
⚠️ static과 dynamic API 혼용
한 시스템에 xTaskCreate와 xTaskCreateStatic이 섞이면 heap 사용 0이라는 보장이 깨집니다. configSUPPORT_DYNAMIC_ALLOCATION = 0으로 링크 단계에서 차단하는 것이 안전합니다.
⚠️ const 데이터를 RAM에
const uint8_t lookup_table[1024 * 1024] = { ... };const인데 BSS에 들어가면 RAM 1 MB를 낭비합니다. linker가 .rodata로 보내는지 확인하고, 안 그러면 __attribute__((section(".rodata.lut")))를 명시합니다. Flash로 가야 정상입니다.
⚠️ 전역 객체 초기화 순서 가정
C++ 전역 객체의 생성 순서는 translation unit 사이에서 정의되지 않습니다. task가 시작되기 전에 의존 객체가 준비되어 있는지 보장이 필요합니다. 의심스러우면 main() 안에서 explicit init 함수를 호출합니다.
#정리
- Static allocation은 모든 RTOS 객체를 컴파일 타임에 결정하여 동적 할당의 위험을 원천적으로 제거합니다.
- FreeRTOS는 거의 모든 객체에
*StaticAPI를 제공하며,configSUPPORT_DYNAMIC_ALLOCATION = 0으로 링크 단계 차단이 가능합니다. - Zephyr는
K_THREAD_DEFINE같은 매크로로 같은 결과를 더 간결하게 표현합니다. - linker section과
__attribute__((section()))로 DTCM·SRAM·SDRAM을 명시 배치할 수 있습니다. - stack 크기는
-fstack-usage정적 분석과uxTaskGetStackHighWaterMark실측으로 둘 다 검증합니다. - MISRA, DO-178C, ASIL-D 등 주요 안전 표준 요구를 공짜로 충족합니다.
- 평균 대비 peak가 큰 워크로드는 static + 작은 pool 절충이 합리적입니다.
다음 편은 4-05 Memory Pool에서 fixed-size block allocator를 다룹니다.
#관련 항목
Practical RTOS Internals · 37 of 53
- 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
- 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
- 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
- 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
- 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
- 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
- 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
- 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
- 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
- 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
- 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
- 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
- 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
- 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
- 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
- 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
- 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
- 18RISC-V Context Switch 분석 — ECALL·mret·CSR
- 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
- 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
- 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
- 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
- 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
- 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
- 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
- 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
- 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
- 28Priority Ceiling Protocol — Immediate vs Original 비교
- 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
- 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
- 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
- 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
- 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
- 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
- 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
- 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
- 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
- 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
- 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
- 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
- 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
- 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
- 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
- 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
- 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
- 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
- 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
- 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
- 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
- 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
- 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
- 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
- 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
관련 글
TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
Masmano 2004의 TLSF 알고리즘을 풀어봅니다. Bitmap과 CLZ 명령으로 alloc·free·coalesce 모두 O(1)을 보장하며, 자동차·로봇·RT 게임의 표준 dynamic allocator가 된 이유를 살펴봅니다.
PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
2024년 9월 Linux 6.12 mainline에 합류한 PREEMPT_RT의 핵심 변경을 정리하고, Xenomai 4·EVL과 함께 RTOS와의 선택 기준을 비교합니다. threaded IRQ·sleeping spinlock·cyclictest까지 한 지도에 모읍니다.
Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
NuttX의 POSIX-compliant 구조를 따라가며 PX4 autopilot과 NASA Ingenuity 화성 헬리콥터 채택 배경을 정리합니다. Flat/Protected/Kernel 빌드, VFS, 네트워크, NSH, micro-ROS 통합까지 한 지도로 모읍니다.
이 글을 참조하는 글 (5)
- Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어— Practical RTOS Internals
- Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함— Practical RTOS Internals
- TLSF Allocator 분석 — Two-Level Segregated Fit O(1)— Practical RTOS Internals
- FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프— Practical RTOS Internals
- 임베디드 동적 메모리 — malloc 위험·결정성·대안 분석— Modern Embedded Recipes