메모리 관리(Memory Management) 개요 (Memory Management Overview)

Linux 커널의 메모리 관리 서브시스템 전체를 조망합니다. 물리 메모리(Physical Memory) 구조, 할당자, 가상 메모리(Virtual Memory), 메모리 회수(Memory Reclaim), 스왑(Swap), 공유 메모리, 고급 기법까지 개요 수준으로 정리하고 각 전문 페이지(Page)로 안내합니다.

관련 표준: Intel SDM (x86 페이징, TLB), ARM ARM (AArch64 메모리 모델), JEDEC DDR5 (메모리 타이밍/구조) — 커널 메모리 관리가 참조하는 하드웨어 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: CPU 캐시(Cache) 문서를 먼저 읽으면 메모리 계층 이해에 도움이 됩니다.
일상 비유: 메모리 관리는 대형 도서관 운영과 비슷합니다. 서가(물리 메모리)를 구역(Zone)으로 나누고, 대출 카드(페이지 테이블(Page Table))로 위치를 추적하며, 자주 찾는 책은 데스크(캐시)에 두고, 공간이 부족하면 창고(스왑)로 보냅니다.

핵심 요약

  • 물리 메모리는 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), 공유 메모리/임시 파일.

단계별 이해

  1. 물리 구조 파악 — Node(NUMA) → Zone(DMA, Normal, Movable) → Page Frame(4KB). cat /proc/buddyinfo로 확인.
  2. Buddy Allocator — 2^n 블록 할당, 해제 시 buddy와 병합(coalescing).
  3. Slab 캐시 — 커널 오브젝트 풀. cat /proc/slabinfo로 확인.
  4. 가상 메모리 — 프로세스별 독립 주소 공간, 페이지 테이블+TLB로 변환.
  5. mmap과 VMA — Demand Paging: 접근 시(page fault) 물리 페이지 할당.
  6. 메모리 회수 — kswapd, direct reclaim, shrinker로 메모리 압박 대응.
  7. 대형 페이지 — THP/HugeTLB로 TLB 미스 감소, DB/JVM 성능 향상.
  8. 모니터링 — /proc/meminfo, /proc/vmstat, /proc/buddyinfo로 상태 파악.

시각적 개요: 메모리 계층 구조

이 다이어그램의 목적: Linux 커널의 복잡한 메모리 관리 계층을 한눈에 파악할 수 있습니다.
Linux 커널 메모리 계층 구조 가상 메모리 (Virtual Memory) 각 프로세스가 보는 연속된 주소 공간 (64비트: 0x0 ~ 0x7FFF_FFFF_FFFF) 페이지 테이블 물리 메모리 (Physical Memory) Node 0 (NUMA) ZONE_ DMA ZONE_ NORMAL ZONE_ MOVABLE Node 1 (NUMA) ZONE_ DMA ZONE_ NORMAL ZONE_ MOVABLE ... Buddy Allocator 페이지 프레임 (Page Frames - 4KB) Buddy Free Lists: 4K 8K 16K 32K ... 최대 4MB Slab/SLUB Caches: task_struct inode dentry 커널 API kmalloc(), kfree(), vmalloc(), __get_free_pages() 유저 API malloc(), free(), mmap(), brk() Zone/Cache Page Frame
핵심 포인트:
  1. 가상 메모리 → 페이지 테이블 → 물리 메모리: 각 프로세스는 독립된 가상 주소 공간을 가지며, MMU가 페이지 테이블을 통해 물리 주소로 변환
  2. Node (NUMA): 멀티 소켓(Socket) 시스템에서 각 CPU 소켓에 가까운 메모리 뱅크. 로컬 액세스가 빠름
  3. Zone: 하드웨어 제약에 따른 구분 (DMA: 24비트, NORMAL: 일반, MOVABLE: 메모리 압축(Memory Compaction)/핫플러그(Hotplug) 가능)
  4. Buddy Allocator: 2^n 페이지 블록 단위로 할당. 외부 단편화 최소화
  5. 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_DMA0 ~ 16MBISA DMA 전용 (레거시 디바이스)
