메모리 풀: 고정 크기 블록 할당
EmbeddedMemory
메모리 풀은 동일한 크기의 블록을 미리 할당해두고, 요청 시 블록 단위로 꺼내 쓰는 할당 방식이다. malloc의 단편화, 비결정적 실행 시간 문제를 해결한다.
구조
메모리 풀 (블록 크기: 64B, 총 8개)
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│ blk 0 │ blk 1 │ blk 2 │ blk 3 │ blk 4 │ blk 5 │ blk 6 │ blk 7 │
│ [used] │ [used] │ [free] │ [free] │ [used] │ [free] │ [free] │ [free] │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┴────────┘
↓ ↓ ↓ ↓ ↓
free list: 2 → 3 → 5 → 6 → 7 → NULL
- 모든 블록이 같은 크기이므로 외부 단편화가 발생하지 않는다
- free list에서 head를 꺼내면 되므로 할당/해제가 O(1)이다
- 블록당 메타데이터(size header)가 필요 없어 메모리 오버헤드가 적다
구현
#define BLOCK_SIZE 64
#define BLOCK_COUNT 16
typedef struct {
uint8_t memory[BLOCK_COUNT][BLOCK_SIZE];
uint8_t *free_list[BLOCK_COUNT];
size_t free_count;
} MemPool;
void mempool_init(MemPool *pool) {
pool->free_count = BLOCK_COUNT;
for (size_t i = 0; i < BLOCK_COUNT; i++) {
pool->free_list[i] = pool->memory[i];
}
}
void *mempool_alloc(MemPool *pool) {
if (pool->free_count == 0) return NULL;
return pool->free_list[--pool->free_count];
}
void mempool_free(MemPool *pool, void *ptr) {
assert(pool->free_count < BLOCK_COUNT);
pool->free_list[pool->free_count++] = (uint8_t *)ptr;
}
free list를 스택처럼 운용한다. alloc은 pop, free는 push. 루프나 탐색이 없으므로 실행 시간이 일정하다.
연결 리스트 방식
블록 자체에 다음 포인터를 저장하면 별도 배열 없이 free list를 구성할 수 있다. 미사용 블록의 메모리 공간을 포인터 저장에 활용한다.
typedef struct MemPool {
uint8_t memory[BLOCK_COUNT * BLOCK_SIZE];
void *free_head;
size_t block_size;
} MemPool;
void mempool_init(MemPool *pool) {
pool->block_size = BLOCK_SIZE;
pool->free_head = pool->memory;
// 각 블록의 첫 부분에 다음 블록 주소를 저장
for (size_t i = 0; i < BLOCK_COUNT - 1; i++) {
void **block = (void **)(pool->memory + i * BLOCK_SIZE);
*block = pool->memory + (i + 1) * BLOCK_SIZE;
}
void **last = (void **)(pool->memory + (BLOCK_COUNT - 1) * BLOCK_SIZE);
*last = NULL;
}
void *mempool_alloc(MemPool *pool) {
if (pool->free_head == NULL) return NULL;
void *block = pool->free_head;
pool->free_head = *(void **)block;
return block;
}
void mempool_free(MemPool *pool, void *ptr) {
*(void **)ptr = pool->free_head;
pool->free_head = ptr;
}
별도 free list 배열이 필요 없어 메모리를 더 절약한다. 단, 블록 크기가 포인터 크기(sizeof(void *)) 이상이어야 한다.
다중 크기 풀
요청 크기가 다양할 때는 크기별 풀을 여러 개 두고 라우팅한다.
static MemPool pool_32; // 32B 블록
static MemPool pool_128; // 128B 블록
static MemPool pool_512; // 512B 블록
void *sized_alloc(size_t size) {
if (size <= 32) return mempool_alloc(&pool_32);
if (size <= 128) return mempool_alloc(&pool_128);
if (size <= 512) return mempool_alloc(&pool_512);
return NULL; // 지원하지 않는 크기
}
요청 크기보다 큰 블록에 할당되므로 내부 단편화가 발생한다. 풀 크기 설계 시 실제 할당 패턴을 프로파일링해서 블록 크기와 개수를 결정해야 한다.
RTOS 통합
FreeRTOS는 heap_2.c에서 고정 크기 블록 풀과 유사한 방식을 사용한다. 멀티태스크 환경에서는 alloc/free를 critical section으로 보호해야 한다.
void *mempool_alloc_safe(MemPool *pool) {
taskENTER_CRITICAL();
void *ptr = mempool_alloc(pool);
taskEXIT_CRITICAL();
return ptr;
}
메모
malloc대비 약 7배 빠르다는 벤치마크 결과가 있다 (Windows 환경, CodeProject 참고)- 블록 크기를 2의 거듭제곱으로 설정하면 정렬 문제가 자연스럽게 해결된다
- 풀 소진 시 동작 정책(NULL 반환, assert, 대기)을 시스템 요구사항에 맞게 결정해야 한다
- double free 방지가 필요하면 블록 상태 비트맵을 추가하거나, 해제 시 주소 범위 검증을 넣는다