프로세스 메모리를 code → data → heap → stack 네 칸으로 그린 그림은 입문용 개념도일 뿐, 모든 운영체제와 실행 파일의 고정된 주소 배치가 아니다. 실제 virtual address space에는 executable·shared library·memory-mapped file·anonymous mapping·thread별 stack 등이 섞이고, ASLR과 loader, ABI, allocator에 따라 위치와 크기가 달라진다.
따라서 ‘stack은 항상 위에서 아래, heap은 하나의 연속 영역으로 아래에서 위’라는 그림을 실제 memory map처럼 외우기보다, mapping의 권한과 용도, allocation 계층을 나눠 보는 편이 정확하다.
개념도와 실제 address space
전통적인 native process 설명에서는 다음 이름을 자주 쓴다.
- text/code: executable instruction이 담긴 mapping. 보통 read·execute 권한을 갖는다.
- read-only data: 상수와 literal 등 write가 필요 없는 data.
- data: 초기값이 있는 global·static data.
- BSS: 실행 파일에 초기 byte를 모두 저장하지 않는 zero-initialized global·static data.
- heap: user-space allocator가 dynamic allocation을 제공하는 영역의 통칭.
- stack: 함수 호출 상태 등에 쓰이는 thread별 영역.
하지만 이 이름만으로 실제 주소나 단일 mapping을 보장하지 않는다. Linux에서는 /proc/<pid>/maps로 virtual memory area의 주소 범위, 권한과 backing file을 확인할 수 있다. shared library와 mmap 영역이 중간에 나타나고, heap allocator도 큰 allocation을 별도 anonymous mapping으로 받을 수 있다. 다른 운영체제에서는 관측 도구와 표시 방식이 달라진다.
또한 C/C++의 automatic storage duration을 무조건 물리적 stack 저장과 같은 말로 보면 안 된다. compiler가 값을 register에 두거나 최적화로 없앨 수 있다. 언어 수준의 수명 규칙과 구현의 저장 위치를 분리해야 한다.
stack과 heap은 다른 종류의 RAM이 아니다
두 영역은 결국 virtual address가 page table을 통해 physical memory 등에 mapping된다는 점에서 같은 memory hierarchy를 사용한다. stack memory는 빠르고 heap memory는 느리다는 말은 memory cell 자체의 속도 차이를 뜻하지 않는다.
차이는 주로 allocation 규칙과 access pattern에서 나온다.
| 관점 | stack 계열 | heap allocation |
|---|---|---|
| 수명 | 호출 scope와 함께 관리되는 경우가 많음 | 명시적 free 또는 runtime·GC 정책 |
| allocation | stack pointer 조정으로 끝날 수 있음 | size class, free list, arena, lock 등을 거칠 수 있음 |
| 크기 | thread별 limit과 guard page 영향 | address space·allocator·resource limit 영향 |
| locality | 연속적인 frame이 locality에 유리할 수 있음 | 배치와 fragmentation에 따라 달라짐 |
| 대표 문제 | 깊은 recursion·큰 frame의 stack overflow | leak, use-after-free, fragmentation 등 |
heap allocation이 언제나 느린 것도 아니다. allocator cache와 size class 덕분에 작은 allocation이 빠를 수 있고, 실제 성능은 allocation 빈도, contention, cache locality, page fault, garbage collection에 좌우된다. stack에 둔 data도 cache miss가 많으면 느릴 수 있다. 측정 없이 저장 위치만으로 병목을 단정하지 않는다.
allocator는 여러 층으로 나뉜다
application의 malloc과 kernel의 page·slab allocator는 같은 층이 아니다.
application / language runtime
│ malloc, object allocator
▼
user-space allocator
│ OS에 mapping 요청
▼
kernel virtual memory + page allocator
│
▼
physical pages / file / swap entry
user-space allocator는 OS에서 받은 큰 영역을 작은 object allocation으로 나눠 재사용한다. Linux kernel의 SLAB/SLUB 계열은 kernel object를 위한 cache이고, application의 일반 객체를 직접 나눠 주는 allocator가 아니다. 두 이름을 한 표에서 같은 선택지처럼 비교하면 계층을 섞게 된다.
page 크기도 ‘항상 4KB’가 아니다. architecture와 OS 설정에 따라 base page가 다르고 huge page도 있을 수 있다. 현재 환경의 값은 다음처럼 조회한다.
getconf PAGESIZE
virtual memory는 swap의 다른 이름이 아니다
virtual memory의 역할은 RAM 부족분을 disk로 넘기는 것보다 넓다.
- process마다 독립적인 address space 제공
- page 단위 권한으로 code와 data 보호
- file mapping과 shared memory 지원
- 필요할 때 page를 mapping하는 demand paging
- 실제 physical page 위치를 숨기는 address translation
page table entry는 physical page뿐 아니라 아직 mapping되지 않은 상태나 swap entry 등을 가리킬 수 있다. OS가 반드시 모든 anonymous page를 disk에 swap하는 것도 아니다. swap 정책과 memory compression, reclaim 방식은 운영체제마다 다르다.
RSS는 ‘내 process만 쓰는 memory’가 아니다
Resident Set Size(RSS)는 process의 page 중 현재 resident한 양을 나타내는 지표지만, 곧바로 private memory나 실제 청구 비용과 같지는 않다. shared library와 shared page가 포함될 수 있고, copy-on-write, page sharing, 측정 시점과 도구의 집계 방식이 영향을 준다.
Linux에서는 /proc/<pid>/smaps 또는 smaps_rollup의 PSS·Private_Clean·Private_Dirty 등을 함께 보면 공유 page를 더 잘 구분할 수 있다. macOS에서는 Activity Monitor, vmmap, footprint가 서로 다른 관점의 수치를 보여 준다. 하나의 RSS 숫자로 leak을 확정하지 말고 시간에 따른 증가, allocation stack과 workload를 함께 본다.
관련 용어는 메모리와 프로세스 용어 정리, Linux mapping별 수치 해석은 /proc/smaps 분석 흐름에서 이어진다.
tracemalloc은 process RSS 측정기가 아니다
Python의 tracemalloc은 Python allocator가 추적하는 memory block의 allocation traceback과 snapshot 차이를 보는 도구다. native extension이 직접 할당한 모든 memory나 process 전체 RSS를 대신하지 않는다.
import tracemalloc
tracemalloc.start()
before = tracemalloc.take_snapshot()
data = list(range(100_000)) # 두 번째 snapshot까지 참조 유지
after = tracemalloc.take_snapshot()
stats = after.compare_to(before, "lineno")
for stat in stats[:5]:
print(stat)
current, peak = tracemalloc.get_traced_memory()
print(f"traced current={current:,}, peak={peak:,}")
기존 예제처럼 함수의 반환값을 저장하지 않으면 allocation한 list가 두 번째 snapshot 전에 해제될 수 있다. 위 코드는 data reference를 유지해 비교 시점을 명확히 했다. 이 예제는 Python allocation 변화가 보이는지 확인하는 용도이며 OS resident memory와 같은 숫자를 기대하지 않는다.
참고 자료
'배움과 성장 > 시스템·성능' 카테고리의 다른 글
| 페이지 교체 알고리즘: FIFO·LRU·OPT와 Belady anomaly (1) | 2024.12.08 |
|---|---|
| 시간 단위 변환 기준: 초·밀리초·나노초부터 날짜 계산까지 (0) | 2024.12.08 |
| Linux CPU 실행 경로: 시스템 콜·하드 IRQ·softirq·workqueue 구분 (2) | 2024.12.06 |
| Linux Kernel 동작 흐름: System Call·Task·Virtual Memory·VFS 연결하기 (0) | 2024.12.06 |
| Linux man 페이지 섹션과 syscall 추적: man 1·2·3·5·7·8 (5) | 2024.12.06 |
댓글