관련 소스: mm/memory.c, mm/mmap.c, mm/page_alloc.c, mm/vmscan.c, mm/oom_kill.c, mm/slub.c, mm/compaction.c, mm/shmem.c, mm/memcontrol.c, mm/huge_memory.c, mm/zswap.c, mm/secretmem.c, mm/hmm.c, mm/memblock.c, mm/vmalloc.c, include/linux/mm_types.h, include/linux/mmzone.h
관련 문서: 01-Buddy Allocator · 02-SLUB · 03-VMA/mmap · 04-Memblock · 05-페이지 회수 · 06-vmalloc · 07-Swap/zswap · 08-OOM · 10-Huge Pages/THP · 11-Compaction · 12-Folio · 13-NUMA · 14-Memory Cgroup · 16-tmpfs/shmem · 18-CMA · 19-KSM · 21-DAMON · 22-Hotplug · 27-디버그 도구 · 30-madvise · 36-mprotect · 37-mremap · 38-Pagewalk · 49-MGLRU · 51-ARM64 vs x86_64 · 53-컨테이너 메모리 · 55-mTHP · 56-virtio-mem
참고 규격: Intel SDM(x86 페이지 테이블과 TLB), ARM ARM(AArch64 메모리 모델), JEDEC DDR5(메모리 타이밍과 구조)
Linux 7.0 커널의 메모리 관리는 크게 4가지 축으로 나뉩니다:
1. 물리 메모리 관리 — Node → Zone → Page 구조. Buddy Allocator가 __alloc_pages()/__get_free_pages()로 2^n 페이지 블록을 할당하고, SLUB가 kmalloc()/kfree()로 커널 오브젝트를 캐싱합니다.
2. 가상 메모리 관리 — 프로세스별 VMA(virtual memory area)와 페이지 테이블. mmap/brk로 가상 주소를 예약하고, page fault가 발생할 때 실제 물리 페이지를 연결합니다.
3. 메모리 회수 & 압력 대응 — kswapd, LRU, shrinker가 메모리 부족 시 페이지를 회수하고, 필요 시 swap/zswap으로 밀어냅니다. 최종적으로 OOM Killer가 작동합니다.
4. 특수 메모리 & 디버깅 — HugeTLB/THP/mTHP, CMA, KSM, DAMON, swap/zswap, tmpfs/shmem, memcg, hotplug, secretmem, HMM/CXL, KASAN/KFENCE가 각자 다른 방식으로 메모리 사용과 관측을 바꿉니다.
일상적으로는 대형 도서관 운영과 비슷합니다. 물리 RAM은 실제 서가, Zone은 서가 구역, 페이지 테이블은 책 위치를 찾는 색인 카드, 캐시는 자주 보는 책을 올려두는 데스크, 스왑은 당장 쓰지 않는 책을 옮겨두는 외부 창고로 보면 됩니다. 아파트 호수처럼 각 프로세스가 같은 번호를 자기만의 주소 공간에서 다시 쓸 수 있다고 생각하면 더 직관적입니다. 이 비유에서 중요한 점은 사용자가 보는 주소가 실제 서가 위치가 아니라 색인 카드의 번호라는 점이며, 커널은 접근 시점에만 필요한 물리 페이지를 연결합니다.
부팅 초기는 BIOS E820, UEFI, Device Tree가 알려준 물리 메모리 맵을 memblock이 먼저 정리하고, 예약 영역을 제외한 뒤 memblock_free_all()에서 Buddy Allocator로 넘기는 단계입니다.
page는 가상/물리 페이지를 잇는 기본 단위이고, page frame은 실제 DRAM 쪽 물리 프레임입니다. 기본 4KB 크기는 TLB 효율과 page table 크기, 내부 단편화 사이의 균형점이고, HugeTLB/THP/mTHP는 이 균형을 워크로드에 맞게 넓히는 수단입니다.
CPU cache와 MMU/TLB, PTI의 기본 용어를 알고 있으면 뒤의 page fault, reclaim, NUMA 설명을 따라가기 훨씬 쉽습니다.
x86_64 4단계 주소 공간에서는 user space가 0x0000000000000000~0x00007fffffffffff, kernel space가 0xffff800000000000~0xffffffffffffffff에 놓입니다. Meltdown 계열 취약점 이후 PTI/KPTI는 유저 모드에서 커널 매핑을 최소화해 이 경계를 더 엄격하게 다룹니다.
읽는 순서는 물리 메모리(Node/Zone/Page) → 가상 메모리(VMA/PTE) → 회수(LRU/swap/OOM) → 특수 메모리(HugeTLB, THP/mTHP, CMA, KSM, DAMON, hotplug, tmpfs/shmem, memcg, secretmem, HMM/CXL)로 잡으면 흐름이 잘 보입니다. CPU cache와 TLB를 함께 보면 지연과 회수 지표도 같이 해석하기 쉬워집니다.
페이지 테이블을 바꾸는 mprotect(), munmap(), COW 경로는 TLB 무효화와 함께 봐야 하고, 물리 메모리 배치와 직접 매핑은 memblock, zone, struct page, vmemmap 흐름에서 다시 확인하면 좋습니다.
MMU는 가상 주소를 물리 주소로 바꾸는 하드웨어 변환 장치이고, TLB는 최근 변환 결과를 캐시합니다. 그래서 같은 VMA라도 TLB가 비어 있으면 page fault 이후의 주소 변환 비용이 드러나고, mprotect()나 COW처럼 PTE를 바꾸는 경로는 TLB 무효화까지 함께 봐야 합니다.
KASAN은 shadow memory로, KFENCE는 샘플링된 guard page로 메모리 손상을 잡습니다.
소스 파일 경로:
mm/memory.c ← Page fault handler (가상 메모리의 핵심)
mm/page_alloc.c ← Buddy Allocator (물리 메모리 할당)
mm/slub.c ← SLUB 할당자 (커널 오브젝트 캐싱)
mm/vmscan.c ← 페이지 회수 (kswapd, LRU)
mm/mmap.c ← VMA 관리 (mmap 시스템 콜)
mm/memblock.c ← 부팅 초기 할당자
include/linux/mm_types.h ← struct page, folio, vm_area_struct, mm_struct
include/linux/mmzone.h ← struct zone, pglist_data, free_area, lruvec
# 1. 시스템 메모리 전체 현황
cat /proc/meminfo | head -10
# 1-1. CPU cache 계층 확인
lscpu | grep -E "L1d cache|L1i cache|L2 cache|L3 cache"
# 2. NUMA 노드 구조
numactl --hardware
# 2-1. NUMA 노드별 메모리 사용량
numastat -m
# 2-2. 특정 프로세스의 NUMA 분포
numastat -p $$
# 3. Zone별 Buddy free list 분포
cat /proc/buddyinfo
# migratetype/order별 세부 분포
cat /proc/pagetypeinfo | head -40
# 4. 프로세스 메모리 사용량
cat /proc/$$/status | grep -E "VmSize|VmRSS|VmPeak"
# 5. 페이지 폴트 통계
cat /proc/vmstat | grep -E "pgfault|pgmajfault|pgalloc|pgfree"
# 6. Slab 캐시 사용 현황
cat /proc/slabinfo | head -5
# 7. 메모리 압력 (PSI)
cat /proc/pressure/memory
# 8. 실제 물리 메모리 범위 요약
lsmem 2>/dev/null
# 펌웨어가 보고한 RAM과 예약 영역 확인
dmesg | grep -E "BIOS-e820|efi: mem|Memory:"
# 9. 현재 커널 페이지 테이블 확인 (debugfs 필요)
sudo cat /sys/kernel/debug/kernel_page_tables 2>/dev/null | head -40
# 기본 페이지 크기 확인
getconf PAGE_SIZE
# 표준 별칭으로도 같은 값을 확인
getconf PAGESIZE
# 현재 셸 프로세스의 minor/major fault 확인
ps -o pid,minflt,majflt -p $$
# 익명 페이지와 파일 캐시 비중, 스왑 여유 확인
grep -E "AnonPages|Cached|SwapTotal|SwapFree" /proc/meminfo
# 회수/스왑 경향을 1초 간격으로 관찰
vmstat 1 10
# 직접 매핑, vmalloc, 페이지 테이블 메모리 사용량 확인
grep -E "DirectMap|Vmalloc|PageTables|KReclaimable" /proc/meminfo
# Zone별 managed/present/spanned 페이지와 watermark 확인
cat /proc/zoneinfo | grep -E "Node|zone|managed|present|spanned|pages free|min|low|high" | head -80
# 펌웨어/커널이 본 물리 주소 공간 확인
cat /proc/iomem | head -40
# vmalloc/ioremap 계열 가상 연속 매핑 확인
sudo cat /proc/vmallocinfo 2>/dev/null | head -20
# 조건별 메모리맵 옵션 확인
zgrep -E "CONFIG_X86_5LEVEL|CONFIG_ARM64_VA_BITS|CONFIG_KASAN|CONFIG_MEMORY_HOTPLUG|CONFIG_ZONE_DEVICE" /proc/config.gz 2>/dev/null
# 메모리 압력과 회수 지표 확인
cat /proc/vmstat | grep -E "pgfault|pgmajfault|pgscan|pgsteal|allocstall"
# 익명 페이지와 파일 캐시 사이의 회수 성향 확인
cat /proc/sys/vm/swappiness
# TLB 미스 경향 확인 (perf 사용 가능할 때)
perf stat -e dTLB-load-misses,dTLB-store-misses -a sleep 1 2>/dev/null
# DIMM 종류와 속도 확인
sudo dmidecode -t memory | grep -E "Type:|Speed:|Configured Memory Speed:" 2>/dev/null
# KASAN / KFENCE 활성화 확인
zgrep -E "CONFIG_KASAN|CONFIG_KFENCE" /proc/config.gz 2>/dev/null
# 런타임 진단 메시지 확인
dmesg | grep -Ei "kasan|kfence"
Node 0 (NUMA) Node 1 (NUMA)
├── ZONE_DMA (0~16MB) ├── ZONE_DMA (0~16MB)
├── ZONE_DMA32 (0~4GB) ├── ZONE_DMA32 (0~4GB)
├── ZONE_NORMAL (4GB~끝) ├── ZONE_NORMAL (4GB~끝)
│ └── free_area[0..10] │ └── free_area[0..10]
│ └── Buddy free lists │ └── Buddy free lists
│ order 0: 4KB order 0: 4KB
│ order 1: 8KB order 1: 8KB
│ ... ...
│ order 10: 4MB order 10: 4MB
└── ZONE_MOVABLE (설정 가능) └── ZONE_MOVABLE (설정 가능)
구조체 관계:
| 구조체 | 파일 | 역할 |
|---|---|---|
| `pglist_data` (pg_data_t) | mmzone.h | NUMA 노드 하나 전체 |
| `zone` | mmzone.h | 물리 메모리 구역 (DMA/Normal/Movable) |
| `free_area` | mmzone.h | Buddy free list (order별) |
| `struct page` | mm_types.h | 물리 페이지 프레임 하나 |
| `struct folio` | mm_types.h | page의 상위 개념 (compound page) |
각 프로세스는 독립된 가상 주소 공간을 가집니다:
가상 주소 공간 (x86-64, 128TB)
┌─────────────────────┐ high address
│ 커널 공간 │ (모든 프로세스 공유)
├─────────────────────┤
│ 스택 │ ↓ grows down
│ ... │
│ mmap 영역 │ (파일 매핑, 공유 메모리)
│ ... │
│ 힙 │ ↑ grows up (brk)
│ BSS │
│ 데이터 │
│ 텍스트 (코드) │ (read-only, exec)
└─────────────────────┘ low address (0x0)
구조체 관계:
| 구조체 | 파일 | 역할 |
|---|---|---|
| `mm_struct` | mm_types.h | 프로세스 전체 가상 주소 공간 |
| `vm_area_struct` (VMA) | mm_types.h | 하나의 연속된 가상 영역 |
| `vm_fault` | mm.h | 페이지 폴트 처리 중 임시 상태 |
| maple tree (`mm_mt`) | mm_types.h | VMA 저장 (rbtree 대체) |
VMA에서 물리 페이지까지의 변환:
VMA (vm_start ~ vm_end)
│
▼ mm_struct.pgd
PGD → P4D → PUD → PMD → PTE
│
▼
물리 페이지 (struct page)
물리 메모리의 각 페이지 하나를 표현. 시스템의 물리 메모리 크기에 비례하여 배열이 생성됩니다.
/* include/linux/mm_types.h:79-116 */
struct page {
memdesc_flags_t flags; /* 원자 플래그. 일부는 비동기적으로 갱신될 수 있음 */
/*
* 이 union에는 5워드(20/40바이트)를 사용할 수 있다.
* 주의: 첫 번째 워드의 bit 0은 PageTail()에 쓰인다.
* 따라서 다른 사용자는 충돌과 잘못된 PageTail() 판정을 피하려고
* 이 비트를 사용하면 안 된다.
*/
union {
struct { /* 페이지 캐시와 익명 페이지 */
/**
* @lru: 페이지 회수 리스트. 예를 들면 active_list는
* lruvec->lru_lock으로 보호된다. 때로는 page owner가
* 일반 리스트로도 사용한다.
*/
union {
struct list_head lru;
/* 또는 빈 페이지 */
struct list_head buddy_list;
struct list_head pcp_list;
struct llist_node pcp_llist;
};
struct address_space *mapping;
union {
pgoff_t __folio_index; /* mapping 안에서의 오프셋 */
unsigned long share; /* fsdax용 공유 개수 */
};
/**
* @private: mapping 전용의 불투명한 데이터.
* PagePrivate이면 보통 buffer_heads에 쓴다.
* swapcache 플래그가 설정되면 swp_entry_t에 쓴다.
* PageBuddy이거나 pcp_llist에 있으면 buddy system의 order를 나타낸다.
*/
unsigned long private;
};
};
핵심 플래그: PG_locked, PG_dirty, PG_lru, PG_anon, PG_slab, PG_compound, PG_buddy
상세 분석: 01-Buddy Allocator (Buddy 할당에서의 사용), 02-SLUB (Slab에서의 사용)
page의 상위 개념. 연속된 물리 메모리 블록을 하나의 단위로 관리합니다. 5.16+에서 도입되어 page 기반 코드를 folio 기반으로 전환하는 중입니다.
상세 분석: 12-Folio / Page Cache
프로세스의 가상 메모리 영역 하나를 표현합니다. mm_struct의 maple tree에 저장됩니다.
상세 분석: 03-VMA / mmap
물리 메모리의 논리적 구역. free_area 배열로 Buddy free list를 관리합니다.
상세 분석: 01-Buddy Allocator
NUMA 노드 하나의 전체 물리 메모리 정보. 존 목록, zonelist, kswapd/kcompactd 스레드를 포함합니다.
상세 분석: 13-NUMA
페이지 회수를 위한 LRU 관리 구조. 활성/비활성 익명·파일 페이지 4개 + unevictable 1개 리스트로 구성됩니다.
상세 분석: 05-페이지 회수
사용자 공간에서는 glibc ptmalloc이 먼저 arena의 free chunk를 재사용하고, 부족하면 작은 할당은 brk()로 힙을 늘리고 큰 할당은 mmap()으로 별도 VMA를 만듭니다. 그 다음 실제 접근 시 page fault가 발생해 handle_mm_fault() 아래로 들어갑니다.
사용자: malloc(N)
│
▼
brk() / mmap() → VMA 생성 (물리 페이지 할당 없음)
│
▼ (실제 접근 시)
Page Fault → handle_mm_fault()
│
├─ 익명 페이지 → do_anonymous_page() → alloc_anon_folio()
│ │
│ ▼
│ __alloc_frozen_pages_noprof()
│ │
│ ▼
│ Buddy Allocator
│ (page_alloc.c)
│ │
│ ▼
│ SLUB (커널 오브젝트)
│ (slub.c)
│
├─ 파일 페이지 → do_fault() → 파일에서 로드
│
└─ 스왑된 페이지 → do_swap_page() → 스왑에서 복원
할당자 계층:
| 할당자 | 단위 | 사용처 | 상세 |
|---|---|---|---|
| Buddy Allocator | 2^n 페이지 (4KB~4MB) | 물리 페이지 할당 | [01](01-page_alloc.html) |
| SLUB | 고정 크기 객체 | kmalloc(), 커널 오브젝트 | [02](02-slab.html) |
| vmalloc | 가상 연속 메모리 | 큰 커널 데이터 구조 | [06](06-vmalloc.html) |
| Memblock | 물리 메모리 블록 | 부팅 초기 | [04](04-memblock.html) |
| CMA | 연속 물리 메모리 | DMA 버퍼 | [18](18-cma.html) |
메모리 부족 (워터마크 이하)
│
├─ [비동기] kswapd 스레드
│ └── LRU에서 오래된 페이지 스캔 → 회수
│
├─ [동기] 직접 회수 (direct reclaim)
│ └── try_to_free_pages() → shrink_node()
│
└─ [최종] OOM Killer
└── oom_badness() → 가장 높은 점수 프로세스 kill
회수 메커니즘:
| 메커니즘 | 설명 | 상세 |
|---|---|---|
| kswapd | 백그라운드 페이지 회수 스레드 | [05](05-page_reclaim.html) |
| direct reclaim | 할당 시 동기적 회수 | [05](05-page_reclaim.html) |
| LRU / MGLRU | Least Recently Used 기반 회수 | [05](05-page_reclaim.html) |
| Swap / zswap | 익명 페이지를 디스크/압축으로 밀어냄 | [07](07-swap_zswap.html) |
| OOM Killer | 최후의 수단 | [08](08-oom.html) |
| Compaction | 외부 단편화 해소 | [11](11-compaction.html) |
회수 비율은 vm.swappiness로도 영향을 받습니다. 값이 높을수록 익명 페이지를 스왑으로 보내는 쪽을 더 적극적으로 고려하고, 값이 낮을수록 파일 캐시를 우선 남기려는 성향이 강해집니다.
| 서브시스템 | 파일 | 역할 | 다음 문서 |
|---|---|---|---|
| Buddy Allocator | page_alloc.c | 2^n 페이지 할당/해제 | [01](01-page_alloc.html) |
| SLUB | slub.c | 커널 오브젝트 캐싱 | [02](02-slab.html) |
| VMA / mmap | mmap.c, memory.c | 가상 메모리 영역 관리 | [03](03-vma_mmap.html) |
| Memblock | memblock.c | 부팅 초기 할당 | [04](04-memblock.html) |
| vmscan | vmscan.c | 페이지 회수 | [05](05-page_reclaim.html) |
| vmpressure | vmpressure.c | 메모리 압력 신호 | [39](39-vmpressure.html) |
| MGLRU | vmscan.c, mm_inline.h | 다세대 LRU 회수 | [49](49-mglru.html) |
| vmalloc | vmalloc.c | 가상 연속 메모리 | [06](06-vmalloc.html) |
| Swap / zswap | swapfile.c, zswap.c | 스와핑 & 압축 캐시 | [07](07-swap_zswap.html) |
| OOM | oom_kill.c | 메모리 부족 시 프로세스 종료 | [08](08-oom.html) |
| Huge Pages | huge_memory.c | THP / hugetlbfs | [10](10-hugepage.html) |
| Compaction | compaction.c | 단편화 해소 | [11](11-compaction.html) |
| Folio | filemap.c | 페이지 캐시 현대화 | [12](12-folio.html) |
| NUMA | numa.c | 멀티소켓 메모리 관리 | [13](13-numa.html) |
| memcg | memcontrol.c | 컨테이너 메모리 제어 | [14](14-memcontrol.html) |
| tmpfs / shmem | shmem.c | 페이지 캐시 기반 공유 메모리 | [16](16-shmem.html) |
| KSM | ksm.c | 동일 페이지 병합 | [19](19-ksm.html) |
| DAMON | damon/ | 접근 패턴 관찰 · 조정 | [21](21-damon.html) |
| Memory Hotplug | memory_hotplug.c | 메모리 온라인 / 오프라인 | [22](22-memory_hotplug.html) |
| Debug tools | kasan/, kfence/, kmemleak.c | 메모리 오류 탐지 | [27](27-debug.html) |
| 방법 | 함수 | 단위 | 컨텍스트 | 가상 주소 |
|---|---|---|---|---|
| Buddy | `__alloc_pages()` | 페이지 | 커널 | 직접 매핑 |
| kmalloc | `kmalloc()` | 고정 크기 | 커널 | 직접 매핑 |
| vmalloc | `vmalloc()` | 임의 크기 | 커널 | vmalloc 영역 |
| mmap | `mmap()` | 임의 크기 | 사용자 | VMA |
| brk | `brk()` | 확장 | 사용자 | heap VMA |
| 타입 | 예시 | 저장 위치 | 회수 방법 |
|---|---|---|---|
| 익명 페이지 | malloc(), 스택 | 물리 메모리 (스왑 가능) | swap out → zswap |
| 파일 기반 페이지 | 파일 read, exec | 물리 메모리 + 파일 | 버림 (다시 읽기 가능) |
| 캐시 페이지 | page cache | 물리 메모리 | writeback 후 회수 |
| 고정 페이지 | mlock() | 물리 메모리 | 회수 불가 |
| 개념 | 의미 | 커널에서 보는 위치 | 확인 포인트 |
|---|---|---|---|
| 가상 주소 | 프로세스가 사용하는 주소 | `mm_struct`, `vm_area_struct`, page table | VMA가 있어도 물리 페이지는 아직 없을 수 있음 |
| 물리 주소 | DRAM page frame의 실제 주소 | PFN, `struct page`, zone | `pfn_to_page()`로 메타데이터 추적 |
| 페이지 테이블 | 가상 페이지 → 물리 프레임 변환표 | PGD→P4D→PUD→PMD→PTE | 접근 권한, dirty/accessed, present bit 확인 |
| 직접 매핑 | RAM을 커널 가상 주소로 선형 매핑 | `page_offset_base` 계열 | `kmalloc()`/`alloc_pages()` 주소와 `vmalloc()` 주소를 혼동하지 않기 |
가상 메모리가 주는 이점도 함께 기억하면 흐름이 훨씬 잘 보입니다.
| 이점 | 설명 |
|---|---|
| 프로세스 격리 | 한 프로세스의 버그가 다른 프로세스 메모리를 직접 침범하지 못함 |
| 주소 공간 독립성 | 모든 프로세스가 같은 가상 주소 범위를 독립적으로 사용할 수 있음 |
| Demand Paging | 실제로 접근한 페이지만 물리 메모리에 올려 RAM 사용을 늦춤 |
| 공유 메모리 | 같은 물리 페이지를 여러 프로세스가 각자 다른 가상 주소로 공유 가능 |
| 메모리 보호 | 페이지별 읽기/쓰기/실행 권한으로 커널/사용자 공간을 분리 |
가상 메모리는 각 프로세스에게 독립된 주소 공간을 제공하므로, 같은 0x400000 주소라도 프로세스마다 다른 물리 페이지를 가리킬 수 있습니다. 실제 물리 페이지는 page fault, COW, 파일 페이지 로드, swap-in 같은 이벤트가 발생할 때 연결됩니다.
가상 메모리가 없으면 같은 주소를 두 프로세스가 동시에 덮어쓰는 충돌이 생기고, 물리 RAM보다 큰 주소 공간을 프로세스마다 독립적으로 주기도 어렵습니다. 커널이 주소를 먼저 예약해 두고, 실제 물리 페이지는 접근 순간에만 붙이는 이유가 여기에 있습니다.
MMU는 페이지 테이블을 따라 가상 주소를 물리 주소로 바꾸고, 자주 쓰는 변환은 TLB에 캐시합니다. 그래서 같은 주소를 반복해서 읽고 쓰면 빠르지만, mprotect(), COW, 페이지 마이그레이션처럼 PTE가 바뀌는 순간에는 TLB 무효화가 따라와야 합니다.
같은 물리 페이지를 여러 프로세스가 공유하는 shared memory와, PTE의 읽기/쓰기/실행 비트로 구현되는 memory protection도 이 층에서 함께 결정됩니다.
| 플래그 | 의미 | 커널 관찰 포인트 |
|---|---|---|
| `MAP_PRIVATE` | 쓰기 시 복사본을 만드는 private 매핑 | COW 분기, private fault |
| `MAP_SHARED` | 여러 프로세스가 같은 매핑을 공유 | writeback, shared fault |
| `MAP_ANONYMOUS` | 파일 없이 anonymous VMA를 생성 | zero page, `alloc_anon_folio()` |
| `MAP_FIXED` | 지정한 주소에 강제로 배치 | 기존 매핑 충돌 여부 확인 |
| `MAP_POPULATE` | 미리 페이지를 채우도록 유도 | 초기 page fault 지연 감소 |
| 유형 | 원인 | 처리 | I/O 여부 | 관찰 방법 |
|---|---|---|---|---|
| minor fault | 물리 페이지는 준비됐지만 PTE가 아직 없음 | PTE만 연결하거나 zero page 사용 | 없음 | `ps -o minflt` |
| major fault | 파일/스왑에서 읽어야 함 | 페이지 할당 후 디스크에서 로드 | 있음 | `ps -o majflt`, `pgmajfault` |
| invalid fault | VMA 없음 또는 권한 위반 | `SIGSEGV` 또는 fault error 반환 | 없음 | dmesg, core dump, `perf trace` |
Demand paging의 핵심은 malloc()이나 mmap()이 곧바로 RAM을 소비하지 않는다는 점입니다. 먼저 VMA만 만들고, 실제 접근 시 handle_mm_fault() 아래에서 anonymous/file/COW/swap 경로로 분기합니다.
fork() 직후에는 부모와 자식이 같은 물리 페이지를 읽기 전용으로 공유합니다. 둘 중 하나가 쓰기를 시도하면 page fault가 발생하고, 커널은 그 페이지만 복제한 뒤 쓰기 가능한 PTE로 바꿉니다. 그래서 fork() 비용은 작게 유지하면서도, 실제 수정이 필요한 페이지만 추가로 소비하게 됩니다.
fork()
├─ 부모/자식이 읽기 전용 공유
├─ 읽기: 같은 물리 페이지를 그대로 사용
└─ 쓰기 fault: 새 페이지 복사 → PTE 갱신
페이지를 실제로 확보할 때는 호출한 컨텍스트가 중요합니다. GFP_KERNEL은 잠들 수 있고 reclaim도 허용하는 일반적인 커널 할당용 플래그이며, GFP_ATOMIC은 잠들 수 없는 상황에서 쓰입니다. GFP_NOFS와 GFP_NOIO는 각각 파일시스템 재진입과 I/O 재진입을 막아, 호출 경로가 다시 같은 계층으로 돌아오지 않게 합니다.
| 플래그 | 잠들기 | reclaim | 주 사용처 |
|---|---|---|---|
| `GFP_KERNEL` | 가능 | 가능 | 일반 커널 할당 |
| `GFP_ATOMIC` | 불가 | 제한적 | IRQ, 원자적 컨텍스트 |
| `GFP_NOWAIT` | 불가 | 최소화 | 즉시 실패를 허용하는 경로 |
| `GFP_NOFS` | 가능 | 가능 | 파일시스템 코드 |
| `GFP_NOIO` | 가능 | 가능 | 블록/I/O 경로 |
__GFP_ZERO처럼 결과 메모리를 0으로 초기화하는 플래그도 함께 쓰이며, 실제 소스에서는 컨텍스트 제약과 조합되어 할당 경로를 결정합니다.
| 구분 | 원인 | 증상 | 커널 대응 |
|---|---|---|---|
| 외부 단편화 | free page가 작은 조각으로 흩어짐 | 총 free는 충분하지만 high-order 할당 실패 | Buddy coalescing, compaction, `ZONE_MOVABLE` |
| 내부 단편화 | 요청보다 큰 할당 단위를 사용 | 작은 객체가 큰 블록을 점유 | SLUB kmalloc size class, slab 캐시 재사용 |
| 회수 불가 페이지 | `mlock`, pinned page, device page | reclaim 효율 저하 | unevictable LRU, migration/compaction 제한 |
/proc/buddyinfo에서 높은 order의 free block이 0에 가까우면 큰 연속 물리 메모리 확보가 어렵습니다. /proc/pagetypeinfo를 같이 보면 order와 migratetype별 분포를 더 세밀하게 볼 수 있습니다. 반대로 /proc/slabinfo에서 특정 캐시의 object 수가 급증하면 내부 단편화나 slab shrinker 대상 여부를 함께 확인해야 합니다.
| 영역 | 대표 의미 | 개발/디버깅 주의점 |
|---|---|---|
| 사용자 공간 | 프로세스별 VMA, heap, stack, mmap | 커널이 직접 역참조하지 말고 uaccess API 사용 |
| 직접 매핑 영역 | 물리 RAM 대부분을 커널 VA로 선형 매핑 | `__pa()`/`__va()`는 직접 매핑 RAM 주소에만 안전 |
| `vmalloc`/`ioremap` | 가상으로는 연속, 물리로는 불연속 가능 | DMA 버퍼로 직접 가정하면 안 됨 |
| `vmemmap` | PFN별 `struct page` 배열 | RAM 내용이 아니라 page metadata 영역 |
| KASAN shadow | 메모리 접근 검사용 shadow | 디버그 빌드에서 주소 공간 사용량 증가 |
| kernel text/modules/fixmap | 커널 이미지, 모듈, 고정 매핑 | KASLR, ftrace, kprobe, EFI/fixmap과 연결 |
조건별로 x86_64 5-level paging, ARM64 VA_BITS, RISC-V Sv39/Sv48/Sv57, PTI, KASAN, memory hotplug, CXL/PMEM 구성은 메모리맵의 크기와 배치를 바꿉니다. 따라서 절대 주소 범위를 외우기보다 /proc/iomem, /proc/vmallocinfo, /proc/meminfo, kernel_page_tables 출력으로 현재 부팅 인스턴스의 실제 배치를 확인하는 것이 안전합니다.
| 환경 | 입력 메모리맵 | 내부 변화 | 먼저 볼 지표 | ||
|---|---|---|---|---|---|
| 레거시 32비트 x86 | BIOS E820, lowmem/highmem 분리 | lowmem은 상시 직접 매핑, highmem은 `kmap_local_page()` 같은 임시 매핑이 필요 | `highmem`, `lowmem`, `kmap_local_page()` | ||
| 베어메탈 x86_64 | BIOS E820, UEFI, ACPI SRAT/HMAT | memblock → Node/Zone → Buddy | `/proc/iomem`, dmesg `BIOS-e820 | efi` | |
| 임베디드 ARM64 | Device Tree `/memory`, `/reserved-memory`, `/chosen` | reserved memory, CMA, ioremap 경로와 DMA API 구분이 중요 | dmesg `OF: fdt | reserved-memory | cma` |
| NUMA 서버 | ACPI SRAT/SLIT/HMAT | node별 `pg_data_t`, zonelist, kswapd | `numactl -H`, `/proc/zoneinfo` | ||
| 가상머신 | guest E820/EFI/DT, EPT/NPT, balloon/hotplug | guest physical이 host physical과 다르고 2단계 번역이 추가됨 | `dmesg`, virtio/balloon 로그 | ||
| 컨테이너 | 호스트 커널 메모리맵 공유 | memcg charge/reclaim/OOM으로 격리 | cgroup v2 `memory.current`, `memory.events` | ||
| CXL/PMEM/DAX | hotplug/DAX region | `ZONE_DEVICE`, `ZONE_MOVABLE`, memmap-on-memory | `/sys/devices/system/memory`, `/proc/iomem` |
물리 메모리의 성능은 용량만이 아니라 DDR 채널, 랭크, 리프레시 비용, HBM 같은 대역폭 특성에도 좌우됩니다. NUMA 서버나 CXL/PMEM 시스템에서는 같은 용량이라도 접근 지연이 다르므로 numastat, /proc/pressure/memory, dmidecode -t memory를 함께 보는 편이 안전합니다.
| 세대 | 핵심 차이 | 커널 관찰 포인트 |
|---|---|---|
| DDR4 | 64-bit 채널, 성숙한 서버 표준 | NUMA 원격 비율, reclaim 지연 |
| DDR5 | 32-bit + 32-bit 서브채널, PMIC, On-Die ECC | 대역폭 증가와 지연 꼬리 확인 |
| DDR6 | 차세대 고대역폭 지향 | 펌웨어/보드 지원 성숙도 |
| HBM | TSV 기반 초고대역폭, 패키지 근접 배치 | 대역폭 병목과 locality |
DDR 계열은 단순한 용량 표기가 아니라 채널 폭, 랭크, 뱅크, 행(row), 열(column) 배치가 지연과 처리량을 함께 결정합니다. 커널 입장에서는 이 차이가 NUMA 원격 접근, reclaim tail latency, 대형 페이지 사용 효율로 드러납니다. HBM은 TSV로 적층한 DRAM 다이를 인터포저 위에서 GPU/가속기와 붙이는 구조라서, 일반 DIMM보다 훨씬 넓은 인터페이스를 제공하지만 시스템 RAM과는 관찰 포인트가 다릅니다.
| 세대 | 핵심 구조 | 커널 관찰 포인트 |
|---|---|---|
| DDR4 | 64-bit 채널, 성숙한 서버 표준 | NUMA 원격 비율, reclaim 지연 |
| DDR5 | 32-bit + 32-bit 서브채널, PMIC, On-Die ECC | 대역폭 증가, 리프레시/지연 꼬리 |
| DDR6 | 차세대 고대역폭 설계 | 펌웨어/보드 성숙도, RAS 검증 |
| HBM | TSV, base die, interposer, 1024-bit 인터페이스 | GPU/가속기 locality, 대역폭 병목 |
HBM은 일반 DIMM 슬롯에 꽂는 메모리와 달리 장치 근접 메모리로 보는 편이 맞습니다. 그래서 실제 시스템에서는 dmidecode -t memory, numastat, dmesg, /proc/pressure/memory를 함께 보고, HBM/DDR의 배치 차이를 워크로드별 지표로 확인하는 것이 안전합니다.
| 함수 | 위치 | 역할 |
|---|---|---|
| `do_mmap()` | `mm/mmap.c:335` | 가상 주소 범위를 예약하고 VMA 생성 전 검증 수행 |
| `handle_mm_fault()` | `mm/memory.c:6589` | page fault 상위 진입점, hugetlb와 일반 fault 분기 |
| `do_anonymous_page()` | `mm/memory.c:5217` | anonymous VMA의 최초 접근에서 zero page 또는 새 folio 연결 |
| `do_fault()` | `mm/memory.c:5903` | file-backed fault에서 read/COW/shared fault 분기 |
| `do_wp_page()` | `mm/memory.c:4149` | write-protect fault와 COW 처리 |
| `__alloc_frozen_pages_noprof()` | `mm/page_alloc.c:5214` | zoned Buddy Allocator의 핵심 페이지 할당 경로 |
| `try_to_free_pages()` | `mm/vmscan.c:6566` | direct reclaim 진입점 |
| `out_of_memory()` | `mm/oom_kill.c:1119` | reclaim 실패 후 OOM victim 선택/kill 경로 |
handle_mm_fault()는 아키텍처별 fault handler에서 들어오는 공통 mm/ 진입점입니다. 먼저 fault flag와 접근 권한을 정리하고, hugetlb VMA는 hugetlb_fault()로, 일반 VMA는 __handle_mm_fault()로 보냅니다.
/* mm/memory.c:6589-6625 */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma, unsigned long address,
unsigned int flags, struct pt_regs *regs)
{
/* fault handler가 mmap_lock을 내려놓으면 vma가 해제될 수 있다 */
struct mm_struct *mm = vma->vm_mm;
vm_fault_t ret;
bool is_droppable;
__set_current_state(TASK_RUNNING);
ret = sanitize_fault_flags(vma, &flags);
if (ret)
goto out;
if (!arch_vma_access_permitted(vma, flags & FAULT_FLAG_WRITE,
flags & FAULT_FLAG_INSTRUCTION,
flags & FAULT_FLAG_REMOTE)) {
ret = VM_FAULT_SIGSEGV;
goto out;
}
is_droppable = !!(vma->vm_flags & VM_DROPPABLE);
/*
* 사용자 공간에서 발생한 fault에 대해 memcg OOM 처리를 활성화한다.
* 커널 fault는 더 완화된 방식으로 처리한다.
*/
if (flags & FAULT_FLAG_USER)
mem_cgroup_enter_user_fault();
lru_gen_enter_fault(vma);
if (unlikely(is_vm_hugetlb_page(vma)))
ret = hugetlb_fault(vma->vm_mm, vma, address, flags);
else
ret = __handle_mm_fault(vma, address, flags);
읽기 fault이면 zero page를 PTE에 연결할 수 있고, 쓰기 fault이면 alloc_anon_folio()로 private folio를 확보합니다. 따라서 malloc() 직후가 아니라 첫 쓰기 접근 시 물리 메모리가 실제로 증가합니다.
/* mm/memory.c:5237-5270 */
/* 읽기에는 제로 페이지를 사용한다. */
if (!(vmf->flags & FAULT_FLAG_WRITE) &&
!mm_forbids_zeropage(vma->vm_mm)) {
entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
vma->vm_page_prot));
vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (!vmf->pte)
goto unlock;
if (vmf_pte_changed(vmf)) {
update_mmu_tlb(vma, vmf->address, vmf->pte);
goto unlock;
}
ret = check_stable_address_space(vma->vm_mm);
if (ret)
goto unlock;
/* 페이지 폴트를 사용자 공간으로 전달하고, PT 잠금 안에서 확인한다. */
if (userfaultfd_missing(vma)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return handle_userfault(vmf, VM_UFFD_MISSING);
}
goto setpte;
}
/* 자체 익명 페이지를 할당한다. */
ret = vmf_anon_prepare(vmf);
if (ret)
return ret;
/* OOM이면 NULL을 반환하고, fault 재시도가 필요하면 ERR_PTR(-EAGAIN)을 반환한다. */
folio = alloc_anon_folio(vmf);
if (IS_ERR(folio))
return 0;
if (!folio)
goto oom;
파일 기반 VMA에서는 읽기 fault, private write fault(COW), shared write fault가 분리됩니다. 이 구분이 page cache, COW, writeback 경로를 나누는 기준입니다.
/* mm/memory.c:5909-5937 */
/*
* mmap()에서 VMA가 완전히 채워지지 않았거나 VM_DONTEXPAND가 없을 수 있다.
*/
if (!vma->vm_ops->fault) {
vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm, vmf->pmd,
vmf->address, &vmf->ptl);
if (unlikely(!vmf->pte))
ret = VM_FAULT_SIGBUS;
else {
/*
* 이것이 PTE의 일시적 비움이 아님을 확인한다.
* ptl을 잡고 다시 확인한다. PTE의 R/M/W 갱신은
* ptl을 잡은 뒤 PTE를 비워 하드웨어와의 동시 수정
* 가능성을 막고, 이후 갱신하는 순서를 따른다.
*/
if (unlikely(pte_none(ptep_get(vmf->pte))))
ret = VM_FAULT_SIGBUS;
else
ret = VM_FAULT_NOPAGE;
pte_unmap_unlock(vmf->pte, vmf->ptl);
}
} else if (!(vmf->flags & FAULT_FLAG_WRITE))
ret = do_read_fault(vmf);
else if (!(vma->vm_flags & VM_SHARED))
ret = do_cow_fault(vmf);
else
ret = do_shared_fault(vmf);
private mapping에서 쓰기 fault가 발생하면 이미 exclusive anonymous folio이면 PTE 쓰기 권한만 재사용하고, 공유 중이면 wp_page_copy()로 새 folio를 만들고 내용을 복사합니다.
/* mm/memory.c:4212-4241 */
/*
* private mapping: 재사용이 불가능하면 독점적인 anonymous page 복사본을 만든다.
* FOLL_FORCE 처리에서 VM_WRITE를 놓칠 수 있다.
*
* exclusive로 표시된 페이지를 만나면 추가 확인 없이 재사용해야 한다.
*/
if (folio && folio_test_anon(folio) &&
(PageAnonExclusive(vmf->page) || wp_can_reuse_anon_folio(folio, vma))) {
if (!PageAnonExclusive(vmf->page))
SetPageAnonExclusive(vmf->page);
if (unlikely(unshare)) {
pte_unmap_unlock(vmf->pte, vmf->ptl);
return 0;
}
wp_page_reuse(vmf, folio);
return 0;
}
/*
* 이제 복사해야 한다.
*/
if (folio)
folio_get(folio);
pte_unmap_unlock(vmf->pte, vmf->ptl);
#ifdef CONFIG_KSM
if (folio && folio_test_ksm(folio))
count_vm_event(COW_KSM);
#endif
return wp_page_copy(vmf);
mprotect()는 do_mprotect_pkey()에서 VMA 권한을 다시 계산한 뒤 walk_page_range()로 대상 페이지 테이블을 훑고, mprotect_fixup()이 실제 PTE/PMD 갱신과 tlb_gather_mmu() / tlb_finish_mmu() 사이의 TLB shootdown 정리를 맡습니다. munmap()은 do_munmap() → do_vmi_munmap()으로 내려가면서 VMA 자체를 떼어내고, pagewalk 계열은 권한 점검이나 상태 조회에 같은 워크 엔진을 재사용합니다.
walk_page_range() / walk_page_range_vma()는 page table tree를 재귀적으로 순회하는 공통 진입점이고, tlb_gather_mmu()는 teardown 시작, tlb_finish_mmu()는 shootdown 종료를 의미합니다. 그래서 페이지 권한 변경과 주소 공간 해제는 fault 처리와 반대로, "읽는 경로"보다 "없애고 다시 동기화하는 경로"를 먼저 떠올리면 흐름이 더 선명합니다.
Linux 7.0 소스에서는 profiling wrapper 때문에 외부에서 흔히 말하는 __alloc_pages() 계열의 실제 구현 경로가 __alloc_pages_noprof()와 __alloc_frozen_pages_noprof()로 보입니다. 빠른 경로는 get_page_from_freelist(), 실패 시 slowpath는 __alloc_pages_slowpath()로 이동합니다.
/* mm/page_alloc.c:5211-5264 */
/* zoned buddy allocator의 핵심 경로다. */
struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct page *page;
unsigned int alloc_flags = ALLOC_WMARK_LOW;
gfp_t alloc_gfp; /* 실제 할당에 사용된 gfp_t */
struct alloc_context ac = { };
/*
* order 값이 정상이라고 가정하는 곳이 여러 군데 있으므로
* 범위를 벗어나면 일찍 반환한다.
*/
if (WARN_ON_ONCE_GFP(order > MAX_PAGE_ORDER, gfp))
return NULL;
gfp &= gfp_allowed_mask;
/*
* scoped allocation 제약을 적용한다.
* 주로 GFP_NOFS, GFP_NOIO를 해당 컨텍스트의 모든 할당 요청에
* 상속해야 하며, PF_MEMALLOC_PIN은 할당 시 movable zone을
* 쓰지 않도록 보장한다.
*/
gfp = current_gfp_context(gfp);
alloc_gfp = gfp;
if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac,
&alloc_gfp, &alloc_flags))
return NULL;
/*
* 모든 local zone을 살펴보기 전까지는,
* 첫 번째 시도에서 단편화를 키우는 타입으로 되돌아가지 않게 막는다.
*/
alloc_flags |= alloc_flags_nofragment(zonelist_zone(ac.preferred_zoneref), gfp);
/* 첫 번째 할당 시도 */
page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
if (likely(page))
goto out;
alloc_gfp = gfp;
ac.spread_dirty_pages = false;
/*
* fast-path 시도를 최적화하려고
* &cpuset_current_mems_allowed로 바뀌었을 수 있으므로 원래 nodemask를 복원한다.
*/
ac.nodemask = nodemask;
page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
할당자가 watermark를 만족하지 못하면 direct reclaim으로 들어갈 수 있습니다. try_to_free_pages()는 scan_control을 구성한 뒤 do_try_to_free_pages()로 LRU 스캔과 shrinker 경로를 실행합니다.
/* mm/vmscan.c:6566-6602 */
unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
gfp_t gfp_mask, nodemask_t *nodemask)
{
unsigned long nr_reclaimed;
struct scan_control sc = {
.nr_to_reclaim = SWAP_CLUSTER_MAX,
.gfp_mask = current_gfp_context(gfp_mask),
.reclaim_idx = gfp_zone(gfp_mask),
.order = order,
.nodemask = nodemask,
.priority = DEF_PRIORITY,
.may_writepage = 1,
.may_unmap = 1,
.may_swap = 1,
};
/*
* scan_control은 order, priority, reclaim_idx에 s8 필드를 쓴다.
* 최대값을 담기에 충분한지 확인한다.
*/
BUILD_BUG_ON(MAX_PAGE_ORDER >= S8_MAX);
BUILD_BUG_ON(DEF_PRIORITY > S8_MAX);
BUILD_BUG_ON(MAX_NR_ZONES > S8_MAX);
/*
* throttled 상태에서 fatal signal을 받았으면 reclaim에 들어가지 않는다.
* 이때 1을 반환해 page allocator가 여기서 OOM kill로 넘어가지 않게 한다.
*/
if (throttle_direct_reclaim(sc.gfp_mask, zonelist, nodemask))
return 1;
set_task_reclaim_state(current, &sc.reclaim_state);
trace_mm_vmscan_direct_reclaim_begin(order, sc.gfp_mask);
nr_reclaimed = do_try_to_free_pages(zonelist, &sc);
실제 Linux 7.0 소스의 struct page는 page cache/anonymous page, buddy/PCP free page, page pool, tail page, ZONE_DEVICE 용도를 같은 union에 겹쳐 담습니다.
/* include/linux/mm_types.h:79-115 */
struct page {
memdesc_flags_t flags; /* 비동기적으로 갱신될 수 있는 원자 플래그 */
/*
* 이 union에는 5워드(20/40바이트)를 사용할 수 있다.
* 주의: 첫 번째 워드의 bit 0은 PageTail()에 쓰인다.
* 따라서 다른 사용자는 충돌과 잘못된 PageTail() 판정을 피하려고
* 이 비트를 사용하면 안 된다.
*/
union {
struct { /* 페이지 캐시와 익명 페이지 */
/**
* @lru: 페이지 회수 리스트. 예를 들면 active_list는
* lruvec->lru_lock으로 보호된다. 때로는 page owner가
* 일반 리스트로도 사용한다.
*/
union {
struct list_head lru;
/* 또는 빈 페이지 */
struct list_head buddy_list;
struct list_head pcp_list;
struct llist_node pcp_llist;
};
struct address_space *mapping;
union {
pgoff_t __folio_index; /* mapping 안에서의 오프셋 */
unsigned long share; /* fsdax용 공유 개수 */
};
/**
* @private: mapping 전용의 불투명한 데이터.
* PagePrivate이면 보통 buffer_heads에 쓴다.
* swapcache 플래그가 설정되면 swp_entry_t에 쓴다.
* PageBuddy이거나 pcp_llist에 있으면 buddy system의 order를 나타낸다.
*/
unsigned long private;