ZONE_DMA320 ~ 4GB32비트 주소 DMA 가능 디바이스
ZONE_NORMAL4GB ~ 끝일반 커널 메모리 할당
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_lockedI/O 진행 중 페이지 잠금(Lock)
PG_referenced최근 접근됨 (LRU 에이징)
PG_uptodate디스크와 동기화 완료
PG_dirty수정됨, writeback 필요
PG_lruLRU 리스트에 포함
PG_activeactive LRU 리스트
PG_slabslab allocator가 사용
PG_reserved커널 예약 (swap 불가)
PG_compoundcompound 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에 반영
DDR5DIMM 서브채널(32+32bit), BL16, PMIC, On-Die ECC대역폭 크게 증가, 지연 변동 관리 중요처리량 향상, reclaim/원격 접근/리프레시 영향으로 지연 꼬리 가능
DDR6차세대 고대역폭 중심 설계(신호 방식/채널 구조 고도화)표준/제품 구현이 초기 단계드라이버/펌웨어(Firmware)/BIOS 성숙도와 RAS 검증이 중요
읽는 법: 세대 비교에서 "최대 전송률"만 보면 오판하기 쉽습니다. 커널 실측에서는 NUMA 원격 비율, 페이지 회수 강도, ECC 오류 추세를 함께 봐야 실제 병목(Bottleneck)이 드러납니다.
# 세대/속도 확인 (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는 단순 클럭 상승이 아니라 내부 병렬성과 전력 관리 방식을 함께 바꿨습니다:

항목DDR4DDR5커널/운영 영향
DIMM 내부 채널64-bit 1채널32-bit + 32-bit 서브채널짧은 버스(Bus)트의 병렬 처리 효율 개선
Burst LengthBL8BL16순차 접근 처리량 유리
전력 관리메인보드 PMICDIMM PMIC 내장고부하 시 발열/전력 안정성 중요
오류 보호모듈 ECC(옵션)On-Die ECC + 모듈 ECCOn-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와 통신합니다.

HBM 세대별 비교

세대JEDEC 표준출시DRAM 적층스택당 대역폭스택당 최대 용량주요 적용 제품
HBMJESD23520134-die128 GB/s1 GBAMD Fiji (Radeon R9 Fury)
HBM2JESD235A20164~8-die256 GB/s8 GBNVIDIA V100, AMD MI25/MI60
HBM2eJESD235B20208-die460 GB/s16 GBNVIDIA A100 (80GB), AMD MI100/MI250
HBM3JESD23820228~12-die665 GB/s24 GBNVIDIA H100, AMD MI300X
HBM3eJESD238A202412-die1.17 TB/s36 GBNVIDIA B200, AMD MI325X

HBM3 이후 세대에서는 ECC(Error-Correcting Code)가 기본 내장되며, 데이터센터 워크로드의 신뢰성 요구를 충족합니다. HBM3e는 스택당 최대 1.17 TB/s의 대역폭을 제공하여, 4-스택 구성(GPU 패키지 기준)에서 약 4.7 TB/s에 달하는 메모리 대역폭을 실현합니다.

DDR5 vs HBM3e 비교

항목DDR5HBM3e비고
인터페이스 폭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 nsHBM이 약간 높을 수 있음
용량 확장DIMM 슬롯 추가 (유연)패키지 내 제한 (4~6 스택)DDR이 확장성 유리
전력 효율~8 pJ/bit~3.9 pJ/bitHBM이 비트당 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로 디모션하는 자동 티어링을 수행합니다.

💡

