Interview/OS
Heap 영역 메모리 동적 할당 내부 로직
김 정출
2024. 9. 26. 11:09
Heap 영역 메모리 동적 할당 내부 로직
힙(Heap) 영역에서 메모리 동적 할당이 이루어지는 내부 로직은 복잡하지만,
기본적인 과정은 다음과 같은 단계로 이루어집니다.
주로 malloc()
과 같은 함수를 호출할 때의 동작을 설명하겠습니다.
1. 프로그램 시작 시 힙 영역 설정
- 프로그램이 시작되면, 운영체제는 프로세스에 힙 영역을 할당합니다. 힙은 일반적으로 메모리 주소 공간의 낮은 주소에서 시작하고, 동적 할당 요청에 따라 힙의 크기가 커질 수 있습니다.
- 운영체제는 sbrk() 또는 mmap() 같은 시스템 호출을 사용하여 힙 영역의 크기를 늘릴 수 있습니다.
sbrk
sbrk
는 힙 영역의 끝을 조정하여 메모리를 할당하는 시스템 호출입니다. 이 함수는 주로 동적 메모리 할당(예:malloc
)에 사용됩니다.sbrk
함수는 매개변수로 주어진 바이트 수만큼 프로그램의 데이터 세그먼트의 크기를 증가시킵니다.sbrk(n)
에서n
은 증가시킬 바이트 수를 나타내며, 음수 값을 주면 메모리 크기를 줄입니다.- 단순함:
sbrk
는 단순히 데이터 세그먼트의 끝을 이동시키므로, 메모리 관리가 쉽습니다. - 전통적 방식:
sbrk
는 오래된 방법으로, 현재의 메모리 할당 라이브러리(malloc
,free
)에서 주로 사용됩니다. - 메모리 파편화: 사용 후 해제된 메모리가 적절하게 관리되지 않으면 메모리 파편화가 발생할 수 있습니다.
#include <unistd.h>
#include <stdio.h>
int main() {
// 현재 힙의 끝 주소를 가져옴
void* current_brk = sbrk(0);
printf("Current brk: %p\n", current_brk);
// 1024바이트 할당
void* new_brk = sbrk(1024);
printf("New brk after allocation: %p\n", new_brk);
return 0;
}
mmap
mmap
는 파일이나 장치의 내용을 메모리에 매핑하거나, 특정 크기의 익명 메모리 영역을 할당할 때 사용하는 시스템 호출입니다.mmap
함수는 여러 매개변수를 받아, 메모리 매핑의 크기, 속성, 파일 디스크립터 등을 설정합니다.mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)
와 같이 사용하여 익명 메모리를 할당할 수 있습니다.- 유연성:
mmap
는 파일 I/O와 메모리 매핑을 지원하여, 파일을 메모리에 직접 매핑할 수 있습니다. 이를 통해 큰 파일을 처리하는 데 유리합니다. - 대량 메모리 할당:
mmap
는 큰 메모리 블록을 효율적으로 할당할 수 있으며, 대용량 메모리를 사용할 때 성능이 뛰어납니다. - 해제 방법:
munmap
을 사용하여 매핑된 메모리를 해제할 수 있으며, 이때 해당 메모리 블록의 전체를 해제합니다. - 메모리 보호:
mmap
는 메모리 보호 플래그를 설정할 수 있어, 읽기, 쓰기, 실행 권한을 세밀하게 조정할 수 있습니다.
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
size_t length = 4096; // 4KB
// 메모리 매핑
void* ptr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap failed");
return EXIT_FAILURE;
}
// 메모리 사용
sprintf((char*)ptr, "Hello, mmap!");
printf("%s\n", (char*)ptr);
// 메모리 해제
if (munmap(ptr, length) == -1) {
perror("munmap failed");
return EXIT_FAILURE;
}
return 0;
}
2. 메모리 할당 요청 (malloc
)
malloc(size_t size)
함수는size
바이트 크기의 메모리를 힙에서 할당하는 요청입니다. 이 함수가 호출되면 다음과 같은 단계를 거칩니다.
3. 메모리 블록 관리
- 힙에서 메모리를 동적으로 할당하기 위해서는 메모리 블록을 관리해야 합니다. 이를 위해 힙에는 각 메모리 블록에 대한 헤더 정보가 포함됩니다.
- 헤더(Header): 각 메모리 블록의 크기, 사용 여부(할당됨/해제됨)를 저장하는 메타데이터입니다.
- 블록(Block): 실제로 프로그램이 사용할 수 있는 메모리 공간입니다.
- 메모리 블록의 상태(사용 중 또는 사용 가능)를 추적하기 위해 관리 데이터가 포함되는데, 이를 프리 리스트(Free List)라고 합니다.
4. 적절한 메모리 블록 검색
malloc()
호출 시, 할당 관리자는 힙에서 적절한 크기의 빈 블록을 찾습니다. 일반적으로 First-fit(처음 맞는 블록을 찾음), Best-fit(가장 알맞은 크기의 블록을 찾음), Worst-fit(가장 큰 블록을 찾음) 알고리즘을 사용하여 메모리 블록을 검색합니다.- 블록이 너무 크면 필요한 크기만큼 할당하고, 남은 부분을 분할하여 나머지 메모리를 다시 프리 리스트에 추가합니다.
5. 메모리 블록 할당
- 적절한 블록을 찾으면 해당 블록을 할당 상태로 표시하고, 사용자가 사용할 수 있도록 포인터를 반환합니다.
- 이 포인터는 실제 메모리 블록의 시작 주소를 가리키며, 헤더 부분은 포함되지 않습니다.
6. 메모리 해제 (free
)
- 할당된 메모리는 사용 후
free(void *ptr)
함수를 사용하여 해제할 수 있습니다. free()
호출 시, 해당 메모리 블록의 헤더를 확인하고 블록을 프리 리스트(Free List)로 반환합니다.- 해제된 블록이 인접한 다른 해제된 블록과 병합(coalescing) 될 수 있습니다. 이렇게 하면 메모리 조각화(fragmentation)를 줄일 수 있습니다.
7. 메모리 병합(Coalescing)
- 해제된 메모리 블록은 인접한 다른 해제된 블록과 병합할 수 있습니다. 즉, 힙에서 인접한 두 개 이상의 빈 블록이 있으면 하나의 큰 블록으로 결합합니다. 이는 외부 단편화(external fragmentation)를 줄이는 데 유용합니다.
8. 메모리 재사용 및 최적화
- 할당된 메모리가 해제되면, 해당 메모리는 다시 프리 리스트에 추가되어 다른
malloc()
요청에 의해 재사용될 수 있습니다. - 시스템은 주기적으로
메모리 풀(pool)
을 유지하고 최적화합니다. 일부 메모리 관리자는 작은 크기의 할당 요청에 대해캐시(caching)
를 사용하여 성능을 향상시킵니다.
9. 힙 확장 및 축소
- 만약 힙에 사용 가능한 메모리 블록이 충분하지 않으면, 메모리 관리자는 운영체제에 추가 메모리 할당을 요청합니다. 이때
sbrk()
또는mmap()
같은 시스템 호출을 사용하여 힙의 크기를 확장합니다. - 반대로, 사용되지 않는 메모리 블록이 많아지면 운영체제에 메모리 반환을 요청할 수 있습니다.
힙 메모리 관리의 문제점
- 단편화(Fragmentation):
- 힙 메모리 관리의 가장 큰 문제 중 하나는 단편화입니다. 외부 단편화는 힙에서 사용 가능한 빈 메모리 공간이 작은 조각들로 나뉘어 있어 큰 메모리 할당 요청을 처리할 수 없는 상황을 말합니다.
- 이를 해결하기 위해 메모리 병합을 하거나 적절한 블록 할당 전략을 사용합니다.
- 메모리 누수(Memory Leak):
- 동적으로 할당된 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다. 이는 프로그램이 종료될 때까지 메모리를 점점 더 많이 차지하게 되어 시스템 성능에 영향을 미칠 수 있습니다.
- 성능 이슈:
- 메모리 할당과 해제는 상대적으로 시간이 많이 소요되는 작업이므로, 잦은 할당과 해제가 발생할 경우 성능 저하가 발생할 수 있습니다.
동적 메모리 할당 예시 (malloc
과 free
사용)
#include <stdio.h>
#include <stdlib.h>
int main() {
// 10개의 정수를 위한 메모리 동적 할당
int *arr = (int *)malloc(10 * sizeof(int));
if (arr == NULL) {
printf("메모리 할당 실패\\n");
return 1;
}
// 동적 할당된 배열에 값 할당
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
// 할당된 메모리 출력
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
printf("\\n");
// 메모리 해제
free(arr);
return 0;
}
이 코드에서 malloc
을 통해 10개의 정수를 저장할 수 있는 메모리를 동적으로 할당하고, 사용 후 free
를 통해 메모리를 해제합니다.
힙 영역에서 메모리 풀을 최적화하는 과정은 프로그램의 메모리 사용 효율성을 높이고, 메모리 할당 및 해제 시 발생할 수 있는 성능 저하를 줄이기 위한 기법입니다. 메모리 풀(Memory Pool)은 사전에 할당된 고정 크기의 메모리 블록을 사용하는 방식으로, 동적 메모리 할당의 오버헤드를 줄이는 데 도움을 줍니다. 다음은 메모리 풀을 최적화하는 과정에 대한 설명입니다.
1. 메모리 풀의 개념
- 정의: 메모리 풀은 동일한 크기의 메모리 블록을 미리 할당하여 사용하는 구조입니다. 메모리 요청이 들어오면, 사용자는 이 풀에서 필요한 메모리 블록을 할당받고, 사용 후 반환합니다.
- 목적: 메모리 할당과 해제의 빈도를 줄이고, 메모리 파편화를 최소화하며, 성능을 향상시키기 위해 사용됩니다.
2. 메모리 풀 구현
- 초기화: 메모리 풀은 시작할 때 미리 정의된 크기의 메모리 블록을 할당합니다. 예를 들어, 100개의 256바이트 블록을 할당할 수 있습니다.
- 구조체 사용: 각 블록의 상태를 관리하기 위해 연결 리스트 또는 다른 데이터 구조를 사용하여 할당된 블록과 사용 가능한 블록을 구분합니다.
3. 메모리 할당
- 할당 요청 처리: 사용자가 메모리를 요청하면, 메모리 풀에서 사용 가능한 블록을 찾아 할당합니다. 이 과정은 일반적으로 매우 빠르며, 운영 체제의 malloc/free보다 성능이 뛰어납니다.
- 프리 리스트 관리: 할당된 블록과 해제된 블록을 관리하기 위해 프리 리스트를 유지합니다. 사용 가능한 블록은 이 리스트에 추가되어 다음 요청 시 재사용됩니다.
4. 메모리 해제
- 반환 처리: 사용자가 메모리를 해제하면 해당 블록은 프리 리스트에 추가됩니다. 이를 통해 다시 사용할 수 있도록 관리합니다.
- 재사용 최적화: 해제된 블록을 재사용하기 위해 메모리 풀 내에서 적절한 크기와 상태를 가진 블록을 우선적으로 선택하여 할당합니다.
5. 성능 최적화
- 블록 크기 조정: 다양한 크기의 블록을 지원하는 메모리 풀을 구현하여, 메모리 요구 사항에 맞는 블록 크기를 할당할 수 있습니다.
- 캐시 최적화: CPU 캐시를 고려하여 메모리 풀의 구조를 설계하면 메모리 접근 속도를 개선할 수 있습니다. 예를 들어, 연속된 블록을 할당하여 캐시 미스(cache miss)를 줄입니다.
- 메모리 파편화 최소화: 풀의 크기를 조정하거나, 해제된 블록을 합치는 방법을 사용하여 메모리 파편화를 줄입니다.
6. 예시 코드
다음은 간단한 메모리 풀 구현의 예입니다:
#include <stdio.h>
#include <stdlib.h>
typedef struct MemoryBlock {
struct MemoryBlock* next;
} MemoryBlock;
typedef struct MemoryPool {
MemoryBlock* freeList;
size_t blockSize;
size_t totalBlocks;
} MemoryPool;
// 메모리 풀 초기화
MemoryPool* createPool(size_t blockSize, size_t totalBlocks) {
MemoryPool* pool = (MemoryPool*)malloc(sizeof(MemoryPool));
pool->blockSize = blockSize;
pool->totalBlocks = totalBlocks;
// 블록을 미리 할당하고 프리 리스트 구성
pool->freeList = (MemoryBlock*)malloc(blockSize * totalBlocks);
MemoryBlock* current = pool->freeList;
for (size_t i = 0; i < totalBlocks - 1; i++) {
current->next = (MemoryBlock*)((char*)current + blockSize);
current = current->next;
}
current->next = NULL; // 마지막 블록의 다음은 NULL로 설정
return pool;
}
// 메모리 할당
void* allocateBlock(MemoryPool* pool) {
if (pool->freeList == NULL) {
return NULL; // 할당할 수 있는 블록이 없음
}
MemoryBlock* block = pool->freeList;
pool->freeList = block->next; // 프리 리스트에서 블록 제거
return (void*)block; // 블록 반환
}
// 메모리 해제
void freeBlock(MemoryPool* pool, void* block) {
MemoryBlock* memBlock = (MemoryBlock*)block;
memBlock->next = pool->freeList; // 블록을 프리 리스트의 앞에 추가
pool->freeList = memBlock;
}
// 메모리 풀 해제
void destroyPool(MemoryPool* pool) {
free(pool->freeList);
free(pool);
}
int main() {
MemoryPool* pool = createPool(256, 100); // 256바이트 블록 100개 할당
void* block1 = allocateBlock(pool);
void* block2 = allocateBlock(pool);
// 블록 사용
freeBlock(pool, block1);
freeBlock(pool, block2);
destroyPool(pool);
return 0;
}
요약
힙 영역에서 메모리 풀을 최적화하는 과정은 미리 할당된 메모리 블록을 효과적으로 관리하여 메모리 할당의 성능을 개선하고, 파편화를 최소화하는 것을 목표로 합니다. 이를 통해 프로그램의 메모리 사용을 효율적으로 관리할 수 있습니다.