CSAPP/9장

[CSAPP] 9장 가상 메모리(Virtual Memory) - 9.9(9.9.11 ~ 9.9.14)

넌뭐가그렇게중요해 2025. 4. 26. 00:12

9.9.11 경계 태그 병합(Boundary-Tag Coalescing)

해제된 블록과 인접한 빈 블록을 헤더·풋터(boundary tags) 정보를 이용해 즉시 합치는 방식입니다.

  • 풋터에 크기 정보를 복사해 두면, 해제 시 이전 블록의 크기를 O(1)로 알 수 있습니다​.
  • free(ptr) 호출 시 왼쪽·오른쪽 이웃 블록의 할당 비트를 검사하고, 모두 자유 상태라면 세 블록을 하나로 합칩니다​. 
  • 즉시 병합(immediate coalescing)을 쓰면 외부 단편화를 효과적으로 줄이지만, 매 free마다 검사 비용이 추가될 수 있습니다​.

경계 태그를 사용하는 힙 블록의 포맷

 

  • Header/​Footer 모두에 블록 크기와 할당 비트(a/f)를 저장합니다.
  • free 시 Footer로 이전 블록 크기를 즉시 알아낼 수 있어 빠른 병합이 가능합니다.

병합 4가지 케이스

경계 태그를 사용

각 블록 이름(m1, n, m2)은 그 블록 크기를, a는 할당(allocated), f는 자유(free)를 나타냅니다.

케이스 상황 동작
1 prev = a, 현재 n = a, next = a 병합 없음: 그냥 n을 f로 표시만(이웃 블록과 모두 할당됨)
2 prev = a, 현재 n = a, next = f 오른쪽 병합: n + m2 크기로 합쳐 하나의 자유 블록으로 생성
3 prev = f, 현재 n = a, next = a 왼쪽 병합: m1 + n 크기로 합쳐 하나의 자유 블록으로 생성
4 prev = f, 현재 n = a, next = f 양쪽 병합: m1 + n + m2 크기로 합쳐 하나의 자유 블록으로 생성

 


9.9.12 간단한 할당기의 구현

#include <stdlib.h>
#include <stdio.h>

/*======================================================
  전역 변수: 힙의 시작과 끝, 현재 brk 포인터를 추적
  - heap_start: 힙의 가장 첫 번째 바이트 주소
  - heap_brk:   현재 힙의 끝(마지막 유효 바이트 + 1)
  - heap_max:   시스템이 허용한 힙의 최대 주소
======================================================*/
static char *heap_start;   // 최초 sbrk로 받은 메모리 시작 주소
static char *heap_brk;     // 다음에 사용할 수 있는 힙 상단 위치
static char *heap_max;     // sbrk를 통해 확장 가능한 최대 영역

/*======================================================
  mem_init: 힙 메모리 초기화 함수
  - 처음에 MAX_HEAP 바이트만큼 malloc으로 확보
  - prologue/epilogue 블록을 설정하여 경계 조건 단순화
======================================================*/
void mem_init(void) {
    /* 1) 시스템으로부터 MAX_HEAP 바이트 확보 */
    heap_start = (char *)malloc(MAX_HEAP);
    if (!heap_start) {
        fprintf(stderr, "mem_init: 초기 힙 메모리 확보 실패\n");
        exit(1);
    }
    heap_brk = heap_start;
    heap_max = heap_start + MAX_HEAP;

    /* 2) Prologue 블록 설정
        - 크기 DSIZE (헤더+풋터) 확보
        - 할당 비트 alloc=1 로 표시하여 실제 데이터 블록과 구분
    */
    PUT(heap_brk, PACK(DSIZE, 1));         // 헤더: 크기 DSIZE, 할당됨
    PUT(heap_brk + WSIZE, PACK(DSIZE, 1)); // 풋터: 크기 DSIZE, 할당됨
    heap_brk += DSIZE;                     // 실제 유효 블록 시작 위치로 이동

    /* 3) Epilogue 블록 설정
        - 크기 0, alloc=1
        - 힙 끝을 표시하며, 반복 탐색 시 종료 조건으로 활용
    */
    PUT(heap_brk, PACK(0, 1));             // Epilogue 헤더
}