관련 문서: HMM 프레임워크와 ZONE_DEVICE 상세는 HMM 개요HMM 상세 페이지를, CXL 기반 메모리 확장은 CXL 메모리 개요CXL 메모리 페이지를, GPU 메모리 계층은 GPUROCm 페이지를 참고하세요.

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 Allocator — 분할(Split) & 병합(Coalescing) 분할 (Split) — order 1 요청 시 order 3 (32KB) — free list에서 가져옴 split order 2 (16KB) order 2 → free list 반환 split order 1 (8KB) 할당! order 1 → free list 병합 (Coalescing) — order 0 해제 시 해제된 4KB Buddy (free) merge order 1 (8KB) Buddy (free) merge order 2 (16KB) — free list에 반환 Buddy Free Lists (per Zone) order 0 4K 4K 4K ... order 1 8K 8K ... order 2 16K ... order 10 4MB (MAX_ORDER) Per-CPU Page Lists (PCP) • order 0 할당의 대부분은 Per-CPU 리스트에서 lockless로 처리 • PCP 고갈 시 Buddy free list에서 batch 단위로 보충 • PCP 초과 시 Buddy free list로 batch 반환 • migrate type별 분리: UNMOVABLE / MOVABLE / RECLAIMABLE • order 1~3까지 PCP 지원 (커널 6.x) • 확인: /proc/zoneinfo → pagesets → count, high, batch 할당됨 Free list 반환 Buddy (인접 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 3단계 할당 구조 CPU 0 1단계 Per-CPU Active Slab freelist → lockless 할당/해제 (가장 빠름) 2단계 CPU Partial List — lockless, active slab 소진 시 전환 3단계 → Node Partial List (락 필요, NUMA 노드 공유) CPU 1 1단계 Per-CPU Active Slab freelist → lockless 할당/해제 (가장 빠름) 2단계 CPU Partial List 3단계 → Node Partial List Node Partial List (NUMA Node 0) 슬랩 페이지 풀 — list_lock 보호, 모든 CPU 공유, 부분 사용 슬랩 보관 CPU Partial도 소진 시 여기서 슬랩을 가져와 CPU Partial로 승격 모두 소진 시 Buddy Allocator — 새 슬랩 페이지 할당 슬랩 페이지 내부: free obj 사용중 free obj 사용중 free obj ... (freelist 연결) 인라인 메타데이터

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_DMA32ISA 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_PRESENT0페이지 존재 여부
_PAGE_RW1읽기/쓰기 권한 (0=읽기전용)
_PAGE_USER2유저 모드 접근 가능
_PAGE_ACCESSED5최근 접근됨 (LRU 에이징에 사용)
_PAGE_DIRTY6수정됨 (writeback 필요)
_PAGE_NX63No-Execute (실행 방지)
x86_64 가상 주소 변환 (4단계 페이지 테이블) 48비트 가상 주소 비트 분해 PGD 인덱스 비트 47:39 (9b) PUD 인덱스 비트 38:30 (9b) PMD 인덱스 비트 29:21 (9b) PTE 인덱스 비트 20:12 (9b) 페이지 오프셋 비트 11:0 (12b = 4KB) → 각 레벨 512 엔트리 (2^9) CR3 (PGD) PGD 512 엔트리 × 8B PGD[idx] PUD 512 엔트리 × 8B PUD[idx] PSE=1 → 1GB 페이지 PMD 512 엔트리 × 8B PMD[idx] PSE=1 → 2MB 페이지 PTE 512 엔트리 × 8B PTE[idx] 4KB 물리 페이지 + 페이지 오프셋 TLB (Translation Lookaside Buffer) 히트 시 테이블 워크 생략 → 1~2 사이클 TLB 히트 → 직접 물리 주소 TLB 미스 시 하드웨어 워크: 메모리 접근 4회 (PGD→PUD→PMD→PTE) + 최종 데이터 접근 = 총 5회 메모리 참조 5단계 (LA57): P4D 추가 → 비트 56:48 = PGD, 47:39 = P4D, 최대 128PB 가상 주소 공간

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_PRIVATECOW: 쓰기 시 사본 생성실행 파일, 라이브러리
MAP_ANONYMOUS파일 없는 익명 매핑heap, 스택
MAP_HUGETLBHuge Page 사용대용량 메모리 앱
MAP_FIXED지정 주소에 정확히 매핑로더(Loader), JIT
# 프로세스의 VMA 매핑 확인
cat /proc/self/maps
# 주소범위          권한 offset   dev   inode  경로
# 5555555544000-5555555546000 r--p 00000000 fd:01 123456 /usr/bin/bash
x86_64 프로세스 가상 주소 공간 레이아웃 0x7FFF... 커널 공간 (유저 접근 불가) Stack [rw-p] 아래로 성장 ↓ (GROWSDOWN) Stack Guard Gap (1MB) mmap 영역 라이브러리, 파일 매핑, 큰 malloc 아래로 성장 ↓ (기본 레이아웃) libc.so, ld.so, MAP_ANONYMOUS 등 사용 가능 가상 공간 Heap [rw-p] 위로 성장 ↑ (brk() 확장) mm→start_brk ~ mm→brk BSS (초기화되지 않은 전역/static) [rw-p] Data (초기화된 전역/static) [rw-p] Text (코드) [r-xp] NULL 매핑 금지 구간 (0x0~0x10000) 0x0 vm_area_struct (VMA) 핵심 필드 vm_start, vm_end — 가상 주소 범위 vm_flags — VM_READ|VM_WRITE|VM_EXEC|VM_SHARED vm_file — 매핑된 파일 (NULL=익명) vm_pgoff — 파일 내 오프셋 (페이지 단위) vm_ops — fault(), map_pages() 콜백 anon_vma — COW 역매핑 (rmap) vm_page_prot — PTE 보호 비트 기본값 VMA 검색: maple tree (6.1+) — O(log n) Demand Paging 흐름 1. mmap() → VMA 생성 (물리 메모리 미할당) 2. 첫 접근 → CPU page fault 예외 발생 3. 커널 폴트 핸들러 → 물리 페이지 할당 + PTE 설정 4. 유저 공간으로 복귀, 명령어 재실행 ASLR (Address Space Layout Randomization) • Stack, mmap, Heap, Text 시작 주소를 무작위화 • ROP/JOP 등 코드 재사용 공격 난이도 상승 • /proc/sys/kernel/randomize_va_space (0/1/2)

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 (일반)4MBPGD→PUD→PMD→PTE→물리 페이지일반 애플리케이션
2MB (HugeTLB)2GBPMD에서 직접 매핑 (PSE 비트)DB, DPDK, KVM, JVM
1GB (HugeTLB)1TBPUD에서 직접 매핑대규모 인메모리 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);
일반 페이지 vs Huge Page — TLB 커버리지 비교 4KB 일반 페이지 (1024 TLB 엔트리) ... 1024개의 4KB 페이지 ... 총 커버리지: 4MB 1024 × 4KB = 4MB — 대용량 DB에 턱없이 부족 2MB Huge Page (1024 TLB 엔트리) ... 총 커버리지: 2GB 1024 × 2MB = 2GB — 512배 향상! Huge Page 유형 비교 Static HugeTLB: 부트 시 또는 런타임에 명시적 예약. 전용 풀. 수동 관리 필요. DPDK/KVM에 적합 THP (Transparent): 커널이 자동으로 2MB 페이지 사용. 애플리케이션 수정 불필요. compaction 비용 발생 가능 mTHP (커널 6.8+): 16KB/32KB/64KB 등 중간 크기 huge page. 2MB 연속 확보 어려울 때 유용 folio (커널 5.16+): compound page의 추상화. head/tail page 구분 제거, 페이지 캐시 성능 개선 권장: THP=madvise + 필요 시 madvise(MADV_HUGEPAGE), DB는 Static HugeTLB 또는 THP 비활성화
💡

