
CSAPP 책은 쌩으로 읽는다면 이해하기 매우 어렵습니다.
따라서 소단원만 그대로 따라가되, 내용을 이해하기 쉽게 재구성했습니다.
C 표준 라이브러리는 malloc 패키지를 제공한다.
이는 명시적 할당기로, malloc, free 같은 함수들이 모여 있는 라이브러리이다.
프로그램은 malloc 함수를 호출하여 힙에서 블록을 할당받아, 필요한만큼 메모리를 사용할 수 있다.
#include <stdlib.h>
void *malloc(size_t size);
Returns: pointer to allocated block if OK, NULL on error
void *: 어떤 타입으로든 변환 가능한 범용 포인터size_t: 메모리 크기를 나타내는 부호 없는 정수형malloc 함수는 요청한 size 바이트 크기의 메모리를 힙에서 할당한다.
성공하면 메모리 주소(pointer)를, 실패하면 NULL을 반환하며 errno 값을 설정한다.
errno: 시스템 호출이나 표준 라이브러리 함수에서 오류 원인을 나타내는 변수이다.ENOMEM 값이 설정된다.malloc은 반환하는 메모리를 초기화하지 않기 때문에, 할당된 메모리에는 쓰레기값(random 값)이 남아있을 수도 있다.
malloc은 항상 요청한 크기 이상의 메모리를 정렬해 제공한다.
이를 통해 어떤 타입의 데이터도 올바르게 저장할 수 있다.
메모리 정렬이란?
성능 향상을 위해 메모리 주소가 특정 기준(8바이트, 16바이트 등)의 배수가 되어야 한다.
정렬되지 않으면 충돌이나 성능 저하가 발생할 수 있다.
비트에 따른 정렬 조건은 시스템에 따라 다르지만, 일반적으로는 다음과 같다.
| 시스템 구분 | 정렬 기준 |
|---|---|
| 32비트 (-m32) | 8바이트 정렬 (8의 배수 주소) |
| 64비트 (기본) | 16바이트 정렬 (16의 배수 주소) |
메모리를 0으로 초기화한 상태로 받고 싶다면 calloc을 사용하는 것이 적절하다.
calloc은 malloc을 감싼 함수로, 할당된 메모리를 0으로 초기화한 뒤 반환한다.
void *calloc(size_t nmemb, size_t size);
이미 할당된 메모리 블록의 크기를 변경하고 싶을 경우 realloc 함수를 사용할 수 있다.
void *realloc(void *ptr, size_t size);
이 함수는 ptr이 가리키는 메모리 블록의 크기 size 크기로 변경한다.
필요 시 새로운 위치에 블록을 복사해서 할당된 메모리를 확장하거나 축소한다.
malloc은 내부적으로 mmap이나 sbrk 같은 시스템 호출을 이용해서 힙 메모리를 관리한다.
즉, malloc은 고수준 인터페이스일 뿐, 메모리를 직접 관리하는 일은 커널 수준 함수들이 수행한다.
sbrk는 힙의 끝(brk)을 조정하여 힙 공간을 직접 확장하거나 축소한다.
brk를 직접 옮겨서 메모리 공간을 넓혀주는 방식으로 작동한다.
#include <unistd.h>
void *sbrk(intptr_t incr);
Returns: old brk pointer on success, −1 on error
성공하면 기존의 brk 값을 반환하고, 실패하면 -1을 반환하며 errno를 ENOMEM으로 설정한다.
sbrk 함수는 incr를 인자로 받아 힙을 incr 바이트만큼 늘리거나 줄인다.
sbrk에 음수 값을 넣을 때에는 반환값이 새로운 힙의 끝에서 abs(incr)만큼 떨어진 이전 주소를 가리키므로 주의가 필요하다.
현대의 malloc 구현은 작은 요청은 sbrk, 큰 요청은 mmap을 사용한다.
mmap은 더 유연하고 페이지 단위의 제어가 가능하기 때문에 특히 대용량 요청에 적합하다.
할당된 메모리를 더 이상 사용하지 않을 경우에는 반드시 free 함수를 호출해 메모리를 반납해야 한다.
#include <stdlib.h>
void free(void *ptr);
Returns: nothing
이 함수는 아무 값도 반환하지 않고 조용히 동작된다.
즉, 오류가 있더라도 알려주지 않는다.
free에 넘기는 ptr 인자는 반드시 malloc, calloc, realloc 중 하나로 할당받은 블록의 시작 주소여야 한다.
그렇지 않으면 동작은 정의되지 않으며(undefined behavior), 프로그램에 치명적인 오류를 일으킬 수 있다.
잘못된 포인터를 넘기면 free의 동작은 정의되지 않는다.
가장 무서운 점은, free는 반환값이 없기 때문에 버그가 숨어서 나중에 엉뚱한 시점에 터질 수 있다는 것이다.
아래 그림은 C 프로그램에서 malloc과 free가 16워드짜리 작은 힙을 어떻게 관리하는지를 보여준다.
malloc과 free를 호출하면 이 힙이 어떻게 분할되고 다시 합쳐지는지를 볼 수 있다.

word는 얼마나 큰가?
32비트 시스템 기준으로, 워드는 일반적으로 4바이트이다.
즉, 16워드는 64바이트짜리 힙이다.
위 그림을 단계별로 확인해보며 malloc과 free의 작동 원리에 대해서 알아보자.
처음에는 힙 전체가 16워드짜리 정렬된 free 블록 하나로 되어 있다.

프로그램이 4워드 블록을 요청한다.
malloc은 힙 앞쪽에서 블록을 잘라 주고, 남은 부분은 free 상태로 남긴다.
이때, p1은 첫 번째 블록을 가리키고 있다.

프로그램이 5워드 블록을 요청한다.
malloc은 힙 앞쪽에서 블록을 잘라 주고, 남은 부분은 free 상태로 남긴다.
이때 정렬(8바이트 기준) 때문에 malloc은 6워드짜리 블록을 만들어준다.

프로그램이 6워드 블록을 요청한다.
malloc은 힙 앞쪽에서 블록을 잘라 주고, 남은 부분은 free 상태로 남긴다.

프로그램이 (b)에서 할당된 6워드 블록의 반환을 요청한다.
free로의 호출이 리턴한 후에도 포인터 p2는 여전히 malloc된 부분을 가리키고 있다.
p2가 새로운 malloc 호출에 의해 다시 초기화되기 전까지 p2를 사용해서는 안된다.

프로그램이 2워드 블록을 요청한다.
(d) 단계에서 free로 생긴 빈 공간 중 일부를 사용한다.