← System Programming DUJINLABS.COM

Virtual Memory · Linux userspace / kernel ABI

malloc, arena, brk, mmap

malloc object 수명과 kernel VMA 수명을 구분하고, free 뒤 RSS가 바로 줄지 않는 이유를 allocator cache와 fragmentation으로 설명합니다.

Series
20 / 37
Build
cc -std=c17 -Wall -Wextra -O2 allocator_rss.c -o allocator_rss
Run
./allocator_rss
Kernel
Linux 6.18.37 LTS

free()를 호출했는데 process RSS가 그대로인 이유는 무엇인가?

malloc/free는 libc allocator API이고 kernel은 개별 allocation 크기나 object type을 모른다. allocator가 brk로 확장한 heap과 mmap으로 얻은 arena/chunk를 나눠 작은 object를 관리한다.

free는 object를 allocator에 반환할 뿐 page를 즉시 munmap하거나 kernel에 돌려준다는 보장이 없다. 같은 arena의 살아 있는 allocation, bin cache, fragmentation 때문에 virtual range와 resident page가 남을 수 있다.

구조 그림

그림 1. malloc object에서 physical page까지의 네 겹 구조
malloc APIrequested size · ownership · free

Thread cache / bin

  • free chunk
  • size class
  • 빠른 재사용

Arena

  • allocated chunk
  • free hole
  • top chunk

Virtual mappings

  • brk heap
  • large mmap
  • allocator metadata

Resident pages

  • faulted page
  • Private_Dirty
  • trim/madvise 반환

free는 user object를 allocator에 돌려준다. 같은 page에 살아 있는 chunk가 있으면 VMA와 RSS는 그대로 남을 수 있다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
malloc size/alignment 요청
allocator bin free chunk 탐색
brk/mmap arena 확장 필요 시
user object caller ownership
free/trim cache 또는 kernel 반환

malloc object graph, allocator chunk, VMA, physical page 네 층의 수명이 다르다. leak 분석과 RSS 분석에 같은 도구 하나만 쓰지 않는다.

그림 3. 커널 내부에서 지나가는 주요 지점
brk syscall heap VMA 끝 조정
mmap 새 anonymous VMA
page fault 실제 page 확보
madvise/munmap page/range 반환
RSS resident accounting

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
mm/mmap.c do_brk_flags(), do_mmap() allocator가 요청하는 virtual range 변경
mm/memory.c do_anonymous_page() heap/mmap page의 demand allocation
mm/madvise.c madvise_dontneed_single_vma() allocator trim 시 resident page 반환 가능 경로

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 allocator_rss.c -o allocator_rss
01#define _GNU_SOURCE
02#include <malloc.h>
03#include <stdio.h>
04#include <stdlib.h>
05#include <string.h>
06#include <unistd.h>
07
08int main(void)
09{
10    size_t length = 128UL * 1024 * 1024;
11    unsigned char *buffer = malloc(length);
12    if (buffer == NULL)
13        return 1;
14    memset(buffer, 0xa5, length);
15    printf("allocated and touched; pid=%ld\n", (long)getpid());
16    getchar();
17
18    free(buffer);
19    puts("freed to allocator");
20    getchar();
21
22    int released = malloc_trim(0);
23    printf("malloc_trim=%d\n", released);
24    getchar();
25    return 0;
26}

코드 조각별 설명

실제 코드 11행malloc(length)

요청은 virtual object allocation이다. glibc는 크기와 설정에 따라 arena top chunk 또는 별도 mmap을 선택한다.

실제 코드 14행memset(buffer

각 page를 write fault-in해 overcommit 예약만 한 상태와 실제 RSS 증가를 구분한다.

실제 코드 18행free(buffer)

pointer ownership은 allocator로 돌아가지만 page가 즉시 kernel에 반환되는지는 chunk 위치와 allocator 정책에 달렸다.

실제 코드 22행malloc_trim(0)

glibc에 사용하지 않는 heap page를 kernel에 돌려보내도록 요청하는 비표준 확장이다. 반환 1도 모든 free page가 사라졌다는 뜻은 아니다.

실제 코드 16행getchar();

각 단계에서 외부 shell이 /proc/PID/smaps_rollup과 pmap을 비교할 수 있도록 멈춘다.

세부 동작

01

large allocation과 small allocation 경로가 다르다

큰 allocation은 별도 mmap을 사용해 free 시 munmap하기 쉽고, 작은 allocation은 arena page 안에 섞여 하나가 살아 있으면 page 전체를 반환하기 어렵다. threshold는 구현·튜닝·실행 이력에 따라 달라진다.

크기 하나를 기준으로 고정 동작을 API 계약처럼 가정하지 않는다.

02

multithread arena는 contention과 RSS를 교환한다

allocator는 lock contention을 줄이려고 여러 arena와 thread cache를 둘 수 있다. peak 뒤 각 arena에 빈 chunk가 흩어지면 process 전체 RSS가 높게 남는다.

MALLOC_ARENA_MAX 같은 tunable은 workload latency와 fragmentation을 측정한 뒤 적용한다.

03

leak과 retention을 구분한다

leak은 도달할 수 없는 object를 application이 free하지 않은 문제이고, retention은 allocator가 free page를 재사용하려고 보유하는 상태다. 둘 다 RSS가 높지만 해결 방법은 다르다.

ASan/LSan, heap profiler, malloc_info, smaps를 함께 사용한다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
user allocationmalloc에서 caller에게 넘어가고 free에서 allocator 소유로 돌아간다requested/usable size, owner
allocator chunk/arenaprocess 수명 동안 재사용되며 trim/unmap에서 일부 반환된다bin, top chunk, fragmentation
anonymous VMA/pagebrk/mmap과 fault에서 생기고 madvise/munmap/reclaim에서 줄어든다Rss, Private_Dirty

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
free 뒤 RSS 유지arena retention/fragmentationmalloc_info와 smaps 비교
malloc NULL/abortENOMEM 또는 heap corruptionerrno, sanitizer, core dump
latency spikearena lock, page fault, mmap/munmapperf lock/fault와 allocator profiler

직접 확인

  1. 세 getchar 단계마다 smaps_rollup Rss/Private_Dirty와 strace brk/mmap/munmap을 기록한다.
  2. 128 MiB 한 개 대신 4 KiB object 여러 개를 섞어 할당·부분 free해 fragmentation 차이를 본다.
  3. MALLOC_ARENA_MAX를 바꿔 multithread allocation benchmark의 peak RSS와 throughput을 함께 측정한다.
실행./allocator_rss
추적strace -e trace=brk,mmap,madvise,munmap ./allocator_rss

원문