상세 문서: 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)
NUMA 토폴로지 — 2소켓 서버 예시 NUMA Node 0 CPU 소켓 0 Core 0~7 (16 HT) L1/L2/L3 캐시 로컬 DDR 64GB (4×16GB) ~100ns 접근 지연 ZONE_DMA32(4GB) | ZONE_NORMAL(~56GB) | ZONE_MOVABLE kswapd0 (노드별) NUMA Node 1 CPU 소켓 1 Core 8~15 (16 HT) L1/L2/L3 캐시 로컬 DDR 64GB (4×16GB) ~100ns 접근 지연 ZONE_DMA32(4GB) | ZONE_NORMAL(~56GB) | ZONE_MOVABLE kswapd1 (노드별) 인터커넥트 (QPI/UPI) 원격 접근: ~200ns (2× 지연) NUMA 메모리 정책 (Memory Policy) MPOL_LOCAL (기본) MPOL_BIND MPOL_INTERLEAVE MPOL_PREFERRED 실행 CPU의 로컬 노드 지정 노드에만 할당 노드 간 라운드로빈 선호 노드 우선 시도 AutoNUMA (NUMA balancing): 접근 패턴 기반 자동 페이지 마이그레이션 (커널 3.13+)

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 동작 원리 — 평시 vs DMA 할당 시 평시: CMA 영역을 movable 페이지가 사용 사용자 캐시 사용자 free 사용자 캐시 free 사용자 movable 페이지가 CMA 영역을 일반 메모리처럼 사용 → 메모리 낭비 없음 DMA 연속 할당 요청! DMA 할당: 마이그레이션 후 연속 공간 확보 DMA 연속 버퍼 (확보됨) free 이동됨 이동됨 이동됨 기존 movable 페이지를 CMA 영역 밖으로 마이그레이션 → 연속 공간 확보 CMA 핵심 특성 • 부트 시 연속 물리 영역을 예약 (cma=NM) • 예약 영역은 MIGRATE_CMA 타입으로 관리 • 평시에는 movable 페이지만 배치 가능 • DMA 요청 시 마이그레이션으로 연속 확보 • IOMMU 없는 디바이스의 scatter-gather 불가 시나리오에서 필수 (비디오 코덱, 카메라 등) • dma_alloc_coherent() → CMA 경로로 자동 연결 디버깅: CONFIG_CMA_DEBUG=y, /sys/kernel/debug/cma/ CMA vs HugeTLB 예약: CMA는 평시 일반 사용 가능 (낭비 없음), HugeTLB는 예약 즉시 전용 (유연성 낮음) CMA 실패 원인: unmovable 페이지 침투(slab, page table), 마이그레이션 타임아웃 → 사전에 MIGRATE_CMA 분리 필수
💡