/*======================================================
  extend_heap: 힙을 incr 바이트만큼 확장
  - 힙이 heap_max를 넘지 않으면 유효 블록으로 추가
  - 새로 생긴 영역을 자유 블록으로 만들고 epilogue 갱신
  - coalesce로 인접 자유 블록과 병합
======================================================*/
void *extend_heap(size_t incr) {
    char *bp;

    /* 1) 확장 가능한지 체크 */
    if (heap_brk + incr + WSIZE > heap_max)
        return NULL;  // 더 이상 확대 불가능

    /* 2) 이전 Epilogue 위치를 새 자유 블록 시작으로 사용 */
    bp = heap_brk;

    /* 3) 새 자유 블록 헤더/풋터 설정
        - 헤더: 크기=incr, alloc=0
        - 풋터: 블록 끝에서 크기=incr, alloc=0
    */
    PUT(HDRP(bp), PACK(incr, 0));
    PUT(FTRP(bp), PACK(incr, 0));

    /* 4) 새로운 Epilogue 블록 설정
        - 위치: bp + incr
        - 크기=0, alloc=1
    */
    PUT(HDRP(NEXT_BLKP(bp)), PACK(0, 1));

    /* 5) heap_brk 포인터를 새 Epilogue 뒤로 이동 */
    heap_brk = (char *)NEXT_BLKP(bp) + WSIZE;

    /* 6) 방금 만든 자유 블록과 인접 자유 블록 병합 */
    return coalesce(bp);
}

/*======================================================
  coalesce: 블록 bp를 해제하면서 이웃 자유 블록과 병합
  - prev_alloc: 이전 블록이 할당되었는지
  - next_alloc: 다음 블록이 할당되었는지
  - size: 현재 블록 크기
  - 네 가지 상황에 따라 병합 수행 후 병합된 블록의 bp 반환
======================================================*/
void *coalesce(void *bp) {
    size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp))); // 이전 풋터로 alloc 비트 확인
    size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp))); // 다음 헤더로 alloc 비트 확인
    size_t size       = GET_SIZE(HDRP(bp));             // 현재 블록 크기

    if (prev_alloc && next_alloc) {
        /* Case 1: 양쪽 다 할당됨 → 병합 없음 */
        return bp;
    } else if (prev_alloc && !next_alloc) {
        /* Case 2: 다음 블록만 자유 → 오른쪽 블록과 병합 */
        size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
        PUT(HDRP(bp), PACK(size, 0));   // 통합된 블록 헤더 갱신
        PUT(FTRP(bp), PACK(size, 0));   // 풋터 갱신
    } else if (!prev_alloc && next_alloc) {
        /* Case 3: 이전 블록만 자유 → 왼쪽 블록과 병합 */
        size += GET_SIZE(HDRP(PREV_BLKP(bp)));
        PUT(FTRP(bp), PACK(size, 0));   // 통합된 블록 풋터 갱신
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0)); // 이전 헤더 갱신
        bp = PREV_BLKP(bp);             // 새로운 블록 시작 위치로 이동
    } else {
        /* Case 4: 양쪽 모두 자유 → 세 블록 모두 병합 */
        size += GET_SIZE(HDRP(PREV_BLKP(bp)))
              + GET_SIZE(FTRP(NEXT_BLKP(bp)));
        PUT(HDRP(PREV_BLKP(bp)), PACK(size, 0));      // 왼쪽 헤더에 합친 크기 저장
        PUT(FTRP(NEXT_BLKP(bp)), PACK(size, 0));      // 오른쪽 풋터에 합친 크기 저장
        bp = PREV_BLKP(bp);                           // 통합된 블록 시작
    }
    return bp;  // 통합된 자유 블록의 bp 반환
}

