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() 같은 시스템 호출을 사용하여 힙의 크기를 확장합니다.
  • 반대로, 사용되지 않는 메모리 블록이 많아지면 운영체제에 메모리 반환을 요청할 수 있습니다.

힙 메모리 관리의 문제점

  1. 단편화(Fragmentation):
    • 힙 메모리 관리의 가장 큰 문제 중 하나는 단편화입니다. 외부 단편화는 힙에서 사용 가능한 빈 메모리 공간이 작은 조각들로 나뉘어 있어 큰 메모리 할당 요청을 처리할 수 없는 상황을 말합니다.
    • 이를 해결하기 위해 메모리 병합을 하거나 적절한 블록 할당 전략을 사용합니다.
  2. 메모리 누수(Memory Leak):
    • 동적으로 할당된 메모리를 해제하지 않으면 메모리 누수가 발생할 수 있습니다. 이는 프로그램이 종료될 때까지 메모리를 점점 더 많이 차지하게 되어 시스템 성능에 영향을 미칠 수 있습니다.
  3. 성능 이슈:
    • 메모리 할당과 해제는 상대적으로 시간이 많이 소요되는 작업이므로, 잦은 할당과 해제가 발생할 경우 성능 저하가 발생할 수 있습니다.

동적 메모리 할당 예시 (mallocfree 사용)

#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;
}

요약

힙 영역에서 메모리 풀을 최적화하는 과정은 미리 할당된 메모리 블록을 효과적으로 관리하여 메모리 할당의 성능을 개선하고, 파편화를 최소화하는 것을 목표로 합니다. 이를 통해 프로그램의 메모리 사용을 효율적으로 관리할 수 있습니다.