상세 문서: CMA 예약 방식, dma_alloc_coherent() 경로, 디바이스 트리 설정, 디버깅 등은 CMA 페이지에서 확인할 수 있습니다.

메모리 회수 (Page Reclaim)

메모리 압박 시 커널은 다양한 메커니즘으로 페이지를 회수합니다. LRU(Least Recently Used) 리스트로 페이지의 활동 빈도를 추적하고, 비활성 페이지부터 회수합니다. 회수 대상은 크게 두 가지입니다: 파일 기반 페이지(page cache — 원본 파일에서 다시 읽을 수 있음)와 익명 페이지(Anonymous Page)(heap, stack — 스왑 영역(Swap Area)으로 내보내야 함).

회수 경로

Watermark 체계

각 Zone은 세 단계의 watermark를 유지합니다:

Watermark트리거동작
highfree > high회수 중단. kswapd 수면
lowfree < lowkswapd 깨움 (백그라운드 회수 시작)
minfree < minDirect 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
ℹ️

관련 문서: LRU 리스트 관리 상세는 LRU Cache, 커널 캐시 회수 프레임워크는 Shrinker 페이지를 참고하세요.

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 보호
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 블록을 만듭니다.

# 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 Out / Swap In 흐름 Swap Out — 메모리 회수 시 익명 페이지 (heap, stack 등) LRU 회수 Swap Cache 중복 I/O 방지 zswap 압축 (활성화 시) 압축 실패 시 스왑 디바이스 디스크/SSD/zram 페이지 해제! PTE 변환: 물리 주소 → swap entry (type:5비트 + offset:스왑 영역 내 위치) | Present=0으로 설정 Swap In — 페이지 폴트 시 페이지 접근! PTE: Present=0 page fault do_swap_page() swap entry 파싱 캐시 확인 Swap Cache 히트 → I/O 생략! 미스 시 디스크/zswap에서 페이지 복구 PTE 복원 재실행 스왑 공간 구성 비교 파티션 스왑: mkswap /dev/sdX + swapon — 성능 최적, 전용 파티션 필요 파일 스왑: dd + mkswap + swapon — 유연하지만 파일시스템 레이어 통과로 약간의 오버헤드 zram 스왑: RAM 압축 블록 디바이스 — 디스크 I/O 없음, CPU 사용, 디스크리스 시스템에 적합

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 서브시스템 페이지에서 확인할 수 있습니다.

주요 개념

# 스왑 상태 확인
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에 구현되며, 다음 용도로 광범위하게 사용됩니다:

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)가 있습니다.

기술 문서: stable/unstable 트리 알고리즘, ksmd 내부 상태 머신, NUMA-aware 병합, side-channel 보안 이슈, 컨테이너(Container)/VM 최적화 등 상세 내용은 KSM (Kernel Same-page Merging) 페이지를 참조하세요.
# 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 대상에서 제외 */
KSM 병합 알고리즘 — Stable & Unstable Tree MADV_MERGEABLE 스캔 대상 페이지 ksmd 주기적 스캔 내용 비교 Stable Tree (Red-Black Tree) 이미 병합 완료된 페이지 (write-protected) 내용 기반 정렬 → memcmp로 O(log n) 검색 매치 → 기존 병합 페이지에 PTE 추가 매핑 내용 변경 불가 (쓰기 시 COW → 분리) Unstable Tree (Red-Black Tree) 아직 병합 안 된 후보 페이지 stable에 매치 없으면 여기서 검색 매치 → 두 페이지를 병합 → stable로 승격 매 스캔 주기마다 초기화 (내용 변경 가능) 미스 병합 성공 → 승격 병합 결과 (메모리 절약) 병합 전: VM1 4KB VM2 4KB VM3 4KB = 12KB 병합 후: 공유 4KB ← VM1,VM2,VM3 공유 = 4KB (8KB 절약!) VM2가 쓰기 시: 공유 COW 사본 ← VM2 독립 KSM 주의사항 CPU 오버헤드: ksmd의 페이지 스캔/비교 비용 — pages_to_scan, sleep_millisecs로 조절 보안: 병합된 페이지의 COW 지연 차이로 side-channel 공격 가능 → 보안 민감 환경에서 주의

DAMON (Data Access Monitor)

