메모리 관리와 가상 메모리
가상 메모리가 필요한 이유
현대 운영체제에서 프로세스는 물리적 RAM 주소를 직접 다루지 않는다. 각 프로세스는 자신만의 가상 주소 공간(virtual address space) 을 가지며, 커널이 가상 주소를 물리 주소로 변환한다.
가상 메모리가 제공하는 핵심 기능:
- 격리: 프로세스 A가 프로세스 B의 메모리를 볼 수 없다.
- 추상화: 물리 메모리보다 더 큰 메모리가 있는 것처럼 사용할 수 있다 (swap과 함께).
- 효율: 지연 할당(lazy allocation), 공유 라이브러리, Copy-on-Write.
x86-64 가상 주소 공간 레이아웃
x86-64 시스템에서 가상 주소는 64비트지만, 실제로는 하위 48비트(4레벨 페이지 테이블) 또는 57비트(5레벨 페이지 테이블)만 사용한다. 4레벨 기준으로 유저 공간 128 TiB + 커널 공간 128 TiB 총 256 TiB가 주소 가능하다.
vmalloc / ioremap 영역 (불연속 물리 페이지)
커널 모듈, fixmap, vmemmap ── 캐노니컬 홀(Canonical Hole): 유효하지 않은 주소 ── 유저 공간 (0x0000000000000000 ~ 0x00007FFFFFFFFFFF) 텍스트(코드) → 초기화 데이터 → BSS → 힙 ↑
공유 라이브러리 (mmap 영역, ASLR로 랜덤화)
스택 ↓ (최상위 주소 근처)
ASLR(Address Space Layout Randomization): mmap, 스택, vdso 영역을 무작위 위치에 배치해 ROP·ret2libc 등 취약점 공략을 어렵게 한다. /proc/sys/kernel/randomize_va_space로 제어(0=비활성, 1=부분, 2=완전).
페이지 테이블: 주소 변환의 구조
가상 주소를 물리 주소로 변환하는 자료구조가 페이지 테이블(page table)이다. 4레벨 페이지 테이블에서 48비트 가상 주소는 다음과 같이 분해된다.
비트 47~39 38~30 29~21 20~12 11~0
PGD PUD PMD PTE 페이지 내 오프셋
9bit 9bit 9bit 9bit 12bit (= 4KB 페이지)변환 과정:
- CR3 레지스터가 현재 프로세스의 PGD(Page Global Directory) 물리 주소를 가리킨다.
- PGD 인덱스(bit 47~39)로 PUD 테이블 주소 획득.
- PUD 인덱스(bit 38~30)로 PMD 주소 획득.
- PMD 인덱스(bit 29~21)로 PTE 테이블 주소 획득.
- PTE 인덱스(bit 20~12)로 물리 페이지 번호(PFN) 획득.
- PFN << 12 + 오프셋(bit 11~0) = 최종 물리 주소.
각 PTE에는 물리 주소 외에도 플래그 비트가 포함된다:
| 플래그 | 의미 |
|---|---|
| P (Present) | 1이면 페이지가 메모리에 있음. 0이면 접근 시 페이지 폴트 발생 |
| R/W | 0이면 읽기 전용, 1이면 읽기/쓰기 허용 |
| U/S | 0이면 커널만 접근(Supervisor), 1이면 유저 접근 허용 |
| D (Dirty) | 페이지에 쓰기가 발생했음 |
| A (Accessed) | 최근에 접근됨 (LRU 판단에 활용) |
| NX (No-Execute) | 실행 불가 페이지 (스택 실행 방지) |
TLB(Translation Lookaside Buffer): 페이지 테이블 탐색은 메모리를 4번 읽는 비용이 있다. CPU 내부의 TLB가 최근 변환 결과를 캐시해 대부분의 접근을 단 1 사이클에 처리한다. 컨텍스트 스위치 시 CR3 교체로 TLB가 flush된다(ASID 지원 CPU는 일부 보존).
페이지 폴트: 지연 할당의 핵심
페이지 폴트(page fault)는 CPU가 가상 주소에 접근했을 때 PTE의 Present 비트가 0이어서 발생하는 예외다. 커널은 do_page_fault() 핸들러로 원인을 판별하고 처리한다.
| 페이지 폴트 유형 | 원인 | 처리 | 비용 |
|---|---|---|---|
| Minor fault | 페이지가 메모리에 있지만 PTE 매핑 없음 (COW, 익명 mmap) | PTE에 물리 페이지 연결, I/O 없음 | 수 µs |
| Major fault | 페이지가 swap이나 파일에 있음 | I/O로 페이지 적재 | 수 ms |
| Invalid fault | 잘못된 주소 접근, 권한 위반 | SIGSEGV 전송 → 프로세스 종료 | — |
지연 할당(lazy allocation): malloc(1GB)를 호출해도 실제 물리 메모리는 즉시 할당되지 않는다. PTE는 설정되지만 물리 페이지는 첫 접근(페이지 폴트) 시에 비로소 할당된다. 이것이 Linux가 물리 메모리보다 더 많은 가상 메모리를 프로세스에 허용(overcommit)하는 원리다.
# 프로세스별 페이지 폴트 카운터
/usr/bin/time -v ./myapp 2>&1 | grep -E 'Minor|Major'
# Minor (reclaiming a frame) page faults: 3847
# Major (requiring I/O) page faults: 12
# 시스템 전체 페이지 폴트 추이
vmstat 1
# pgfault: minor + major 합계, pgmajfault: major만메모리 존(Zone): 물리 메모리 분류
Linux 커널은 DMA 가용성과 주소 범위 기준으로 물리 메모리를 존으로 분류한다.
| Zone | 범위 (x86-64) | 용도 |
|---|---|---|
ZONE_DMA | 0 ~ 16 MB | 구식 ISA DMA 장치용 |
ZONE_DMA32 | 0 ~ 4 GB | 32비트 PCI DMA 장치용 |
ZONE_NORMAL | 4 GB 이상 | 일반 커널·유저 메모리 |
ZONE_MOVABLE | 상위 영역 | 메모리 핫플러그, 대용량 페이지 이동 가능 |
# 존별 free 페이지 수 확인
cat /proc/zoneinfo | grep -A5 "Node 0, zone"버디 할당자와 SLUB은 요청에 맞는 존에서 메모리를 할당하되, 해당 존이 부족하면 상위 존(fallback)에서 가져온다.
버디 할당자: 페이지 단위 물리 메모리 관리
버디 할당자(buddy allocator)는 Linux의 물리 페이지 할당을 담당한다. 메모리를 2의 거듭제곱 크기(order)의 블록으로 관리하며, 총 11가지 order(0~10)를 사용한다.
- order 0 = 4 KB (1페이지), order 1 = 8 KB, ..., order 10 = 4 MB (1024페이지)
- 각 order마다 존별로 free list(연결 리스트)를 유지한다.
버디 할당자는 외부 단편화(external fragmentation)를 최소화하지만, 4 KB보다 훨씬 작은 객체에는 부적합하다. 그 역할은 SLUB이 담당한다.
SLUB 할당자: 커널 객체 캐시
커널 내부에서 자주 생성·소멸되는 작은 구조체(task_struct, inode, dentry, sk_buff 등)를 위해 SLUB 할당자가 있다. Linux 2.6.23부터 SLAB을 대체한 기본 구현이다.
동작 원리:
- 버디 할당자로 연속 페이지를 확보한다.
- 동일 크기의 객체 슬롯(slot)으로 쪼갠다.
- per-CPU free list를 통해 락 없이(lock-free) 빠르게 할당·해제한다.
- per-CPU 리스트가 비면 numa node별 partial slab → 새 슬랩(버디 요청) 순으로 확장.
# 슬랩 캐시 목록 및 메모리 사용량 확인
sudo slabtop
# 또는 직접 읽기
cat /proc/slabinfo | awk 'NR<=20'
# 예시 출력 (컬럼: name, active, num, objsize, ...)
# task_struct 1024 1024 5376 ...
# inode_cache 8192 8192 704 ...
# dentry 32768 32768 192 ...kmalloc(size, GFP_KERNEL)은 size에 맞는 슬랩 캐시를 찾아 객체를 반환하며, kfree(ptr)는 슬랩에 반환한다.
mmap: 메모리와 파일의 교차점
mmap() 시스템 콜은 파일이나 디바이스를 프로세스의 가상 주소 공간에 직접 매핑한다. read()/write()와 달리 커널 버퍼 → 유저 버퍼 복사가 없어 제로카피(zero-copy)에 가깝다.
// 파일 내용을 메모리처럼 읽기 (file-backed mmap)
int fd = open("/data/bigfile.bin", O_RDONLY);
void *p = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, offset);
// p[i]로 파일 내용 접근 — 페이지 폴트로 필요한 페이지만 적재
// 익명 mmap (glibc의 malloc이 큰 할당에 사용)
void *buf = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);| 유형 | 플래그 | 특징 | |
|---|---|---|---|
| File-backed shared | MAP_SHARED | 변경이 파일에 반영, 프로세스 간 공유 | |
| File-backed private | MAP_PRIVATE | COW 적용, 변경이 파일에 미반영 | |
| Anonymous private | `MAP_PRIVATE\ | MAP_ANONYMOUS` | 순수 RAM, malloc/스택 확장 |
# 프로세스의 가상 주소 공간 매핑 보기
cat /proc/1234/maps
# 55b8a3000000-55b8a3001000 r--p 00000000 fd:01 1234567 /usr/bin/myapp
# 7f9b2c000000-7f9b2c200000 r-xp 00000000 fd:01 2345678 /lib/x86_64-linux-gnu/libc.so.6
# 7ffd3e200000-7ffd3e221000 rw-p 00000000 00:00 0 [stack]거대 페이지(Huge Pages): TLB 부담 줄이기
기본 4 KB 페이지는 대용량 메모리를 다룰 때 TLB 항목을 과도하게 소모한다. MySQL, PostgreSQL, Redis 같은 메모리 집약적 DB는 거대 페이지로 TLB miss를 크게 줄일 수 있다.
명시적 HugeTLBfs (Explicit Huge Pages):
- 부팅 시 또는
/proc/sys/vm/nr_hugepages로 미리 예약한다. - 기본 크기 2 MB (x86-64 PMD 레벨),
mmap(MAP_HUGETLB)또는hugetlbfs마운트로 사용. - 예약된 후 조각화 없이 항상 사용 가능하다는 장점이 있다.
THP (Transparent Huge Pages):
- 커널이 런타임에 연속된 4 KB 페이지를 2 MB 페이지로 자동 승격(promote)한다.
khugepaged커널 데몬이 주기적으로 스캔·병합한다.- 애플리케이션 코드 수정 없이 동작하지만, 병합 과정의 지연 스파이크가 발생할 수 있다.
# THP 설정 확인 및 변경
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
# madvise 모드: 앱이 명시적으로 요청한 범위에만 THP 적용
# madvise(addr, length, MADV_HUGEPAGE); // C 코드에서
# /proc/meminfo에서 THP 사용량 확인
grep AnonHugePages /proc/meminfo
# AnonHugePages: 524288 kB (= 256개의 2MB 페이지)DB 운영 주의: MySQL InnoDB나 Elasticsearch는 THP의
always모드가 오히려 지연 스파이크를 유발할 수 있다.madvise또는never로 설정하는 것이 안전하다.
OOM Killer: 메모리 고갈 시 최후 수단
Linux는 기본적으로 overcommit을 허용한다. 물리 RAM + 스왑보다 더 많은 가상 메모리를 프로세스에 할당한다. 실제 물리 메모리가 고갈되면 OOM Killer가 개입한다.
OOM Killer 동작:
- 각 프로세스에 oom_score(0~1000)를 계산한다. 메모리를 많이 쓸수록, 자식이 많을수록 높다.
oom_score_adj(-1000~+1000)로 휴리스틱을 조정할 수 있다. -1000은 "절대 종료하지 말 것"을 의미한다.- 가장 높은 oom_score를 가진 프로세스를 SIGKILL로 강제 종료.
# 현재 oom_score 확인
cat /proc/1234/oom_score
# 중요한 프로세스 OOM에서 보호 (예: 데이터베이스)
echo -1000 > /proc/1234/oom_score_adj
# overcommit 정책 확인/변경
cat /proc/sys/vm/overcommit_memory
# 0: 휴리스틱 (기본) — 명백히 불가능한 요청만 거부
# 1: 항상 허용
# 2: 엄격 제한 (RAM + swap × overcommit_ratio% 이하만 허용)OOM이 발생하면 커널은 dmesg에 상세 로그를 남긴다.
dmesg | grep -A20 "Out of memory"
# Out of memory: Killed process 4567 (java) total-vm:8GB, anon-rss:6GB/proc/meminfo 핵심 필드 읽기
cat /proc/meminfo
# MemTotal: 32716804 kB ← 전체 물리 메모리
# MemFree: 1234567 kB ← 즉시 사용 가능한 빈 메모리
# MemAvailable: 15000000 kB ← 앱이 실제로 쓸 수 있는 양(캐시 회수 포함)
# Buffers: 512000 kB ← 블록 디바이스 메타데이터 버퍼
# Cached: 8000000 kB ← 파일 페이지 캐시 (회수 가능)
# SwapTotal: 8388608 kB
# SwapFree: 8000000 kB
# AnonHugePages: 524288 kB ← THP 사용량
# SReclaimable: 1000000 kB ← 회수 가능한 슬랩(inode/dentry 캐시 포함)
# SUnreclaim: 200000 kB ← 회수 불가 슬랩(task_struct 등)MemAvailable은 MemFree + 회수 가능한 캐시의 근삿값으로, 실제로 애플리케이션이 사용 가능한 메모리를 나타낸다. MemFree만 보면 파일 캐시를 무시해 메모리가 부족한 것으로 오해하기 쉽다.
References
- Linux Memory Management: Virtual Memory Implementation and Memory Allocation Strategies — CodeLucky
- 5-level paging — The Linux Kernel documentation
- How Linux Manages Memory: From Virtual Addresses to Huge Pages — Sourav Paul, Medium
- Memory management: Zones, Buddy allocator, Slab allocator — MIMUW
- Linux SLUB Allocator Internals and Debugging, Part 1 of 4 — Oracle Blogs
- Transparent Hugepage Support — The Linux Kernel documentation
- Understanding Linux Kernel Memory Statistics — Oracle Blogs
- A Quick Dive Into The Linux Kernel Page Allocator — syst3mfailure.io
- Linux Memory Management: Understanding Page Tables, Swapping, and Memory Allocation — Linux Journal