왜 이 문제를 파헤쳐야 할까?
인프라 제공자로서 우리 서비스가 얼마나 많은 메모리를 사용하는지, 왜 그만큼 사용하는지 명확히 설명할 수 있어야 한다. 클라우드 개발자라면 이 정도 문제는 당연히 이해하고 있어야 한다.
§ 0x01 범위
실용성을 고려하여 Linux X86 플랫폼만 다룬다. 메모리 페이지 크기는 4KiB이다.
목표: 대규모 Go 프로세스의 메모리 소비 지점을 명확히 설명한다.
Go는 자체 런타임을 가지고 있어 메모리 구성이 복잡하다. 설명하고 이해하기 어려운 부분이 있으므로, 먼저 간단한 C 프로그램부터 시작한다.
§ 0x02 일반적인 C 프로그램
의존성이 없는 가장 간단한 프로그램을 어셈블리로 작성한다. AT&T 스타일을 선호한다:
.section .data
.section .text
.globl _start
_start:
movl $1, %eax
movl $5, %ebx
int $0x80
Makefile을 작성한다:
all: demo
demo: demo.o
ld -static --no-dynamic-linker -o $@ -lc $<
demo.o: demo.s
as -gstabs -o $@ $<
clean:
rm ./demo ./demo.o
as로 컴파일하고 ld로 링크하여 정적 링크 바이너리를 생성한다. libc에 의존하지 않으며 Linux에서 가능한 최소 프로그램이다.
gcc와 gas 등 구성 요소가 필요하다. make 실행 후 demo 바이너리가 생성된다. 이 바이너리는 종료 코드를 설정하는 단일 시스템 호출만 있다. 실행해도 출력이 없으며, echo $?로 확인할 수 있다.
$ ./demo; echo $?
5
gdb로 프로그램을 중단시키고 메모리 사용량을 확인한다:
ulin 7116 0.0 0.0 168 24 pts/1 t 09:17 0:00 /mnt/d/code/lean/go-memory/asm/demo
RSS 값은 24KiB이다. 즉, 최소 프로그램도 24KiB를 차지한다.
왜 최소 프로그램이 24KiB를 차지하는지 분석한다. 프로세스는 바이너리가 메모리에 로드되어 형성되므로, 바이너리(ELF) 구조부터 살펴본다.
readelf -S로 섹션 크기를 확인한다:
| 섹션 | 크기(B) | 설명 |
|---|---|---|
| NULL | 0 | ELF의 빈 섹션, 4KiB 차지 |
| .text (코드) | 12 | 12B지만 정렬 후 16B |
| .symtab (심볼 테이블) | 144 | strip으로 제거 가능 |
| .strtab (문자열) | 25 | |
| .shstrtab | 33 |
readelf -a -W demo로 ELF 레이아웃을 확인:
- ELF 헤더
- 프로그램 헤더
- 섹션들
- 섹션 헤더 테이블
| 구역 | 오프셋 | 크기 |
|---|---|---|
| ELF 헤더 | 0 | 64 |
| 프로그램 헤더 | 64 | 56 × 2 = 112 |
| 섹션 | 176 | 4096 + 16 + 144 + 25 = 4281 |
| 섹션 헤더 테이블 | 4320 | 5 × 64 = 320 |
섹션 중 .text만 메모리에 로드된다. 코드가 12B인데 왜 RSS가 24KiB일까?
/proc/<pid>/smaps에 상세 메모리 레이아웃이 있다. Pss와 Rss의 차이점: Rss는 실제 세그먼트 크기, Pss는 프라이빗 세그먼트 크기다. .so 파일이 다른 프로세스와 공유되면 Pss = Rss / N이다. 실제 점유는 모든 세그먼트의 Pss 합계다.
| 세그먼트 | Pss | 설명 |
|---|---|---|
| 00400000-00401000 | 4KiB | 프로그램 헤더에 따라 NULL 섹션 로드, 읽기 전용 |
| 00401000-00402000 | 4KiB | 코드 세그먼트, rxp (실행 가능) |
| 7ffff7ff9000-7ffff7ffd000 | 4KiB | 환경 변수, 읽기 전용 |
| 7ffff7ffd000-7ffff7fff000 | 4KiB | vdso 세그먼트, 커널 매핑된 가상 공유 객체 |
| 7ffffffdd000-7ffffffff000 | 8KiB | 스택 |
총 24KiB다. 메모리 페이지는 최소 단위(4KiB)로 할당되므로 코드 세그먼트가 4KiB 미만이어도 4KiB를 차지한다.
§ 0x03 요약
최소 C 프로그램의 메모리 사용량을 분석하여 RSS 구성을 명확히 했다. 전체 결론은 다음과 같다:
- ELF의 일부 섹션이 프로세스 메모리에 매핑된다.
- 프로세스의 최종 메모리 섹션은 두 가지 출처가 있다:
- 프로그램 헤더에서 로더에게 어떤 섹션을 메모리에 로드할지와 해당 플래그를 알려준다.
- 로더가 자체적으로 정의한 공유 세그먼트 (vdso, 스택, 환경 변수).
의문: 힙 세그먼트가 보이지 않는 이유는? 추측컨대 glibc가 정의하는 영역이며, 현재 어셈블리로 구현된 프로그램은 힙이 필요하지 않다.