DAMON은 커널 5.15에서 도입된 메모리 접근 패턴 모니터링 프레임워크입니다. 영역(region)을 적응적으로 분할/병합하며 sampling 기반으로 접근 빈도를 추적합니다. 오버헤드가 낮아(1% 미만) 프로덕션에서도 사용 가능합니다. DAMON 기반 정책 엔진(Policy Engine)으로 reclaim(damon_reclaim)과 LRU 정렬(damon_lru_sort)을 자동화합니다.

기술 문서: DAMOS 액션/필터/쿼터, sysfs 인터페이스 전체 구조, damo 도구, DAMON vs MGLRU 비교, NUMA 티어링 연동, 내부 자료구조 등은 DAMON (Data Access MONitor) 페이지를 참조하세요.
# 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 비율을 조정할 수 있습니다.

Memory Hotplug 상태 전이 미등록 DIMM/CXL 장착 Hot-Add Offline (등록됨) struct page 생성 완료 Online Offline (마이그레이션) Online (활성) Buddy free list에 추가됨 Hot-Remove 제거됨 online_movable: ZONE_MOVABLE에 추가 → 나중에 오프라인 가능 (권장) online_kernel: ZONE_NORMAL에 추가 → 커널 할당 가능하나 제거 어려움 auto_online: /sys/devices/system/memory/auto_online_blocks = online_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 리스트와 회수 정책을 가집니다.

Memory Cgroup v2 — 계층적 메모리 제어 / (root cgroup) — 시스템 전체 메모리 system.slice memory.max = 4G memory.high = 3.5G (soft) memory.low = 1G (보호) app.slice memory.max = 8G memory.high = 7G memory.swap.max = 2G guest.slice (KVM/container) memory.max = 16G memory.oom.group = 1 (그룹 OOM: 전체 kill) cgroup v2 메모리 제어 계층 memory.min (절대 보호) memory.low (노력 보호) memory.high (throttle) memory.max (초과 → OOM) 이 양 이하로는 절대 회수 안 됨 가능하면 보호, 전역 압박 시 회수 초과 시 reclaim 유도 + 할당 지연 하드 제한 — 초과 시 cgroup OOM Killer min ≤ low ≤ high ≤ max — 계층적으로 자식 cgroup 합계가 부모를 초과할 수 없음 (과할당 가능하나 실제 사용량은 부모 제한)
기술 문서: charge/uncharge 메커니즘, memcg OOM(memory.oom.group), PSI 연동, 커널 메모리 계정, K8s/Docker/systemd 연동, v1→v2 마이그레이션, 내부 자료구조 등은 Memory Cgroup (메모리 컨트롤 그룹) 페이지를 참조하세요.
# 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.32KSMKernel Samepage Merging — 동일 페이지 병합
2.6.36oom_score_adjOOM Killer 점수 계산 단순화 (RSS 기반)
2.6.38THPTransparent Huge Pages 도입
3.5CMAContiguous Memory Allocator 도입
3.11zswap압축 기반 swap 캐시 도입
3.15zram메인라인 staging 졸업, 압축 RAM 블록 디바이스
3.17memfd_create파일 디스크립터 기반 익명 메모리 + sealing
4.12PSI 기초lowmemorykiller 제거, 유저스페이스 lmkd로 전환
4.14memcg v2cgroup v2 메모리 컨트롤러 안정화
4.20PSIPressure Stall Information — /proc/pressure/memory
5.9Proactive Compaction백그라운드 자동 compaction
5.12KFENCE프로덕션용 경량 메모리 오류 감지
5.15DAMON데이터 접근 모니터링 프레임워크
5.18DAMON sysfs / ashmem 제거DAMON sysfs 인터페이스, ashmem 드라이버 제거
6.0DAMON LRU SortDAMON 기반 LRU 리스트 최적화
6.1maple tree / MGLRUVMA 관리에 maple tree 도입, Multi-Gen LRU
6.5SLAB/SLOB 제거SLUB이 유일한 slab allocator로 통합
6.8mTHPMulti-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=yCONFIG_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=yslub_debug 활성화. (4) 모듈 exit에서 init의 모든 자원 해제 확인.

⚠️

이중 해제(double free) 위험: 이미 해제된 slab 객체를 다시 kfree()하면 freelist가 손상됩니다. slub_debug=FZPU 부트 옵션으로 조기 탐지할 수 있으며, 해제 후 포인터를 NULL로 초기화하는 것이 안전합니다.

Dirty COW (CVE-2016-5195) — Copy-on-Write 경쟁 조건