/*======================================================
  find_fit: 첫 번째 적합 블록 검색 (First-Fit)
  - asize: 요청한 블록 크기(정렬 + 헤더/풋터 포함)
  - 힙 시작부터 Epilogue 전까지 순차 탐색
  - 크기 ≥ asize && alloc=0인 첫 블록 반환
  - 없으면 NULL
======================================================*/
void *find_fit(size_t asize) {
    void *bp = heap_start + DSIZE;  // Prologue 뒤 첫 블록
    while (GET_SIZE(HDRP(bp)) > 0) { // Epilogue(크기=0) 전까지
        if (!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
            return bp;               // 적합 블록 발견
        bp = NEXT_BLKP(bp);
    }
    return NULL;  // 찾지 못함
}

/*======================================================
  place: bp 블록을 asize 바이트만큼 할당하고, 잔여 부분 분할
  - csize: 기존 빈 블록의 크기
  - if 분할 후 남은 부분이 최소 블록 크기(2*DSIZE) 이상이면 분할
  -   분할: 앞부분 할당, 뒷부분 자유 블록으로 남김
  - else 전체 블록 할당(분할 없이)
======================================================*/
void place(void *bp, size_t asize) {
    size_t csize = GET_SIZE(HDRP(bp));

    if ((csize - asize) >= (2 * DSIZE)) {
        /* 분할 가능 */
        PUT(HDRP(bp), PACK(asize, 1));        // 앞부분 할당 헤더
        PUT(FTRP(bp), PACK(asize, 1));        // 앞부분 할당 풋터
        bp = NEXT_BLKP(bp);                   // 뒷부분 빈 블록 시작
        PUT(HDRP(bp), PACK(csize - asize, 0));// 뒷부분 자유 헤더
        PUT(FTRP(bp), PACK(csize - asize, 0));// 뒷부분 자유 풋터
    } else {
        /* 분할 불가: 전체 블록 할당 */
        PUT(HDRP(bp), PACK(csize, 1));
        PUT(FTRP(bp), PACK(csize, 1));
    }
}

/*======================================================
  mm_malloc: asize 바이트 만큼 동적 할당
  - size = 실제 요청 바이트 수
  - 1) size=0 면 NULL 반환
  - 2) asize = ALIGN(size)+DSIZE를 최소 블록 크기(2*DSIZE)로 올림
  - 3) find_fit으로 적합 블록 검색 → 있으면 place
  - 4) 없으면 extend_heap(최소 CHUNKSIZE 또는 asize) → 다시 place
======================================================*/
void *mm_malloc(size_t size) {
    size_t asize;       // 실제 배치할 블록 크기
    size_t extendsize;  // 힙 확장 단위
    char *bp;

    if (size == 0)
        return NULL;

    /* 1) 요청 크기 정렬 + 헤더/풋터 오버헤드 */
    asize = MAX(ALIGN(size) + DSIZE, 2 * DSIZE);

    /* 2) 기존 힙에서 빈 블록 검색 */
    if ((bp = find_fit(asize)) != NULL) {
        place(bp, asize);
        return bp;
    }

    /* 3) 빈 블록 없으면 힙 확장 */
    extendsize = MAX(asize, CHUNKSIZE);
    if ((bp = extend_heap(extendsize)) == NULL)
        return NULL;  // 메모리 부족
    place(bp, asize);
    return bp;
}

/*======================================================
  mm_free: ptr 블록을 자유(free) 상태로 전환
  - 1) 헤더/풋터 alloc 비트를 0으로
  - 2) coalesce 호출하여 인접 자유 블록 병합
======================================================*/
void mm_free(void *ptr) {
    size_t size = GET_SIZE(HDRP(ptr));

    /* 1) 헤더/풋터에 자유 블록 표시 */
    PUT(HDRP(ptr), PACK(size, 0));
    PUT(FTRP(ptr), PACK(size, 0));

    /* 2) 인접 블록과 병합 */
    coalesce(ptr);
}

동작 흐름 요약

  1. 초기화(mem_init)
    • 프로로그·에필로그 블록 설정
  2. 할당(mm_malloc)
    • 요청 크기를 정렬(ALIGN) 후 헤더·풋터 오버헤드(DSIZE) 포함해 조정
    • find_fit로 첫 적합 블록 검색 → place로 분할·할당
    • 없으면 extend_heap으로 힙 확장 후 다시 배치
  3. 해제(mm_free)
    • 헤더·풋터의 alloc 비트를 0으로 바꾼 뒤 coalesce 실행
    • 양쪽 이웃 블록들과 즉시 병합

9.9.13 명시적 가용 리스트(Explicit Free List)

명시적 자유 리스트 방식에서는 자유 블록(free block)만 별도의 이중 연결 리스트(doubly linked list)로 관리하여, malloc 시 빈 블록 탐색 비용을 크게 줄이는 기법입니다.

이중 연결 가용 리스트를 사용하는 힙 블록의 포맷

 

  • 할당된 블록(a) 은 사용자 페이로드만 있고, 추가 메타데이터(링크 포인터)를 저장하지 않습니다.
  • 자유 블록(b) 은 사용자 페이로드 영역에 pred(이전)와 succ(다음) 포인터를 저장해, 이중 연결 리스트 노드로 사용합니다.
  • 헤더와 풋터에 남은 것은 암묵적 할당기와 동일하게 크기와 할당 비트를 저장하는 boundary tag 역할을 합니다.\

주요 동작 알고리즘

 

  1. 자유 리스트 초기화
    • mem_init 또는 extend_heap 시 전체 힙을 하나의 자유 블록으로 만들고, 그 블록의 pred = NULL, succ = NULL 로 설정합니다.
  2. 할당 (malloc)
    • 탐색: 헤드 포인터부터 succ 링크를 따라가며, 요청 크기(asize) 이상인 첫 자유 블록을 찾습니다.
    • 분할(선택적): place(bp, asize)에서 분할이 일어나면, 분할된 뒷부분 새로운 자유 블록을 리스트에 새 노드로 남겨둡니다.
    • 제거: 선택된 블록 bp를 자유 리스트에서 삭제(remove) 합니다. (pred→succ, succ→pred 링크 갱신)
  3. 해제 (free)
    • 헤더·풋터 alloc=0 으로 표시 후, coalesce(bp)로 인접 블록 병합
    • 병합된 블록 bp′ 를 자유 리스트의 헤드에 삽입(insert)하여 새 노드로 추가합니다.

성능 및 장단점

  • 탐색 속도: 자유 블록만 순회 → 평균 블록 수 절반 이하로 줄어들고, O(n_free) 탐색
  • 삭제/삽입 비용: O(1)
  • 메타데이터 오버헤드: 각 자유 블록 페이로드 일부를 주소 포인터(8~16바이트)로 사용
  • 외부 단편화 제어: coalesce로 즉시 병합, 분할 전략과 함께 단편화 관리

 


9.9.14 분리 가용 리스트 (Segregated Free Lists)

크기별로 여러 개의 리스트(“bins”)를 두어, 요청 크기에 가까운 리스트만 탐색하는 기법입니다.

  • 예: 8B, 16B, 32B, 64B… 단위로 리스트를 구분하고, malloc(k)는 k 이상인 첫 리스트에서만 조회합니다.
  • 리스트가 비어 있으면 상위 크기 리스트를 검색하거나, sbrk로 새 블록을 확보해 해당 리스트에 추가합니다.
  • First-Fit이나 분할/병합 정책을 각 리스트별로 독립 적용해 탐색 시간이 크게 단축됩니다​.
  • GNU malloc(dlmalloc/ptmalloc)은 boundary-tag allocator이면서, segregated bins 방식을 사용합니다.

정리 : 암시적 자유 리스트 VS 명시적 자유 리스트 VS  분리 가용 리스트

구분 묵시적 가용 리스트
(Implicit Free List)
명시적 가용 리스트
(Explicit Free List)
분리 가용 리스트
(Segregated Free List)
메타 데이터 위치 각 블록 헤더, 풋터에만 크기, 할당 비트 저장 자유 블록 페이로드에 pred/succ 포인터 + 헤더, 푸터 자유 블록 페이로드에 포인터 + 헤더, 푸터(크기별 리스트 헤드)
탐색 방식 힙 전체 선형 탐색(First-Fit, Next-Fit, Best-Fit) 자유 리스트만 순회(First-Fit 등) 요청 크기 클래스 리스트 조회 후 탐색
탐색 시간 복잡도 O(n) O(n₍free₎)
탐색 비용이 자유 블록의 개수에 비례
O(1)~O(k) (k=size classes)
삽입/삭제 비용 free 시 colease 만 O(1) (pred/succ 포인터 업데이트) O(1)
분할/병합 가능(split), coalesce by boundary tags 가능(split), coalesce, 리스트에서 삽입/삭제 가능(split), coalesce, 리스트에 삽입
내부 단편화 보통(split 시 패딩) 보통(split 시 패딩) 보통(split 시 패딩)
외부 단편화 높음(지속적 splinter) 중간 (자유 블록 집중 관리) 낮음(크기 분류로 큰 블록 유지)
메타 데이터 오버헤드 작음(2워드) 중간 (pred, succ 포함, +2워드) 높음 (여러 리스트 헤드 + pred/succ)
구현 난이도 쉬움 보통  어려움
실제 활용 예 간단한 임베디드 애플리케이션 TLSF ptmalloc2(glibc), Windows Low-Frag heap