glibc malloc은 메모리를 어디서 얻나: brk·mmap·재사용

반응형

malloc(100)을 호출했다고 해서 glibc가 매번 kernel에 100 byte를 요청하는 것은 아니다. 먼저 allocator가 이미 확보한 free chunk를 찾고, 재사용할 영역이 부족할 때 process의 data segment를 늘리거나 anonymous mmap()으로 새 mapping을 만든다.

여기서 범위를 분명히 할 필요가 있다. C 표준은 malloc()의 API와 반환값 규칙을 정할 뿐, 내부에서 brkmmap 중 무엇을 써야 하는지는 정하지 않는다. 아래 설명은 Linux에서 현재 GNU C Library(glibc)의 allocator를 사용하는 경우를 기준으로 한다. jemalloc·tcmalloc·musl은 전략이 다를 수 있다.

첫 번째 후보는 이미 확보한 chunk다

glibc allocator가 처리하는 기본 흐름은 다음과 가깝다.

malloc(size)
    ↓
재사용 가능한 chunk 탐색
    ├─ 있으면: allocator 내부에서 반환
    └─ 없으면: arena 확장 또는 별도 mmap mapping 확보

빠른 경로에서는 kernel system call이 필요하지 않다. free()된 chunk를 allocator의 자료구조에 보관해 뒀다가 크기가 맞는 다음 요청에 재사용할 수 있기 때문이다. “malloc은 OS에서 메모리를 받아오는 함수”보다 “확보한 영역을 나누고 재사용하는 allocator interface”라고 이해하는 편이 정확하다.

data segment 확장과 mmap은 역할이 다르다

glibc는 arena에 더 많은 공간이 필요하면 전통적으로 program break를 조정해 data segment를 늘릴 수 있다. Linux에서는 낮은 수준의 brk() system call이 이 경계를 다루지만, application이 이를 직접 호출하는 방식은 권장되지 않는다.

별도로 anonymous mmap()을 사용하면 기존 data segment와 떨어진 virtual address range를 만들 수 있다. 큰 allocation에 이 경로가 쓰일 수 있고, 해당 mapping 전체가 더 이상 필요 없으면 munmap()으로 독립적으로 제거하기 쉽다.

process virtual address space
├─ data segment / arena가 사용하는 영역
├─ shared libraries
├─ anonymous mmap allocation
└─ thread stacks

둘 다 우선 만드는 것은 virtual mapping이다. mapping 크기만큼 physical page가 즉시 resident한다고 단정할 수 없다. 처음 page에 접근할 때 demand paging과 zero-fill 비용이 나타날 수 있다.

‘작으면 brk, 크면 mmap’은 고정 규칙이 아니다

입문 설명에서는 작은 요청은 heap, 큰 요청은 mmap이라고 줄여 말하기 쉽다. 방향을 잡는 데는 유용하지만 실제 경계는 고정 상수가 아니다.

glibc에는 mmap_threshold, trim_threshold, arena 관련 tunable이 있고, mmap_threshold는 명시적으로 고정하지 않으면 동적으로 조정될 수 있다. 요청 크기뿐 아니라 allocator 버전, tunable, free chunk와 top chunk의 상태, thread별 arena 상황이 경로에 영향을 준다.

따라서 특정 크기 하나만 보고 “이 allocation은 반드시 mmap”이라고 결론 내리지 않는다. 필요하면 대상 binary가 실제로 쓰는 allocator와 runtime 설정부터 확인해야 한다.

free가 kernel 반환을 뜻하지 않는 이유

free(ptr)는 우선 해당 allocation을 allocator가 다시 사용할 수 있게 만든다. 그 뒤 실제 page가 process에서 분리되는지는 allocation 경로와 주변 상태에 달려 있다.

  • 별도 mmap mapping으로 관리된 allocation은 free 과정에서 munmap()될 수 있다.
  • arena 내부의 free chunk는 다음 allocation을 위해 남을 수 있다.
  • data segment 끝의 충분한 free space는 조건이 맞으면 trim될 수 있다.
  • 중간에 흩어진 free chunk는 process address space에 남아 재사용될 수 있다.

그러므로 “free하면 항상 RSS 감소”와 “free해도 절대 kernel에 반환되지 않음”은 둘 다 지나친 단정이다. 자세한 지표 해석은 free 후 RSS가 유지되는 이유에서 이어진다.

allocator 경로를 확인할 때의 안전한 순서

운영 process에 곧바로 tunable을 적용하거나 allocator를 교체하기 전에 다음을 먼저 기록한다.

  1. binary가 사용하는 libc와 별도 allocator preload 여부
  2. allocation size 분포와 thread 수
  3. 같은 workload 구간의 RSS·RssAnon·mapping 변화
  4. 변경 전후 allocation latency와 tail latency

strace나 profiler는 호출량이 많은 process에 관측 비용을 만들 수 있다. production에 붙이기 전에 동일한 binary와 설정의 test 환경에서 overhead를 확인하고, 수집 시간과 PID 범위를 제한한다.

allocator 내부의 chunk·tcache·bin 관계는 glibc malloc 내부 구조, virtual mapping 자체는 리눅스 가상 메모리 구조와 함께 보면 계층이 분리된다.

참고 자료

반응형
KEEP READING
카테고리 전체 보기 →

댓글