심각도: 높음 (CVSS 7.8) 2007년 Linux 2.6.22에서 도입되어 9년간 잠복한 뒤 2016년 Linux 4.8.2에서 수정된 로컬 권한 상승 취약점입니다. 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);
}
교훈: COW처럼 지연(lazy) 처리하는 코드는 경쟁 조건에 취약합니다. TOCTOU 패턴에서 검증(check)과 사용(use) 사이에 상태가 변경되면 보안 경계가 무너질 수 있습니다.

KASAN이 발견한 Use-After-Free (CVE-2016-8655)

심각도: 높음 (CVSS 7.8) AF_PACKET 소켓의 race condition으로 인한 use-after-free 취약점입니다. 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 탐지 윈도우 확대 */
교훈: Use-After-Free는 커널에서 가장 빈번한 메모리 취약점입니다. 해제 전 타이머(del_timer_sync())와 워크큐(cancel_work_sync())를 반드시 정리하고, RCU 기반 수명 관리로 안전한 해제 시점을 보장해야 합니다.

Stack Guard Page 우회 — Stack Clash (CVE-2017-1000364)

심각도: 높음 (CVSS 7.4) 스택과 인접 메모리 영역(heap, mmap) 사이의 guard page를 큰 alloca()나 VLA로 한 번에 건너뛰어 우회하는 공격입니다.
Guard Page 레이아웃 및 우회 공격 정상 상태 (Guard Page 유효) 높은 주소 Stack (grows downward) Guard Page (4KB → 수정 후 1MB) 접근 시 SIGSEGV (PROT_NONE) Heap / mmap 동적 할당 영역 공격: 큰 alloca()로 Guard 우회 Stack alloca(user_size) — size > 4KB Guard Page 건너뜀! Guard Page (우회됨) Heap / mmap (덮어씀!) 커널 패치 (CVE-2017-1000364 대응) Guard gap 확대: 4KB(1 page) → 1MB(256 pages) — STACK_GUARD_GAP = 256 expand_stack()에서 인접 VMA와의 거리 검증 강화, stack_guard_gap 부트 파라미터 도입

커널 수정 사항:

