메모리 관리(Memory Management) 개요 (Memory Management Overview)
Linux 커널의 메모리 관리 서브시스템 전체를 조망합니다. 물리 메모리(Physical Memory) 구조, 할당자, 가상 메모리(Virtual Memory), 메모리 회수(Memory Reclaim), 스왑(Swap), 공유 메모리, 고급 기법까지 개요 수준으로 정리하고 각 전문 페이지(Page)로 안내합니다.
핵심 요약
- 물리 메모리는 Node → Zone → Page(4KB) 계층으로 관리됩니다.
- Buddy Allocator — 2^n 페이지 블록 할당, 외부 단편화(Fragmentation) 최소화.
- Slab/SLUB — 커널 오브젝트 캐싱으로 빈번한 할당 최적화.
- 페이지 테이블 — PGD→PUD→PMD→PTE 다단계 가상→물리 주소(Physical Address) 매핑(Mapping).
- kmalloc/vmalloc — 물리 연속 vs 가상 연속 커널 메모리 할당.
- mmap — 프로세스(Process) 가상 주소 공간(Address Space)에 파일/디바이스 직접 매핑.
- CMA/HugeTLB — 대용량 연속 메모리 예약 및 2MB/1GB 대형 페이지.
- KSM/zswap — 동일 페이지 병합, 압축으로 메모리 절약.
- OOM Killer — 메모리 고갈 시 프로세스 강제 종료로 시스템 보호.
- tmpfs/shmem — RAM 기반 파일시스템(Filesystem), 공유 메모리/임시 파일.
단계별 이해
- 물리 구조 파악 — Node(NUMA) → Zone(DMA, Normal, Movable) → Page Frame(4KB).
cat /proc/buddyinfo로 확인. - Buddy Allocator — 2^n 블록 할당, 해제 시 buddy와 병합(coalescing).
- Slab 캐시 — 커널 오브젝트 풀.
cat /proc/slabinfo로 확인. - 가상 메모리 — 프로세스별 독립 주소 공간, 페이지 테이블+TLB로 변환.
- mmap과 VMA — Demand Paging: 접근 시(page fault) 물리 페이지 할당.
- 메모리 회수 — kswapd, direct reclaim, shrinker로 메모리 압박 대응.
- 대형 페이지 — THP/HugeTLB로 TLB 미스 감소, DB/JVM 성능 향상.
- 모니터링 — /proc/meminfo, /proc/vmstat, /proc/buddyinfo로 상태 파악.
시각적 개요: 메모리 계층 구조
- 가상 메모리 → 페이지 테이블 → 물리 메모리: 각 프로세스는 독립된 가상 주소 공간을 가지며, MMU가 페이지 테이블을 통해 물리 주소로 변환
- Node (NUMA): 멀티 소켓(Socket) 시스템에서 각 CPU 소켓에 가까운 메모리 뱅크. 로컬 액세스가 빠름
- Zone: 하드웨어 제약에 따른 구분 (DMA: 24비트, NORMAL: 일반, MOVABLE: 메모리 압축(Memory Compaction)/핫플러그(Hotplug) 가능)
- Buddy Allocator: 2^n 페이지 블록 단위로 할당. 외부 단편화 최소화
- Slab/SLUB: 커널 오브젝트 캐시. 빈번한 할당/해제 최적화 (내부 단편화 감소)
물리 메모리 구조 (Physical Memory Organization)
Linux 커널은 물리 메모리를 노드(Node), 존(Zone), 페이지 프레임(Page Frame)의 3단계 계층으로 관리합니다. 이 구조는 NUMA(Multi-socket, 각 소켓에 로컬 메모리)와 UMA(단일 메모리 뱅크) 시스템을 모두 지원하도록 설계되었습니다. UMA 시스템에서도 커널 내부적으로는 Node 0 하나만 존재하는 NUMA 구조로 처리됩니다.
노드(Node): NUMA 시스템에서 각 CPU 소켓에 연결된 메모리 뱅크 단위입니다. struct pglist_data(= pg_data_t)로 관리되며, 각 노드는 독립적인 Zone, 워터마크(Watermark), kswapd 인스턴스를 가집니다.
메모리 존 (Memory Zones)
각 NUMA 노드의 물리 메모리는 하드웨어 특성에 따라 존(Zone)으로 분류됩니다:
| Zone | 범위 (x86_64) | 용도 |
|---|---|---|
ZONE_DMA | 0 ~ 16MB | ISA DMA 전용 (레거시 디바이스) |
ZONE_DMA32 | 0 ~ 4GB | 32비트 주소 DMA 가능 디바이스 |
ZONE_NORMAL | 4GB ~ 끝 | 일반 커널 메모리 할당 |
ZONE_MOVABLE | 설정 가능 | 메모리 핫플러그, 마이그레이션 가능 |
32비트 시스템에서는 ZONE_HIGHMEM (896MB 이상)이 존재하지만, 64비트 시스템에서는 모든 물리 메모리가 직접 매핑되므로 ZONE_HIGHMEM이 필요 없습니다.
struct page와 페이지 플래그
커널은 모든 물리 페이지 프레임을 struct page로 추적합니다. 시스템의 물리 메모리 크기에 비례하여 struct page 배열이 생성되므로(4GB RAM → ~100만 개), 구조체(Struct) 크기 최소화가 중요합니다. 이 구조체는 메모리 효율을 위해 union을 적극 활용합니다:
struct page {
unsigned long flags; /* Atomic flags (PG_locked, PG_dirty, ...) */
union {
struct {
union {
struct list_head lru; /* LRU list */
struct {
void *__filler;
unsigned int mlock_count;
};
};
struct address_space *mapping;
pgoff_t index;
unsigned long private;
};
struct { /* slab allocator */
unsigned long _slab_cache;
void *freelist;
};
struct { /* compound page (huge page) */
unsigned long compound_head;
};
};
atomic_t _refcount;
atomic_t _mapcount;
};
주요 페이지 플래그 (flags 필드):
| 플래그 | 의미 |
|---|---|
PG_locked | I/O 진행 중 페이지 잠금(Lock) |
PG_referenced | 최근 접근됨 (LRU 에이징) |
PG_uptodate | 디스크와 동기화 완료 |
PG_dirty | 수정됨, writeback 필요 |
PG_lru | LRU 리스트에 포함 |
PG_active | active LRU 리스트 |
PG_slab | slab allocator가 사용 |
PG_reserved | 커널 예약 (swap 불가) |
PG_compound | compound page의 일부 (huge page) |
folio (커널 5.16+): struct folio는 compound page(huge page)를 효율적으로 다루기 위해 도입된 추상화입니다. head page를 대표하며, tail page에 대한 불필요한 검사를 제거하여 코드 안전성과 성능을 개선합니다.
DDR 하드웨어 기초
커널의 페이지 할당과 NUMA 정책은 결국 DDR DRAM의 물리적 특성 위에서 동작합니다. DDR은 단순히 "RAM 용량"이 아니라 채널 병렬성, 행 활성화(activate)/프리차지(precharge) 비용, 리프레시(refresh) 정지 구간 같은 제약을 가지며, 이 특성이 메모리 지연(Latency) 시간과 처리량(Throughput)을 결정합니다. 커널 개발자가 DDR 하드웨어를 직접 제어하지는 않지만, NUMA 정책, 메모리 대역폭(Bandwidth) 활용, 오류 관측 방식에 직접적인 영향을 미치므로 기초 이해가 필요합니다.
DDR 물리 조직
| 계층 | 의미 | 커널/시스템 영향 |
|---|---|---|
| 채널 (Channel) | 독립 데이터 경로 (DDR5는 DIMM 내부 서브채널 분할) | 채널 수가 늘면 병렬 처리량 증가 |
| 랭크 (Rank) | 동시에 선택되는 DRAM 칩 묶음 | 랭크 인터리빙으로 버블 감소 |
| 뱅크 그룹/뱅크 | 동시 접근 단위를 세분화한 내부 배열 | 은행 충돌(bank conflict) 시 지연 증가 |
| 행/열 (Row/Column) | DRAM 셀의 실제 저장 좌표 | row hit는 빠르고 row miss는 tRP + tRCD + tCL 비용 발생 |
현대 DDR 세대 비교 (DDR4/5/6)
| 세대 | 주요 특징 | 성능/안정성 특성 | 커널 관점 포인트 |
|---|---|---|---|
| DDR4 | 뱅크 그룹 구조 강화, 고클럭/고용량화 | 대역폭 효율 개선, 서버 표준으로 장기 사용 | 원격 NUMA 접근과 reclaim 지연이 tail latency에 반영 |
| DDR5 | DIMM 서브채널(32+32bit), BL16, PMIC, On-Die ECC | 대역폭 크게 증가, 지연 변동 관리 중요 | 처리량 향상, reclaim/원격 접근/리프레시 영향으로 지연 꼬리 가능 |
| DDR6 | 차세대 고대역폭 중심 설계(신호 방식/채널 구조 고도화) | 표준/제품 구현이 초기 단계 | 드라이버/펌웨어(Firmware)/BIOS 성숙도와 RAS 검증이 중요 |
# 세대/속도 확인 (SMBIOS)
sudo dmidecode -t memory | grep -E "Type:|Speed:|Configured Memory Speed:"
# 메모리 병목 징후 확인
numastat -p $(pidof 주요프로세스)
cat /proc/pressure/memory
cat /proc/vmstat | grep -E "allocstall|pgscan|pgsteal"
DDR5 핵심 변화
DDR5는 단순 클럭 상승이 아니라 내부 병렬성과 전력 관리 방식을 함께 바꿨습니다:
| 항목 | DDR4 | DDR5 | 커널/운영 영향 |
|---|---|---|---|
| DIMM 내부 채널 | 64-bit 1채널 | 32-bit + 32-bit 서브채널 | 짧은 버스(Bus)트의 병렬 처리 효율 개선 |
| Burst Length | BL8 | BL16 | 순차 접근 처리량 유리 |
| 전력 관리 | 메인보드 PMIC | DIMM PMIC 내장 | 고부하 시 발열/전력 안정성 중요 |
| 오류 보호 | 모듈 ECC(옵션) | On-Die ECC + 모듈 ECC | On-Die ECC는 칩 내부 신뢰성용, 시스템 ECC 대체 아님 |
HBM (High Bandwidth Memory) 개요
HBM(High Bandwidth Memory)은 기존 DDR 메모리의 대역폭 한계를 극복하기 위해 설계된 고대역폭 메모리 표준입니다. DDR이 마더보드의 DIMM 슬롯에 장착되어 PCB 트레이스(Trace)로 CPU와 연결되는 반면, HBM은 관통 실리콘 비아(TSV, Through-Silicon Via)로 DRAM 다이를 수직 적층하고, 인터포저(Interposer) 위에서 GPU/SoC 다이와 나란히 배치하는 2.5D 패키징 구조를 사용합니다.
HBM의 핵심 장점은 1024비트 와이드 인터페이스입니다. DDR5의 64비트 대비 16배 넓은 데이터 버스를 통해 상대적으로 낮은 클럭에서도 초고대역폭을 달성합니다. 이러한 특성 때문에 HBM은 GPU, AI 가속기, HPC 프로세서 등 메모리 대역폭이 성능 병목인 워크로드에 필수적인 메모리 기술로 자리잡았습니다.
HBM 하드웨어 구조
HBM 스택은 최하단의 베이스 다이(Base Die/Logic Die)와 그 위에 적층된 여러 장의 DRAM 다이로 구성됩니다. 베이스 다이는 메모리 컨트롤러 인터페이스, ECC 로직, 테스트 회로 등을 포함하며, 인터포저를 통해 GPU/SoC와 통신합니다.
- TSV(Through-Silicon Via): 수천 개의 구리 기둥이 DRAM 다이를 수직으로 관통하여 상하 다이를 전기적으로 연결합니다. 이 짧은 수직 경로 덕분에 신호 지연이 최소화되고 전력 소모가 줄어듭니다.
- 마이크로범프(Microbump): 각 다이 사이의 TSV 연결점에 위치하는 미세 솔더(Solder) 접합부로, 다이 간 물리적·전기적 연결을 담당합니다.
- 인터포저(Interposer): 실리콘 또는 유기(Organic) 소재의 중간 기판으로, GPU 다이와 HBM 스택을 수평으로 연결합니다. TSMC CoWoS(Chip-on-Wafer-on-Substrate), 삼성 I-Cube 등의 패키징 기술이 사용됩니다.
- 채널 구조: HBM 스택 하나는 독립된 128비트 채널 8개를 제공하여 총 1024비트 인터페이스를 구성합니다. 각 채널은 독립적으로 동작하므로 높은 병렬성을 제공합니다.
HBM 세대별 비교
| 세대 | JEDEC 표준 | 출시 | DRAM 적층 | 스택당 대역폭 | 스택당 최대 용량 | 주요 적용 제품 |
|---|---|---|---|---|---|---|
| HBM | JESD235 | 2013 | 4-die | 128 GB/s | 1 GB | AMD Fiji (Radeon R9 Fury) |
| HBM2 | JESD235A | 2016 | 4~8-die | 256 GB/s | 8 GB | NVIDIA V100, AMD MI25/MI60 |
| HBM2e | JESD235B | 2020 | 8-die | 460 GB/s | 16 GB | NVIDIA A100 (80GB), AMD MI100/MI250 |
| HBM3 | JESD238 | 2022 | 8~12-die | 665 GB/s | 24 GB | NVIDIA H100, AMD MI300X |
| HBM3e | JESD238A | 2024 | 12-die | 1.17 TB/s | 36 GB | NVIDIA B200, AMD MI325X |
HBM3 이후 세대에서는 ECC(Error-Correcting Code)가 기본 내장되며, 데이터센터 워크로드의 신뢰성 요구를 충족합니다. HBM3e는 스택당 최대 1.17 TB/s의 대역폭을 제공하여, 4-스택 구성(GPU 패키지 기준)에서 약 4.7 TB/s에 달하는 메모리 대역폭을 실현합니다.
DDR5 vs HBM3e 비교
| 항목 | DDR5 | HBM3e | 비고 |
|---|---|---|---|
| 인터페이스 폭 | 64비트 (서브채널 32×2) | 1024비트 (128비트×8채널) | HBM이 16배 넓은 버스 |
| 데이터 레이트 | ~8400 MT/s | ~9.2 Gbps | 클럭은 유사하나 버스 폭 차이 |
| 채널당 대역폭 | ~67 GB/s (듀얼 채널) | ~1.17 TB/s (스택당) | HBM 압도적 |
| 지연(Latency) | ~80–100 ns | ~100–120 ns | HBM이 약간 높을 수 있음 |
| 용량 확장 | DIMM 슬롯 추가 (유연) | 패키지 내 제한 (4~6 스택) | DDR이 확장성 유리 |
| 전력 효율 | ~8 pJ/bit | ~3.9 pJ/bit | HBM이 비트당 2배 효율적 |
| 연결 방식 | PCB 트레이스 (수 cm) | TSV + 인터포저 (수 mm) | 짧은 경로 = 저전력 |
| 주 용도 | 범용 서버, PC, 노트북 | GPU, AI 가속기, HPC | 상호 보완적 |
Linux 커널에서의 HBM 관리
Linux 커널은 HBM을 크게 두 가지 방식으로 인식하고 관리합니다.
1. NUMA 노드로 노출 (CPU 인접 HBM): Intel Sapphire Rapids-HBM처럼 CPU 패키지에 HBM이 통합된 경우, BIOS/UEFI가 ACPI SRAT(System Resource Affinity Table)과 HMAT(Heterogeneous Memory Attribute Table)을 통해 HBM 영역을 별도 NUMA 노드로 보고합니다. HMAT는 각 이니시에이터(CPU)↔타겟(메모리) 쌍의 대역폭과 지연 수치를 정량적으로 제공하여, 커널의 메모리 티어링 결정에 활용됩니다.
# NUMA 토폴로지에서 HBM 노드 확인
numactl --hardware
# node 0: DDR5 (256 GB), node 1: HBM (64 GB) 등으로 표시
# 노드별 메모리 정보
cat /sys/devices/system/node/node*/meminfo
# HMAT 기반 메모리 티어 확인 (커널 6.1+)
ls /sys/devices/virtual/memory_tiering/
# memory_tier0/ memory_tier1/ ...
# HBM 노드에 프로세스 바인딩
numactl --membind=1 ./bandwidth-intensive-app
2. ZONE_DEVICE로 등록 (GPU 부착 HBM): GPU/가속기에 탑재된 HBM(예: NVIDIA H100, AMD MI300X)은 CPU가 직접 접근할 수 없는 디바이스 전용 메모리입니다. 커널은 ZONE_DEVICE + MEMORY_DEVICE_PRIVATE 타입으로 이 영역을 등록하고, HMM(Heterogeneous Memory Management) 프레임워크의 migrate_vma()를 통해 DDR↔HBM 간 페이지를 투명하게 마이그레이션합니다.
3. 메모리 티어링(Memory Tiering): 커널 6.1에서 도입된 memory_tier 프레임워크는 HBM(고대역폭·저용량)을 상위 티어, DDR(범용)을 기본 티어, CXL 메모리(고용량·고지연)를 하위 티어로 계층화합니다. DAMON이 접근 빈도를 모니터링하여 핫 데이터를 HBM으로 프로모션하고, 콜드 데이터를 DDR이나 CXL로 디모션하는 자동 티어링을 수행합니다.
Buddy Allocator 개요
Buddy Allocator는 물리 페이지 할당의 핵심 알고리즘입니다. 메모리를 2^n 크기의 블록(order 0~10, 4KB~4MB)으로 관리하며, 할당 시 요청된 order의 free list에서 블록을 꺼내고, 없으면 상위 order를 분할(split)합니다. 해제 시에는 인접한 buddy 블록과 병합(coalescing)하여 외부 단편화를 최소화합니다.
Buddy 할당의 핵심 흐름: Zone 순회 → Watermark 검사 → 요청 order의 free list 확인 → 없으면 상위 order 분할 → 모든 zone 고갈 시 kswapd 깨움(slowpath). /proc/buddyinfo에서 각 존의 order별 free 블록 수를 확인할 수 있습니다.
| API | 설명 |
|---|---|
alloc_pages(gfp, order) | 2^order 페이지 블록 할당 (struct page* 반환) |
__free_pages(page, order) | 할당된 블록 해제 |
__get_free_pages(gfp, order) | 가상 주소(unsigned long) 반환 버전 |
get_zeroed_page(gfp) | 0으로 초기화된 단일 페이지 |
alloc_page(gfp) | 단일 페이지 할당 단축 매크로(Macro) |
# Buddy 상태 확인
cat /proc/buddyinfo
# Node 0, zone Normal 1024 512 256 128 64 32 16 8 4 2 1
# order0 order1 ... order10
# order N의 free 블록 수 × 2^N × 4KB = 해당 order의 free 메모리
Buddy 알고리즘 핵심 원리: 물리 메모리를 2^n 크기의 블록으로 관리하며, 인접한 두 블록은 항상 "buddy" 관계입니다. buddy의 물리 주소는 XOR 연산으로 O(1)에 계산할 수 있습니다 (buddy_pfn = pfn ^ (1 << order)). 할당 시 정확한 order의 free 블록이 없으면 상위 order를 반복 분할하고, 해제 시 buddy가 free이면 재귀적으로 병합하여 최대 order까지 복원합니다. 이 과정에서 외부 단편화가 자연스럽게 최소화됩니다.
Migrate Type과 단편화 방지: 커널은 페이지를 이동 가능성에 따라 세 가지 타입으로 분류합니다. MIGRATE_UNMOVABLE(커널 slab, 페이지 테이블 등 이동 불가), MIGRATE_MOVABLE(사용자 페이지, compaction으로 이동 가능), MIGRATE_RECLAIMABLE(page cache, 회수 가능). Buddy free list는 migrate type별로 분리되어, 이동 불가 페이지가 이동 가능 영역을 오염시키는 것을 방지합니다. 이 분리는 Memory Compaction과 Memory Hotplug의 전제 조건입니다.
상세 문서: Buddy Allocator의 분할/병합 알고리즘, watermark(min/low/high), fallback 메커니즘, Per-CPU page lists, migrate type(UNMOVABLE/MOVABLE/RECLAIMABLE) 등은 페이지 할당자 (Buddy Allocator) 페이지에서 확인할 수 있습니다.
Slab/SLUB Allocator 개요
Buddy Allocator는 최소 한 페이지(4KB) 단위로 할당하므로, 작은 오브젝트(수 바이트~수 KB)에는 비효율적입니다. Slab Allocator는 동일 크기의 커널 오브젝트를 미리 할당된 슬랩 페이지에 배치하여 내부 단편화를 줄이고, 할당/해제 속도를 극대화합니다. 현대 Linux 커널(6.x)은 기본적으로 SLUB을 사용합니다(기존 SLAB과 SLOB은 커널 6.5에서 제거됨).
SLUB은 3단계 계층으로 할당을 최적화합니다: (1) Per-CPU slab — 현재 CPU 전용 활성 슬랩, lockless 할당. (2) CPU Partial list — CPU별 부분 사용 슬랩, lockless. (3) Node Partial list — NUMA 노드별 공유 슬랩, 락 필요.
| 항목 | SLAB (제거됨) | SLUB (현재 기본) | SLOB (제거됨) |
|---|---|---|---|
| 설계 철학 | 복잡한 per-CPU 큐 | 간결한 per-CPU slab + 인라인 메타데이터 | K&R 스타일 first-fit |
| 캐시 머징 | 미지원 | 동일 크기/정렬 캐시 자동 병합 | 해당 없음 |
| NUMA 지원 | 있음 | 있음 (노드별 partial list) | 없음 |
| 디버깅(Debugging) | 제한적 | 풍부 (red zone, poisoning, tracking) | 없음 |
| 대상 환경 | 범용 | 범용 (모든 환경) | 임베디드 (<64MB) |
kmalloc()은 내부적으로 미리 생성된 SLUB 캐시(kmalloc-8, kmalloc-16, ..., kmalloc-8192)에서 오브젝트를 할당합니다. 요청 크기는 가장 가까운 크기 클래스로 반올림됩니다.
# Slab 캐시 현황 확인
cat /proc/slabinfo | head -10
# name <active_objs> <num_objs> <objsize> ...
# 실시간 모니터링
slabtop -s c # 캐시 크기 순 정렬
# SLUB 디버깅 (부트 옵션)
# slub_debug=FZPU — F:sanity, Z:red zone, P:poisoning, U:tracking
# slub_debug=FZPU,kmalloc-256 — 특정 캐시만 디버깅
SLUB 할당 성능 계층: 1단계(Per-CPU active slab)에서 대부분의 할당이 cmpxchg_double 단일 명령어로 완료됩니다. 이는 spinlock 없이 동작하므로 멀티코어 환경에서 경합이 발생하지 않습니다. active slab이 가득 차면 2단계(CPU Partial list)에서 부분 사용 슬랩을 가져와 active로 전환합니다. 이것도 lockless입니다. CPU Partial마저 소진되면 3단계(Node Partial list)에서 list_lock을 잡고 슬랩을 가져오는데, 이때만 CPU 간 경합이 발생합니다. 최종적으로 Node Partial에도 빈 슬랩이 없으면 Buddy Allocator에서 새 페이지를 할당받아 슬랩을 생성합니다.
kmalloc 크기 클래스: kmalloc()은 요청 크기를 가장 가까운 2^n 또는 192/96 바이트 크기 클래스로 반올림합니다. 예를 들어 100바이트 요청은 kmalloc-128 캐시에서 처리됩니다. 내부 단편화(최대 50%)가 발생할 수 있으므로, 빈번히 할당되는 고정 크기 오브젝트는 kmem_cache_create()로 전용 캐시를 만드는 것이 효율적입니다. 캐시 머징: SLUB은 동일한 크기·정렬·플래그를 가진 캐시를 자동으로 병합하여 슬랩 수를 줄입니다 (/sys/kernel/slab/에서 확인).
상세 문서: SLUB 3단계 할당 구조(Per-CPU → CPU Partial → Node Partial), kmem_cache 생성/소멸, kmalloc 크기 클래스 상세, SLUB 디버깅 옵션 등은 Slab Allocator (SLUB) 페이지에서 확인할 수 있습니다.
vmalloc 개요
vmalloc()은 가상적으로 연속된 메모리를 할당하지만, 물리적으로는 비연속일 수 있습니다. 개별 페이지를 할당한 후 페이지 테이블을 조작하여 가상 주소 공간에서 연속으로 매핑합니다. DMA에는 사용할 수 없으며, 페이지 테이블 설정 비용으로 kmalloc보다 느립니다. kvmalloc()은 kmalloc을 먼저 시도하고 실패 시 vmalloc으로 fallback하므로, 할당 크기가 불확실한 경우에 유용합니다.
| API | 물리 연속 | 최대 크기 | 용도 |
|---|---|---|---|
kmalloc() | 연속 | ~4MB | 일반 커널 오브젝트, DMA 버퍼(Buffer) |
vmalloc() | 비연속 가능 | 가상 주소 공간 한도 | 큰 버퍼, 모듈 로딩 |
kvmalloc() | 시도 후 fallback | 무제한 | 크기 불확실 시 자동 선택 |
상세 문서: vmalloc 내부 구현, vmap area 관리, ioremap과의 관계 등은 vmalloc 페이지에서 확인할 수 있습니다.
GFP 플래그 (할당 컨텍스트 제약)
메모리 할당 함수에 전달하는 GFP 플래그는 할당 동작을 결정합니다. 호출 컨텍스트에 따라 사용 가능한 플래그가 엄격히 제한됩니다:
| 컨텍스트 | 허용 GFP | 슬립(Sleep) 가능 | 설명 |
|---|---|---|---|
| 프로세스 컨텍스트 | GFP_KERNEL | 예 | 가장 일반적. 직접 회수(Direct Reclaim), I/O, 스왑 모두 가능 |
| 인터럽트(Interrupt) 컨텍스트 (hardirq) | GFP_ATOMIC | 아니오 | 비상 예비 풀 사용. 실패 가능성 높음 |
| 소프트IRQ / 타이머(Timer) | GFP_ATOMIC | 아니오 | hardirq와 동일한 제약 |
| 스핀락(Spinlock) 보유 중 | GFP_ATOMIC | 아니오 | 슬립 시 데드락 발생 |
| 프로세스 (I/O 불가) | GFP_NOIO | 예 | 블록 I/O 재귀 방지 (블록 드라이버 내부) |
| 프로세스 (FS 불가) | GFP_NOFS | 예 | 파일시스템 재귀 방지 (VFS 코드 내부) |
| 사용자 공간(User Space) 대신 | GFP_USER | 예 | 사용자 프로세스 대신 할당, OOM killer 대상 |
| DMA 영역 필요 | GFP_DMA / GFP_DMA32 | 예 | ISA DMA(16MB 이하) 또는 32비트 DMA 장치 |
/* 올바른 컨텍스트별 할당 패턴 */
/* 인터럽트 핸들러에서 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *data;
data = kmalloc(sizeof(*data), GFP_ATOMIC);
if (!data)
return IRQ_HANDLED; /* GFP_ATOMIC 실패는 흔함 — 반드시 처리 */
/* ... */
}
/* 블록 I/O 경로에서 */
static int my_block_submit(struct bio *bio)
{
/* GFP_NOIO: submit_bio → 할당 → submit_bio 무한 재귀 방지 */
buf = kmalloc(BUF_SIZE, GFP_NOIO);
/* ... */
}
치명적 실수: 인터럽트 컨텍스트에서 GFP_KERNEL 사용 시 슬립이 발생하여 BUG: scheduling while atomic 패닉이 발생합니다. CONFIG_DEBUG_ATOMIC_SLEEP=y로 탐지할 수 있습니다.
MMU & TLB 개요
x86_64에서 Linux는 4~5단계 페이지 테이블(PGD→P4D→PUD→PMD→PTE) 구조로 가상 주소를 물리 주소로 변환합니다. 48비트 가상 주소의 각 9비트 필드가 페이지 테이블의 한 레벨 인덱스로 사용되며, 최종 12비트는 4KB 페이지 내 오프셋(Offset)입니다.
TLB(Translation Lookaside Buffer)는 이 변환 결과를 하드웨어 수준에서 캐싱하여 성능을 보장합니다. TLB 미스가 발생하면 CPU가 페이지 테이블 워크를 수행하므로(메모리 접근 4~5회), TLB 효율은 시스템 성능에 직접적인 영향을 미칩니다.
| PTE 플래그 | 비트 | 설명 |
|---|---|---|
_PAGE_PRESENT | 0 | 페이지 존재 여부 |
_PAGE_RW | 1 | 읽기/쓰기 권한 (0=읽기전용) |
_PAGE_USER | 2 | 유저 모드 접근 가능 |
_PAGE_ACCESSED | 5 | 최근 접근됨 (LRU 에이징에 사용) |
_PAGE_DIRTY | 6 | 수정됨 (writeback 필요) |
_PAGE_NX | 63 | No-Execute (실행 방지) |
TLB 성능과 PCID: TLB는 최근 가상→물리 주소 변환 결과를 캐싱하는 CPU 내부 하드웨어입니다. 일반적인 x86_64 CPU의 L1 dTLB는 4KB 페이지용 64개, 2MB 페이지용 32개 엔트리를 보유합니다. 컨텍스트 스위치 시 CR3가 변경되면 TLB를 무효화(flush)해야 하는데, PCID(Process-Context Identifiers)를 사용하면 프로세스별로 TLB 엔트리에 태그를 달아 전체 flush 없이 전환할 수 있습니다. KPTI 환경에서 PCID는 커널/유저 페이지 테이블 전환 시 성능 저하를 크게 완화합니다.
TLB Shootdown: 멀티코어 시스템에서 한 CPU가 페이지 테이블을 수정하면, 다른 CPU의 TLB에 캐싱된 오래된 매핑도 무효화해야 합니다. 이를 위해 커널은 IPI(Inter-Processor Interrupt)를 보내 원격 TLB flush를 수행하는데, 이 과정을 TLB Shootdown이라 합니다. munmap(), mprotect(), COW 해제 시 발생하며, CPU 수가 많을수록 오버헤드가 증가합니다.
상세 문서: 전체 PTE 플래그, TLB flush 전략, PCID(Process-Context Identifiers), CR3 전환, KPTI, ARM64/RISC-V 페이지 테이블 등은 MMU & TLB 페이지에서 확인할 수 있습니다.
VMA / mmap 개요
mmap()은 파일이나 디바이스를 프로세스의 가상 주소 공간에 직접 매핑하는 시스템 콜(System Call)입니다. 커널은 각 매핑을 vm_area_struct(VMA)로 관리합니다. VMA는 가상 주소 범위, 접근 권한(rwx), 매핑된 파일/offset, 페이지 폴트(Page Fault) 핸들러(Handler) 등을 포함합니다. 커널 6.1부터 maple tree로 VMA를 탐색하며(기존 rbtree 대체), O(log n) 검색 성능을 제공합니다. 실제 물리 페이지는 접근 시(page fault) 할당되는 Demand Paging 방식으로, mmap() 호출 자체는 가상 주소만 예약하고 물리 메모리를 소비하지 않습니다.
mmap 주요 플래그
| 플래그 | 설명 | 용도 |
|---|---|---|
MAP_SHARED | 쓰기가 파일/다른 프로세스에 반영 | IPC, 파일 매핑 |
MAP_PRIVATE | COW: 쓰기 시 사본 생성 | 실행 파일, 라이브러리 |
MAP_ANONYMOUS | 파일 없는 익명 매핑 | heap, 스택 |
MAP_HUGETLB | Huge Page 사용 | 대용량 메모리 앱 |
MAP_FIXED | 지정 주소에 정확히 매핑 | 로더(Loader), JIT |
# 프로세스의 VMA 매핑 확인
cat /proc/self/maps
# 주소범위 권한 offset dev inode 경로
# 5555555544000-5555555546000 r--p 00000000 fd:01 123456 /usr/bin/bash
COW (Copy-on-Write) 메커니즘: fork() 시 부모와 자식 프로세스는 동일한 물리 페이지를 공유하되, PTE를 읽기 전용으로 설정합니다. 어느 한쪽이 쓰기를 시도하면 write protection fault가 발생하고, do_wp_page()가 해당 페이지의 복사본을 생성하여 독립적인 쓰기를 허용합니다. 이 방식으로 fork()의 메모리 비용은 페이지 테이블 복사 비용만큼으로 최소화됩니다. MAP_PRIVATE 파일 매핑에서도 동일한 COW 메커니즘이 적용됩니다.
상세 문서: mmap 시스템 콜 내부 흐름, VMA 구조체 필드, 페이지 폴트 핸들러(minor/major), MAP_SHARED/PRIVATE COW 메커니즘, mremap/madvise, userfaultfd 등은 VMA / mmap 페이지에서 확인할 수 있습니다.
Huge Pages 개요
기본 4KB 대신 2MB(x86 PMD) 또는 1GB(x86 PUD) 크기의 페이지를 사용하여 TLB 미스를 획기적으로 줄입니다. 일반적인 x86_64 CPU의 TLB는 4KB 엔트리를 수백~수천 개만 캐시할 수 있어, 대용량 메모리를 접근하면 빈번한 TLB 미스가 발생합니다. 2MB 페이지를 사용하면 동일한 TLB 엔트리 수로 512배 더 넓은 주소 공간을 커버할 수 있습니다.
커널 내부에서 Huge Page는 compound page(커널 5.16+ folio)로 관리됩니다. 연속된 물리 페이지들을 하나의 논리 단위로 묶어, 첫 번째 페이지(head page)가 전체를 대표합니다. 2MB huge page는 512개의 연속 struct page(order-9)로 구성됩니다.
| 페이지 크기 | TLB 커버리지 (1024 엔트리 기준) | 매핑 방식 | 주요 용도 |
|---|---|---|---|
| 4KB (일반) | 4MB | PGD→PUD→PMD→PTE→물리 페이지 | 일반 애플리케이션 |
| 2MB (HugeTLB) | 2GB | PMD에서 직접 매핑 (PSE 비트) | DB, DPDK, KVM, JVM |
| 1GB (HugeTLB) | 1TB | PUD에서 직접 매핑 | 대규모 인메모리 DB, HPC |
# Static HugeTLB 설정
echo 512 > /proc/sys/vm/nr_hugepages # 2MB 페이지 512개 (1GB)
cat /proc/meminfo | grep -i huge
# THP 설정
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 프로세스에서 THP 요청
# madvise(addr, length, MADV_HUGEPAGE);
상세 문서: Static HugeTLB, THP(Transparent Huge Pages), mTHP(Multi-size THP, 커널 6.8+), hugetlbfs 프로그래밍, defrag 모드, compound page/folio 구조 등은 Huge Pages 페이지에서 확인할 수 있습니다.
mprotect 개요
mprotect()는 이미 매핑된 가상 메모리 영역의 보호 속성(읽기/쓰기/실행)을 변경하는 시스템 콜입니다. 내부적으로 대상 VMA를 필요에 따라 분할(split)하고, 해당 범위의 PTE 플래그를 업데이트합니다. W^X(Write XOR Execute) 정책, JIT 컴파일러의 코드 생성, 가드 페이지 설정(PROT_NONE), 메모리 보호 키(pkey) 기반 접근 제어(Access Control) 등에 핵심적으로 사용됩니다.
/* JIT 컴파일러의 전형적인 mprotect 사용 패턴 */
void *code = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* 1. RW로 코드 작성 */
memcpy(code, generated_code, code_size);
/* 2. RX로 전환 (W^X 정책) */
mprotect(code, size, PROT_READ | PROT_EXEC);
/* 3. 실행 */
((void (*)())code)();
상세 문서: mprotect 내부 구현, VMA 분할, pkey(Memory Protection Keys), SELinux/AppArmor 연동 등은 mprotect 페이지에서 확인할 수 있습니다.
NUMA 개요
NUMA(Non-Uniform Memory Access) 시스템에서는 CPU마다 로컬 메모리 노드가 있어 접근 지연 시간이 다릅니다. 로컬 노드 접근은 약 100ns인 반면, 원격 노드는 인터커넥트를 경유하여 약 200ns 이상이 소요됩니다. 커널은 pg_data_t로 각 NUMA 노드를 관리하며, NUMA-aware 할당 API를 제공합니다.
/* NUMA-aware allocation */
void *kmalloc_node(size_t size, gfp_t gfp, int node);
struct page *alloc_pages_node(int nid, gfp_t gfp, unsigned int order);
/* Memory policy (유저 공간) */
set_mempolicy(MPOL_BIND, &nodemask, maxnode); /* 특정 노드에만 할당 */
set_mempolicy(MPOL_INTERLEAVE, &nodemask, maxnode); /* 노드 간 교차 할당 */
# NUMA 토폴로지 확인
numactl --hardware
# available: 2 nodes (0-1)
# node 0: cpus 0 1 2 3, size 16384 MB
# node 1: cpus 4 5 6 7, size 16384 MB
# node distances:
# node 0 1
# 0: 10 21
# 1: 21 10
# 프로세스별 NUMA 통계
numastat -p $(pidof my_app)
AutoNUMA (NUMA Balancing): 커널 3.13부터 도입된 자동 NUMA 밸런싱은 주기적으로 PTE를 PROT_NONE으로 설정하여 의도적인 NUMA 힌트 폴트를 발생시킵니다. 이를 통해 어떤 CPU가 어떤 페이지에 접근하는지 추적하고, 원격 노드의 페이지를 로컬 노드로 자동 마이그레이션합니다. 또한 태스크 자체를 데이터가 있는 노드로 이동시키기도 합니다 (task_numa_placement()). /proc/sys/kernel/numa_balancing으로 활성화/비활성화할 수 있으며, DB처럼 명시적 NUMA binding을 사용하는 경우 비활성화가 권장됩니다.
상세 문서: NUMA distance matrix, memory policy(BIND/INTERLEAVE/PREFERRED), AutoNUMA(NUMA balancing), numactl 사용법, NUMA-aware 자료구조 설계 등은 NUMA 페이지에서 확인할 수 있습니다.
CMA 개요
CMA(Contiguous Memory Allocator)는 DMA 디바이스를 위한 대용량 연속 물리 메모리 할당을 지원합니다. CMA 영역은 평소에 movable 페이지로 사용되다가, DMA 할당 요청 시 마이그레이션을 통해 연속 공간을 확보합니다. 커널 부트 파라미터(cma=256M) 또는 디바이스 트리(Device Tree)로 영역을 예약합니다.
/* CMA 할당/해제 API */
struct page *pages = cma_alloc(cma, count, align, gfp_mask);
bool ok = cma_release(cma, pages, count);
/* DMA API를 통한 사용 (일반적인 방법) */
void *vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, vaddr, dma_handle);
# 커널 부트 파라미터로 CMA 영역 설정
cma=256M # 기본 CMA 영역 256MB
cma=256M@0-4G # 0~4GB 범위에 256MB CMA
# CMA 상태 확인
cat /proc/meminfo | grep Cma
# CmaTotal: 262144 kB
# CmaFree: 245760 kB
상세 문서: CMA 예약 방식, dma_alloc_coherent() 경로, 디바이스 트리 설정, 디버깅 등은 CMA 페이지에서 확인할 수 있습니다.
메모리 회수 (Page Reclaim)
메모리 압박 시 커널은 다양한 메커니즘으로 페이지를 회수합니다. LRU(Least Recently Used) 리스트로 페이지의 활동 빈도를 추적하고, 비활성 페이지부터 회수합니다. 회수 대상은 크게 두 가지입니다: 파일 기반 페이지(page cache — 원본 파일에서 다시 읽을 수 있음)와 익명 페이지(Anonymous Page)(heap, stack — 스왑 영역(Swap Area)으로 내보내야 함).
회수 경로
- kswapd — 백그라운드 데몬. 각 NUMA 노드마다 하나씩, free 페이지가 low watermark 아래로 내려가면 활성화되어 high watermark까지 회수. 프로세스를 차단하지 않음
- Direct Reclaim — 할당 요청 시 free 페이지가 min watermark 아래이면, 할당 경로에서 직접 회수 수행. 프로세스가 차단되어 지연(latency) 스파이크 발생
- Shrinker — 커널 캐시(dentry, inode, 파일시스템 메타데이터)를 회수하는 콜백(Callback) 프레임워크.
register_shrinker()로 등록 - MGLRU (Multi-Gen LRU) — 커널 6.1+, 접근 빈도를 세대(generation)로 분류하여 회수 정확도 개선. 기존 active/inactive 2단계보다 정밀한 에이징
Watermark 체계
각 Zone은 세 단계의 watermark를 유지합니다:
| Watermark | 트리거 | 동작 |
|---|---|---|
| high | free > high | 회수 중단. kswapd 수면 |
| low | free < low | kswapd 깨움 (백그라운드 회수 시작) |
| min | free < min | Direct Reclaim 발동 (할당 경로에서 직접 회수) |
# 회수 압박 모니터링
cat /proc/vmstat | grep -E "pgscan|pgsteal|allocstall|kswapd"
# pgscan_kswapd: kswapd가 스캔한 페이지
# pgsteal_kswapd: kswapd가 회수한 페이지
# allocstall_normal: ZONE_NORMAL direct reclaim 횟수
# pgsteal_direct: direct reclaim이 회수한 페이지
# PSI (Pressure Stall Information) — 커널 4.20+
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
# some: 일부 태스크가 메모리 때문에 대기 (비율, 마이크로초)
# full: 모든 태스크가 메모리 때문에 대기 (심각한 상태)
# Watermark 확인
cat /proc/zoneinfo | grep -A 5 "Node 0, zone Normal"
# pages free 12345
# min 1234
# low 1543
# high 1852
OOM Killer 개요
OOM Killer는 시스템의 모든 메모리가 고갈되었을 때 프로세스를 강제 종료하여 시스템을 보호하는 최후의 수단입니다. Direct Reclaim, compaction, swap 등 모든 메모리 확보 수단이 실패했을 때 비로소 발동됩니다. oom_badness() 함수가 RSS(Resident Set Size) 기반으로 점수를 계산하고, 가장 높은 점수의 프로세스에 SIGKILL을 보냅니다. oom_reaper 커널 스레드(Kernel Thread)가 종료된 프로세스의 메모리를 비동기로 빠르게 회수합니다.
OOM 점수 계산
OOM 점수는 다음 공식으로 계산됩니다:
points = RSS + swap 사용량 + 페이지 테이블 크기
points = points × 1000 / totalpages /* 전체 메모리 대비 비율 */
points += oom_score_adj /* 사용자 조정값 (-1000 ~ 1000) */
- oom_score_adj —
/proc/PID/oom_score_adj(-1000~1000)로 프로세스별 OOM 우선순위(Priority) 조정. -1000은 OOM 면제 - memory.oom.group — cgroup v2에서 그룹 단위 OOM 처리 (그룹 내 모든 프로세스를 한번에 종료)
- oom_reaper — OOM으로 종료된 프로세스의 메모리를 비동기로 빠르게 회수하는 커널 스레드
- PSI 모니터링 —
/proc/pressure/memory로 메모리 압박을 사전 감지하여 OOM 전에 대응
# 중요 서비스 OOM 보호
echo -1000 > /proc/$(pidof sshd)/oom_score_adj
# OOM 이벤트 확인
dmesg | grep -i "oom\|out of memory"
# 현재 OOM 점수 확인
cat /proc/$(pidof my_app)/oom_score
상세 문서: OOM 스코어링 알고리즘, cgroup OOM, oom_reaper, 진단/모니터링 등은 OOM Killer 페이지에서 확인할 수 있습니다.
Memory Compaction 개요
Memory Compaction은 메모리 단편화를 해소하여 고차(high-order) 페이지 할당(예: THP의 2MB, CMA의 대용량 연속 할당)을 가능하게 합니다. 시간이 지남에 따라 free 페이지가 불규칙하게 흩어지면(외부 단편화), 작은 free 블록은 많아도 큰 연속 블록은 확보할 수 없습니다. 두 개의 스캐너가 양쪽에서 접근하며, movable 페이지를 이동시켜 연속된 free 블록을 만듭니다.
- Migration Scanner — zone 시작부터 위로 이동하며 movable 페이지를 찾음
- Free Scanner — zone 끝에서 아래로 이동하며 free 페이지를 찾음
- Proactive Compaction (커널 5.9+) — 백그라운드에서 자동으로 단편화 해소.
/proc/sys/vm/compaction_proactiveness(0~100)로 제어
# compaction 통계 확인
cat /proc/vmstat | grep compact
# compact_stall: 직접 compaction 횟수
# compact_success: compaction 성공 횟수
# compact_fail: compaction 실패 횟수
# 수동 compaction 트리거
echo 1 > /proc/sys/vm/compact_memory
상세 문서: Two-Scanner 메커니즘, compaction 트리거 조건, 튜닝 파라미터, fragmentation index 등은 Memory Compaction 페이지에서 확인할 수 있습니다.
Swapping 서브시스템
Swapping은 물리 메모리(RAM) 부족 시 익명 페이지(anonymous page)를 디스크의 스왑 영역으로 내보내고(swap out), 다시 접근할 때 복원하는(swap in) 메커니즘입니다. 파일 기반 페이지(page cache)는 원본 파일에서 다시 읽을 수 있지만, 힙/스택/mmap(MAP_ANONYMOUS) 등 backing store가 없는 익명 페이지는 스왑 영역이 유일한 저장소입니다.
Swap Entry 구조: 스왑 아웃된 페이지의 PTE에는 물리 주소 대신 swap entry가 저장됩니다. 이 엔트리는 스왑 타입(어떤 스왑 영역인지, 5비트로 최대 32개)과 오프셋(스왑 영역 내 위치)으로 구성됩니다. Present 비트가 0이므로 이 주소에 접근하면 page fault가 발생하고, do_swap_page()가 swap entry를 파싱하여 해당 스왑 영역에서 페이지를 복구합니다.
Swap Cache의 역할: 스왑 아웃된 페이지가 여러 프로세스에서 COW로 공유되는 경우(fork 후), 한 프로세스가 swap in하면 Swap Cache에 보관됩니다. 다른 프로세스가 같은 페이지를 접근하면 디스크 I/O 없이 Swap Cache에서 바로 제공합니다. 또한 swap out 직후 바로 다시 접근하는 경우에도 I/O를 절약합니다.
상세 문서: Swapping 서브시스템의 심층 분석은 Swapping 서브시스템 페이지에서 확인할 수 있습니다.
주요 개념
- Swap 공간: 전용 파티션 또는 파일로 구성, 우선순위 기반 다중 스왑 지원
- Swap Cache: 스왑 아웃된 페이지를 메모리에 캐싱하여 중복 I/O 방지. 같은 페이지가 여러 프로세스에서 공유되면 한 번만 읽음
- kswapd: 백그라운드 메모리 회수 데몬. 각 NUMA 노드마다 하나씩 동작
- swappiness: 익명 페이지 vs 파일 캐시 회수 비율 조정. 기본값 60, 0이면 스왑 최소화, 200이면 적극적 스왑
- zswap/zram: 압축 기반 스왑으로 디스크 I/O 부하 감소
# 스왑 상태 확인
swapon --show
free -h
# swappiness 조정
cat /proc/sys/vm/swappiness # 기본 60
echo 10 > /proc/sys/vm/swappiness # DB 서버: 스왑 최소화
zswap 개요
zswap은 스왑 아웃되는 페이지를 디스크에 쓰기 전에 메모리 내에서 압축하여 캐싱하는 메커니즘입니다. 디스크 I/O를 줄이고 swap 성능을 개선합니다. 압축률이 좋은 페이지(텍스트, 0-fill 등)는 메모리에 압축 상태로 유지되고, 압축률이 낮은 페이지만 실제 디스크로 내보냅니다.
유사한 zram은 압축된 RAM 블록 디바이스를 생성하여 swap 영역으로 사용합니다. zswap이 기존 디스크 swap의 앞단 캐시인 반면, zram은 독립적인 swap 디바이스입니다.
# zswap 활성화 확인
cat /sys/module/zswap/parameters/enabled
# Y
# 압축 알고리즘 및 zpool 백엔드
cat /sys/module/zswap/parameters/compressor
# lz4 (또는 zstd, lzo)
cat /sys/module/zswap/parameters/zpool
# zsmalloc (또는 zbud, z3fold)
# zswap 통계
grep -r . /sys/kernel/debug/zswap/ 2>/dev/null || true
상세 문서: zswap 아키텍처, zpool 백엔드 비교, 압축 알고리즘 선택, zram 설정 및 튜닝 등은 zswap 페이지에서 확인할 수 있습니다.
shmem / tmpfs 개요
tmpfs는 페이지 캐시(Page Cache)와 스왑을 저장소로 사용하는 메모리 기반 파일시스템입니다. 커널 내부적으로 mm/shmem.c에 구현되며, 다음 용도로 광범위하게 사용됩니다:
- /dev/shm — POSIX shared memory (
shm_open()) - /run, /tmp — 시스템 임시 파일
- System V shared memory —
shmget()내부 backing store - mmap(MAP_ANONYMOUS) — 익명 매핑의 내부 backing store
size= 옵션으로 크기를 제한할 수 있으며(기본: 물리 메모리의 50%), 메모리 부족 시 스왑으로 페이지가 이동합니다. ramfs와 달리 크기 제한과 스왑 지원이 있어 OOM 위험이 낮습니다.
# tmpfs 마운트
mount -t tmpfs -o size=2G tmpfs /mnt/tmpfs
# 시스템에서 이미 사용 중인 tmpfs
df -h -t tmpfs
# /dev/shm, /run, /tmp 등
ramfs의 위험성: ramfs는 크기 제한 메커니즘이 없어, 대용량 데이터를 쓰면 시스템 전체 메모리를 소진하여 OOM이 발생합니다. 프로덕션 환경에서는 반드시 tmpfs를 사용하세요.
상세 문서: tmpfs vs ramfs 비교, quota, THP 지원, devtmpfs, POSIX SHM, ramfs 내부 구현 등은 shmem / tmpfs 페이지에서 확인할 수 있습니다.
memfd 개요
memfd_create()는 파일 디스크립터(File Descriptor) 기반의 익명 메모리 객체를 생성하는 시스템 콜(커널 3.17+)입니다. 파일시스템에 이름이 나타나지 않는 익명 파일을 생성하며, F_SEAL_* 플래그로 크기 변경/쓰기를 봉인(sealing)할 수 있어, 프로세스 간 안전한 메모리 공유에 적합합니다. Android의 ashmem을 대체하는 메인라인 표준이며, MFD_HUGETLB(커널 4.14+)로 huge page 기반 공유 메모리도 지원합니다. MFD_SECRET(커널 5.14+)는 커널조차 접근할 수 없는 보안 메모리를 제공합니다.
/* memfd_create 기본 사용법 */
int fd = memfd_create("shared_buf", MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, size);
fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW); /* 크기 봉인 */
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* fd를 SCM_RIGHTS로 다른 프로세스에 전달 가능 */
상세 문서: memfd sealing 전체 플래그, MFD_HUGETLB, SCM_RIGHTS 전달, secretmem(MFD_SECRET) 등은 memfd 페이지에서 확인할 수 있습니다.
HMM (Heterogeneous Memory Management) 개요
HMM은 GPU/가속기 등 이기종 디바이스의 메모리를 커널이 통합 관리하는 프레임워크입니다. ZONE_DEVICE로 디바이스 메모리를 등록하고, migrate_vma()로 시스템 메모리(DDR)↔디바이스 메모리(HBM, VRAM) 간 페이지를 투명하게 이동시킵니다. NVIDIA GPU(nouveau), AMD GPU(amdgpu), Intel GPU 등의 드라이버가 HMM을 활용합니다.
핵심 아이디어는 GPU/가속기가 CPU와 동일한 가상 주소 공간을 공유하는 것입니다. 페이지 폴트가 디바이스 측에서 발생하면, 커널이 해당 페이지를 디바이스 메모리로 마이그레이션합니다. 이를 통해 프로그래머가 명시적인 데이터 전송 코드를 작성하지 않아도 됩니다.
/* ZONE_DEVICE 등록 개념 예시 */
struct dev_pagemap pgmap = {
.type = MEMORY_DEVICE_PRIVATE, /* 디바이스 전용 */
.range = {
.start = hbm_phys_start,
.end = hbm_phys_end,
},
};
int ret = memremap_pages(&pgmap, NUMA_NO_NODE);
상세 문서: ZONE_DEVICE, dev_pagemap, 페이지 마이그레이션 경로, 드라이버 통합 패턴 등은 HMM 페이지에서 확인할 수 있습니다.
CXL 메모리 개요
CXL (Compute Express Link)은 PCIe 기반의 CPU-디바이스/메모리 간 캐시 일관성(Cache Coherency) 인터커넥트입니다. CXL 2.0+ Type-3 디바이스는 호스트에 추가 메모리 풀을 제공하며, 커널은 이를 NUMA 노드로 노출합니다. DDR보다 지연은 높지만(100~300ns 추가) 용량 확장이 용이하여, 메모리 티어링 전략(핫 데이터→DDR, 콜드 데이터→CXL 메모리)에 활용됩니다. DAMON이 접근 패턴을 모니터링하여 자동 티어링을 수행할 수 있습니다.
| CXL 디바이스 유형 | 설명 | 커널 지원 |
|---|---|---|
| Type-1 | 캐시 일관성 가속기 (FPGA 등) | CXL.cache + CXL.io |
| Type-2 | 가속기 + 디바이스 메모리 (GPU 등) | CXL.cache + CXL.mem + CXL.io |
| Type-3 | 메모리 확장 디바이스 | CXL.mem + CXL.io → NUMA 노드 |
상세 문서: CXL 프로토콜, Type-1/2/3 디바이스 상세, 메모리 티어링 정책, cxl-cli 도구, 커널 드라이버 등은 CXL 메모리 페이지에서 확인할 수 있습니다.
KSM (Kernel Samepage Merging)
KSM은 내용이 동일한 익명 페이지를 자동으로 병합하여 메모리를 절약합니다. KVM 가상화(Virtualization)에서 동일 게스트 OS 이미지를 여러 VM이 공유할 때 특히 유용합니다. 병합된 페이지에 쓰기가 발생하면 COW(Copy-on-Write)로 분리됩니다.
KSM은 ksmd 커널 스레드가 MADV_MERGEABLE로 표시된 영역을 주기적으로 스캔하여 동일 내용의 페이지를 찾고, stable/unstable tree(red-black tree)로 관리합니다. 병합 시 write-protected 페이지를 공유하므로, 쓰기 시 COW가 발생하여 CPU 오버헤드(Overhead)가 있습니다.
# KSM 활성화
echo 1 > /sys/kernel/mm/ksm/run
# KSM 상태 확인
cat /sys/kernel/mm/ksm/pages_shared # 공유된 페이지 수
cat /sys/kernel/mm/ksm/pages_sharing # 공유로 절약된 페이지 수
cat /sys/kernel/mm/ksm/pages_unshared # 고유한 페이지 수
# 튜닝 파라미터
echo 200 > /sys/kernel/mm/ksm/sleep_millisecs # 스캔 간격
echo 256 > /sys/kernel/mm/ksm/pages_to_scan # 스캔당 페이지 수
/* 애플리케이션에서 KSM 영역 지정 */
madvise(addr, length, MADV_MERGEABLE); /* KSM 스캔 대상 등록 */
madvise(addr, length, MADV_UNMERGEABLE); /* KSM 대상에서 제외 */
DAMON (Data Access Monitor)
DAMON은 커널 5.15에서 도입된 메모리 접근 패턴 모니터링 프레임워크입니다. 영역(region)을 적응적으로 분할/병합하며 sampling 기반으로 접근 빈도를 추적합니다. 오버헤드가 낮아(1% 미만) 프로덕션에서도 사용 가능합니다. DAMON 기반 정책 엔진(Policy Engine)으로 reclaim(damon_reclaim)과 LRU 정렬(damon_lru_sort)을 자동화합니다.
# DAMON sysfs 인터페이스 (v5.18+)
ls /sys/kernel/mm/damon/
# DAMON 기반 reclaim (DAMON_RECLAIM)
echo Y > /sys/module/damon_reclaim/parameters/enabled
echo 200 > /sys/module/damon_reclaim/parameters/min_age # 최소 비접근 시간(ms)
# DAMON 기반 LRU 정렬 (DAMON_LRU_SORT, v6.0+)
echo Y > /sys/module/damon_lru_sort/parameters/enabled
Memory Hotplug
가상화 환경(KVM, Hyper-V, Xen)이나 CXL 장치에서 런타임에 메모리를 추가/제거할 수 있습니다. 각 메모리 블록(기본 128MB)은 sysfs를 통해 online/offline 상태를 전환합니다. 오프라인 시 해당 블록의 모든 페이지가 다른 블록으로 마이그레이션되어야 하므로, ZONE_MOVABLE 영역의 페이지만 이동 가능합니다.
Hotplug 아키텍처: 메모리 핫플러그는 두 단계로 구성됩니다. (1) Hot-Add: ACPI 이벤트 또는 sysfs를 통해 물리 메모리가 커널에 등록되고, struct page 배열이 생성됩니다 (vmemmap). 이 시점에서 메모리는 아직 사용 불가 상태입니다. (2) Online: 등록된 메모리 블록을 활성화하여 Buddy Allocator의 free list에 추가합니다. online_movable로 추가하면 나중에 제거가 가능합니다. 제거 시에는 반대로 Offline(모든 페이지 마이그레이션) → Hot-Remove(커널에서 등록 해제)로 진행합니다.
오프라인 실패 원인: 메모리 블록에 MIGRATE_UNMOVABLE 타입의 페이지(커널 slab 객체, 페이지 테이블 등)가 존재하면 마이그레이션이 불가능하여 오프라인이 실패합니다. 이를 방지하려면 핫플러그 대상 메모리를 online_movable로 추가하여 ZONE_MOVABLE에 배치해야 합니다. kernelcore= 또는 movablecore= 부트 파라미터로 시스템 전체 수준에서 ZONE_MOVABLE 비율을 조정할 수 있습니다.
# 메모리 블록 상태 확인
ls /sys/devices/system/memory/
cat /sys/devices/system/memory/memory32/state
# online
# 메모리 블록 오프라인/온라인
echo offline > /sys/devices/system/memory/memory32/state
echo online > /sys/devices/system/memory/memory32/state
# online 시 zone 선택 (movable zone에 추가하면 나중에 제거 가능)
echo online_movable > /sys/devices/system/memory/memory32/state
Memory Cgroup (memcg)
cgroup v2를 통해 프로세스 그룹별 메모리 사용량을 제한하고 모니터링합니다. 컨테이너 환경(Docker, Kubernetes)에서 메모리 격리(Isolation)의 핵심 도구이며, 각 cgroup은 독립적인 LRU 리스트와 회수 정책을 가집니다.
# cgroup v2 메모리 제한 설정
mkdir /sys/fs/cgroup/myapp
echo 512M > /sys/fs/cgroup/myapp/memory.max # 하드 제한 (초과 시 OOM)
echo 400M > /sys/fs/cgroup/myapp/memory.high # 소프트 제한 (초과 시 reclaim 유도)
echo 256M > /sys/fs/cgroup/myapp/memory.low # 보호 수준 (best-effort)
echo 128M > /sys/fs/cgroup/myapp/memory.min # 절대 보호
# 프로세스를 cgroup에 할당
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
# 메모리 사용량 확인
cat /sys/fs/cgroup/myapp/memory.current # 현재 사용량
cat /sys/fs/cgroup/myapp/memory.stat # 상세 통계
cat /sys/fs/cgroup/myapp/memory.events # OOM 등 이벤트
/proc 메모리 인터페이스
# /proc/meminfo 주요 항목
cat /proc/meminfo
# MemTotal: 32768000 kB ← 전체 물리 메모리
# MemFree: 10240000 kB ← 미사용 메모리
# MemAvailable: 20480000 kB ← 실제 사용 가능 추정치
# Buffers: 512000 kB ← 블록 디바이스 캐시
# Cached: 8192000 kB ← 페이지 캐시
# SwapCached: 10240 kB ← 스왑에서 다시 읽은 캐시
# Slab: 1024000 kB ← Slab 할당자 사용량
# SReclaimable: 768000 kB ← 회수 가능 Slab
# /proc/vmstat - VM 통계
cat /proc/vmstat | grep -E "pgfault|pgmajfault|pswpin|pswpout|compact"
# /proc/zoneinfo - 존별 상세 정보
cat /proc/zoneinfo | head -50
커널 버전별 주요 변경
| 버전 | 기능 | 설명 |
|---|---|---|
| 2.6.32 | KSM | Kernel Samepage Merging — 동일 페이지 병합 |
| 2.6.36 | oom_score_adj | OOM Killer 점수 계산 단순화 (RSS 기반) |
| 2.6.38 | THP | Transparent Huge Pages 도입 |
| 3.5 | CMA | Contiguous Memory Allocator 도입 |
| 3.11 | zswap | 압축 기반 swap 캐시 도입 |
| 3.15 | zram | 메인라인 staging 졸업, 압축 RAM 블록 디바이스 |
| 3.17 | memfd_create | 파일 디스크립터 기반 익명 메모리 + sealing |
| 4.12 | PSI 기초 | lowmemorykiller 제거, 유저스페이스 lmkd로 전환 |
| 4.14 | memcg v2 | cgroup v2 메모리 컨트롤러 안정화 |
| 4.20 | PSI | Pressure Stall Information — /proc/pressure/memory |
| 5.9 | Proactive Compaction | 백그라운드 자동 compaction |
| 5.12 | KFENCE | 프로덕션용 경량 메모리 오류 감지 |
| 5.15 | DAMON | 데이터 접근 모니터링 프레임워크 |
| 5.18 | DAMON sysfs / ashmem 제거 | DAMON sysfs 인터페이스, ashmem 드라이버 제거 |
| 6.0 | DAMON LRU Sort | DAMON 기반 LRU 리스트 최적화 |
| 6.1 | maple tree / MGLRU | VMA 관리에 maple tree 도입, Multi-Gen LRU |
| 6.5 | SLAB/SLOB 제거 | SLUB이 유일한 slab allocator로 통합 |
| 6.8 | mTHP | Multi-size THP (16KB~1MB PTE-mapped large folio) |
참고 자료: 커널 메모리 관리 문서, LWN의 memory management 시리즈, Documentation/mm/
메모리 관리 주요 버그 사례
리눅스 커널의 메모리 관리 서브시스템은 오랜 역사 속에서 다양한 버그와 취약점(Vulnerability)을 경험해왔습니다. 하드웨어 수준의 취약점(Meltdown), COW 경쟁 조건(Dirty COW), Use-After-Free, 가드 페이지 우회(Stack Clash), 성능 결함(THP OOM), 컨텍스트 오류(GFP atomic), 메모리 누수(slab leak) 등 다양한 유형이 존재합니다. 이러한 사례를 학습하면 커널 개발 시 동일한 실수를 피하고, 보다 견고한 코드를 작성할 수 있습니다.
Meltdown (CVE-2017-5754) 과 KPTI
2018년 1월 공개된 Meltdown 취약점은 현대 CPU의 투기적 실행(speculative execution)을 악용하여, 사용자 공간 프로세스가 커널 메모리를 읽을 수 있는 하드웨어 수준의 결함이었습니다.
Meltdown의 핵심 위협: 사용자 공간 프로그램이 커널 주소 공간의 패스워드, 암호화(Encryption) 키 등 민감한 데이터를 side-channel 공격으로 추출할 수 있었습니다. 클라우드 환경에서 VM 간 격리를 우회할 수 있어 영향이 광범위했습니다.
커널은 KPTI (Kernel Page Table Isolation)를 도입하여 이 문제를 해결했습니다. KPTI는 사용자 모드와 커널 모드에서 서로 다른 페이지 테이블 세트를 사용하도록 분리합니다. 사용자 모드 페이지 테이블에는 커널 메모리 매핑이 최소한으로만 포함되어(시스템 콜 진입점(Entry Point), 인터럽트 핸들러 등 필수 부분만), 투기적 실행으로도 커널 데이터에 접근할 수 없게 됩니다.
/* arch/x86/mm/pti.c — KPTI 초기화 핵심 로직 */
void __init pti_init(void)
{
if (!boot_cpu_has_bug(X86_BUG_CPU_MELTDOWN))
return;
pr_info("Kernel/User page tables isolation: enabled\\n");
pti_clone_user_shared();
pti_setup_espfix64();
pti_setup_vsyscall();
}
# KPTI 활성화 상태 확인
cat /sys/devices/system/cpu/vulnerabilities/meltdown
# 출력 예: Mitigation: PTI
# 부트 옵션으로 KPTI 비활성화 (테스트 환경에서만)
nopti
성능 영향: KPTI 활성화 시 시스템 콜마다 CR3 전환으로 TLB flush가 발생하여 5~30% 성능 저하가 관측됩니다. PCID 지원 CPU에서는 영향이 감소하며, Intel Ice Lake 이후 CPU는 하드웨어 수준에서 Meltdown이 수정되어 KPTI가 불필요합니다.
GFP_KERNEL in atomic context 버그 패턴
커널 메모리 할당에서 가장 빈번한 버그 패턴은 atomic context에서 sleep 가능한 할당 함수를 호출하는 것입니다. 인터럽트 핸들러, 스핀락 보유 구간, RCU read-side critical section 등 sleep이 불가능한 컨텍스트에서 GFP_KERNEL 플래그로 메모리를 할당하면, 할당자가 메모리 회수를 위해 sleep을 시도하여 데드락 또는 스케줄러(Scheduler) 오류가 발생합니다.
치명적 결과: atomic context에서 GFP_KERNEL을 사용하면 커널이 직접 메모리 회수(direct reclaim)를 시도하면서 schedule()을 호출합니다. 스핀락을 보유한 상태에서 스케줄링이 발생하면 다른 CPU가 같은 스핀락을 획득하려 할 때 데드락이 발생하며, 인터럽트 컨텍스트에서는 BUG: scheduling while atomic 커널 패닉(Kernel Panic)이 발생합니다.
/* 잘못된 패턴: 스핀락 내에서 GFP_KERNEL 사용 */
void buggy_handler(void)
{
spin_lock_irqsave(&my_lock, flags);
char *buf = kmalloc(4096, GFP_KERNEL); /* DEADLOCK 위험! */
spin_unlock_irqrestore(&my_lock, flags);
}
/* 올바른 패턴: atomic context에서는 GFP_ATOMIC 사용 */
void correct_handler(void)
{
spin_lock_irqsave(&my_lock, flags);
char *buf = kmalloc(4096, GFP_ATOMIC);
if (!buf) {
spin_unlock_irqrestore(&my_lock, flags);
return -ENOMEM;
}
spin_unlock_irqrestore(&my_lock, flags);
}
/* 더 나은 패턴: 스핀락 밖에서 미리 할당 */
void best_handler(void)
{
char *buf = kmalloc(4096, GFP_KERNEL); /* 스핀락 밖: sleep OK */
if (!buf)
return -ENOMEM;
spin_lock_irqsave(&my_lock, flags);
do_work(buf);
spin_unlock_irqrestore(&my_lock, flags);
kfree(buf);
}
디버깅 팁: CONFIG_DEBUG_ATOMIC_SLEEP=y와 CONFIG_PROVE_LOCKING=y(lockdep)를 함께 활성화하면, sleep-in-atomic 패턴을 효과적으로 탐지할 수 있습니다.
OOM Killer 오동작 사례
OOM Killer의 알고리즘은 커널 버전에 따라 크게 변해왔습니다. 초기 Linux(2.6.x)의 복잡한 휴리스틱은 예측이 어려워, 커널 2.6.36에서 RSS 기반 비례 점수 + oom_score_adj로 단순화되었습니다.
cgroup v1 memcg OOM과 전역 OOM 충돌: cgroup v1에서 memory.limit_in_bytes를 설정한 경우, memcg OOM이 전체 시스템 OOM과 독립적으로 동작합니다. memcg 내 프로세스가 종료되어도 전역 메모리 압박이 해소되지 않을 수 있으며, 이중 kill이 발생하는 사례가 보고되었습니다. cgroup v2의 memory.oom.group으로 개선되었습니다.
Slab 메모리 누수 탐지
커널 모듈(Kernel Module)이나 드라이버에서 kmalloc()/kmem_cache_alloc()으로 할당한 slab 객체를 해제하지 않으면 slab 메모리 누수가 발생합니다. 커널은 프로세스 종료 시 자동 정리가 되지 않으므로, 누수된 메모리는 재부팅 전까지 소실됩니다.
# slab 캐시별 사용 현황
cat /proc/slabinfo | head -20
slabtop -s c # 캐시 크기 순 정렬 (누수 시 특정 캐시 지속 증가)
# kmemleak 활성화 (CONFIG_DEBUG_KMEMLEAK=y)
echo scan > /sys/kernel/debug/kmemleak # 수동 스캔
cat /sys/kernel/debug/kmemleak # 누수 보고서
/* 올바른 에러 처리 패턴: goto 기반 정리 */
int init_device_fixed(void)
{
struct resource *res_a, *res_b;
res_a = kmalloc(sizeof(*res_a), GFP_KERNEL);
if (!res_a)
return -ENOMEM;
res_b = kmalloc(sizeof(*res_b), GFP_KERNEL);
if (!res_b)
goto err_free_a;
return 0;
err_free_a:
kfree(res_a);
return -ENOMEM;
}
kmemleak은 커널의 메모리 누수 탐지기로, mark-and-sweep 가비지 컬렉터와 유사하게 동작합니다. 주기적으로 커널 메모리를 스캔하여 어디에서도 참조되지 않는 할당된 객체를 찾아냅니다.
slab 누수 방지 모범 사례: (1) 모든 에러 경로에서 goto 기반 정리 패턴 사용. (2) devm_kmalloc() 등 devres API 활용. (3) 개발 단계에서 CONFIG_DEBUG_KMEMLEAK=y와 slub_debug 활성화. (4) 모듈 exit에서 init의 모든 자원 해제 확인.
이중 해제(double free) 위험: 이미 해제된 slab 객체를 다시 kfree()하면 freelist가 손상됩니다. slub_debug=FZPU 부트 옵션으로 조기 탐지할 수 있으며, 해제 후 포인터를 NULL로 초기화하는 것이 안전합니다.
Dirty COW (CVE-2016-5195) — Copy-on-Write 경쟁 조건
madvise(MADV_DONTNEED)와 write() 사이의 race condition을 악용하여
읽기 전용(Read-Only) 파일(예: /etc/passwd)에 쓰기가 가능합니다.
근본 원인: get_user_pages()에서 COW 처리 도중, write 권한 없이도 dirty 플래그가 설정될 수 있는 경쟁 조건(TOCTOU)이 존재했습니다.
/* Dirty COW 공격 흐름 (개념적 설명) */
/* Thread 1: /proc/self/mem을 통해 읽기 전용 매핑에 쓰기 시도 */
lseek(fd_mem, map_addr, SEEK_SET);
write(fd_mem, payload, payload_len);
/* 내부: get_user_pages(FOLL_WRITE) → COW break → 사본 페이지 할당 */
/* Thread 2: madvise()로 페이지 무효화 */
madvise(map_addr, page_size, MADV_DONTNEED);
/* → COW 사본 폐기 → 원본 페이지로 복귀 */
/* → Thread 1이 원본 페이지에 직접 쓰기하게 됨! */
커널 수정: FOLL_COW 플래그를 도입하여 COW break 완료 여부를 명시적으로 확인합니다.
/* mm/gup.c - 수정된 COW 처리 */
static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
{
if (flags & FOLL_COW)
return pte_dirty(pte);
return pte_write(pte);
}
KASAN이 발견한 Use-After-Free (CVE-2016-8655)
packet_set_ring()과 packet_setsockopt() 사이의 경쟁 조건으로
해제된 타이머 구조체에 접근하여 로컬 권한 상승이 가능합니다.
/* Use-After-Free 일반 패턴 */
struct my_object *obj;
/* 1. 할당 */
obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
obj->data = 42;
/* 2. 해제 */
kfree(obj);
/* 3. 다른 할당이 같은 메모리를 재사용 */
another = kmem_cache_alloc(my_cache, GFP_KERNEL);
/* 4. Dangling pointer 접근 → Use-After-Free! */
printk("data = %d\\n", obj->data); /* 이미 해제된 메모리 */
CVE-2016-8655 구체적 흐름: AF_PACKET 소켓에서 packet_set_ring()이 ring buffer와 타이머를 초기화하는 중에, 다른 스레드(Thread)가 packet_setsockopt(PACKET_VERSION)으로 ring buffer를 해제합니다. 해제 후에도 타이머가 pending 상태로 남아, 타이머 만료 시 해제된 ring buffer 메모리에 접근하여 UAF가 발생합니다.
KASAN 탐지 메커니즘:
/* KASAN(Kernel Address SANitizer) 동작 원리 */
/* 1. Shadow memory: 8바이트 실제 메모리당 1바이트 shadow */
/* shadow 값: 0 = 전체 접근 가능, N(1-7) = 처음 N바이트만 가능 */
/* 음수 = 접근 불가 (red zone, freed 등) */
/* 2. kfree() 시 shadow를 KASAN_KMALLOC_FREE(0xFB)로 마킹 */
/* 3. 이후 접근 시 shadow 값 확인 → BUG 리포트 출력 */
/* BUG: KASAN: use-after-free in prb_retire_rx_blk_timer_expired */
/* Read of size 8 at addr ffff8800XXXXXXXX by task swapper/0 */
/* 4. Quarantine: 해제된 object를 즉시 재사용하지 않고 격리 */
/* → UAF 탐지 윈도우 확대 */
del_timer_sync())와 워크큐(cancel_work_sync())를 반드시 정리하고, RCU 기반 수명 관리로 안전한 해제 시점을 보장해야 합니다.
Stack Guard Page 우회 — Stack Clash (CVE-2017-1000364)
alloca()나 VLA로 한 번에 건너뛰어 우회하는 공격입니다.
커널 수정 사항:
/* 수정 1: Guard page 크기를 1MB로 확대 */
#define STACK_GUARD_GAP 256 /* 256 pages = 1MB (4KB pages 기준) */
/* 수정 2: expand_stack()에서 guard gap 검증 강화 */
int expand_stack(struct vm_area_struct *vma, unsigned long address)
{
struct vm_area_struct *prev;
prev = find_vma_prev(vma->vm_mm, address, &prev);
if (prev && prev->vm_end + stack_guard_gap > address)
return -ENOMEM; /* guard gap 침범 → 확장 거부 */
/* ... */
}
| 항목 | 수정 전 (취약) | 수정 후 (안전) |
|---|---|---|
| Guard Gap 크기 | 4KB (1 page) | 1MB (256 pages, 조정 가능) |
| VMA 확장 검증 | 인접 VMA 거리 미확인 | stack_guard_gap 이상 거리 강제 |
| 커널 파라미터 | 없음 | stack_guard_gap=N (페이지 단위) |
alloca()는 커널 코드에서 사용을 지양하며, Linux 커널은 -Wvla로 VLA 사용을 금지하고 있습니다.
Transparent Huge Pages (THP) OOM 문제
khugepaged 커널 스레드가 과도한 CPU를 소모하여
프로덕션 서버 성능이 심각하게 저하되는 문제입니다.
문제 발생 메커니즘: 프로세스가 페이지 폴트를 발생시키면, THP가 always로 설정된 경우 2MB huge page 할당을 시도합니다. 연속 2MB 물리 메모리가 없으면 compaction을 시작하는데, 메모리 단편화가 심한 경우 compaction이 반복 실패합니다. 동시에 khugepaged 커널 스레드가 기존 4KB 페이지들을 2MB로 합체(collapse)하려 시도하면서 CPU 100%를 점유하고, 실제 워크로드에 CPU 자원이 부족해져 응답 지연 → OOM killer 발동으로 이어질 수 있습니다.
| 애플리케이션 | THP 영향 | 권장 설정 |
|---|---|---|
| Redis | fork() 기반 RDB/AOF 시 COW로 메모리 2배 사용, latency spike | THP 비활성화 필수 |
| MongoDB | WiredTiger 엔진과 충돌, 성능 불안정 | THP 비활성화 권장 |
| Oracle DB | HugePages(명시적)와 THP 혼용 시 예측 불가 | THP 비활성화, 명시적 HugePages |
| Java (JVM) | GC pause 증가, 메모리 비예측적 증가 | madvise 모드 또는 비활성화 |
# THP 제어
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# khugepaged 스캔 간격 조정 (CPU 부하 완화)
echo 60000 > /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
madvise 모드를 사용하여 애플리케이션이 명시적으로 huge page 사용 여부를 결정하도록 하는 것이 안전합니다.
Android 메모리 관리 특화
ashmem → memfd_create 전환: 초기 Android의 ashmem(Anonymous Shared Memory)은 커널 전용 드라이버(/dev/ashmem)로, POSIX 미준수이고 sealing 기능이 없어 보안/유지보수 문제가 있었습니다. 커널 5.18에서 제거되었으며, 메인라인의 memfd_create()로 대체되었습니다. sealing 지원(F_SEAL_SHRINK, F_SEAL_WRITE), 표준 인터페이스, MFD_HUGETLB 지원이 장점입니다.
lowmemorykiller → lmkd: 커널 내 lowmemorykiller 드라이버는 OOM killer와 충돌 문제로 제거되었으며(4.12+), 유저스페이스 lmkd 데몬이 /proc/pressure/memory(PSI)를 모니터링하여 메모리 부족 시 앱 중요도(OOM adj 점수)에 따라 적절한 프로세스를 종료합니다. CONFIG_PSI=y가 lmkd의 필수 전제 조건입니다.
MGLRU와 Android Go: MGLRU(Multi-Gen LRU, 커널 6.1+)는 기존 active/inactive 2-리스트 LRU보다 정밀한 에이징을 제공하여, Android 저사양 워크로드에서 페이지 에이징 정확도와 재클레임 효율을 개선합니다. 관련 내용은 Android 커널 — 메모리 관리를 참고하세요.
Android 메모리 요약: ashmem(제거) → memfd_create(표준), lowmemorykiller(제거) → lmkd(PSI 기반). CONFIG_PSI=y가 lmkd의 필수 전제 조건입니다.
버그 사례 비교 요약
| 버그 | 유형 | 잠복 기간 | 영향 | 핵심 교훈 |
|---|---|---|---|---|
| Meltdown | HW 투기적 실행 | 수년 | 커널 메모리 유출 | KPTI로 페이지 테이블 분리 |
| Dirty COW | Race Condition (TOCTOU) | 9년 (2007~2016) | 로컬 권한 상승 | COW 경로의 원자성 보장 필수 |
| AF_PACKET UAF | Use-After-Free | 수개월 | 로컬 권한 상승 | KASAN 활용, 해제 전 정리 |
| Stack Clash | Guard Page 우회 | 수년 | 로컬 권한 상승 | 방어 크기 가정 재검토 |
| THP OOM | 성능 결함 | 지속적 | 서비스 장애 | 워크로드별 최적화 필요 |
| GFP atomic | 컨텍스트 오류 | 즉시 | 데드락/패닉 | 컨텍스트별 GFP 준수 |
| Slab 누수 | 리소스 누수 | 점진적 | 메모리 고갈 | kmemleak, goto 정리 패턴 |
KASAN / KFENCE
커널 메모리 안전성 검증 도구입니다. 개발/테스트 단계에서는 KASAN, 프로덕션 환경에서는 KFENCE를 사용합니다.
KASAN (Kernel Address Sanitizer)
KASAN은 shadow memory 기법으로 out-of-bounds, use-after-free, double-free 등 메모리 오류를 탐지합니다. 실제 메모리 8바이트당 1바이트의 shadow memory를 유지하며, 모든 메모리 접근을 계측(instrument)합니다.
# KASAN 활성화 (커널 빌드 시)
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y # 소프트웨어 기반 (모든 아키텍처)
# 또는 CONFIG_KASAN_SW_TAGS=y (ARM64 MTE 활용)
# 또는 CONFIG_KASAN_HW_TAGS=y (ARM64 MTE 하드웨어, 최소 오버헤드)
# 성능 오버헤드: ~2-3x, 메모리 오버헤드: ~2-3x
# KASAN 오류 로그 예시
# BUG: KASAN: slab-use-after-free in my_function+0x42/0x80
# Read of size 8 at addr ffff888012345678 by task test/1234
# Allocated by task 1234:
# kmalloc+0x4a/0x80
# Freed by task 1234:
# kfree+0x35/0x60
KFENCE (Kernel Electric Fence)
KFENCE는 프로덕션 환경에서 사용 가능한 경량 메모리 오류 감지 도구(커널 5.12+)입니다. 확률적 샘플링 기반으로, 일부 할당만 guard page로 보호하여 오버헤드를 1% 미만으로 유지합니다.
# KFENCE 활성화
CONFIG_KFENCE=y
# 샘플링 간격 조정 (기본 100ms마다 1개 할당을 KFENCE 풀에서 처리)
echo 100 > /sys/module/kfence/parameters/sample_interval # ms
# KFENCE 통계
cat /sys/kernel/debug/kfence/stats
사용 전략: 개발/CI 환경에서는 CONFIG_KASAN=y로 모든 메모리 오류를 조기 탐지하고, 프로덕션에서는 CONFIG_KFENCE=y로 실시간(Real-time) 오류 감시를 유지합니다. 두 도구를 동시에 활성화할 수는 없습니다.
메모리 운영 플레이북
메모리 이슈는 단일 지표만으로 판단하면 오진하기 쉽습니다. 할당(buddy/slab), 회수(kswapd/shrinker), 매핑(page table), 단편화(compaction) 네 축을 함께 확인해야 정확한 원인 분리가 가능합니다. 아래 표는 증상별 진단 포인트와 권장 조치를 정리합니다.
| 증상 | 우선 점검 | 권장 조치 |
|---|---|---|
| 간헐적 OOM | /proc/meminfo, dmesg | grep -i oom | 누수 경로 점검, reclaim 정책/limit 재조정 |
| 고차 페이지 할당 실패 | /proc/buddyinfo | compaction 튜닝, hugepage 정책 점검 |
| slab 급증 | /proc/slabinfo, SReclaimable | 캐시별 shrinker 경로 점검 |
| 지속적 swap I/O | vmstat si/so, swappiness | working set 축소, zswap/zram 재검토 |
| fault pressure | minor/major fault 비율 | working set 불일치 여부 확인 |
| compaction 실패 | /proc/vmstat compact_* | THP defrag 모드 조정 |
# 메모리 문제 기본 수집 세트
cat /proc/meminfo > meminfo.log
cat /proc/buddyinfo > buddyinfo.log
cat /proc/slabinfo > slabinfo.log
vmstat 1 10 > vmstat.log
dmesg | grep -Ei "oom|page allocation failure|memory" > mem-dmesg.log
cat /proc/vmstat | grep -E "thp|compact|pgscan|pgsteal|pgfault" > vmstat-detail.log
# PSI 모니터링 (커널 4.20+)
cat /proc/pressure/memory
# NUMA 관련 진단
numastat
numastat -p $(pidof 주요프로세스) 2>/dev/null || true
# perf를 이용한 메모리 이벤트 추적
perf stat -e page-faults,minor-faults,major-faults -p $PID -- sleep 10
# cgroup 메모리 이벤트 확인 (컨테이너 환경)
cat /sys/fs/cgroup/*/memory.events 2>/dev/null | head -20
진단 우선순위: (1) /proc/meminfo의 MemAvailable이 전체의 10% 이하이면 즉시 대응. (2) allocstall이 증가하면 direct reclaim 발생 중 — 지연 원인. (3) pgmajfault가 높으면 working set이 RAM을 초과 — swap 또는 메모리 증설 검토. (4) slab 지속 증가는 커널 메모리 누수 의심.
페이지 폴트 핸들러 호출 체인 (Page Fault Handler Chain)
유저 공간이나 커널이 매핑되지 않은 가상 주소에 접근하면 CPU는 페이지 폴트(Page Fault) 예외를 발생시킵니다. Linux 커널은 이 예외를 아키텍처별 진입점에서 받아 공통 mm/memory.c 경로로 전달합니다. 전체 호출 체인은 다음과 같습니다:
/* x86_64 페이지 폴트 전체 호출 체인 (mm/memory.c 기반, 6.x 커널) */
/* 1단계: 아키텍처별 진입점 (arch/x86/mm/fault.c) */
exc_page_fault(regs, error_code)
└─ handle_page_fault(regs, error_code, address)
└─ do_user_addr_fault(regs, error_code, address)
├─ find_vma(mm, address) /* VMA 검색 (maple tree) */
├─ check_vma_access() /* 접근 권한 확인 */
└─ handle_mm_fault(vma, address, flags, regs)
/* 2단계: 공통 MM 경로 (mm/memory.c) */
handle_mm_fault(vma, address, flags, regs)
├─ hugetlb_fault() /* HugeTLB 페이지인 경우 */
└─ __handle_mm_fault(vma, address, flags)
├─ pgd = pgd_offset(mm, address)
├─ p4d = p4d_alloc(mm, pgd, address)
├─ pud = pud_alloc(mm, p4d, address)
├─ pmd = pmd_alloc(mm, pud, address)
└─ handle_pte_fault(vmf)
/* 3단계: PTE 수준 디스패치 (mm/memory.c) */
handle_pte_fault(vmf)
├─ PTE 없음 (pte_none):
│ ├─ vma→vm_ops→fault 존재 → do_fault() /* 파일 매핑 */
│ └─ 익명 매핑 → do_anonymous_page() /* 새 제로 페이지 */
├─ 스왑 엔트리 (!pte_present):
│ └─ do_swap_page() /* 스왑에서 복구 */
├─ NUMA 힌트 폴트 (pte_protnone):
│ └─ do_numa_page() /* NUMA 밸런싱 */
└─ 쓰기 폴트 (write && !pte_write):
└─ do_wp_page() /* Copy-on-Write */
코드 설명
- 4행
exc_page_fault()는 x86_64의 IDT 엔트리에서 호출되는 최초 진입점입니다. error_code에 읽기/쓰기, 유저/커널, 명령어 페치 등의 정보가 담깁니다. - 6행
do_user_addr_fault()는 유저 주소 폴트를 처리합니다. 커널 주소 폴트는do_kern_addr_fault()가 별도 처리합니다. - 7행
find_vma()는 maple tree에서 해당 주소를 포함하는 VMA를 O(log n)으로 검색합니다. 없으면 SIGSEGV입니다. - 13행
handle_mm_fault()는 아키텍처 독립적인 공통 진입점입니다. HugeTLB 페이지는 별도 경로로 분기합니다. - 15~19행
__handle_mm_fault()는 PGD→P4D→PUD→PMD 각 수준의 페이지 테이블 엔트리를 할당하거나 찾습니다. 없으면*_alloc()이 새 테이블을 생성합니다. - 23행
pte_none()이면 해당 가상 주소에 물리 페이지가 한 번도 매핑된 적이 없는 상태입니다. - 24행
do_fault()는 파일 매핑의 폴트를 처리합니다. 읽기면do_read_fault(), 쓰기면do_cow_fault(), 공유 쓰기면do_shared_fault()로 다시 분기합니다. - 25행
do_anonymous_page()는 새 제로 페이지를 할당하고 PTE에 매핑합니다. 읽기 전용이면 공유 제로 페이지를 사용합니다. - 27행
do_swap_page()는 스왑 아웃된 페이지를 디스크에서 읽어와 다시 매핑합니다. zswap/zram 경우 압축 해제도 수행합니다. - 29행
do_numa_page()는 NUMA 밸런싱을 위해 의도적으로 PROT_NONE으로 설정된 PTE의 폴트를 처리하며, 필요 시 페이지를 로컬 노드로 마이그레이션합니다. - 31행
do_wp_page()는 Copy-on-Write를 수행합니다. fork() 후 부모/자식이 공유하던 페이지에 쓰기가 발생하면 복사본을 만듭니다.
성능 관점: 마이너 폴트(minor fault)는 디스크 I/O 없이 메모리 내에서 처리되므로 수 마이크로초 이내에 완료됩니다. 메이저 폴트(major fault)는 디스크에서 페이지를 읽어야 하므로 수 밀리초가 소요됩니다. perf stat -e minor-faults,major-faults로 구분 측정할 수 있습니다.
메모리 할당 진입점 (Memory Allocation Entry Points)
커널 메모리 할당 API는 용도에 따라 여러 진입점을 제공합니다. 모든 경로는 궁극적으로 Buddy Allocator의 __alloc_pages()로 수렴합니다.
| API | 물리 연속 | 최대 크기 | 컨텍스트 | 내부 경로 |
|---|---|---|---|---|
kmalloc() | O | ~8MB (KMALLOC_MAX) | 슬립 가능 (GFP_KERNEL) | Slab → Buddy |
vmalloc() | X (가상만 연속) | VMALLOC 영역 크기 | 슬립 필수 | 페이지별 Buddy → vmap |
alloc_pages() | O | MAX_ORDER (보통 4MB) | GFP 플래그 의존 | 직접 Buddy |
__get_free_pages() | O | MAX_ORDER | GFP 플래그 의존 | alloc_pages() + page_address() |
kvmalloc() | 시도 후 fallback | kmalloc 실패 시 vmalloc | 슬립 가능 | kmalloc → vmalloc |
핵심 할당 경로인 __alloc_pages()의 내부 호출 체인입니다:
/* mm/page_alloc.c — Buddy Allocator 핵심 경로 (6.x 커널) */
struct page *__alloc_pages(gfp_t gfp, unsigned int order,
int preferred_nid, nodemask_t *nodemask)
{
struct alloc_context ac = { };
/* 1단계: GFP 플래그 → 할당 컨텍스트 변환 */
prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac);
/* 2단계: 빠른 경로 — freelist에서 직접 할당 시도 */
page = get_page_from_freelist(gfp, order, alloc_flags, &ac);
if (page)
goto out;
/* 3단계: 느린 경로 — 메모리 회수, compaction 등 시도 */
page = __alloc_pages_slowpath(gfp, order, &ac);
out:
return page;
}
/* get_page_from_freelist() 내부: 워터마크 확인 */
static struct page *get_page_from_freelist(...)
{
for_each_zone_zonelist_nodemask(zone, z, ac->zonelist, ...) {
/* 워터마크 확인: zone의 free pages가 충분한지 */
if (!zone_watermark_fast(zone, order, mark,
ac->highest_zoneidx, alloc_flags))
continue;
/* Buddy free list에서 페이지 추출 */
page = rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp, alloc_flags, ac->migratetype);
if (page) {
prep_new_page(page, order, gfp, alloc_flags);
return page;
}
}
return NULL;
}
코드 설명
- 3행
__alloc_pages()는 Buddy Allocator의 최상위 진입점입니다.gfp플래그가 할당 동작(슬립 허용, 회수 시도 등)을 결정합니다. - 9행
prepare_alloc_pages()는 GFP 플래그를 파싱하여 할당 컨텍스트(zonelist, migratetype 등)를 설정합니다. - 12행빠른 경로(fast path):
get_page_from_freelist()는 적절한 zone의 free list에서 바로 페이지를 가져옵니다. 대부분의 할당이 여기서 성공합니다. - 17행느린 경로(slow path): 빠른 경로 실패 시
__alloc_pages_slowpath()가 direct reclaim, compaction, OOM kill 등을 시도합니다. - 28~30행워터마크 확인: 각 zone의 free 페이지 수가 WMARK_MIN/LOW/HIGH 임계값 이상인지 검사합니다. 부족하면 해당 zone을 건너뜁니다.
- 33행
rmqueue()는 Buddy free list에서 요청 order의 페이지 블록을 추출합니다. 정확한 order가 없으면 상위 order를 분할(split)합니다. - 36행
prep_new_page()는 할당된 페이지의 플래그 초기화, 제로링(GFP_ZERO 시), poisoning(디버그) 등을 수행합니다.
워터마크(Watermark) 체계: 각 zone은 3단계 워터마크를 유지합니다.
- WMARK_HIGH — 이 이상이면 kswapd가 휴면합니다.
- WMARK_LOW — 이 아래로 떨어지면 kswapd가 깨어나 백그라운드 회수를 시작합니다.
- WMARK_MIN — 이 아래로 떨어지면 direct reclaim이 발생합니다. 할당 호출자가 직접 페이지를 회수해야 합니다.
struct mm_struct 분석
struct mm_struct는 프로세스의 가상 주소 공간 전체를 기술하는 핵심 구조체입니다. 모든 VMA, 페이지 테이블, 메모리 사용 통계를 관리하며, task_struct→mm으로 접근합니다. 스레드는 같은 mm_struct를 공유합니다.
/* include/linux/mm_types.h — struct mm_struct 주요 필드 (6.x 커널) */
struct mm_struct {
struct {
/*
* VMA 관리: maple tree (6.1+, 기존 rb-tree 대체)
* find_vma(), mmap() 등에서 O(log n) 검색
*/
struct maple_tree mm_mt;
unsigned long mmap_base; /* mmap 시작 주소 */
unsigned long task_size; /* 유저 공간 크기 */
pgd_t *pgd; /* 최상위 페이지 테이블 */
/**
* @mm_users: 이 mm을 사용하는 스레드 수
* fork() 시 증가, exit() 시 감소
* @mm_count: mm_struct 자체의 참조 카운트
* mm_users가 0이 되어도 lazy TLB 등이
* 참조할 수 있으므로 별도 관리
*/
atomic_t mm_users;
atomic_t mm_count;
int map_count; /* VMA 개수 */
struct rw_semaphore mmap_lock; /* VMA 트리 보호 락 */
/* 메모리 사용 통계 (페이지 단위) */
unsigned long total_vm; /* 전체 매핑된 페이지 수 */
unsigned long locked_vm; /* mlock()된 페이지 수 */
atomic64_t pinned_vm; /* pin_user_pages()된 페이지 */
unsigned long data_vm; /* 데이터 세그먼트 */
unsigned long exec_vm; /* 실행 가능 세그먼트 */
unsigned long stack_vm; /* 스택 세그먼트 */
/* 프로세스 메모리 레이아웃 */
unsigned long start_code, end_code; /* 코드 영역 */
unsigned long start_data, end_data; /* 초기화된 데이터 */
unsigned long start_brk, brk; /* 힙 영역 */
unsigned long start_stack; /* 스택 시작 */
unsigned long arg_start, arg_end; /* argv[] */
unsigned long env_start, env_end; /* envp[] */
};
/* 별도 캐시라인: 빈번하지 않은 필드 */
unsigned long flags; /* MMF_DUMPABLE 등 */
struct user_namespace *user_ns;
struct file __rcu *exe_file; /* /proc/pid/exe */
};
코드 설명
- 9행
mm_mt는 커널 6.1에서 rb-tree를 대체한 maple tree입니다. VMA의 삽입/삭제/검색 성능이 개선되었으며, RCU를 활용한 lock-free 읽기를 지원합니다. - 14행
pgd는 페이지 테이블의 최상위 디렉터리(Page Global Directory)를 가리킵니다. 컨텍스트 스위치 시 CR3 레지스터(x86)에 이 값이 로드됩니다. - 23~24행
mm_users는 이 주소 공간을 적극 사용하는 스레드 수,mm_count는 mm_struct 자체의 참조 카운트입니다. mm_users가 0이 되어도 lazy TLB 모드의 커널 스레드가 mm_count를 유지할 수 있습니다. - 28행
mmap_lock은 읽기/쓰기 세마포어로 VMA 트리를 보호합니다. 페이지 폴트(읽기)와 mmap/munmap(쓰기)이 동시에 안전하게 동작할 수 있게 합니다. 커널 6.x에서는 VMA lock 등으로 경합이 줄어들고 있습니다. - 31~36행메모리 사용 통계는 모두 페이지(4KB) 단위입니다.
total_vm은/proc/pid/status의 VmSize,locked_vm은 VmLck에 대응합니다. - 39~44행프로세스 메모리 레이아웃 필드는 ELF 로더가 설정합니다.
brk는 현재 힙 끝 주소로sys_brk()가 이 값을 변경합니다. - 49행
exe_file은/proc/pid/exe심볼릭 링크가 가리키는 실행 파일입니다. RCU로 보호되어 lock-free 읽기가 가능합니다.
brk() / mmap() 커널 경로
유저 공간에서 메모리를 할당하는 두 가지 주요 시스템 콜인 brk()와 mmap()의 커널 내부 처리 경로를 분석합니다.
brk() 경로: 힙 확장/축소
brk()는 프로세스의 힙(heap) 영역 끝 주소를 변경합니다. glibc의 malloc()은 작은 할당에 brk(), 큰 할당(기본 128KB 이상)에 mmap()을 사용합니다.
/* mm/mmap.c — brk() 시스템 콜 커널 경로 (6.x 커널) */
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
struct mm_struct *mm = current->mm;
unsigned long newbrk, oldbrk, origbrk;
/* 1. 현재 brk 값 가져오기 */
origbrk = mm->brk;
/* 2. 페이지 정렬 */
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(origbrk);
/* 3. RLIMIT_DATA 제한 확인 */
if (check_data_rlimit(...))
goto out;
/* 4a. 축소 요청: VMA 영역 해제 */
if (brk <= mm->brk) {
do_vmi_munmap(&vmi, mm, newbrk, oldbrk - newbrk, ...);
goto set;
}
/* 4b. 확장 요청: 새 VMA 생성 또는 기존 VMA 확장 */
do_brk_flags(&vmi, vma, oldbrk, newbrk - oldbrk, 0);
set:
mm->brk = brk; /* brk 업데이트 */
out:
return origbrk;
}
/* do_brk_flags() 내부 핵심 */
static int do_brk_flags(struct vma_iterator *vmi,
struct vm_area_struct *vma,
unsigned long addr, unsigned long len,
unsigned long flags)
{
/* 기존 VMA와 병합 가능하면 병합 */
vma = vma_merge_new_range(...);
if (vma)
goto out;
/* 새 VMA 할당 */
vma = vm_area_alloc(mm);
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT;
/* maple tree에 삽입 */
vma_iter_store(vmi, vma);
mm->map_count++;
out:
return 0;
}
코드 설명
- 3행
SYSCALL_DEFINE1(brk, ...)는sys_brk()를 정의하는 매크로입니다. 인자는 새로운 힙 끝 주소입니다. - 12~13행brk 값을
PAGE_ALIGN으로 페이지 경계에 정렬합니다. 메모리 관리는 페이지 단위로 수행되기 때문입니다. - 16행
check_data_rlimit()는RLIMIT_DATA(데이터 세그먼트 크기 제한)를 확인합니다. 초과하면 확장이 거부됩니다. - 21행축소 시
do_vmi_munmap()이 해당 영역의 VMA를 해제하고 페이지 테이블 엔트리를 정리합니다. - 26행확장 시
do_brk_flags()가 새 VMA를 생성합니다. 이 시점에서 물리 페이지는 할당되지 않습니다 (demand paging). - 41행
vma_merge_new_range()는 인접한 VMA와 플래그가 동일하면 병합합니다. VMA 수를 줄여 maple tree 효율을 높입니다. - 49행
VM_DATA_DEFAULT_FLAGS는VM_READ | VM_WRITE | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC로, 힙의 기본 보호 속성입니다.
mmap() 경로: 범용 메모리 매핑
/* mm/mmap.c — mmap() 시스템 콜 커널 경로 (6.x 커널) */
SYSCALL_DEFINE6(mmap, ...)
└─ ksys_mmap_pgoff(addr, len, prot, flags, fd, off >> PAGE_SHIFT)
└─ do_mmap(file, addr, len, prot, flags, 0, pgoff, &populate, &uf)
/* do_mmap() 핵심 흐름 */
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, ...)
{
/* 1. 주소 공간에서 빈 영역 탐색 */
addr = get_unmapped_area(file, addr, len, pgoff, flags);
/* 2. 플래그 → vm_flags 변환 */
vm_flags = calc_vm_prot_bits(prot, ...) | calc_vm_flag_bits(flags, ...);
/* 3. 보안 모듈(LSM) 검사 */
security_mmap_file(file, prot, flags);
/* 4. VMA 생성 및 삽입 */
vma = vm_area_alloc(mm);
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
if (file) {
vma->vm_file = get_file(file);
/* 파일 시스템의 mmap 핸들러 호출 */
call_mmap(file, vma); /* → file→f_op→mmap(file, vma) */
}
/* 5. maple tree에 VMA 삽입 */
vma_iter_store(&vmi, vma);
/* 6. MAP_POPULATE이면 즉시 페이지 폴트 */
if (populate)
mm_populate(addr, populate); /* 미리 페이지 할당 */
return addr;
}
코드 설명
- 4행
ksys_mmap_pgoff()는 오프셋을 페이지 단위로 변환하여do_mmap()에 전달하는 래퍼입니다. - 12행
get_unmapped_area()는 요청 크기에 맞는 빈 가상 주소 영역을 찾습니다. ASLR이 활성화되면 무작위 오프셋이 적용됩니다. - 15행
calc_vm_prot_bits()는 유저 공간의 PROT_READ/WRITE/EXEC를 커널 내부 VM_READ/WRITE/EXEC로 변환합니다. - 18행
security_mmap_file()은 SELinux/AppArmor 등 LSM 모듈이 매핑을 허용하는지 검사합니다. - 29행
call_mmap()은 파일 시스템의mmap핸들러를 호출합니다. ext4의 경우ext4_file_mmap()이 DAX 여부를 확인하고vm_ops를 설정합니다. - 36행
MAP_POPULATE플래그가 설정되면mm_populate()가 매핑 직후 모든 페이지를 미리 폴트 인(fault-in)합니다. 초기 접근 지연을 줄이지만 메모리를 즉시 소비합니다.
brk vs mmap 핵심 차이:
brk()는 힙 전용으로 연속된 가상 주소를 단방향으로 확장/축소합니다. 익명 매핑만 가능하며 간단합니다.
mmap()은 범용으로 임의 주소에 파일/익명/장치를 매핑할 수 있고, 공유(MAP_SHARED)와 개인(MAP_PRIVATE) 매핑을 모두 지원합니다.
상세한 VMA 관리와 매핑 기법은 VMA / mmap 문서를 참고하세요.
메모리 회수 진입점 (Memory Reclaim Entry Points)
메모리가 부족할 때 커널은 사용 중인 페이지를 회수(reclaim)하여 free 페이지를 확보합니다. 회수는 세 가지 경로로 트리거됩니다:
- kswapd — 백그라운드 데몬. 워터마크가 LOW 아래로 떨어지면 깨어나 HIGH까지 회수합니다.
- Direct reclaim — 할당 요청자가 직접 회수합니다. 워터마크가 MIN 아래일 때 발생하며, 할당 지연의 주요 원인입니다.
- OOM Killer — 회수로도 메모리를 확보하지 못하면 프로세스를 강제 종료합니다.
/* mm/vmscan.c — 메모리 회수 핵심 호출 체인 (6.x 커널) */
/* Direct reclaim 진입점 (할당 실패 시 호출) */
unsigned long try_to_free_pages(struct zonelist *zonelist,
int order, gfp_t gfp_mask)
{
struct scan_control sc = {
.nr_to_reclaim = SWAP_CLUSTER_MAX, /* 32 페이지 */
.gfp_mask = gfp_mask,
.order = order,
.may_writeback = !laptop_mode,
.may_unmap = 1,
.may_swap = 1,
};
return do_try_to_free_pages(zonelist, &sc);
}
/* 회수 메인 루프 */
static unsigned long do_try_to_free_pages(...)
{
do {
/* 각 노드에서 회수 시도 */
shrink_zones(zonelist, sc);
└─ shrink_node(pgdat, sc)
├─ shrink_node_memcgs(pgdat, sc)
│ └─ shrink_lruvec(lruvec, sc)
│ ├─ shrink_list(LRU_INACTIVE_ANON, ...)
│ ├─ shrink_list(LRU_ACTIVE_ANON, ...)
│ ├─ shrink_list(LRU_INACTIVE_FILE, ...)
│ └─ shrink_list(LRU_ACTIVE_FILE, ...)
└─ shrink_slab(sc) /* dentry/inode 캐시 회수 */
/* 충분히 회수했으면 종료 */
if (sc->nr_reclaimed >= sc->nr_to_reclaim)
break;
/* priority 상승 (스캔 범위 확대) */
sc->priority--;
} while (sc->priority >= 0);
/* priority 0까지 실패하면 OOM 경로로 */
}
코드 설명
- 4행
try_to_free_pages()는__alloc_pages_slowpath()에서 직접 호출되는 direct reclaim 진입점입니다. - 8행
SWAP_CLUSTER_MAX(32)는 한 번에 회수를 시도할 최소 페이지 수입니다. 배치 처리로 오버헤드를 줄입니다. - 11~13행
scan_control의 may_writeback/may_unmap/may_swap은 회수가 dirty 쓰기, 언맵, 스왑 아웃을 수행할 수 있는지를 제어합니다. - 26행
shrink_node()는 특정 NUMA 노드에서 회수를 수행합니다. cgroup 계층을 순회하며 각 memcg의 LRU를 스캔합니다. - 28행
shrink_lruvec()는 LRU 벡터에서 실제 페이지를 스캔합니다. inactive 리스트의 꼬리에서 페이지를 꺼내 회수를 시도합니다. - 29~32행4개 LRU 리스트: active/inactive x anon/file. 파일 캐시 페이지(file)는 디스크에 원본이 있어 회수가 저렴하고, 익명 페이지(anon)는 스왑 아웃이 필요합니다.
- 33행
shrink_slab()은 등록된 shrinker 콜백을 호출하여 dentry/inode 캐시 등 커널 내부 캐시를 회수합니다. - 39행priority는 12에서 시작하여 매 루프마다 감소합니다. priority가 낮을수록 더 많은 페이지를 스캔합니다 (스캔 비율 = 전체 / 2^priority).
Direct reclaim 성능 영향: direct reclaim이 빈번하면 할당 요청이 수십~수백 밀리초 지연될 수 있습니다. /proc/vmstat의 allocstall_normal 카운터가 증가하면 워터마크 조정(vm.watermark_scale_factor) 또는 메모리 증설을 검토하세요. 상세 회수 메커니즘은 Page Reclaim (vmscan) 문서를 참고하세요.
참고자료
커널 문서
- Memory Management Admin Guide — 커널 메모리 관리 관리자 가이드
- Memory Management Internal Documentation — 커널 MM 내부 문서
- Memory Management Concepts — 메모리 관리 핵심 개념
LWN 기사
- What every programmer should know about memory — Ulrich Drepper의 메모리 심층 분석 시리즈입니다 (2007)
- The state of memory management — 커널 메모리 관리 현황 종합 정리입니다 (2014)
- Memory management locking — 메모리 관리 잠금 메커니즘을 다룹니다 (2013)
- Memory tiering — CXL 기반 메모리 티어링 기술을 설명합니다 (2022)
커널 소스 코드
- mm/ 디렉토리 — 메모리 관리 서브시스템 전체 소스 코드입니다
참고 서적
- Mel Gorman, Understanding the Linux Virtual Memory Manager — 리눅스 가상 메모리 관리자의 구조를 체계적으로 분석한 참고서입니다
관련 문서
메모리 관리의 각 전문 주제를 심층적으로 다루는 문서입니다. 이 개요 페이지에서 전체 구조를 파악한 후, 관심 분야의 상세 문서로 이동하세요.