전체 여유 메모리는 충분한데 연속된 공간이 없어 할당에 실패하는 현상.
두 종류
외부 단편화 (external fragmentation)
[사용][빈 32][사용][빈 48][사용][빈 40] 총 여유 = 120바이트
- 그런데 64바이트 요청 → 실패! — 연속 공간이 없다
내부 단편화 (internal fragmentation)
- 30바이트를 요청했는데 할당기가 32바이트 블록을 준다
- 2바이트가 낭비되지만 쓸 수 없다
임베디드에서 문제가 되는 것은 주로 외부 단편화다.
왜 임베디드에서 치명적인가
- 서버 — : 메모리가 GB 단위, 프로세스 재시작이 쉽다, MMU가 가상 연속성 제공
- 임베디드 — : RAM 수십 KB, 수년간 재부팅 없이 동작, MMU 없음
MMU가 없으면 물리적으로 연속이어야 한다. 가상 주소로 흩어진 페이지를 연속처럼 보이게 하는 기법을 쓸 수 없다.
게다가 몇 달 뒤에 실패한다. 개발·테스트 중에는 멀쩡하다가 현장에서 장기 가동 후 터지기 때문에 재현과 진단이 매우 어렵다.
그래서 임베디드는 malloc을 피한다
MISRA C 규칙 21.3 — 동적 메모리 할당 함수 사용 금지
이유
- ① 단편화로 장기 실행 시 실패
- ② 할당 시간이 일정하지 않다 (실시간성 파괴)
- ③ 실패 시 대응이 어렵다 (돌아갈 곳이 없다)
- ④ 메모리 누수가 곧 시스템 정지
- ⑤ 힙과 스택이 충돌해도 감지되지 않는다
대안 ① — 정적 할당
static uint8_t buffer[MAX_SIZE]; // 컴파일 시점에 확정
링커가 RAM 사용량을 계산해 준다. 빌드가 성공하면 메모리가 부족할 일이 없다. 가장 확실하고 임베디드의 기본이다.
대안 ② — 고정 크기 메모리 풀
#define POOL_SIZE 32
#define BLOCK_SIZE 64
static uint8_t pool[POOL_SIZE][BLOCK_SIZE];
static uint32_t used_bitmap;
void *pool_alloc(void) {
for (int i = 0; i < POOL_SIZE; i++)
if (!(used_bitmap & (1U << i))) {
used_bitmap |= (1U << i);
return pool[i];
}
return NULL;
}
모든 블록의 크기가 같으므로 외부 단편화가 원리적으로 발생하지 않는다. 할당·해제가 O(1)이라 시간도 일정하다.
크기가 다른 객체가 필요하면 크기별로 여러 풀을 둔다(32B 풀, 128B 풀…). RTOS 대부분이 이 기능을 제공한다(FreeRTOS의 정적 할당, CMSIS-RTOS의 메모리 풀).
대안 ③ — 링 버퍼
typedef struct {
uint8_t buf[SIZE];
volatile uint16_t head, tail;
} RingBuffer;
스트리밍 데이터(UART 수신, 로그)에는 링 버퍼가 정답이다.
할당이 아예 없고, SIZE를 2의 거듭제곱으로 하면
인덱스 갱신이 & (SIZE-1) 한 번으로 끝난다.
굳이 힙을 써야 한다면
- ① 초기화 시점에만 할당하고 절대 해제하지 않는다
- 단편화가 생길 수 없다. 사실상 정적 할당과 같다
- ② 할당 실패를 반드시 처리한다 (NULL 검사)
- ③ 힙 크기와 사용량을 모니터링한다
- ④ FreeRTOS의 heap_4(병합 지원) 또는 heap_5 를 쓴다
FreeRTOS 힙 구현 비교 heap_1 : 해제 불가. 가장 단순하고 안전 heap_2 : 해제 가능하지만 병합 안 함 → 단편화 발생
- heap_4 : 인접 빈 블록을 병합 → 단편화 완화 — ← 권장 heap_5 : heap_4 + 여러 메모리 영역 지원
진단
// FreeRTOS
xPortGetFreeHeapSize(); // 현재 여유
xPortGetMinimumEverFreeHeapSize(); // 역대 최저 ← 이것을 봐야 한다
"역대 최저 여유량"이 핵심 지표다. 현재 여유가 넉넉해도 피크에서 바닥을 쳤다면 위험하다. 장기 시험에서 이 값이 계속 줄어들면 누수 또는 단편화가 진행 중인 것이다.