/* 수정 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 (페이지 단위)
교훈: 단일 guard page(4KB)는 큰 스택 할당에 대한 보호로 불충분합니다. 방어 메커니즘은 공격자가 한 번에 건너뛸 수 있는 최대 크기를 고려해야 합니다. 사용자 입력에 의존하는 VLA와 alloca()는 커널 코드에서 사용을 지양하며, Linux 커널은 -Wvla로 VLA 사용을 금지하고 있습니다.

Transparent Huge Pages (THP) OOM 문제

프로덕션 영향: 높음 THP의 compaction 과정에서 khugepaged 커널 스레드가 과도한 CPU를 소모하여 프로덕션 서버 성능이 심각하게 저하되는 문제입니다.

문제 발생 메커니즘: 프로세스가 페이지 폴트를 발생시키면, THP가 always로 설정된 경우 2MB huge page 할당을 시도합니다. 연속 2MB 물리 메모리가 없으면 compaction을 시작하는데, 메모리 단편화가 심한 경우 compaction이 반복 실패합니다. 동시에 khugepaged 커널 스레드가 기존 4KB 페이지들을 2MB로 합체(collapse)하려 시도하면서 CPU 100%를 점유하고, 실제 워크로드에 CPU 자원이 부족해져 응답 지연 → OOM killer 발동으로 이어질 수 있습니다.

애플리케이션THP 영향권장 설정
Redisfork() 기반 RDB/AOF 시 COW로 메모리 2배 사용, latency spikeTHP 비활성화 필수
MongoDBWiredTiger 엔진과 충돌, 성능 불안정THP 비활성화 권장
Oracle DBHugePages(명시적)와 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의 필수 전제 조건입니다.

버그 사례 비교 요약

버그유형잠복 기간영향핵심 교훈
MeltdownHW 투기적 실행수년커널 메모리 유출KPTI로 페이지 테이블 분리
Dirty COWRace Condition (TOCTOU)9년 (2007~2016)로컬 권한 상승COW 경로의 원자성 보장 필수
AF_PACKET UAFUse-After-Free수개월로컬 권한 상승KASAN 활용, 해제 전 정리
Stack ClashGuard 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/buddyinfocompaction 튜닝, hugepage 정책 점검
slab 급증/proc/slabinfo, SReclaimable캐시별 shrinker 경로 점검
지속적 swap I/Ovmstat si/so, swappinessworking set 축소, zswap/zram 재검토
fault pressureminor/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/meminfoMemAvailable이 전체의 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() 후 부모/자식이 공유하던 페이지에 쓰기가 발생하면 복사본을 만듭니다.
페이지 폴트 처리 결정 트리 exc_page_fault() handle_mm_fault() handle_pte_fault() do_fault() 파일 매핑 폴트 pte_none + vm_ops do_read_fault() do_cow_fault() do_shared_fault() do_anonymous_page() 새 제로 페이지 할당 pte_none + 익명 do_swap_page() 스왑에서 복구 !pte_present do_numa_page() NUMA 밸런싱 pte_protnone do_wp_page() Copy-on-Write write + !pte_write 핵심 흐름 요약 1. CPU 예외 → exc_page_fault() → VMA 검색 → handle_mm_fault() 2. PGD→P4D→PUD→PMD 테이블 워크 → handle_pte_fault()에서 PTE 상태 확인 3. PTE 상태에 따라 5가지 핸들러 중 하나로 디스패치 4. 물리 페이지 할당/매핑 후 유저 공간으로 복귀 (재실행)
💡

성능 관점: 마이너 폴트(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()OMAX_ORDER (보통 4MB)GFP 플래그 의존직접 Buddy
__get_free_pages()OMAX_ORDERGFP 플래그 의존alloc_pages() + page_address()
kvmalloc()시도 후 fallbackkmalloc 실패 시 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단계 워터마크를 유지합니다.

메모리 할당 API 계층 구조 kmalloc() kvmalloc() vmalloc() __get_free_pages() alloc_pages() fallback Slab/SLUB 할당자 오브젝트 캐시 vmalloc 영역 페이지별 할당 + vmap 페이지별 슬랩 페이지 확보 __alloc_pages() Buddy Allocator 핵심 진입점 get_page_from_freelist() 빠른 경로 (워터마크 확인) __alloc_pages_slowpath() 느린 경로 (회수/압축/OOM) rmqueue() — Buddy free list ① direct_reclaim() ② try_to_compact_pages() ③ wake_all_kswapds() ④ out_of_memory() — OOM kill 빠른 경로 느린 경로 핵심 함수 가상 연속

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 읽기가 가능합니다.
mm_struct — VMA — 페이지 테이블 관계 task_struct →mm mm_struct mm_mt (maple tree → VMAs) pgd (페이지 테이블 루트) mmap_lock (rw_semaphore) mm_users mm_count total_vm, locked_vm, data_vm, ... start_code..brk, start_stack VMAs (vm_area_struct) [코드] 0x400000–0x4FF000 r-x [데이터] 0x600000–0x601000 rw- [힙] 0x1000000–0x1100000 rw- [스택] 0x7FFF...–0x7FFF... rw- 페이지 테이블 (4단계) PGD PUD PMD PTE 물리 페이지 물리 메모리 (RAM) 4KB 4KB 4KB 4KB struct page 배열로 추적

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_FLAGSVM_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 페이지를 확보합니다. 회수는 세 가지 경로로 트리거됩니다:

/* 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).
메모리 회수 트리거 경로 Zone 워터마크 WMARK_HIGH kswapd 휴면 정상 영역 WMARK_LOW kswapd 깨어남 백그라운드 회수 WMARK_MIN direct reclaim 발생 kswapd (백그라운드) NUMA 노드별 데몬 Direct Reclaim 할당 호출자가 직접 회수 shrink_node() shrink_lruvec() inactive_file (회수 최우선) active_file (승격 경쟁) inactive_anon (스왑 필요) active_anon (최후 수단) OOM Killer 회수 실패 시 프로세스 종료 실패 shrink_slab() 회수 우선순위: inactive_file → active_file → inactive_anon (스왑) → active_anon MGLRU (v6.1+)는 세대 기반 알고리즘으로 LRU 스캔 효율을 대폭 개선합니다
⚠️

Direct reclaim 성능 영향: direct reclaim이 빈번하면 할당 요청이 수십~수백 밀리초 지연될 수 있습니다. /proc/vmstatallocstall_normal 카운터가 증가하면 워터마크 조정(vm.watermark_scale_factor) 또는 메모리 증설을 검토하세요. 상세 회수 메커니즘은 Page Reclaim (vmscan) 문서를 참고하세요.

참고자료

커널 문서

LWN 기사

커널 소스 코드

참고 서적

메모리 관리의 각 전문 주제를 심층적으로 다루는 문서입니다. 이 개요 페이지에서 전체 구조를 파악한 후, 관심 분야의 상세 문서로 이동하세요.