임베디드 면접 용어 사전
메모리·아키텍처fragmentation

메모리 단편화

빈 공간이 조각나 총량은 충분한데 할당이 실패하는 현상. 임베디드에서 동적 할당을 피하는 주된 이유다.

전체 여유 메모리는 충분한데 연속된 공간이 없어 할당에 실패하는 현상.

두 종류

외부 단편화 (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();   // 역대 최저 ← 이것을 봐야 한다

"역대 최저 여유량"이 핵심 지표다. 현재 여유가 넉넉해도 피크에서 바닥을 쳤다면 위험하다. 장기 시험에서 이 값이 계속 줄어들면 누수 또는 단편화가 진행 중인 것이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 메모리·아키텍처 — 메모리맵·MMU/MPU·캐시·DMA·엔디안·정렬