VMA / mmap

Linux 커널 가상 메모리(Virtual Memory) 매핑(Mapping): mmap 시스템 콜(System Call) 인터페이스, VMA 구조와 관리, 페이지 폴트(Page Fault) 처리, MAP_SHARED/PRIVATE 공유 메모리, mremap/mprotect/madvise, userfaultfd, 프로세스(Process) 주소 공간(Address Space) 레이아웃 종합 가이드.

관련 페이지(Page): 기본 메모리 관리(Memory Management)는 메모리 관리 개요, 고급 메모리 관리는 메모리 관리 개요 페이지를 참고하세요. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 메모리 관리 개요CPU 캐시(Cache) 문서를 먼저 읽으세요. 메모리 서브시스템은 가상 메모리와 물리 메모리(Physical Memory) 정책이 동시에 동작하므로, 주소 변환(Address Translation)과 회수 정책을 같이 보는 관점이 필요합니다.
일상 비유: 이 주제는 창고 적재와 재배치(Relocation) 운영과 비슷합니다. 빈 공간을 잘 배치해야 출고가 빨라지듯이, 페이지 배치/회수 정책이 성능과 지연(Latency)을 직접 좌우합니다.

핵심 요약

  • mmap() — 파일, 익명 메모리, 디바이스 등을 프로세스의 가상 주소 공간에 매핑하는 시스템 콜입니다.
  • VMA (vm_area_struct) — 프로세스 주소 공간의 연속된 가상 메모리 영역을 나타내는 커널 자료구조입니다.
  • Demand Paging — mmap 시 가상 주소(Virtual Address)만 확보하고, 실제 물리 페이지는 첫 접근 시(page fault) 할당하는 지연 할당 기법입니다.
  • MAP_SHARED / MAP_PRIVATE — 공유 매핑은 모든 프로세스가 동일한 물리 페이지를 공유하고, 사유 매핑은 쓰기 시 COW(Copy-On-Write)로 복사합니다.
  • mprotect / mremap / madvise — 매핑된 영역의 보호 속성 변경, 크기 조정, 접근 힌트 제공 등 mmap 후 동적 제어 시스템 콜입니다.

단계별 이해

  1. mmap 호출 → VMA 생성mmap()을 호출하면 커널은 vm_area_struct를 생성하여 주소 범위, 권한, 파일 매핑 정보를 기록합니다.

    이 시점에는 페이지 테이블(Page Table) 엔트리가 없고, 가상 주소만 예약된 상태입니다.

  2. 첫 접근 → Page Fault — 프로세스가 매핑된 주소에 접근하면 page fault가 발생합니다. 커널은 VMA를 조회하여 유효한 접근인지 확인합니다.

    유효하면 물리 페이지를 할당하고 페이지 테이블에 매핑을 생성합니다. 파일 매핑이면 파일에서 데이터를 읽어옵니다.

  3. COW (Copy-On-Write) — MAP_PRIVATE 매핑에서 쓰기를 시도하면 물리 페이지를 복사하여 프로세스 전용 페이지로 분리합니다.

    fork() 후 부모-자식이 동일한 VMA를 공유하다가, 쓰기 시 비로소 복사되어 메모리를 절약합니다.

  4. 메모리 보호와 제어 — mprotect()로 실행 금지(NX), 읽기 전용(Read-Only) 등 권한을 동적으로 변경할 수 있습니다.

    JIT 컴파일러, 샌드박스(Sandbox), 디버거 등이 이를 활용하며, 보안 강화(ASLR, DEP)의 핵심 메커니즘입니다.

mmap — 가상 메모리 매핑

mmap()은 프로세스의 가상 주소 공간에 메모리 영역을 매핑하는 핵심 시스템 콜입니다. 파일 I/O, 공유 메모리, 익명 메모리 할당, 디바이스 메모리 접근 등 커널의 거의 모든 메모리 관련 기능이 mmap()을 통해 구현됩니다.

mmap()이 중요한 이유는 리눅스에서 메모리를 다루는 거의 모든 경로가 이 시스템 콜을 거치기 때문입니다. malloc()으로 큰 블록을 할당할 때, 공유 라이브러리(.so)를 로드할 때, 프로세스 간 공유 메모리를 설정할 때, GPU나 NIC 같은 디바이스 메모리를 사용자 공간에서 접근할 때 모두 내부적으로 mmap()이 동작합니다.

mmap()의 핵심 개념은 지연 할당(Lazy Allocation)입니다. mmap()을 호출하면 커널은 즉시 물리 메모리를 할당하지 않고, 가상 주소 공간에 VMA(Virtual Memory Area)만 생성합니다. 실제 물리 페이지는 프로세스가 해당 주소에 처음 접근할 때 페이지 폴트(Page Fault)를 통해 할당됩니다. 이 Demand Paging 방식 덕분에 수 GB를 mmap()해도 실제 메모리 소비는 접근한 페이지 분량만큼만 발생합니다.

mmap 주요 사용 장면

사용 장면매핑 방식대표 사례
파일 I/O 가속파일 매핑 (MAP_SHARED / MAP_PRIVATE)데이터베이스 엔진(SQLite, LMDB), 로그 분석
대용량 메모리 할당익명 매핑 (MAP_ANONYMOUS | MAP_PRIVATE)malloc() 내부 (glibc mmap threshold 초과 시)
프로세스 간 공유 메모리MAP_SHARED + shm_open 또는 memfd_createIPC, 공유 캐시, 멀티미디어 버퍼
공유 라이브러리 로딩파일 매핑 (MAP_PRIVATE + PROT_EXEC)동적 링커(ld-linux.so)의 .so 로드
디바이스 메모리 접근드라이버 mmap 핸들러GPU(DRM), NIC(DPDK), FPGA
JIT 컴파일MAP_ANONYMOUS + mprotect(PROT_EXEC)V8, LuaJIT, eBPF JIT

기본 사용 예제: 파일 매핑

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

int main(void)
{
    int fd = open("/etc/passwd", O_RDONLY);
    if (fd < 0) { perror("open"); return 1; }

    /* 파일 크기 조회 */
    struct stat st;
    fstat(fd, &st);

    /* 파일을 읽기 전용으로 프로세스 주소 공간에 매핑
     * - addr=NULL: 커널이 적절한 가상 주소를 선택
     * - MAP_PRIVATE: 쓰기 시 COW 복사 (원본 파일 변경 안 됨)
     * - 이 시점에서는 VMA만 생성되고, 물리 페이지는 아직 할당되지 않음 */
    char *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if (map == MAP_FAILED) { perror("mmap"); close(fd); return 1; }

    /* 매핑 완료 후 fd를 닫아도 매핑은 유지됨
     * (커널이 struct file 참조 카운트를 유지) */
    close(fd);

    /* 매핑된 메모리에 접근 → 이 순간 page fault 발생 → 물리 페이지 할당
     * 페이지 캐시에 파일 데이터가 있으면 minor fault (디스크 I/O 없음)
     * 없으면 major fault (디스크에서 읽기) */
    printf("첫 128바이트: %.128s\n", map);

    /* 매핑 해제 — VMA 삭제, 페이지 테이블 엔트리 제거, TLB 플러시 */
    munmap(map, st.st_size);
    return 0;
}
설명

mmap()으로 파일을 매핑하면 read()/write() 시스템 콜 없이 메모리 접근만으로 파일 데이터를 읽고 쓸 수 있습니다. 핵심 동작 순서는 다음과 같습니다:

  • mmap 호출 시점 — 커널은 vm_area_struct를 할당하고 Maple Tree에 삽입합니다. 이때 물리 메모리 할당은 일어나지 않습니다 (가상 주소 예약만 수행).
  • 첫 접근 시점 — CPU가 해당 가상 주소를 참조하면 PTE(Page Table Entry)가 비어 있으므로 페이지 폴트가 발생합니다. 커널은 VMA를 확인하여 파일 매핑임을 인식하고, 페이지 캐시에서 데이터를 가져옵니다.
  • fd close 후에도 매핑 유지mmap() 시 커널이 struct file의 참조 카운트(f_count)를 증가시키므로, 사용자 공간에서 close()해도 매핑은 정상 동작합니다.
  • munmap 시점 — VMA 제거, PTE 클리어, TLB 무효화가 순차적으로 진행됩니다. 페이지 캐시의 데이터는 다른 참조가 없으면 LRU에 의해 자연스럽게 회수됩니다.

mmap 시스템 콜 인터페이스

#include <sys/mman.h>

void *mmap(
    void    *addr,    /* 요청 시작 주소 (NULL이면 커널이 선택) */
    size_t   len,     /* 매핑 길이 (바이트) */
    int      prot,    /* 보호 플래그: PROT_READ|WRITE|EXEC|NONE */
    int      flags,   /* 매핑 플래그: MAP_SHARED|PRIVATE|ANONYMOUS|... */
    int      fd,      /* 파일 디스크립터 (MAP_ANONYMOUS이면 -1) */
    off_t   offset   /* 파일 내 오프셋 (PAGE_SIZE 배수) */
);
int munmap(void *addr, size_t len);
/* 커널 측 syscall 정의 (arch/x86/kernel/sys_x86_64.c) */
SYSCALL_DEFINE6(mmap, unsigned long, addr, unsigned long, len,
    unsigned long, prot, unsigned long, flags,
    unsigned long, fd,   unsigned long, off)
{
    if (offset_in_page(off))
        return -EINVAL;
    return ksys_mmap_pgoff(addr, len, prot, flags, fd,
                            off >> PAGE_SHIFT);
}

/* ksys_mmap_pgoff → vm_mmap_pgoff → do_mmap 호출 체인 */
unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flags, vm_flags_t vm_flags,
    unsigned long pgoff, unsigned long *populate,
    struct list_head *uf);

매핑 플래그 상세

보호 플래그 (prot)

플래그설명
PROT_NONE0x0접근 불가 — guard page, 주소 공간 예약에 사용
PROT_READ0x1읽기 허용
PROT_WRITE0x2쓰기 허용 (x86에서 PROT_READ 자동 포함)
PROT_EXEC0x4실행 허용 — NX bit 지원 시 W^X 정책과 상호작용
W^X (Write XOR Execute): SELinux의 execmem 정책은 PROT_WRITE | PROT_EXEC 동시 사용을 금지합니다. JIT 컴파일러는 먼저 PROT_WRITE로 코드를 쓴 후 mprotect()PROT_EXEC로 전환하는 2단계 패턴을 사용합니다.

매핑 유형 플래그 (flags) — 필수

플래그설명COW디스크 반영
MAP_SHARED공유 매핑 — 여러 프로세스가 동일 물리 페이지 공유없음msync/munmap 시 반영
MAP_SHARED_VALIDATEMAP_SHARED + 알 수 없는 플래그 시 EOPNOTSUPP 반환없음반영
MAP_PRIVATE사적 매핑 — 쓰기 시 COW로 사본 생성있음반영 안 됨

매핑 동작 플래그 (flags) — 선택

플래그설명
MAP_ANONYMOUS0x20파일 없는 익명 매핑 — 힙 확장, 큰 메모리 할당에 사용
MAP_FIXED0x10addr에 정확히 배치 — 기존 매핑 덮어씀 (위험)
MAP_FIXED_NOREPLACE0x100000addr에 배치하되, 충돌 시 EEXIST 반환 (안전한 대안)
MAP_POPULATE0x8000매핑 시 모든 페이지를 사전 폴트 — read-ahead 효과
MAP_LOCKED0x2000mlock과 동일 — 페이지를 RAM에 고정 (스왑(Swap) 방지)
MAP_NORESERVE0x4000스왑 영역(Swap Area) 예약 없이 매핑 — overcommit 의존
MAP_GROWSDOWN0x100스택처럼 아래로 성장하는 매핑
MAP_HUGETLB0x40000Huge Page 사용 — MAP_HUGE_2MB, MAP_HUGE_1GB 조합
MAP_SYNC0x80000DAX/PMEM — 영속 메모리에 대한 동기화 보장
MAP_32BIT0x40x86-64 전용 — 하위 2GB 영역에 매핑
/* 매핑 유형별 사용 패턴 */

/* 1. 파일 매핑 (MAP_SHARED) — 여러 프로세스가 같은 파일을 공유 */
int fd = open("/data/shared.db", O_RDWR);
void *shared = mmap(NULL, size, PROT_READ | PROT_WRITE,
                     MAP_SHARED, fd, 0);
/* → 쓰기가 파일에 반영됨, 다른 프로세스에서도 즉시 가시 */

/* 2. 파일 매핑 (MAP_PRIVATE) — 읽기 전용 데이터, COW */
void *priv = mmap(NULL, size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE, fd, 0);
/* → 쓰기 시 사본 생성, 원본 파일 변경 없음 */
/* → .text 세그먼트(실행 코드), 공유 라이브러리 로딩에 사용 */

/* 3. 익명 매핑 — 큰 메모리 할당 (glibc malloc > 128KB) */
void *anon = mmap(NULL, 1 << 20, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
/* → 제로 페이지에 매핑, 쓰기 시 실제 물리 페이지 할당 */

/* 4. 공유 익명 매핑 — 부모-자식 프로세스 간 IPC */
void *ipc = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANONYMOUS, -1, 0);
fork();
/* → 부모와 자식이 같은 물리 페이지를 직접 공유 */

/* 5. Huge Page 매핑 */
void *huge = mmap(NULL, 2 * 1024 * 1024, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_2MB,
                   -1, 0);

VMA (Virtual Memory Area) 구조체(Struct)

커널은 프로세스의 가상 주소 공간을 VMA 단위로 관리합니다. 각 VMA는 동일한 속성을 가진 연속된 가상 주소 범위를 나타내며, mm_struct의 maple tree (6.1+, 이전엔 red-black tree)로 관리됩니다.

/* include/linux/mm_types.h */
struct vm_area_struct {
    unsigned long vm_start;      /* VMA 시작 주소 (포함) */
    unsigned long vm_end;        /* VMA 끝 주소 (미포함) */

    struct mm_struct *vm_mm;     /* 소속 mm_struct */
    pgprot_t vm_page_prot;       /* PTE 보호 비트 */
    vm_flags_t vm_flags;         /* VM_READ, VM_WRITE, VM_EXEC, ... */

    const struct vm_operations_struct *vm_ops; /* 폴트 핸들러 등 */

    unsigned long vm_pgoff;      /* 파일 내 페이지 오프셋 */
    struct file *vm_file;        /* 매핑된 파일 (익명이면 NULL) */
    void *vm_private_data;       /* 드라이버 전용 데이터 */
};
코드 설명

vm_area_struct는 프로세스 가상 주소 공간의 기본 단위입니다. 각 필드의 역할을 자세히 살펴봅니다:

  • vm_start / vm_end — VMA가 커버하는 가상 주소 범위입니다. [vm_start, vm_end) 형태로, vm_start는 포함하고 vm_end는 미포함합니다. 두 값 모두 PAGE_SIZE(4KB) 정렬이 필수입니다. Maple Tree에서 vm_start가 index, vm_end - 1이 last로 사용됩니다.
  • vm_mm — 이 VMA가 속한 mm_struct 포인터입니다. current->mm을 통해 프로세스의 전체 주소 공간에 접근하며, 커널 스레드는 mm이 NULL입니다.
  • vm_page_prot — PTE(Page Table Entry)에 기록되는 하드웨어 보호 비트입니다. vm_flags에서 vm_get_page_prot()를 통해 아키텍처별 비트로 변환됩니다.
  • vm_flags — 소프트웨어 수준의 VMA 속성 플래그입니다. VM_READ, VM_WRITE, VM_EXEC 등 접근 권한과 VM_SHARED, VM_IO 등 매핑 유형을 결정합니다.
  • vm_opsvm_operations_struct 포인터로, 페이지 폴트(.fault), 매핑 열기/닫기(.open/.close), huge page 폴트(.huge_fault) 등의 콜백을 제공합니다. 파일 시스템이나 디바이스 드라이버가 이를 구현합니다.
  • vm_pgoff — 파일 매핑의 경우 파일 내 페이지 오프셋(offset >> PAGE_SHIFT)입니다. 익명 매핑에서도 vma_merge() 시 연속성 판단에 사용됩니다.
  • vm_file — 매핑된 파일의 struct file 포인터입니다. 익명 매핑이면 NULL이며, 파일 매핑이면 vm_file->f_mapping을 통해 페이지 캐시에 접근합니다.
  • vm_private_data — 드라이버 전용 데이터 포인터입니다. GPU 드라이버(DRM), 네트워크 디바이스, RDMA 등이 디바이스 고유 상태를 저장하는 데 사용합니다.

커널 6.1+에서는 vm_area_struct에 다음 필드들이 추가/변경되었습니다:

/* include/linux/mm_types.h — 6.1+ 추가 필드 */
struct vm_area_struct {
    /* ... 위의 기본 필드들 ... */

    struct anon_vma *anon_vma;     /* 익명 VMA의 역매핑 (Reverse Mapping) 구조체 */

#ifdef CONFIG_PER_VMA_LOCK
    struct rw_semaphore vm_lock;   /* per-VMA lock (6.4+) */
    int vm_lock_seq;               /* lock 시퀀스 번호 — mm->mm_lock_seq와 비교 */
#endif

    struct mempolicy *vm_policy;   /* NUMA 메모리 정책 */
    struct vma_numab_state *numab_state; /* NUMA balancing 상태 (6.3+) */
};
코드 설명

6.1+ 추가 필드의 역할:

  • anon_vma — 익명 페이지의 역매핑(Reverse Mapping)을 위한 구조체입니다. 페이지 회수(reclaim) 시 물리 페이지를 참조하는 모든 PTE를 찾기 위해 사용합니다. fork() 시 부모-자식 간 anon_vma 체인이 형성됩니다.
  • vm_lock — 커널 6.4에서 도입된 per-VMA lock입니다. 페이지 폴트 경로에서 전역 mmap_lock 대신 개별 VMA lock을 사용하여 멀티스레드 확장성을 극적으로 향상시킵니다.
  • vm_lock_seq — VMA lock의 유효성을 검증하는 시퀀스 번호입니다. mm->mm_lock_seq와 불일치하면 VMA가 수정되었으므로 mmap_lock으로 재시도합니다.
  • vm_policyset_mempolicy()mbind()로 설정된 NUMA 메모리 배치 정책입니다. NULL이면 프로세스/시스템 기본 정책을 따릅니다.
  • numab_state — NUMA Balancing AutoNUMA 상태 추적 구조체로, 페이지 접근 패턴을 기반으로 최적 NUMA 노드로 페이지를 마이그레이션합니다.

주요 vm_flags

플래그의미설명
VM_READ읽기 허용PROT_READ에 대응
VM_WRITE쓰기 허용PROT_WRITE에 대응
VM_EXEC실행 허용PROT_EXEC에 대응
VM_SHARED공유 매핑MAP_SHARED에 대응
VM_MAYREAD/WRITE/EXECmprotect 허용 범위mprotect()로 추가 가능한 최대 권한
VM_GROWSDOWN아래로 확장스택 VMA에 설정
VM_DONTEXPAND확장 금지mremap 확장 방지
VM_DONTCOPYfork 시 복사 금지VM_WIPEONFORK: fork 시 제로화
VM_IOI/O 메모리디바이스 메모리 매핑 — core dump 제외
VM_PFNMAPPFN 직접 매핑struct page 없는 물리 주소(Physical Address) 매핑
VM_LOCKEDmlock됨페이지 스왑/회수 방지
VM_HUGETLBHuge pagehugetlb 매핑

Maple Tree 기반 VMA 관리 (커널 6.1+)

Linux 6.1에서 VMA 관리 자료구조가 red-black tree + linked list 조합에서 Maple Tree로 전면 교체되었습니다 (Liam R. Howlett, Oracle). Maple Tree는 B-tree 변형으로 설계되어, 범위 기반(range-based) 인덱싱에 최적화된 캐시 친화적 자료구조입니다.

교체 배경: rbtree + linked list의 한계

문제rbtree + linked list (6.0 이전)Maple Tree (6.1+)
자료구조 수rbtree + linked list + 인터벌 트리 (3개 동시 유지)단일 Maple Tree로 통합
VMA 포인터vm_next, vm_prev, vm_rb 각각 유지모든 포인터 제거 → VMA 구조체 축소
캐시 효율rbtree 노드가 메모리에 분산 → 캐시 미스 빈번노드당 최대 16개 엔트리 → 캐시 라인(Cache Line) 활용 극대화
범위 연산gap 탐색에 augmented rbtree 필요범위 기반 인덱싱이 기본 → gap 탐색 자연스러움
RCU 호환rbtree 회전 시 RCU 안전성 보장 어려움RCU-safe 설계 (노드 교체 방식)
Lock 범위mmap_lock 전체 보유 필수per-VMA lock 도입 기반 마련 (6.4+)

Maple Tree 노드 구조

Maple Tree는 두 가지 노드 타입을 사용합니다. 내부 노드(maple_range_64)는 최대 16개의 pivot과 자식 포인터를 저장하고, 리프 노드(maple_arange_64)는 데이터 포인터(VMA 주소)를 직접 저장합니다. 각 노드는 256바이트로 정렬되어 캐시 라인 경계에 맞춥니다.

/* include/linux/maple_tree.h */
struct maple_tree {
    union {
        spinlock_t ma_lock;          /* 내부 락 */
        lockdep_map_p ma_external_lock; /* 외부 락 사용 시 */
    };
    unsigned int ma_flags;           /* 트리 플래그 (MT_FLAGS_*) */
    void __rcu *ma_root;             /* 루트 노드 포인터 */
};

/* 노드 타입 — 내부 노드 (최대 16개 피벗) */
struct maple_range_64 {
    struct maple_pnode *parent;     /* 부모 노드 */
    unsigned long pivot[MAPLE_RANGE64_SLOTS - 1];  /* 키 구간 경계 (최대 15개) */
    union {
        void __rcu *slot[MAPLE_RANGE64_SLOTS];  /* 자식/데이터 포인터 (최대 16개) */
        struct {
            void __rcu *pad[MAPLE_RANGE64_SLOTS - 1];
            struct maple_metadata meta;  /* 엔드 인덱스, gap 정보 */
        };
    };
};

/* 리프/내부 공용 — augmented 노드 (gap 추적 포함) */
struct maple_arange_64 {
    struct maple_pnode *parent;
    unsigned long pivot[MAPLE_ARANGE64_SLOTS - 1];  /* 키 구간 경계 (최대 9개) */
    void __rcu *slot[MAPLE_ARANGE64_SLOTS];           /* 자식/데이터 (최대 10개) */
    unsigned long gap[MAPLE_ARANGE64_SLOTS];           /* 각 서브트리 최대 gap 크기 */
    struct maple_metadata meta;
};
노드 크기와 캐시 효율: Maple Tree 노드는 256바이트로 고정되어 4개의 캐시 라인(64B x 4)에 정확히 맞습니다. rbtree에서는 단일 VMA 조회에 트리 깊이만큼의 캐시 라인 접근이 필요했지만, Maple Tree는 한 노드 내에서 최대 16개 엔트리를 선형 탐색하므로 프리페치(prefetch)가 효과적으로 동작합니다.

Maple Tree 동작 원리

Maple Tree는 범위 인덱싱을 기본으로 합니다. 각 엔트리는 (index, last) 범위를 가지며, VMA의 경우 vm_start가 index, vm_end - 1이 last가 됩니다. pivot 배열은 정렬된 경계값을 저장하여, slot[i]는 pivot[i-1]+1 ~ pivot[i] 범위의 데이터를 가리킵니다.

루트 (maple_arange_64) pivot[0]=0x4000 pivot[1]=0xA000 pivot[2]=ULONG_MAX slot[0] slot[1] slot[2] 리프 (범위: 0~0x4000) 0x1000-0x2FFF 0x3000-0x3FFF 리프 (0x4001~0xA000) 0x5000-0x8FFF 리프 (0xA001~MAX) 0xB000-0xCFFF 0xF000-0xFFFF VMA: [heap] VMA: [anon] VMA: [libc] VMA: [mmap] VMA: [stack] * pivot[i]는 slot[i]가 커버하는 범위의 상한값 * slot[i] → pivot[i-1]+1 ~ pivot[i] 범위의 데이터 (또는 자식 노드) * gap[i] = slot[i] 서브트리 내 가장 큰 빈 공간 크기 (arange 노드) * 빈 슬롯(NULL) = 해당 범위에 VMA 없음 → gap으로 활용

mm_struct의 Maple Tree

/* include/linux/mm_types.h */
struct mm_struct {
    struct {
        struct maple_tree mm_mt;  /* VMA를 관리하는 maple tree (6.1+) */
        unsigned long mmap_base;   /* mmap 영역 시작 (ASLR 적용) */
        unsigned long task_size;   /* 유저 주소 공간 크기 */
        int map_count;             /* VMA 개수 */
        unsigned long total_vm;    /* 총 매핑된 페이지 수 */
        unsigned long locked_vm;   /* mlock된 페이지 수 */
        unsigned long data_vm;     /* 데이터 매핑 페이지 수 */
        unsigned long stack_vm;    /* 스택 매핑 페이지 수 */
    };
    /* ... */
};

Maple State (ma_state) — 핵심 순회 인터페이스

Maple Tree의 모든 연산은 Maple State(ma_state)를 통해 수행됩니다. ma_state는 트리 내 현재 위치(커서)를 추적하며, 연속된 연산에서 탐색 경로를 재활용(Recycling)하여 성능을 최적화합니다.

/* include/linux/maple_tree.h */
struct ma_state {
    struct maple_tree *tree;     /* 대상 maple tree */
    unsigned long index;          /* 현재 탐색 시작 인덱스 */
    unsigned long last;           /* 현재 탐색 끝 인덱스 */
    struct maple_enode *node;    /* 현재 위치한 노드 */
    unsigned long min;            /* 현재 노드의 최소 범위 */
    unsigned long max;            /* 현재 노드의 최대 범위 */
    struct maple_alloc *alloc;   /* 사전 할당된 노드 목록 */
    unsigned char depth;          /* 현재 트리 깊이 */
    unsigned char offset;         /* 현재 노드 내 슬롯 오프셋 */
    unsigned char mas_flags;      /* 상태 플래그 */
};

/* Maple State 초기화 매크로 */
#define MA_STATE(name, mt, first, end) \
    struct ma_state name = {      \
        .tree = mt,                 \
        .index = first,             \
        .last = end,                \
        .node = MAS_START,         \
        .min = 0,                   \
        .max = ULONG_MAX,          \
        .alloc = NULL,              \
        .mas_flags = 0,            \
    }

주요 Maple Tree API

함수동작복잡도설명
mas_walk(&mas)정확한 인덱스 조회O(log n)mas.index 위치의 엔트리를 찾아 반환
mas_find(&mas, max)범위 내 다음 엔트리O(log n)현재 위치~max 범위에서 다음 non-NULL 엔트리
mas_find_rev(&mas, min)역방향 탐색O(log n)현재 위치~min 범위에서 이전 non-NULL 엔트리
mas_store(&mas, entry)엔트리 저장O(log n)[mas.index, mas.last] 범위에 entry 저장
mas_store_gfp(&mas, entry, gfp)GFP(Get Free Pages) 지정 저장O(log n)노드 할당 시 GFP 플래그 지정 가능
mas_erase(&mas)엔트리 삭제O(log n)현재 위치의 엔트리를 NULL로 설정
mas_empty_area(&mas, min, max, size)빈 공간 탐색O(log n)size 이상의 gap을 찾아 mas.index/last 설정
mas_empty_area_rev(&mas, min, max, size)역방향 gap 탐색O(log n)top-down 할당을 위한 역방향 gap 탐색
mas_prev(&mas, min)이전 엔트리O(1) 평균인접 엔트리는 같은 노드 내에서 O(1)
mas_next(&mas, max)다음 엔트리O(1) 평균순차 순회 시 캐시 친화적

VMA 관리 실전 코드

/* VMA 조회 — find_vma() 내부는 Maple Tree 기반 (mm/mmap.c) */
struct vm_area_struct *find_vma(struct mm_struct *mm,
                                unsigned long addr)
{
    struct vm_area_struct *vma;

    /* Maple Tree에서 addr 이상의 첫 VMA 탐색 */
    vma = mt_find(&mm->mm_mt, &addr, ULONG_MAX);

    return vma;
}

/* VMA 삽입 — mas_store_gfp 사용 */
static int vma_mas_store(struct vm_area_struct *vma,
                          struct ma_state *mas)
{
    mas->index = vma->vm_start;
    mas->last = vma->vm_end - 1;
    mas_store_gfp(mas, vma, GFP_KERNEL);
    return 0;
}

/* 빈 가상 주소 공간 탐색 (mmap 할당용) */
unsigned long unmapped_area_topdown(struct vm_unmapped_area_info *info)
{
    struct mm_struct *mm = current->mm;
    MA_STATE(mas, &mm->mm_mt, 0, 0);

    /* gap[i]를 활용하여 O(log n)으로 충분한 크기의 빈 공간 탐색 */
    if (mas_empty_area_rev(&mas, info->low_limit,
                           info->high_limit - 1,
                           info->length))
        return -ENOMEM;

    return mas.index;  /* 찾은 빈 공간의 시작 주소 */
}

/* VMA Iterator — 순차 순회 래퍼 (6.1+) */
struct vm_area_struct *vma;
VMA_ITERATOR(vmi, mm, 0);    /* ma_state를 래핑한 VMA 전용 이터레이터 */
for_each_vma(vmi, vma) {
    pr_info("VMA: %lx-%lx flags=%lx\\n",
            vma->vm_start, vma->vm_end, vma->vm_flags);
}

/* 특정 범위 내 VMA만 순회 */
VMA_ITERATOR(vmi, mm, start_addr);
for_each_vma_range(vmi, vma, end_addr) {
    /* start_addr ~ end_addr 범위와 겹치는 VMA만 순회 */
}
💡

VMA Iterator와 Maple State: VMA_ITERATOR는 내부적으로 ma_state를 래핑합니다. for_each_vmamas_find를 반복 호출하며, 같은 리프 노드 내의 연속 VMA 접근은 O(1)입니다. 6.0 이전의 vma = vma->vm_next linked list 순회를 대체합니다.

Maple Tree VMA 연산 내부 구현

Maple Tree 기반 VMA 관리의 핵심 연산들이 내부적으로 어떻게 동작하는지 살펴봅니다:

/* mm/mmap.c — vma_iter 기반 VMA 관리 API (6.1+) */

/* VMA 삽입 — Maple Tree에 VMA를 저장 */
static inline void vma_iter_store(
    struct vma_iterator *vmi,
    struct vm_area_struct *vma)
{
    vmi->mas.index = vma->vm_start;
    vmi->mas.last = vma->vm_end - 1;
    mas_store_prealloc(&vmi->mas, vma);
}

/* VMA 삭제 — 범위를 NULL로 설정 */
static inline void vma_iter_clear(
    struct vma_iterator *vmi,
    unsigned long start, unsigned long end)
{
    vmi->mas.index = start;
    vmi->mas.last = end - 1;
    mas_store_prealloc(&vmi->mas, NULL);
}

/* gap 탐색 — unmapped_area에서 빈 주소 찾기 */
static inline int vma_iter_area_lowest(
    struct vma_iterator *vmi,
    unsigned long min, unsigned long max,
    unsigned long size)
{
    return mas_empty_area(&vmi->mas, min, max - 1, size);
}

/* top-down gap 탐색 — 높은 주소에서 아래로 */
static inline int vma_iter_area_highest(
    struct vma_iterator *vmi,
    unsigned long min, unsigned long max,
    unsigned long size)
{
    return mas_empty_area_rev(&vmi->mas, min, max - 1, size);
}
코드 설명

Maple Tree 기반 VMA 관리의 핵심 연산을 분석합니다:

  • vma_iter_store() — VMA를 Maple Tree에 삽입합니다. [vm_start, vm_end-1] 범위에 VMA 포인터를 저장합니다. mas_store_prealloc()은 사전 할당된 노드를 사용하여 삽입 과정에서 메모리 할당 실패를 방지합니다. 사전 할당은 mas_preallocate()로 수행됩니다.
  • vma_iter_clear() — 지정된 범위를 NULL로 설정하여 VMA를 제거합니다. munmap()이나 VMA 분할 시 사용됩니다. Maple Tree는 NULL 엔트리를 gap으로 인식합니다.
  • vma_iter_area_lowest() — Bottom-up 방식으로 size 이상의 빈 주소 공간을 탐색합니다. Maple Tree의 gap[] 배열을 활용하여 O(log n)에 탐색합니다. 32비트 시스템이나 MAP_32BIT에서 사용됩니다.
  • vma_iter_area_highest() — Top-down 방식으로 높은 주소부터 빈 공간을 탐색합니다. arch_get_unmapped_area_topdown()에서 호출되며, 대부분의 64비트 시스템에서 기본 전략입니다. 스택 근처에서 아래로 할당하여 주소 공간 단편화를 줄입니다.

성능 특성: rbtree 기반에서는 gap 탐색이 augmented rbtree를 필요로 했으나, Maple Tree는 각 노드에 gap[] 배열을 내장하여 서브트리 내 최대 빈 공간 크기를 O(1)로 조회할 수 있습니다. 이를 통해 get_unmapped_area()의 성능이 크게 향상되었습니다.

RCU-safe 읽기와 per-VMA Lock (6.4+)

Maple Tree의 핵심 설계 목표 중 하나는 RCU-safe 읽기입니다. 읽기 측은 rcu_read_lock()만으로 트리를 안전하게 순회할 수 있으며, 쓰기 측이 노드를 수정할 때는 기존 노드를 수정하지 않고 새 노드를 생성한 뒤 포인터를 교체(publish)합니다.

/* RCU-safe VMA 조회 (mmap_lock 없이) — 6.4+ per-VMA lock */
struct vm_area_struct *lock_vma_under_rcu(
    struct mm_struct *mm,
    unsigned long address)
{
    struct vm_area_struct *vma;

    rcu_read_lock();
    /* Maple Tree에서 RCU 보호 하에 VMA 조회 */
    vma = mt_find(&mm->mm_mt, &address, ULONG_MAX);
    if (!vma || vma->vm_start > address) {
        rcu_read_unlock();
        return NULL;
    }

    /* per-VMA lock 획득 시도 (실패 시 mmap_lock fallback) */
    if (!vma_start_read(vma)) {
        rcu_read_unlock();
        return NULL;  /* 호출자가 mmap_lock으로 재시도 */
    }
    rcu_read_unlock();
    return vma;  /* per-VMA lock 보유 상태로 반환 */
}

/* Page Fault 경로에서의 활용 (6.4+) */
/*
 * 1단계: rcu_read_lock + per-VMA lock 시도 (빠른 경로)
 * 2단계: 실패 시 mmap_read_lock() fallback (느린 경로)
 *
 * → 대부분의 page fault에서 mmap_lock 경합 없이 VMA 접근 가능
 * → 멀티스레드 워크로드에서 극적인 확장성 향상
 */
CVE-2023-3269 (StackRot)과 스택 VMA 경쟁 조건(Race Condition):

Linux 6.1~6.4에서 MAP_GROWSDOWN 스택 영역 자동 확장 시 발생하는 Use-After-Free 취약점(Vulnerability)입니다. expand_downwards()가 스택 VMA 하단 경계를 확장하는 과정에서, 다른 스레드(Thread)가 경쟁적으로 해당 VMA를 교체·해제할 수 있었습니다. 공격자는 이 경쟁 조건을 통해 로컬 권한 상승(LPE)을 달성할 수 있었습니다. Linux 6.4.1에서 수정되었으며, 이후 스택 VMA 확장의 동기화가 강화되었습니다.

Maple Tree의 범용 활용

Maple Tree는 VMA 관리 외에도 커널 내 다양한 범위 기반 인덱싱에 활용됩니다.

사용처인덱스 키저장 값도입 버전
VMA 관리 (mm_struct)가상 주소vm_area_struct *6.1
PID 할당 (idr 대체)PID 번호task_struct *6.1
regmap 캐시 (REGCACHE_MAPLE)레지스터(Register) 주소레지스터 값6.4
파일 페이지 캐시 (계획)파일 오프셋(Offset)struct folio *논의 중
6.1 이전 vs 이후 요약: Linux 6.1에서 VMA 관리가 red-black tree + linked list에서 Maple Tree로 전환되었습니다. vm_area_struct에서 vm_next, vm_prev, vm_rb 필드가 모두 제거되어 구조체 크기가 줄었고, 6.4에서는 이를 기반으로 per-VMA lock이 도입되어 page fault 경로에서 mmap_lock 경합(Contention)을 획기적으로 줄였습니다.

do_mmap 내부 처리 흐름

mmap() 시스템 콜이 커널 내부에서 처리되는 과정을 단계별로 살펴봅니다.

mmap() 내부 처리 흐름 userspace: mmap(addr, len, prot, flags, fd, off) ksys_mmap_pgoff() → vm_mmap_pgoff() security_mmap_file() LSM 보안 검사 후 do_mmap() 진입 get_unmapped_area() 파일 매핑: f_op->get_unmapped_area() 익명 매핑: arch_get_unmapped_area_topdown() 가상 주소 영역 결정 mmap_region() vma_merge() (인접/동일 속성 병합) vm_area_alloc() (신규 VMA 할당) file->f_op->mmap() (파일 매핑 시) vma_link()로 maple tree에 삽입 옵션 처리 MAP_POPULATE면 선행 fault/populate 수행 populate 포인터에 후속 작업 정보 전달 반환: 매핑된 가상 주소 또는 음수 에러 코드
/* mm/mmap.c — do_mmap 핵심 로직 (간략화) */
unsigned long do_mmap(struct file *file, unsigned long addr,
    unsigned long len, unsigned long prot,
    unsigned long flags, vm_flags_t vm_flags,
    unsigned long pgoff, unsigned long *populate,
    struct list_head *uf)
{
    struct mm_struct *mm = current->mm;

    /* 1. 길이 정렬 및 오버플로 검사 */
    len = PAGE_ALIGN(len);
    if (!len || len > TASK_SIZE)
        return -ENOMEM;

    /* 2. vm_flags 계산 (prot + flags → VM_READ|VM_WRITE|...) */
    vm_flags |= calc_vm_prot_bits(prot, 0) |
                calc_vm_flag_bits(flags);

    /* 3. MAP 개수 제한 확인 (/proc/sys/vm/max_map_count) */
    if (mm->map_count > sysctl_max_map_count)
        return -ENOMEM;

    /* 4. 적절한 가상 주소 탐색 */
    addr = get_unmapped_area(file, addr, len, pgoff, flags);
    if (IS_ERR_VALUE(addr))
        return addr;

    /* 5. 실제 매핑 생성 */
    addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);

    /* 6. MAP_POPULATE 처리 — 사전 페이지 폴트 */
    if (populate)
        *populate = (flags & MAP_POPULATE) ? len : 0;

    return addr;
}
max_map_count: /proc/sys/vm/max_map_count (기본값 65530)은 프로세스당 VMA 최대 개수를 제한합니다. 많은 공유 라이브러리(Shared Library)를 로드하거나, JVM처럼 다수의 mmap을 사용하는 애플리케이션에서 이 한계에 도달할 수 있습니다. sysctl -w vm.max_map_count=262144로 조정 가능합니다.
코드 설명

do_mmap()의 각 단계를 순서대로 분석합니다:

  • 1단계: 길이 정렬PAGE_ALIGN(len)으로 페이지 크기 배수로 올림합니다. 0이거나 TASK_SIZE를 초과하면 -ENOMEM을 반환합니다.
  • 2단계: vm_flags 계산 — 유저 공간의 prot(PROT_READ 등)과 flags(MAP_SHARED 등)를 커널 내부 VM_* 플래그로 변환합니다. calc_vm_prot_bits()는 PROT → VM 매핑, calc_vm_flag_bits()는 MAP → VM 매핑을 수행합니다.
  • 3단계: MAP 개수 제한mm->map_countsysctl_max_map_count(기본 65530)를 초과하면 실패합니다. 이 검사가 없으면 공격자가 무한 VMA를 생성하여 커널 메모리를 고갈시킬 수 있습니다.
  • 4단계: 주소 탐색get_unmapped_area()가 Maple Tree의 gap 추적을 활용하여 O(log n)으로 적절한 빈 가상 주소를 찾습니다. MAP_FIXED이면 이 단계를 건너뛰고 지정된 주소를 사용합니다.
  • 5단계: mmap_region() — 실제 VMA 할당과 삽입을 수행합니다. 이 함수가 VMA 병합 시도, 새 VMA 할당, 파일 시스템 mmap 콜백 호출, Maple Tree 삽입을 담당합니다.
  • 6단계: MAP_POPULATE — 이 플래그가 설정되면 매핑 직후 mm_populate()를 호출하여 모든 페이지를 사전 할당합니다. 이후 페이지 폴트를 방지하므로 지연 시간에 민감한 애플리케이션에 유용합니다.

do_mmap() 호출 체인 상세

sys_mmap 시스템 콜이 최종적으로 mmap_region()에 도달하기까지의 전체 호출 체인을 살펴봅니다.

mmap() 호출 체인: 유저 공간 → 커널 내부 mmap(addr, len, prot, flags, fd, offset) syscall SYSCALL_DEFINE6(mmap) — arch/x86/kernel/sys_x86_64.c offset >> PAGE_SHIFT ksys_mmap_pgoff() — mm/mmap.c fget(fd) + mmap_write_lock vm_mmap_pgoff() — security_mmap_file() 호출 LSM 보안 검사 통과 do_mmap() — get_unmapped_area() + vm_flags 계산 mmap_region() — VMA 할당 + 병합 + Maple Tree 삽입 vma_merge() / vm_area_alloc() file->f_op->mmap() (파일 매핑) vma_link() → mas_store_gfp() mm->map_count++ / total_vm 갱신

mmap_region() 함수 구현 분석

mmap_region()do_mmap()의 핵심으로, 실제 VMA 생성과 삽입을 담당합니다. 다음은 커널 6.1+ 기준 간략화된 구현입니다:

/* mm/mmap.c — mmap_region() 핵심 로직 (간략화, 6.1+) */
unsigned long mmap_region(struct file *file, unsigned long addr,
    unsigned long len, vm_flags_t vm_flags,
    unsigned long pgoff, struct list_head *uf)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma, *prev, *merge;
    int error;
    VMA_ITERATOR(vmi, mm, addr);

    /* 1. 기존 매핑 영역과 겹치면 먼저 해제 */
    if (find_vma_links(mm, addr, addr + len, &prev, ...))
        do_vma_munmap(&vmi, prev, addr, addr + len, uf, ...);

    /* 2. VMA 병합 시도 — 인접 VMA와 속성이 같으면 병합 */
    merge = vma_merge(&vmg);
    if (merge) {
        vma = merge;
        goto out;  /* 병합 성공 — 새 VMA 할당 불필요 */
    }

    /* 3. 새 VMA 할당 (slab cache: vm_area_cachep) */
    vma = vm_area_alloc(mm);
    if (!vma) {
        error = -ENOMEM;
        goto unacct_error;
    }

    /* 4. VMA 필드 초기화 */
    vma->vm_start = addr;
    vma->vm_end = addr + len;
    vma->vm_flags = vm_flags;
    vma->vm_page_prot = vm_get_page_prot(vm_flags);
    vma->vm_pgoff = pgoff;

    /* 5. 파일 매핑이면 파일 시스템의 mmap 콜백 호출 */
    if (file) {
        vma->vm_file = get_file(file);
        error = call_mmap(file, vma);  /* → file->f_op->mmap(file, vma) */
        if (error)
            goto unmap_and_free_vma;
    } else if (vm_flags & VM_SHARED) {
        /* 공유 익명 매핑 → shmem_zero_setup() */
        error = shmem_zero_setup(vma);
    }

    /* 6. Maple Tree에 VMA 삽입 */
    vma_iter_store(&vmi, vma);
    mm->map_count++;

out:
    /* 7. 통계 갱신 */
    vm_stat_account(mm, vm_flags, len >> PAGE_SHIFT);
    perf_event_mmap(vma);

    return addr;
}
코드 설명

mmap_region()의 핵심 처리 단계:

  • 1단계: 기존 매핑 해제MAP_FIXED로 인해 기존 매핑 영역과 겹칠 수 있습니다. 이 경우 do_vma_munmap()으로 기존 VMA를 먼저 제거합니다.
  • 2단계: VMA 병합vma_merge()가 인접한 prev/next VMA와 속성(vm_flags, file, pgoff 등)이 동일한지 검사합니다. 병합에 성공하면 새 VMA를 할당하지 않아 메모리와 탐색 비용을 절약합니다.
  • 3단계: VMA 할당 — 병합 실패 시 vm_area_alloc()으로 vm_area_cachep slab 캐시에서 새 VMA를 할당합니다. 이 캐시는 kmem_cache_create("vm_area_struct", ...)로 초기화됩니다.
  • 4단계: 필드 초기화 — 시작/끝 주소, 플래그, 보호 비트, 파일 오프셋을 설정합니다. vm_get_page_prot()은 소프트웨어 vm_flags를 아키텍처별 하드웨어 PTE 비트(pgprot_t)로 변환합니다.
  • 5단계: 파일 mmap 콜백 — 파일 매핑이면 파일 시스템의 f_op->mmap()을 호출합니다. ext4는 ext4_file_mmap(), tmpfs는 shmem_mmap(), 디바이스 드라이버는 remap_pfn_range()를 호출하는 방식입니다. 공유 익명 매핑은 shmem_zero_setup()으로 tmpfs 백엔드를 설정합니다.
  • 6단계: Tree 삽입vma_iter_store()가 내부적으로 mas_store_gfp()를 호출하여 Maple Tree에 VMA를 삽입합니다. map_count를 증가시킵니다.
  • 7단계: 통계 갱신vm_stat_account()total_vm, data_vm 등 mm 통계를 갱신하고, perf_event_mmap()으로 perf 이벤트를 발생시킵니다.

mmap 페이지 폴트 처리

mmap()lazy allocation을 사용합니다. 매핑 시점에 물리 메모리를 할당하지 않고, 실제 접근 시 page fault를 통해 물리 페이지를 할당합니다.

Page Fault 처리 흐름 *ptr = 42; → MMU → #PF (Page Fault 예외) do_page_fault() → handle_mm_fault() __handle_mm_fault() → handle_pte_fault() PTE 없음 (첫 접근) 파일 매핑 → do_fault() 읽기 → do_read_fault() → filemap_fault() 쓰기 → do_cow_fault() → COW 사본 공유쓰기 → do_shared_fault() 익명 매핑 → do_anonymous_page() 제로 페이지 할당 후 CoW 설정 PTE 존재 + !present (스왑 또는 마이그레이션) do_swap_page() → 스왑 I/O do_numa_page() → NUMA 마이그레이션 결과: 페이지 테이블 업데이트 PTE 설정 → TLB flush → 사용자 프로세스 재시작 OOM 또는 SIGSEGV: 매핑 없는 주소 접근 시

vm_operations_struct — 폴트 핸들러(Handler) 콜백(Callback)

/* include/linux/mm.h */
struct vm_operations_struct {
    void (*open)(struct vm_area_struct *vma);
    void (*close)(struct vm_area_struct *vma);

    /* 핵심: 페이지 폴트 시 호출 */
    vm_fault_t (*fault)(struct vm_fault *vmf);

    /* 공유 쓰기 가능 매핑에서 쓰기 시 호출 (page_mkwrite) */
    vm_fault_t (*page_mkwrite)(struct vm_fault *vmf);

    /* DAX(PMEM) PFN 폴트 */
    vm_fault_t (*pfn_mkwrite)(struct vm_fault *vmf);

    /* huge page 폴트 */
    vm_fault_t (*huge_fault)(struct vm_fault *vmf,
                             unsigned int order);
};

/* vm_fault 구조체 — 폴트 핸들러에 전달되는 컨텍스트 */
struct vm_fault {
    struct vm_area_struct *vma;  /* 폴트 발생 VMA */
    unsigned int flags;          /* FAULT_FLAG_WRITE 등 */
    pgoff_t pgoff;               /* 파일 내 페이지 오프셋 */
    unsigned long address;       /* 폴트 주소 (페이지 정렬) */
    struct page *page;           /* 핸들러가 채우는 결과 페이지 */
};

filemap_fault — 파일 매핑 폴트의 핵심

/* mm/filemap.c — 파일 매핑 페이지 폴트 (간략화) */
vm_fault_t filemap_fault(struct vm_fault *vmf)
{
    struct file *file = vmf->vma->vm_file;
    struct address_space *mapping = file->f_mapping;
    struct folio *folio;

    /* 1. 페이지 캐시에서 folio 검색 */
    folio = filemap_get_folio(mapping, vmf->pgoff);
    if (IS_ERR(folio)) {
        /* 2. 캐시 미스 → 디스크에서 읽기 (Major Fault) */
        folio = filemap_alloc_folio(vmf->gfp_mask, 0);
        filemap_read_folio(file, mapping->a_ops->read_folio,
                          folio);
        /* → I/O 대기 발생 → major fault로 카운트 */
    }
    /* 3. 캐시 히트 → minor fault (I/O 없음) */

    vmf->page = folio_file_page(folio, vmf->pgoff);
    return VM_FAULT_LOCKED;
}
Major vs Minor Fault: Minor fault는 페이지가 이미 메모리(페이지 캐시)에 있어 디스크 I/O 없이 처리됩니다 (수 μs). Major fault는 디스크에서 읽어와야 하므로 수 ms가 소요됩니다. /proc/[pid]/stat의 minflt, majflt 필드로 프로세스별 폴트 횟수를 확인할 수 있습니다.

COW (Copy-On-Write) 상세 메커니즘

COW는 MAP_PRIVATE 매핑과 fork()에서 핵심적으로 사용되는 지연 복사 기법입니다. 쓰기가 발생하기 전까지 물리 페이지를 공유하고, 실제 쓰기 시에만 사본을 생성하여 메모리를 절약합니다.

COW (Copy-On-Write) 동작 과정 1단계: fork() 직후 (공유 상태) 부모 PTE (R/O) 자식 PTE (R/O) 물리 페이지 A mapcount=2, refcount=2, PTE: R/O 2단계: 자식이 쓰기 시도 부모 PTE (R/O) 자식: *ptr = 42 물리 페이지 A ⚡ Write Fault (#PF, error=WRITE) 3단계: COW 완료 (분리 상태) 부모 PTE → 페이지 A 자식 PTE → 페이지 B (R/W) 페이지 A (원본) 페이지 B (사본) memcpy(B, A, PAGE_SIZE) mapcount=1 mapcount=1, 쓰기 반영됨 커널 처리 흐름 (do_wp_page) Write Fault 발생 handle_pte_fault() do_wp_page() wp_page_copy() PTE 갱신 wp_page_copy(): alloc_page() → copy_user_highpage() → set_pte_at(new_pte, R/W) → dec_mm_counter(old) reuse 최적화: page_count()==1이면 복사 없이 PTE를 R/W로 변경 (wp_page_reuse) GUP(get_user_pages) pinned 페이지: wp_page_copy() 강제 (Dirty COW 패치)
/* mm/memory.c — do_wp_page() COW 핵심 로직 (커널 6.x 간략화) */
static vm_fault_t do_wp_page(struct vm_fault *vmf)
{
    const bool unshare = vmf->flags & FAULT_FLAG_UNSHARE;
    struct vm_area_struct *vma = vmf->vma;
    struct folio *folio = NULL;

    vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
    if (!vmf->page) {
        /* VM_PFNMAP (디바이스 메모리): PFN 직접 매핑은 항상 복사 */
        return wp_pfn_shared(vmf);
    }

    folio = page_folio(vmf->page);

    /* 공유 매핑의 쓰기: page_mkwrite 콜백 호출 */
    if (vma->vm_flags & (VM_SHARED | VM_MAYSHARE)) {
        return wp_page_shared(vmf, folio);
    }

    /* Reuse 최적화: 참조자가 자기 자신뿐이면 복사 불필요 */
    if (folio_ref_count(folio) == 1 &&
        folio_mapcount(folio) == 1 &&
        !folio_test_ksm(folio) &&
        !folio_test_pinned(folio)) {
        /* 복사 없이 PTE를 R/W로 변경 */
        wp_page_reuse(vmf, folio);
        return 0;
    }

    /* 실제 COW: 새 페이지 할당 + 데이터 복사 */
    return wp_page_copy(vmf);
}
코드 설명

do_wp_page()의 핵심 판단 로직:

  • vm_normal_page() — PTE에서 struct page를 추출합니다. VM_PFNMAP(디바이스 메모리)이면 NULL을 반환하며, 이 경우 항상 복사가 필요합니다.
  • wp_page_shared()VM_SHARED 매핑에서의 쓰기입니다. COW가 아닌 page_mkwrite() 콜백을 호출하여 파일 시스템에 dirty 알림을 전달합니다. ext4, XFS 등이 저널링 준비를 수행합니다.
  • wp_page_reuse()folio_ref_count == 1이고 folio_mapcount == 1이면 이 프로세스만 페이지를 사용 중입니다. 복사 없이 PTE의 쓰기 비트만 설정하여 오버헤드를 제거합니다. KSM 페이지나 GUP으로 pinned된 페이지는 reuse 대상에서 제외됩니다.
  • wp_page_copy() — 실제 COW를 수행합니다. alloc_page_vma()로 새 페이지를 할당하고, copy_user_highpage()로 데이터를 복사한 뒤, 새 PTE(R/W)를 설정합니다. 이전 페이지의 참조 카운트를 감소시킵니다.
  • Dirty COW 패치folio_test_pinned() 검사가 추가되어, get_user_pages(FOLL_FORCE)로 pinned된 페이지는 reuse하지 않고 반드시 복사합니다. 이것이 CVE-2016-5195의 핵심 수정입니다.

wp_page_copy() — COW 복사 상세

/* mm/memory.c — wp_page_copy() 핵심 (간략화) */
static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *old_page = vmf->page;
    struct page *new_page;
    pte_t entry;

    /* 1. 새 페이지 할당 (NUMA 정책 적용) */
    new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
    if (!new_page)
        return VM_FAULT_OOM;

    /* 2. 데이터 복사 (kmap_local_page 사용) */
    copy_user_highpage(new_page, old_page, vmf->address, vma);
    __SetPageUptodate(new_page);

    /* 3. PTE 스핀락 획득 후 원자적 교체 */
    vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
                                     vmf->address, &vmf->ptl);

    /* 4. Race 검사: PTE가 변경되었으면 중단 */
    if (!pte_same(ptep_get(vmf->pte), vmf->orig_pte)) {
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        put_page(new_page);
        return 0;  /* 다른 스레드가 이미 처리 */
    }

    /* 5. 새 PTE 설정 (쓰기 가능 + dirty) */
    entry = mk_pte(new_page, vma->vm_page_prot);
    entry = pte_sw_mkyoung(entry);
    entry = maybe_mkwrite(pte_mkdirty(entry), vma);

    /* 6. 페이지 테이블 업데이트 */
    ptep_clear_flush(vma, vmf->address, vmf->pte);
    page_add_new_anon_rmap(new_page, vma, vmf->address);
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);

    /* 7. 이전 페이지 참조 해제 */
    page_remove_rmap(old_page, vma, false);
    put_page(old_page);

    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return VM_FAULT_WRITE;
}
코드 설명

wp_page_copy()의 각 단계:

  • 1단계: 페이지 할당alloc_page_vma()는 VMA의 NUMA 정책(vm_policy)에 따라 적절한 노드에서 페이지를 할당합니다. GFP_HIGHUSER_MOVABLE은 사용자 공간 페이지에 적합한 할당 플래그입니다.
  • 2단계: 데이터 복사copy_user_highpage()kmap_local_page()로 고메모리(HIGHMEM) 페이지도 안전하게 접근하며, x86에서는 rep movsb 최적화를 활용합니다.
  • 3~4단계: 원자적 교체 — PTE 수준 스핀락을 획득한 후, pte_same()으로 다른 스레드가 이미 COW를 수행했는지 검사합니다. 이 double-check 패턴이 COW 경쟁 조건을 방지합니다.
  • 5~6단계: PTE 설정ptep_clear_flush()로 기존 PTE를 무효화하고 TLB를 flush한 뒤, 새 PTE를 설정합니다. page_add_new_anon_rmap()으로 역매핑 정보를 추가합니다.
  • 7단계: 정리 — 이전 페이지의 rmap과 참조 카운트를 감소시킵니다. mapcount가 0이 되면 페이지는 회수 대상이 됩니다.

do_anonymous_page() — 익명 페이지 폴트 상세

익명 매핑(MAP_ANONYMOUS)에서 첫 접근 시 호출되는 핵심 함수입니다. 읽기 접근은 제로 페이지(Zero Page)를 공유하고, 쓰기 접근만 실제 물리 페이지를 할당합니다.

/* mm/memory.c — do_anonymous_page() 핵심 (간략화) */
static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    struct page *page;
    pte_t entry;

    /* userfaultfd 등록 영역이면 사용자 공간으로 위임 */
    if (userfaultfd_missing(vma))
        return handle_userfault(vmf, VM_UFFD_MISSING);

    /* 읽기 폴트: 글로벌 제로 페이지 공유 (메모리 할당 없음) */
    if (!(vmf->flags & FAULT_FLAG_WRITE) &&
        !mm_forbids_zeropage(vma->vm_mm)) {
        entry = pte_mkspecial(
            pfn_pte(my_zero_pfn(vmf->address),
                    vma->vm_page_prot));
        /* 제로 페이지를 R/O로 매핑 → 쓰기 시 COW 발생 */
        vmf->pte = pte_offset_map_lock(...);
        set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
        pte_unmap_unlock(vmf->pte, vmf->ptl);
        return 0;
    }

    /* 쓰기 폴트: 실제 물리 페이지 할당 */
    page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
    if (!page)
        return VM_FAULT_OOM;
    __SetPageUptodate(page);

    /* anon_vma 준비 (역매핑용) */
    if (anon_vma_prepare(vma))
        goto oom_free_page;

    /* PTE 설정: 쓰기 가능 + dirty */
    entry = mk_pte(page, vma->vm_page_prot);
    entry = pte_sw_mkyoung(entry);
    if (vma->vm_flags & VM_WRITE)
        entry = pte_mkwrite(pte_mkdirty(entry), vma);

    vmf->pte = pte_offset_map_lock(...);
    /* Race 검사: PTE가 비어있어야 함 */
    if (!pte_none(ptep_get(vmf->pte)))
        goto release;

    inc_mm_counter(vma->vm_mm, MM_ANONPAGES);
    page_add_new_anon_rmap(page, vma, vmf->address);
    lru_cache_add_inactive_or_unevictable(page, vma);
    set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
    pte_unmap_unlock(vmf->pte, vmf->ptl);
    return 0;
}
코드 설명

do_anonymous_page()의 핵심 동작:

  • 제로 페이지 최적화 — 읽기 접근 시 전역 제로 페이지(ZERO_PAGE)를 R/O로 매핑합니다. 모든 프로세스가 동일한 물리 페이지를 공유하므로 메모리를 전혀 소비하지 않습니다. malloc() 후 읽기만 하는 영역은 물리 메모리를 차지하지 않는 이유입니다.
  • 쓰기 시 할당alloc_zeroed_user_highpage_movable()은 제로로 초기화된 새 페이지를 할당합니다. 보안을 위해 이전 프로세스의 데이터가 노출되지 않도록 반드시 제로화합니다.
  • anon_vma 준비anon_vma_prepare()가 이 VMA에 처음으로 익명 페이지가 할당될 때 anon_vma 구조체를 생성합니다. 이 구조체는 페이지 회수 시 역매핑에 사용됩니다.
  • LRU 등록lru_cache_add_inactive_or_unevictable()로 새 페이지를 LRU inactive 리스트에 추가합니다. VM_LOCKED VMA이면 unevictable 리스트에 배치하여 회수를 방지합니다.
  • Race 검사pte_none()으로 다른 스레드가 이미 같은 주소에 페이지를 매핑했는지 확인합니다. 멀티스레드 환경에서 두 스레드가 동시에 같은 주소에 폴트할 수 있기 때문입니다.
제로 페이지와 오버커밋: mmap(MAP_ANONYMOUS) 후 실제로 쓰기 전까지는 물리 메모리를 소비하지 않습니다. 이것이 Linux의 메모리 오버커밋(Overcommit) 기반입니다. /proc/sys/vm/overcommit_memory 설정: 0=휴리스틱(기본), 1=항상 허용, 2=swap+RAM*ratio 이내만 허용. overcommit_ratio(기본 50%)는 모드 2에서 커밋 가능한 RAM 비율입니다.

mprotect — 매핑 보호 속성 변경

int mprotect(void *addr, size_t len, int prot);

/* JIT 컴파일러 패턴: Write → Exec 전환 */
void *code = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(code, jit_output, code_len);    /* 코드 쓰기 */
mprotect(code, page_size, PROT_READ | PROT_EXEC); /* W→X 전환 */
((void(*)())code)();                    /* 실행 */

/* 커널: mprotect → do_mprotect_pkey() → VMA 분할/병합 + PTE 갱신 */
/* VMA의 vm_flags 변경 → 페이지 테이블 walk → PTE 보호 비트 갱신 */
/* TLB flush 필요 (다른 CPU에도 전파) */

mremap — 매핑 크기 변경/이동

void *mremap(void *old_addr, size_t old_size,
             size_t new_size, int flags, ...);

/* 확장 — 인접 공간이 비어있으면 제자리 확장, 아니면 이동 */
void *new_ptr = mremap(old_ptr, old_size, new_size, MREMAP_MAYMOVE);
/* → glibc realloc()이 내부적으로 사용 */
/* → 이동 시 페이지 테이블 엔트리만 재배치 (데이터 복사 없음) */

/* 고정 주소로 이동 */
void *moved = mremap(old_ptr, old_size, new_size,
                      MREMAP_MAYMOVE | MREMAP_FIXED, new_addr);

/* 커널 경로: mremap → do_mremap()
 *   → 축소: do_munmap() 으로 끝부분 제거
 *   → 확장: vma_merge() 시도 → 실패 시 move_vma()
 *   → move_vma(): 새 VMA 생성 + move_page_tables() (PTE 이동)
 */

madvise — 매핑 접근 패턴 힌트

int madvise(void *addr, size_t len, int advice);
advice동작용도
MADV_NORMAL기본 read-ahead 정책일반 접근
MADV_SEQUENTIAL공격적 read-ahead, 지나간 페이지 조기 회수순차 파일 처리
MADV_RANDOMread-ahead 비활성화DB 인덱스 랜덤 접근
MADV_WILLNEED비동기 prefetch (readahead)곧 접근할 데이터 사전 로드
MADV_DONTNEED페이지 즉시 해제 (재접근 시 재폴트)메모리 해제, GC
MADV_FREElazy 해제 — 메모리 압박 시에만 회수malloc free pool (4.5+)
MADV_HUGEPAGETHP 사용 권장대용량 힙, DB 버퍼(Buffer)풀
MADV_NOHUGEPAGETHP 사용 금지latency 민감한 워크로드
MADV_MERGEABLEKSM 대상으로 등록VM 중복 페이지 병합
MADV_COLD페이지를 inactive 리스트로 이동우선순위(Priority) 낮은 캐시 (5.4+)
MADV_PAGEOUT페이지를 스왑으로 강제 이동프로액티브 메모리 회수 (5.4+)
/* 실전 예: DB 엔진의 버퍼풀 관리 */
void *buf = mmap(NULL, POOL_SIZE, PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
madvise(buf, POOL_SIZE, MADV_HUGEPAGE);   /* THP 활성화 */

/* 특정 영역을 곧 사용할 예정 */
madvise(hot_region, HOT_SIZE, MADV_WILLNEED);

/* 사용 완료된 영역 해제 */
madvise(cold_region, COLD_SIZE, MADV_DONTNEED);

/* MADV_FREE vs MADV_DONTNEED:
 * DONTNEED: 즉시 페이지 해제 → 재접근 시 제로 페이지
 * FREE:     lazy 해제 → 메모리 충분하면 기존 데이터 유지 (더 빠름)
 */

msync — 매핑 데이터 디스크 동기화

int msync(void *addr, size_t len, int flags);
/* flags: MS_SYNC (동기 flush), MS_ASYNC (비동기), MS_INVALIDATE */

/* MAP_SHARED 파일 매핑 데이터 보장 */
memcpy(mapped_data + offset, new_data, len);
msync(mapped_data + offset, len, MS_SYNC);
/* → dirty 페이지를 디스크에 flush → writeback 완료까지 블록 */

/* 커널: msync → vfs_fsync_range() → 파일시스템별 fsync 호출 */

mlock / mlockall — 페이지 고정

int mlock(const void *addr, size_t len);
int mlock2(const void *addr, size_t len, unsigned int flags);
int mlockall(int flags);  /* MCL_CURRENT, MCL_FUTURE, MCL_ONFAULT */

/* 실시간 애플리케이션: 스왑에 의한 지연 방지 */
mlockall(MCL_CURRENT | MCL_FUTURE);
/* → 현재 + 미래 모든 매핑을 RAM에 고정 */
/* → RLIMIT_MEMLOCK에 의해 제한 (CAP_IPC_LOCK으로 해제) */

/* mlock2 (4.4+): MLOCK_ONFAULT — 접근 시점에만 잠금 */
mlock2(addr, len, MLOCK_ONFAULT);
/* → 매핑 전체를 사전 폴트하지 않고, 폴트 발생 시에만 잠금 */

디바이스 드라이버 mmap 구현

디바이스 드라이버는 file_operations.mmap 콜백을 통해 디바이스 메모리(MMIO)나 DMA 버퍼를 사용자 공간(User Space)에 직접 매핑할 수 있습니다.

/* 디바이스 드라이버 mmap 콜백 기본 구조 */
static int my_dev_mmap(struct file *filp,
                       struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long pfn = dev->phys_addr >> PAGE_SHIFT;

    /* 크기 검증 */
    if (size > dev->mem_size)
        return -EINVAL;

    /* I/O 메모리 매핑: uncacheable 설정 */
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;

    /* 물리 주소를 사용자 가상 주소에 매핑 */
    if (remap_pfn_range(vma, vma->vm_start, pfn,
                        size, vma->vm_page_prot))
        return -EAGAIN;

    return 0;
}

static const struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .mmap    = my_dev_mmap,
    /* ... */
};

remap_pfn_range vs vm_insert_page

함수대상struct page 필요용도
remap_pfn_range()연속 물리 주소불필요MMIO, 연속 DMA 버퍼
io_remap_pfn_range()I/O 물리 주소불필요PCI BAR 등 I/O 메모리
vm_insert_page()개별 페이지필요커널 할당 페이지 (kmalloc 등)
vmf_insert_pfn()PFN불필요fault handler에서 PFN 직접 삽입
dma_mmap_coherent()DMA 버퍼내부 관리dma_alloc_coherent() 버퍼 매핑
/* DMA 버퍼를 사용자 공간에 매핑 */
static int my_dma_mmap(struct file *filp,
                       struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;

    return dma_mmap_coherent(dev->dev,
                             vma,
                             dev->dma_vaddr,   /* 커널 가상 주소 */
                             dev->dma_handle,   /* DMA 물리 주소 */
                             dev->dma_size);
}

/* 폴트 기반 매핑 — 필요한 페이지만 점진적으로 매핑 */
static vm_fault_t my_fault(struct vm_fault *vmf)
{
    struct my_device *dev = vmf->vma->vm_private_data;
    unsigned long pfn = dev->phys_addr >> PAGE_SHIFT;

    pfn += vmf->pgoff;  /* 오프셋 계산 */

    return vmf_insert_pfn(vmf->vma, vmf->address, pfn);
}

static const struct vm_operations_struct my_vm_ops = {
    .fault = my_fault,
};

static int my_fault_mmap(struct file *filp,
                         struct vm_area_struct *vma)
{
    vma->vm_ops = &my_vm_ops;
    vma->vm_private_data = filp->private_data;
    vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND;
    return 0;
}
보안 주의: 디바이스 드라이버 mmap 구현 시 반드시 매핑 범위를 검증해야 합니다. 사용자가 요청한 vm_pgoff와 크기가 디바이스 메모리 범위를 초과하지 않는지 확인하지 않으면, 임의 물리 메모리 접근으로 이어지는 권한 상승 취약점이 발생합니다.

프로세스 주소 공간 레이아웃

x86-64 프로세스의 가상 주소 공간에서 mmap 영역의 위치와 역할을 확인합니다.

x86-64 프로세스 가상 주소 공간 (128TB 유저 영역) 주소 커널 공간 (128TB) 커널 코드, 데이터, vmalloc, physmap FFFF_8000 _0000_0000 Canonical Hole (비표준 영역 — 하드웨어 제약) 7FFF_FFFF _FFFF Stack ↓ (성장 방향: 하향) RLIMIT_STACK 제한, argv/envp/auxv + guard page mmap 영역 ↓ (성장 방향: 하향) 공유 라이브러리 (libc.so, ld-linux.so, libpthread.so) MAP_ANONYMOUS, MAP_SHARED 파일 매핑 Heap ↑ (성장 방향: 상향) malloc → brk() / mmap(MAP_ANONYMOUS) 사용 ELF 세그먼트 (고정 주소 or ASLR) BSS (미초기화) Data (초기화) Text (코드, RX) 0000_0000 _0000_0000 0x0 (NULL — 접근 시 SIGSEGV) 주요 레지스터/정보: ASLR: 스택/mmap/힙 기반 주소 무작위화 KASLR: 커널 공간도 무작위화 cat /proc/self/maps 로 확인 가능 total: 유저 128TB + 커널 128TB = 256TB 5-level paging: 유저 64PB (Linux 6.x) VMA: vm_area_struct의 Maple Tree로 관리 (6.1+)
# 프로세스의 VMA 목록 확인
cat /proc/self/maps
# 주소범위             권한  오프셋   장치   inode  경로
# 55a3b2400000-55a3b2428000 r--p 00000000 fd:01 1234  /usr/bin/bash
# 55a3b2428000-55a3b24f0000 r-xp 00028000 fd:01 1234  /usr/bin/bash
# 55a3b2600000-55a3b2610000 rw-p 00000000 00:00 0     [heap]
# 7f1a3c000000-7f1a3c021000 rw-p 00000000 00:00 0     (anonymous)
# 7f1a3d200000-7f1a3d3c0000 r--p 00000000 fd:01 5678  /lib/libc.so.6
# 7ffc12300000-7ffc12321000 rw-p 00000000 00:00 0     [stack]
# 7ffc123fe000-7ffc12400000 r--p 00000000 00:00 0     [vvar]
# 7ffc12400000-7ffc12401000 r-xp 00000000 00:00 0     [vdso]

# 상세 VMA 정보 (/proc/[pid]/smaps)
cat /proc/self/smaps
# 7f1a3c000000-7f1a3c021000 rw-p 00000000 00:00 0
# Size:               132 kB
# Rss:                 80 kB    ← 실제 물리 메모리 사용량
# Pss:                 80 kB    ← 공유 비례 크기
# Shared_Clean:         0 kB
# Shared_Dirty:         0 kB
# Private_Clean:        0 kB
# Private_Dirty:       80 kB
# Referenced:          80 kB
# Anonymous:           80 kB
# LazyFree:             0 kB
# VmFlags: rd wr mr mw me ac sd

ASLR 구현 상세

ASLR(Address Space Layout Randomization)은 프로세스의 주요 영역(스택, mmap, 힙, vDSO)의 시작 주소를 실행마다 무작위화하여 공격자가 메모리 주소를 예측하기 어렵게 만드는 보안 메커니즘입니다.

ASLR: 프로세스 주소 공간 무작위화 (x86-64) 0x0000_0000_0000 ─────────────────────────── 0x7FFF_FFFF_FFFF (유저 공간 128TB) ELF (PIE) load_elf_binary() 엔트로피: 28비트 ≈ 256TB 범위 내 무작위 Heap (brk) arch_randomize_brk() 엔트로피: 13비트 ≈ 32MB 범위 내 무작위 mmap 영역 mmap_base() 엔트로피: 28비트 ≈ 1TB 범위 내 무작위 Stack randomize_stack_top() 엔트로피: 22비트 ≈ 16MB 범위 내 무작위 ASLR 설정과 엔트로피 /proc/sys/kernel/randomize_va_space: 0=Off, 1=스택/mmap/vDSO, 2=+brk (기본) x86-64 엔트로피 소스: get_random_long() → arch_mmap_rnd() → mmap_base 계산 32비트 호환: 엔트로피 8~16비트 (mmap_rnd_compat_bits), 공간 제약으로 감소 CONFIG_ARCH_MMAP_RND_BITS: 기본 28, 최대 32 (aarch64: 18~33) KASLR (Kernel ASLR) 커널 텍스트: 물리 주소 최대 1GB 범위, 가상 주소 1.5GB 범위 내 무작위 모듈: module_alloc() 영역 1.5GB 범위 내 무작위 배치 nokaslr 커널 파라미터로 비활성화 가능 (디버깅용)
/* arch/x86/mm/mmap.c — mmap 기반 주소 무작위화 */
static unsigned long mmap_base(unsigned long rnd,
    struct rlimit *rlim_stack)
{
    unsigned long gap = rlim_stack->rlim_cur;
    unsigned long pad = stack_guard_gap;

    /* 스택 크기에 따른 gap 계산 */
    if (gap < MIN_GAP)
        gap = MIN_GAP;
    else if (gap > MAX_GAP)
        gap = MAX_GAP;

    /* TASK_SIZE(128TB) - gap - rnd = mmap 시작 주소 */
    return PAGE_ALIGN(TASK_SIZE - gap - rnd);
}

static unsigned long arch_mmap_rnd(void)
{
    unsigned long rnd;

    if (current->flags & PF_RANDOMIZE) {
        /* get_random_long()에서 28비트 엔트로피 추출 */
        rnd = get_random_long() & ((1UL << mmap_rnd_bits) - 1);
    } else {
        rnd = 0;
    }

    return rnd << PAGE_SHIFT;  /* 페이지 정렬 (4KB 단위) */
}

/* fs/binfmt_elf.c — ELF 로더의 ASLR 적용 */
static int load_elf_binary(...)
{
    /* PIE(Position-Independent Executable)이면 로드 주소 무작위화 */
    if (elf_ex->e_type == ET_DYN) {
        load_bias = ELF_ET_DYN_BASE;  /* 0x555555554000 근처 */
        if (current->flags & PF_RANDOMIZE)
            load_bias += arch_mmap_rnd();
        load_bias = ELF_PAGESTART(load_bias);
    }
    /* ... */

    /* brk 무작위화 */
    current->mm->brk = arch_randomize_brk(current->mm);
    /* arch_randomize_brk: brk + get_random_long() % 0x02000000 */
}
# ASLR 설정 확인/변경
cat /proc/sys/kernel/randomize_va_space   # 2 = 완전 활성화

# mmap 엔트로피 비트 확인
cat /proc/sys/vm/mmap_rnd_bits           # 28 (x86-64 기본)
cat /proc/sys/vm/mmap_rnd_compat_bits    # 8 (32비트 호환)

# 같은 프로그램을 여러 번 실행하면 주소가 매번 다름
$ cat /proc/self/maps | head -1
55d3a1200000-55d3a1220000 r--p ...
$ cat /proc/self/maps | head -1
563e8a400000-563e8a420000 r--p ...

# Stack entropy 확인
$ for i in $(seq 5); do cat /proc/self/maps | grep stack; done
7ffd8a300000-7ffd8a321000 rw-p ... [stack]
7ffc12100000-7ffc12121000 rw-p ... [stack]
7ffd45600000-7ffd45621000 rw-p ... [stack]
ASLR 우회 기법과 방어:

정보 누출(Information Leak)이 ASLR의 주요 위협입니다. 포맷 스트링 취약점, 버퍼 오버리드, /proc/[pid]/maps 읽기 등으로 주소가 누출되면 ASLR이 무력화됩니다. 방어책: (1) 프로세스 격리 (hidepid=2), (2) kernel.kptr_restrict=2로 커널 심볼 주소 숨기기, (3) PIE 컴파일 필수 (-fPIE -pie), (4) Stack Canary + NX(No-Execute) + CFI(Control-Flow Integrity) 결합.

특수 매핑

vDSO / vvar — 커널→사용자 공유 매핑

/* vDSO (virtual Dynamic Shared Object):
 * 커널이 모든 프로세스에 자동 매핑하는 가상 공유 라이브러리.
 * gettimeofday(), clock_gettime(), getcpu() 등을
 * 시스템 콜 없이 사용자 공간에서 직접 실행.
 */

/* vvar: vDSO가 참조하는 커널 데이터 페이지 (읽기 전용) */
/* vsyscall_gtod_data, tk_fast_mono 등 시간 데이터 포함 */

/* arch/x86/entry/vdso/vma.c */
static int map_vdso(const struct vdso_image *image,
                    unsigned long addr)
{
    /* vvar 영역 매핑 (읽기 전용 데이터) */
    _install_special_mapping(mm, addr, -image->sym_vvar_start,
                            VM_READ | VM_MAYREAD, &vvar_mapping);

    /* vDSO 코드 매핑 (읽기+실행) */
    _install_special_mapping(mm, text_start, image->size,
                            VM_READ | VM_EXEC | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC,
                            &vdso_mapping);
}

userfaultfd — 사용자 공간 폴트 처리

/* userfaultfd: 페이지 폴트를 사용자 공간에서 처리 (4.3+)
 * 용도: VM 라이브 마이그레이션, 사용자 공간 스왑, CRIU(checkpoint/restore)
 */
int uffd = syscall(SYS_userfaultfd, O_CLOEXEC | O_NONBLOCK);

/* 모니터링할 매핑 등록 */
struct uffdio_register reg = {
    .range = { .start = (unsigned long)addr, .len = size },
    .mode = UFFDIO_REGISTER_MODE_MISSING,  /* 미할당 페이지 폴트 */
};
ioctl(uffd, UFFDIO_REGISTER, &reg);

/* 폴트 이벤트 수신 (poll/epoll 가능) */
struct uffd_msg msg;
read(uffd, &msg, sizeof(msg));
/* msg.arg.pagefault.address → 폴트 주소 */

/* 페이지 공급 */
struct uffdio_copy copy = {
    .dst = msg.arg.pagefault.address,
    .src = (unsigned long)page_data,
    .len = 4096,
};
ioctl(uffd, UFFDIO_COPY, &copy);
/* → 커널이 페이지를 매핑하고 폴트를 해제 */

mmap 성능 최적화

시나리오문제최적화
대용량 파일 순차 읽기minor fault 누적MAP_POPULATE + MADV_SEQUENTIAL
DB 랜덤 I/O불필요한 read-aheadMADV_RANDOM
대용량 익명 매핑TLB 미스MAP_HUGETLB 또는 MADV_HUGEPAGE
실시간(Real-time) 시스템스왑에 의한 지연MAP_LOCKED 또는 mlockall()
메모리 할당/해제 반복VMA 단편화(Fragmentation)MADV_FREE (해제 대신 lazy reclaim)
VM 라이브 마이그레이션다운타임userfaultfd로 점진적 전송
NUMA 노드 미스원격 메모리 접근mbind() / set_mempolicy()

성능 모니터링:

  1. 프로세스별 폴트 통계: /proc/[pid]/stat → field 10(minflt), 12(majflt)
  2. 시스템 전체: perf stat -e page-faults,minor-faults,major-faults ./app
  3. ftrace로 폴트 추적: echo 1 > /sys/kernel/debug/tracing/events/exceptions/page_fault_user/enable
  4. VMA 개수 모니터링:
    • grep VmPTE /proc/[pid]/status — 페이지 테이블 크기
    • wc -l /proc/[pid]/maps — VMA 개수

실습: perf/ftrace로 mmap 성능 분석

실제 애플리케이션의 mmap 성능을 측정하는 구체적인 예제입니다.

# 1. 페이지 폴트 이벤트 측정 (perf)
$ perf stat -e page-faults,minor-faults,major-faults \
            -e dTLB-load-misses,dTLB-store-misses \
            ./my_app

 Performance counter stats for './my_app':

         12,345      page-faults              #  123.450 K/sec
         12,280      minor-faults             #  99.5% (캐시 히트)
             65      major-faults             #   0.5% (디스크 I/O)
        234,567      dTLB-load-misses         #  TLB 미스율 분석

# 2. mmap 시스템 콜 추적 (strace)
$ strace -e mmap,munmap,mprotect,madvise -c ./my_app

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 45.23    0.000234          12        19           mmap
 32.10    0.000166           9        18           munmap
 22.67    0.000117          29         4           mprotect

# 3. ftrace로 페이지 폴트 세부 추적
$ echo 1 > /sys/kernel/debug/tracing/events/exceptions/page_fault_user/enable
$ echo 1 > /sys/kernel/debug/tracing/tracing_on
$ ./my_app
$ echo 0 > /sys/kernel/debug/tracing/tracing_on
$ cat /sys/kernel/debug/tracing/trace

# 출력 예시:
  my_app-1234  [000] ....  1234.567890: page_fault_user: \
    address=0x7f1234567000 ip=0x400abc error_code=0x6 (WRITE|USER)
  my_app-1234  [000] ....  1234.567923: page_fault_user: \
    address=0x7f1234568000 ip=0x400abc error_code=0x6

# 4. BPF로 실시간 폴트 분석 (bpftrace)
$ bpftrace -e 'kprobe:handle_mm_fault {
    @faults[comm] = count();
    @latency[comm] = hist(nsecs);
  }'

# Ctrl-C 후 출력:
@faults[my_app]: 12345

@latency[my_app]:
[256, 512)            3421 |@@@@@@@@@@@@@@                        |
[512, 1K)             6789 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@        |
[1K, 2K)              2135 |@@@@@@@@@                               |
mmap vs read/write 성능 비교: mmap()은 커널↔사용자 공간 데이터 복사를 제거하므로 대용량 파일 랜덤 접근에서 read()/write()보다 유리합니다. 반면, 소규모 순차 읽기에서는 read()의 VFS 최적화(read-ahead)가 더 효율적일 수 있습니다. 매핑 생성/해제의 오버헤드(VMA 할당, 페이지 테이블 구성, TLB flush)도 고려해야 합니다.

보안: VMA/mmap 관련 취약점 사례

VMA와 mmap 구현의 버그는 권한 상승(privilege escalation)이나 임의 메모리 접근으로 이어질 수 있어, 커널 보안의 핵심 영역입니다. 대표적인 CVE 사례를 통해 안전한 드라이버 및 애플리케이션 개발 방법을 학습할 수 있습니다.

CVE-2016-5195: Dirty COW (Copy-On-Write 경쟁 조건)

영향: Linux 2.6.22 (2007) ~ 4.8.3 (2016) — 약 9년간 존재한 권한 상승 취약점

원인: do_wp_page()의 COW 처리와 get_user_pages()의 경쟁 조건(race condition). 두 스레드가 동시에 실행될 때 읽기 전용 매핑에 쓰기가 가능해집니다.

/* 취약점 악용 시나리오 */
/* Thread 1: madvise(MADV_DONTNEED) 반복 — PTE 제거 */
while (1) {
    madvise(map, size, MADV_DONTNEED);
    /* → PTE를 제거하여 다음 접근 시 새로운 폴트 발생 유도 */
}

/* Thread 2: write() 시스템 콜 반복 — 읽기 전용 파일에 쓰기 시도 */
while (1) {
    lseek(fd, offset, SEEK_SET);
    write(fd, payload, size);
    /* → get_user_pages()가 COW를 건너뛰고 원본 페이지에 직접 쓰기 */
}

/* 결과: /etc/passwd 등 읽기 전용 파일 변조 가능 → root 권한 획득 */

패치(Patch): get_user_pages()에서 COW 페이지 감지 시 재시도 강제

/* mm/gup.c — 패치 후 (간략화) */
static int faultin_page(struct vm_area_struct *vma, ...)
{
    unsigned int fault_flags = FAULT_FLAG_ALLOW_RETRY;

    if (*flags & FOLL_WRITE)
        fault_flags |= FAULT_FLAG_WRITE;
+   if (*flags & FOLL_FORCE)
+       fault_flags |= FAULT_FLAG_TRIED;  /* COW 재시도 방지 */

    return handle_mm_fault(vma, address, fault_flags, NULL);
}

CVE-2023-0179: Netfilter nft_set_pipapo 잘못된 범위 검증

영향: Linux 5.6 ~ 6.1.7 — 로컬 권한 상승 (CVSS 7.8)

원인: 커널 모듈(Kernel Module)의 mmap 핸들러가 사용자 요청 범위를 검증하지 않아, 임의 커널 메모리를 사용자 공간에 매핑 가능

/* 취약한 드라이버 패턴 (예시) */
static int bad_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long pfn = vma->vm_pgoff;  /* 사용자 제공 오프셋 */
    unsigned long size = vma->vm_end - vma->vm_start;

    /* ❌ 잘못됨: 범위 검증 없음 */
    return remap_pfn_range(vma, vma->vm_start, pfn, size,
                          vma->vm_page_prot);
    /* → 공격자가 pfn에 커널 메모리 주소를 지정하면 매핑됨 */
}

올바른 구현:

static int safe_mmap(struct file *filp, struct vm_area_struct *vma)
{
    struct my_device *dev = filp->private_data;
    unsigned long size = vma->vm_end - vma->vm_start;
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;

    /* ✅ 필수: 범위 검증 */
    if (offset + size > dev->mem_size)
        return -EINVAL;
    if (offset & ~PAGE_MASK)
        return -EINVAL;

    /* ✅ 필수: 디바이스 메모리 범위 내로 제한 */
    unsigned long pfn = (dev->phys_base + offset) >> PAGE_SHIFT;

    vma->vm_flags |= VM_IO | VM_DONTEXPAND | VM_DONTDUMP;
    return remap_pfn_range(vma, vma->vm_start, pfn, size,
                          pgprot_noncached(vma->vm_page_prot));
}

CVE-2022-0847: mremap 크기 검증 우회

영향: Linux 5.8 ~ 5.16.11 — 권한 상승 (CVSS 7.8)

원인: mremap()MREMAP_DONTUNMAP 플래그 처리 시 이전 매핑이 해제되지 않아, 동일한 물리 페이지를 두 개의 VMA로 매핑 가능

/* 악용 예시 */
void *map1 = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE,
                   MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);

/* mremap으로 동일한 물리 페이지를 두 번 매핑 */
void *map2 = mremap(map1, PAGE_SIZE, PAGE_SIZE,
                     MREMAP_MAYMOVE | MREMAP_DONTUNMAP, NULL);

/* map1과 map2가 동일한 물리 페이지를 가리킴 */
/* → 한쪽을 읽기 전용 pipe 버퍼로 splice하고, 다른 쪽으로 쓰기 */
/* → Dirty Pipe 취약점과 유사한 권한 상승 */

VMA/mmap 보안 체크리스트

영역검증 항목위험
드라이버 mmapvm_pgoff + size 범위 검증임의 물리 메모리 접근
권한 검사PROT_WRITE + MAP_SHARED 조합 시 파일 쓰기 권한 확인읽기 전용 파일 변조
정수 오버플로(Integer Overflow)offset + size 계산 시 오버플로 검사범위 검증 우회
경쟁 조건멀티스레드 접근 시 적절한 락 사용TOCTOU (Time-Of-Check-Time-Of-Use)
VMA 플래그VM_IO, VM_PFNMAP, VM_DONTEXPAND 설정예상치 못한 VMA 확장/병합
캐시 일관성(Cache Coherency)DMA 버퍼는 pgprot_noncached() 또는 dma_mmap_coherent() 사용데이터 손상
참고: 최신 CVE 목록은 CVE Database에서 "linux kernel mmap" 검색으로 확인 가능합니다. 커널 보안 패치는 kernel.org의 stable/longterm 릴리스 노트를 참고하세요.

mmap/페이지 폴트 디버깅(Debugging) 루틴

mmap 문제는 권한, 정렬, 매핑 플래그, 드라이버 remap 경로 중 하나에서 주로 발생합니다. fault 유형(minor/major/SIGBUS/SIGSEGV)을 먼저 분류하면 진단 속도가 빨라집니다.

증상우선 점검대응
SIGSEGV주소 범위/권한VMA 권한 및 경계 체크
SIGBUS파일/디바이스 backing매핑 길이/오프셋/디바이스 범위 점검
major fault 급증working set vs 메모리 압박readahead/lock/populate 전략 재검토
# fault/매핑 점검
cat /proc/<pid>/maps | head
cat /proc/<pid>/smaps | head -n 120
perf stat -e page-faults,minor-faults,major-faults ./workload

SIGSEGV 디버깅 절차

SIGSEGV는 프로세스가 유효하지 않은 메모리 주소에 접근하거나, 접근 권한이 없는 영역에 접근할 때 발생합니다. 디버깅의 핵심은 폴트 주소가 어느 VMA에 속하는지(또는 어디에도 속하지 않는지)를 확인하는 것입니다.

# 1단계: 코어 덤프에서 폴트 주소 확인
# coredump 활성화 후 SIGSEGV 재현
ulimit -c unlimited
echo /tmp/core.%e.%p > /proc/sys/kernel/core_pattern
./crash_program

# 2단계: /proc/pid/maps에서 폴트 주소의 VMA 소속 확인
# 폴트 주소가 0x7f1234567890이라면:
grep -i "7f12345" /proc/<pid>/maps
# 결과 없음 → 매핑 범위 밖 접근 (NULL 포인터, use-after-free, 스택 오버플로)
# 결과 있으나 권한 불일치 → 읽기 전용 영역에 쓰기 시도 등

# 3단계: smaps로 해당 VMA의 상세 메모리 상태 확인
cat /proc/<pid>/smaps | grep -A 20 "7f1234500000"
# Rss, Referenced, Anonymous 필드로 실제 메모리 상태 파악

# 4단계: dmesg로 커널 측 폴트 정보 확인
dmesg | tail -20
# [12345.678] crash_program[1234]: segfault at 7f1234567890
#   ip 55b5c8421234 sp 7ffd12345678 error 4 in crash_program[55b5c8400000+e0000]
# error 코드 해석:
#   bit 0: 0=page not present, 1=protection fault
#   bit 1: 0=read, 1=write
#   bit 2: 0=kernel, 1=user

GDB를 이용한 VMA 상태 확인

# GDB로 코어 덤프 분석
gdb ./crash_program /tmp/core.crash_program.1234

# 폴트 발생 지점 확인
(gdb) bt                              # 백트레이스
(gdb) info registers rip rsp          # 폴트 시 PC와 스택 포인터

# 매핑 정보 확인
(gdb) info proc mappings              # /proc/pid/maps와 동일
# 0x55b5c8400000  0x55b5c8420000  0x20000  0x0  r--p  /usr/bin/crash_program
# 0x55b5c8420000  0x55b5c84e0000  0xc0000  0x20000  r-xp  /usr/bin/crash_program
# ...

# 특정 주소가 어느 매핑에 속하는지 확인
(gdb) info symbol 0x55b5c8421234      # 폴트 주소의 심볼 확인
(gdb) x/10i 0x55b5c8421234            # 폴트 주소의 명령어 디스어셈블

# 메모리 접근 가능 여부 테스트
(gdb) x/1b 0x7f1234567890             # 폴트 주소 읽기 시도
# Cannot access memory at address 0x7f1234567890 → 매핑 없음 확인

crash tool로 VMA 덤프

커널 패닉이나 코어 덤프 분석 시 crash 유틸리티를 사용하면 커널 내부 자료구조 수준에서 VMA를 직접 확인할 수 있습니다.

# crash tool 시작 (vmcore 또는 실행 중인 커널)
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/vmcore

# 특정 프로세스의 VMA 목록 출력
crash> vm <pid>
# PID: 1234   TASK: ffff8881a0c00000  CPU: 3   COMMAND: "crash_program"
#        MM               PGD          RSS    TOTAL_VM
# ffff8881b2345000  ffff8881c4567000  12340k  234560k
#   VMA           START          END        FLAGS  FILE
# ffff888100a00000  55b5c8400000  55b5c8420000  8000875  /usr/bin/crash_program
# ffff888100a00100  55b5c8420000  55b5c84e0000  8000075  /usr/bin/crash_program
# ffff888100a00200  55b5c9a00000  55b5c9b20000  8100073  [heap]

# 특정 VMA의 vm_area_struct 상세 출력
crash> struct vm_area_struct ffff888100a00200
# vm_start = 0x55b5c9a00000
# vm_end   = 0x55b5c9b20000
# vm_flags = 0x8100073 (VM_READ|VM_WRITE|VM_MAYREAD|VM_MAYWRITE|...)
# vm_page_prot = {pgprot = 0x8000000000000027}
# vm_file  = 0x0 (anonymous)
# anon_vma = 0xffff888123456000

# 주소로 VMA 역추적
crash> vm -p <pid> | grep 7f1234
# VMA가 없으면 해당 주소가 매핑 범위 밖임을 확인

# 페이지 테이블 워크: 가상 주소 → 물리 주소 변환 확인
crash> vtop -u 0x55b5c9a01000 <pid>
# VIRTUAL   PHYSICAL
# 55b5c9a01000  1a2b3c000  → 물리 페이지 매핑 존재
# PTE: 800000001a2b3067 (PRESENT|RW|USER|ACCESSED|DIRTY)
💡

디버깅 순서 요약: SIGSEGV가 발생하면 (1) dmesg의 segfault 로그에서 폴트 주소와 error 코드를 확인하고, (2) /proc/pid/maps에서 해당 주소의 VMA 소속을 파악합니다. VMA 범위 밖이면 잘못된 포인터(NULL, dangling)이고, VMA 범위 안이면 권한 위반(읽기 전용 영역에 쓰기 등)입니다. (3) 커널 패닉 상황이면 crash tool의 vm 명령으로 커널 자료구조를 직접 확인합니다.

VMA 병합 (VMA Merging)

mmap()이나 mprotect() 호출 시 인접한 VMA의 속성(flags, file, offset 등)이 동일하면 커널이 자동으로 병합합니다. VMA 수를 줄이면 페이지 폴트 시 find_vma() 탐색 시간과 메모리 오버헤드가 감소합니다:

/* mm/mmap.c — VMA 병합 조건 (8가지 모두 일치해야 병합 가능) */
static struct vm_area_struct *vma_merge(
    struct vma_merge_struct *vmg)
{
    /*
     * 병합 조건 체크 (can_vma_merge_before/after):
     * 1. vm_flags 동일 (보호 속성, MAP_SHARED/PRIVATE 등)
     * 2. 동일 파일 (또는 둘 다 익명 매핑)
     * 3. 파일 오프셋이 연속
     * 4. anon_vma 호환 가능
     * 5. vm_policy (NUMA 정책) 동일
     * 6. vm_userfaultfd_ctx 동일
     * 7. anon_name 동일 (prctl PR_SET_VMA_ANON_NAME)
     * 8. 주소 범위가 인접
     */

    /* 4가지 병합 케이스:
     * Case 1: [prev][new][next] → [prev+new+next] (3→1)
     * Case 2: [prev][new]       → [prev+new]       (2→1)
     * Case 3:       [new][next] → [new+next]       (2→1)
     * Case 4: 병합 불가          → 새 VMA 생성
     */
}
prev new next 1개 Case 1: 양쪽 병합 (3→1) prev new 1개 Case 2: prev에 병합 (2→1) new next 1개 Case 3: next에 병합 (2→1) new new VMA Case 4: 병합 불가 → 새 VMA 생성
VMA 병합 케이스: 인접 VMA의 속성이 호환되면 자동으로 병합하여 VMA 수를 최소화합니다.
💡

VMA 수 확인: cat /proc/<pid>/maps | wc -l로 프로세스의 VMA 수를 확인할 수 있습니다. vm.max_map_count(기본 65530)을 초과하면 mmap()ENOMEM을 반환합니다. 특히 Java/Elasticsearch 등 많은 스레드를 사용하는 애플리케이션에서 주의가 필요합니다.

vma_merge() 8가지 병합 케이스 분석

커널의 vma_merge()는 실제로 8가지 케이스를 고려합니다. 위의 4가지 기본 케이스 외에, mprotect()에 의한 기존 VMA 내부 범위 변경에서 발생하는 추가 케이스들이 있습니다:

/* mm/mmap.c — vma_merge() 8가지 케이스 (커널 소스 주석 기반) */
/*
 * 기존 VMA 배치:  [prev]  [vma]  [next]
 * 병합 요청 범위:         [addr...end)
 *
 * Case 1: [prev][addr...end)[next]  → [prev+addr..end+next]  (3→1)
 * Case 2: [prev][addr...end)        → [prev+addr..end]       (2→1)
 * Case 3:       [addr...end)[next]  → [addr..end+next]       (2→1)
 * Case 4:       [addr...end)        → 병합 불가, 새 VMA
 *
 * mprotect() 부분 범위 변경 시 추가 케이스:
 * Case 5: [prev][addr..end)[vma]    → [prev+addr..end]+[vma] (vma 앞부분 병합)
 * Case 6: [prev][addr..........end) → [prev+addr..end]       (vma 전체를 prev에 병합)
 * Case 7: [vma][addr..end)[next]    → [vma]+[addr..end+next] (vma 뒷부분 병합)
 * Case 8: [addr..........end)[next] → [addr..end+next]       (vma 전체를 next에 병합)
 */

static struct vm_area_struct *vma_merge(
    struct vma_merge_struct *vmg)
{
    struct vm_area_struct *prev = vmg->prev;
    struct vm_area_struct *next, *res;
    bool merge_prev = false, merge_next = false;

    /* 이전 VMA와 병합 가능한지 검사 */
    if (prev && prev->vm_end == vmg->start &&
        can_vma_merge_after(prev, vmg->flags, vmg->anon_vma,
                           vmg->file, vmg->pgoff, ...))
        merge_prev = true;

    /* 다음 VMA와 병합 가능한지 검사 */
    next = find_vma(vmg->mm, vmg->end);
    if (next && vmg->end == next->vm_start &&
        can_vma_merge_before(next, vmg->flags, vmg->anon_vma,
                            vmg->file, vmg->pgoff + ..., ...))
        merge_next = true;

    if (merge_prev && merge_next) {
        /* Case 1: 양쪽 병합 — prev를 확장하고 next를 삭제 */
        prev->vm_end = next->vm_end;
        __vma_unlink(vmg->mm, next);
        vm_area_free(next);
        res = prev;
    } else if (merge_prev) {
        /* Case 2: prev에 병합 — prev.vm_end 확장 */
        prev->vm_end = vmg->end;
        res = prev;
    } else if (merge_next) {
        /* Case 3: next에 병합 — next.vm_start 축소 */
        next->vm_start = vmg->start;
        next->vm_pgoff -= (vmg->end - vmg->start) >> PAGE_SHIFT;
        res = next;
    } else {
        /* Case 4: 병합 불가 */
        return NULL;
    }

    return res;
}
코드 설명

vma_merge()의 병합 조건과 동작을 상세히 분석합니다:

  • can_vma_merge_after(prev, ...) — prev VMA의 뒤쪽에 새 영역을 이어붙일 수 있는지 검사합니다. vm_flags, vm_file, vm_pgoff 연속성, anon_vma 호환성, vm_policy(NUMA), vm_userfaultfd_ctx, anon_name — 이 7가지 조건이 모두 일치해야 합니다.
  • can_vma_merge_before(next, ...) — next VMA의 앞쪽에 새 영역을 이어붙일 수 있는지 검사합니다. 검사 조건은 동일합니다.
  • Case 1 (양쪽 병합) — prev를 next의 끝까지 확장하고, next VMA를 Maple Tree에서 제거한 뒤 vm_area_free()로 slab에 반환합니다. VMA가 3개에서 1개로 줄어 가장 효율적입니다.
  • Case 2 (prev 병합) — prev의 vm_end만 확장합니다. 새 VMA 할당이 불필요합니다.
  • Case 3 (next 병합) — next의 vm_start를 새 영역 시작으로 당기고, vm_pgoff도 그만큼 역방향으로 조정합니다. 파일 매핑에서 오프셋 연속성이 깨지지 않도록 하는 것이 핵심입니다.
  • Case 5~8 (mprotect 부분 변경)mprotect()가 기존 VMA 내부 일부 범위의 속성을 변경할 때 발생합니다. 변경된 부분이 인접 VMA와 속성이 같아지면 그쪽으로 병합하고, 나머지 부분은 분할(__split_vma)합니다.

VMA 분할 (VMA Splitting)

munmap()으로 VMA 중간 영역만 해제하거나, mprotect()로 VMA 일부의 보호 속성을 변경하면 기존 VMA가 분할됩니다. 아래 다이어그램은 하나의 VMA가 munmap()이나 mprotect()에 의해 2개 또는 3개로 분할되는 과정을 보여줍니다.

VMA 분할(Split) 시나리오 A) munmap — 뒤쪽 해제 (VMA.end 축소) Before: VMA [0x1000 — 0x5000] rw-p munmap(0x3000, 0x2000) After: VMA [0x1000 — 0x3000] 해제됨 B) munmap — 앞쪽 해제 (VMA.start 이동) Before: VMA [0x1000 — 0x5000] rw-p munmap(0x1000, 0x2000) After: 해제됨 VMA [0x3000 — 0x5000] C) munmap — 중간 해제 (3분할, __split_vma 2회 호출) Before: VMA [0x1000 — 0x7000] rw-p munmap(0x3000, 0x2000) After: VMA₁ [0x1000 — 0x3000] 해제됨 VMA₂ [0x5000 — 0x7000] D) mprotect — 중간 영역 속성 변경 (3분할) Before: VMA [0x1000 — 0x7000] rw-p mprotect(0x3000, 0x2000, PROT_READ) After: VMA₁ [0x1000-0x3000] rw-p VMA₂ [0x3000-0x5000] r--p VMA₃ [0x5000-0x7000] rw-p __split_vma() 호출 흐름: 1. vm_area_dup(vma) — 기존 VMA를 복사하여 새 VMA 할당 (kmem_cache) 2. new_below=1이면 new_vma.vm_end=addr, vma.vm_start=addr (아래쪽이 새 VMA) 3. new_below=0이면 new_vma.vm_start=addr, vma.vm_end=addr (위쪽이 새 VMA) 4. vma_iter_store(vmi, new_vma) — Maple Tree에 새 VMA 삽입 주의: 중간 해제(C)나 중간 mprotect(D)는 __split_vma()가 2회 호출되어 ENOMEM 위험이 2배입니다
/* mm/mmap.c — VMA 분할 */
static int __split_vma(struct vma_iterator *vmi,
                        struct vm_area_struct *vma,
                        unsigned long addr, int new_below)
{
    struct vm_area_struct *new_vma;

    /* 새 VMA를 kmem_cache에서 할당 */
    new_vma = vm_area_dup(vma);
    if (!new_vma)
        return -ENOMEM;

    if (new_below) {
        /* 분할점 아래가 새 VMA */
        new_vma->vm_end = addr;
        vma->vm_start = addr;
    } else {
        /* 분할점 위가 새 VMA */
        new_vma->vm_start = addr;
        vma->vm_end = addr;
    }

    /* Maple tree에 삽입 */
    vma_iter_store(vmi, new_vma);
    return 0;
}

/* munmap 시나리오:
 * 1. [AAAA] 전체 해제     → VMA 삭제
 * 2. [AA--] 뒤쪽 해제     → VMA.end 축소
 * 3. [--AA] 앞쪽 해제     → VMA.start 확장
 * 4. [-AA-] 중간 해제     → 2개로 분할 (ENOMEM 가능!)
 */
⚠️

munmap 중간 영역 분할 시 ENOMEM: VMA 중간을 munmap()하면 2개의 새 VMA가 필요합니다. 메모리 부족 시 ENOMEM이 반환될 수 있으며, 이 경우 munmap()실패합니다. 대부분의 프로그램이 munmap() 반환값을 무시하므로 주의가 필요합니다.

do_munmap() → __do_munmap() 경로 분석

munmap() 시스템 콜은 __vm_munmap()__do_munmap()을 거쳐 VMA 분할, 페이지 테이블 해제, VMA 제거를 수행합니다:

/* mm/mmap.c — __do_munmap() 핵심 로직 (간략화, 6.1+) */
int __do_munmap(struct mm_struct *mm, unsigned long start,
    size_t len, struct list_head *uf, bool unlock)
{
    struct vm_area_struct *vma;
    VMA_ITERATOR(vmi, mm, start);
    unsigned long end = start + len;

    /* 1. 해제 범위와 겹치는 첫 번째 VMA 탐색 */
    vma = vma_find(&vmi, end);
    if (!vma)
        return 0;  /* 해제할 매핑 없음 */

    /* 2. 시작 경계 분할 — VMA 앞부분이 범위 밖이면 분할 */
    if (start > vma->vm_start) {
        int error = __split_vma(&vmi, vma, start, 1);
        if (error)
            return error;
    }

    /* 3. 끝 경계 분할 — VMA 뒷부분이 범위 밖이면 분할 */
    struct vm_area_struct *last = find_vma_intersection(mm, start, end);
    if (last && end < last->vm_end) {
        int error = __split_vma(&vmi, last, end, 0);
        if (error)
            return error;
    }

    /* 4. 범위 내 모든 VMA의 페이지 테이블 해제 */
    unmap_region(mm, &vmi, vma, prev, last, start, end);
    /*
     * unmap_region() 내부:
     *   lru_add_drain()          — LRU 배치 큐 비우기
     *   tlb_gather_mmu()         — TLB 배치 해제 준비
     *   unmap_vmas()             — PTE 항목 제거
     *   free_pgtables()          — 페이지 테이블 자체 해제
     *   tlb_finish_mmu()         — TLB flush 실행
     */

    /* 5. VMA를 Maple Tree에서 제거하고 해제 */
    remove_vma_list(mm, vma);
    /*
     * remove_vma_list() 내부:
     *   각 VMA에 대해:
     *     vm_area_free(vma)     — slab 캐시에 반환
     *     mm->map_count--
     */

    return 0;
}
코드 설명

__do_munmap()의 핵심 동작을 단계별로 분석합니다:

  • 1단계: VMA 탐색 — Maple Tree에서 [start, end) 범위와 겹치는 첫 VMA를 찾습니다. 겹치는 VMA가 없으면 바로 반환합니다.
  • 2단계: 시작 경계 분할start가 VMA 내부에 있으면 __split_vma()로 VMA를 분할합니다. 분할 실패(-ENOMEM)가 munmap() 실패의 주요 원인입니다.
  • 3단계: 끝 경계 분할end가 마지막 VMA 내부에 있으면 마찬가지로 분할합니다. 최악의 경우 VMA 중간 해제 시 2번의 분할이 발생합니다.
  • 4단계: 페이지 테이블 해제unmap_region()이 TLB 배치 해제를 사용하여 효율적으로 PTE를 제거합니다. unmap_vmas()는 각 VMA의 페이지 테이블을 walk하며 PTE를 클리어하고, 매핑된 페이지의 참조 카운트를 감소시킵니다. free_pgtables()는 비어있는 페이지 테이블 자체를 해제합니다.
  • 5단계: VMA 제거remove_vma_list()가 Maple Tree에서 VMA를 제거하고 vm_area_free()로 slab에 반환합니다. 파일 매핑이면 fput()으로 파일 참조를 해제합니다.

TLB 배치 해제: tlb_gather_mmu()tlb_finish_mmu() 사이에서 여러 페이지의 TLB 무효화를 모아서 한 번에 처리합니다. 이는 IPI(Inter-Processor Interrupt) 호출 횟수를 줄여 멀티코어 환경에서 성능을 크게 향상시킵니다.

mmap_lock 경합과 per-VMA lock (커널 6.4+)

기존에는 모든 VMA 접근이 mm->mmap_lock(read-write semaphore)으로 보호되어, 멀티스레드 환경에서 심각한 경합이 발생했습니다. 커널 6.4부터 페이지 폴트 경로에서 per-VMA lock을 사용하여 경합을 대폭 줄였습니다:

구분 기존 (mmap_lock) 커널 6.4+ (per-VMA lock)
보호 범위 프로세스 전체 mm_struct 개별 VMA (vm_lock)
페이지 폴트 시 mmap_read_lock(mm) — 전역 RW lock vma_start_read(vma) — per-VMA lock
경합 수준 높음 (모든 스레드가 같은 lock) 낮음 (다른 VMA 접근은 병렬)
mmap/munmap 시 mmap_write_lock(mm) mmap_write_lock(mm) (변경 없음)
성능 영향 기준 페이지 폴트 처리량(Throughput) 75%+ 향상 (고스레드 환경)
/* mm/memory.c — per-VMA lock 페이지 폴트 경로 (커널 6.4+) */
static vm_fault_t do_user_addr_fault(
    struct pt_regs *regs,
    unsigned long error_code,
    unsigned long address)
{
    struct vm_area_struct *vma;

    /* Fast path: per-VMA lock 시도 (mmap_lock 없이) */
    vma = lock_vma_under_rcu(mm, address);
    if (vma) {
        /* per-VMA lock 획득 성공 — 다른 VMA 접근과 병렬 */
        fault = handle_mm_fault(vma, address, flags, regs);
        vma_end_read(vma);
        return fault;
    }

    /* Slow path: mmap_read_lock 폴백 */
    mmap_read_lock(mm);
    vma = find_vma(mm, address);
    fault = handle_mm_fault(vma, address, flags, regs);
    mmap_read_unlock(mm);
    return fault;
}
mmap_lock (기존) vs per-VMA lock (6.4+) 비교 기존: mmap_lock — 전체 직렬화 Thread A Thread B Thread C Thread D mmap_lock (RW) VMA₁ [text] VMA₂ [heap] VMA₃ [lib] VMA₄ [stack] 시간 → A: fault VMA₂ B: fault VMA₃ C: fault VMA₁ D: fault VMA₄ 모든 fault가 순차 처리 (병목) 6.4+: per-VMA lock — 병렬 처리 Thread A Thread B Thread C Thread D VMA₁ [text] VMA₂ [heap] VMA₃ [lib] VMA₄ [stack] vm_lock vm_lock vm_lock vm_lock 시간 → A: fault VMA₂ B: fault VMA₃ C: fault VMA₁ D: fault VMA₄ 4개 fault 동시 병렬 처리 lock_vma_under_rcu() 동작 흐름: 1. RCU read lock 획득 → Maple Tree에서 VMA 조회 2. vma->vm_lock_seq != mm->mm_lock_seq 확인 (무효화 여부) 3. vma_start_read(vma) — per-VMA rw_semaphore read lock 획득 4. 성공 시: handle_mm_fault() 실행 → vma_end_read(vma) 5. 실패 시: mmap_read_lock(mm) 폴백 (slow path) mmap/munmap은 여전히 mmap_write_lock(mm)을 사용하며, mm_lock_seq++로 모든 per-VMA lock을 무효화합니다

lock_vma_under_rcu() 커널 코드

/* mm/memory.c — lock_vma_under_rcu() (커널 6.4+, 간략화) */
struct vm_area_struct *lock_vma_under_rcu(
    struct mm_struct *mm,
    unsigned long address)
{
    struct vm_area_struct *vma;

    rcu_read_lock();

    /* RCU 보호 하에 Maple Tree에서 VMA 탐색
     * mmap_lock 없이도 안전하게 VMA를 찾을 수 있음 */
    vma = mas_walk(&mas);
    if (!vma)
        goto inval;

    /* 주소가 VMA 범위 내인지 확인 */
    if (address < vma->vm_start || address >= vma->vm_end)
        goto inval;

    /* per-VMA lock 시도:
     * vm_lock_seq와 mm_lock_seq를 비교하여
     * mmap_write_lock()으로 VMA가 변경되었는지 확인 */
    if (!vma_start_read(vma))
        goto inval;

    /* lock 획득 후 VMA가 여전히 유효한지 재검증
     * (VMA가 분할/병합/제거되었을 수 있음) */
    if (unlikely(vma->vm_start > address ||
                  vma->vm_end <= address)) {
        vma_end_read(vma);
        goto inval;
    }

    rcu_read_unlock();
    return vma;  /* fast path 성공: per-VMA lock 보유 */

inval:
    rcu_read_unlock();
    return NULL;  /* slow path로 폴백 필요 */
}
설명

lock_vma_under_rcu()는 per-VMA lock의 핵심 진입점입니다:

  • RCU read lockrcu_read_lock()으로 VMA가 탐색 중에 해제되는 것을 방지합니다. Maple Tree 노드와 VMA 구조체는 RCU에 의해 보호되므로, mmap_lock 없이도 안전하게 읽을 수 있습니다.
  • vma_start_read()vma->vm_lock_seqmm->mm_lock_seq를 비교합니다. mmap_write_lock()이 호출되면 mm_lock_seq가 증가하여 모든 per-VMA lock이 자동으로 무효화됩니다. 이 메커니즘 덕분에 개별 VMA를 일일이 lock/unlock하지 않아도 일관성이 보장됩니다.
  • 재검증 — lock 획득 후에도 VMA가 분할/병합/제거되었을 수 있으므로 주소 범위를 다시 확인합니다. 이 이중 검사는 TOCTOU(Time-Of-Check-Time-Of-Use) 경쟁 조건을 방지합니다.
  • 폴백 — per-VMA lock 획득에 실패하면 NULL을 반환하고, 호출자(do_user_addr_fault)는 기존 mmap_read_lock() 경로로 폴백합니다.
ℹ️

per-VMA lock의 핵심: vm_area_structvm_lock(rw_semaphore)과 vm_lock_seq(시퀀스 카운터)가 추가되었습니다. mmap_write_lock()mm->mm_lock_seq를 증가시키면 모든 per-VMA lock이 무효화(Invalidation)되어 일관성을 보장합니다. 자세한 동기화 메커니즘은 동기화 기법 페이지를 참고하세요.

fork()와 VMA 복제 (dup_mmap)

fork() 시스템 콜은 부모 프로세스의 전체 주소 공간을 자식에게 복제합니다. 이 과정에서 모든 VMA가 복사되고, 사적 매핑의 페이지는 COW로 설정됩니다.

fork() → dup_mmap(): VMA 복제 과정 부모 mm_struct VMA: [text] 0x5500-0x5600 r-xp /bin/app VMA: [heap] 0x5600-0x5700 rw-p (anonymous) VMA: [libc] 0x7f00-0x7f20 r-xp /lib/libc.so VMA: [stack] 0x7ffd-0x7fff rw-p (anonymous) VMA: [mmap] 0x7f30-0x7f40 rw-s /dev/shm/buf dup_mmap() 자식 mm_struct VMA copy: [text] r-xp (공유, COW 불필요) VMA copy: [heap] rw-p → PTE R/O (COW 설정) VMA copy: [libc] r-xp (읽기 전용, 공유) VMA copy: [stack] rw-p → PTE R/O (COW 설정) VMA copy: [mmap] rw-s (MAP_SHARED: 공유 유지) 페이지 테이블 복제와 COW 설정 copy_page_range() → 각 PTE를 순회하며: • MAP_PRIVATE + R/W → 부모/자식 모두 PTE를 R/O로 변경 (COW 마커) • MAP_SHARED → PTE를 그대로 복사 (같은 물리 페이지 공유, 쓰기 가능) • 읽기 전용 영역 → PTE를 그대로 복사 (COW 불필요, 이미 R/O) • VM_DONTCOPY/VM_WIPEONFORK → 복사 건너뛰기 또는 제로화 fork() 성능 특성 시간 복잡도: O(VMA 수 + 페이지 테이블 크기) — VMA가 많을수록 느림 대안: vfork() = mm 공유 (VMA 복제 없음), clone(CLONE_VM) = 스레드 (mm 공유) 최적화: THP(2MB 페이지)는 512개 PTE 대신 1개 PMD 복사 → fork() 가속 문제: Redis 등 대용량 메모리 + fork() → COW storm (모든 페이지에 쓰기 시 대량 복사 발생)
/* kernel/fork.c → mm/mmap.c — dup_mmap() 핵심 (간략화) */
static inline int dup_mmap(struct mm_struct *mm,
                            struct mm_struct *oldmm)
{
    struct vm_area_struct *mpnt, *tmp;
    VMA_ITERATOR(old_vmi, oldmm, 0);
    VMA_ITERATOR(vmi, mm, 0);

    /* 부모의 mmap_lock write-lock 획득 */
    mmap_write_lock(oldmm);

    /* 부모의 모든 VMA를 순회하며 복제 */
    for_each_vma(old_vmi, mpnt) {
        /* VM_DONTCOPY: 이 VMA는 자식에게 복사하지 않음 */
        if (mpnt->vm_flags & VM_DONTCOPY)
            continue;

        /* VMA 구조체 복사 (vm_area_dup = kmem_cache_alloc + memcpy) */
        tmp = vm_area_dup(mpnt);
        if (!tmp)
            goto fail_nomem;

        /* VM_WIPEONFORK: 자식에게 제로 페이지 제공 (비밀 데이터 보호) */
        if (tmp->vm_flags & VM_WIPEONFORK) {
            tmp->anon_vma = NULL;
            /* 페이지 테이블 복사 건너뛰기 → 접근 시 제로 페이지 */
        } else if (tmp->anon_vma) {
            /* anon_vma 체인 연결 (역매핑 유지) */
            anon_vma_fork(tmp, mpnt);
        }

        /* 파일 매핑: file 참조 카운트 증가 */
        if (tmp->vm_file)
            get_file(tmp->vm_file);

        /* vm_ops->open() 콜백 (드라이버 통지) */
        if (tmp->vm_ops && tmp->vm_ops->open)
            tmp->vm_ops->open(tmp);

        /* 자식의 Maple Tree에 VMA 삽입 */
        vma_iter_store(&vmi, tmp);
        mm->map_count++;

        /* 페이지 테이블 복제 (COW 설정 포함) */
        copy_page_range(tmp, mpnt);
    }

    mmap_write_unlock(oldmm);
    return 0;
}
코드 설명

dup_mmap()의 핵심 동작:

  • vm_area_dup()vm_area_cachep slab에서 새 VMA를 할당하고 부모 VMA의 모든 필드를 복사합니다. vm_mm만 자식의 mm_struct로 변경됩니다.
  • VM_DONTCOPYmadvise(MADV_DONTFORK)으로 설정. GPU 메모리, DMA 버퍼 등 자식에게 복사하면 위험한 매핑에 사용됩니다.
  • VM_WIPEONFORKmadvise(MADV_WIPEONFORK)으로 설정. 자식 프로세스에서 해당 영역을 제로 페이지로 초기화합니다. 비밀번호, 암호화 키 등 민감한 데이터가 자식에게 노출되지 않도록 보호합니다.
  • anon_vma_fork() — 부모와 자식의 anon_vma를 체인으로 연결합니다. 페이지 회수(reclaim) 시 같은 물리 페이지를 참조하는 모든 PTE를 찾기 위해 필요합니다.
  • copy_page_range() — 부모의 페이지 테이블을 순회하며 자식에게 복사합니다. MAP_PRIVATE + R/W 페이지는 부모와 자식 모두의 PTE를 R/O로 변경하여 COW를 설정합니다.

anon_vma 역매핑(Reverse Mapping) 상세

커널이 물리 페이지를 회수(reclaim)하려면, 해당 페이지를 참조하는 모든 PTE를 찾아 무효화해야 합니다. anon_vma는 익명 페이지의 역매핑(Reverse Mapping)을 구현하는 핵심 자료구조입니다.

anon_vma 역매핑: 물리 페이지 → 모든 PTE 찾기 물리 페이지 (struct folio) folio->mapping anon_vma (루트) rb_root_cached 인터벌 트리 (anon_vma_chain들의 rbtree) anon_vma_chain anon_vma_chain anon_vma_chain 부모 VMA PTE → 물리 페이지 자식1 VMA PTE → 물리 페이지 (COW) 자식2 VMA (손자) PTE → 물리 페이지 (COW) 역매핑 동작: 페이지 회수(reclaim) 시 1. folio->mapping에서 anon_vma 루트를 찾음 2. anon_vma의 인터벌 트리를 순회하여 해당 페이지 오프셋과 겹치는 모든 VMA를 찾음 3. 각 VMA에서 PTE를 walk하여 해당 물리 페이지를 참조하는 PTE를 unmap (try_to_unmap)
/* include/linux/rmap.h — anon_vma 관련 구조체 */
struct anon_vma {
    struct anon_vma *root;          /* 루트 anon_vma (최초 할당자) */
    struct rw_semaphore rwsem;      /* 역매핑 순회 보호 */
    atomic_t refcount;               /* 참조 카운트 */
    unsigned long num_children;      /* 자식 anon_vma 수 */
    unsigned long num_active_vmas;   /* 활성 VMA 수 */
    struct anon_vma *parent;         /* 부모 anon_vma (fork 체인) */
    struct rb_root_cached rb_root;   /* VMA 인터벌 트리 */
};

/* anon_vma_chain: anon_vma ↔ VMA 연결 브리지 */
struct anon_vma_chain {
    struct vm_area_struct *vma;     /* 이 체인이 가리키는 VMA */
    struct anon_vma *anon_vma;      /* 이 체인이 속한 anon_vma */
    struct list_head same_vma;      /* VMA의 anon_vma_chain 리스트 */
    struct rb_node rb;              /* anon_vma의 인터벌 트리 노드 */
    unsigned long rb_subtree_last;  /* augmented rbtree: 서브트리 최대 end */
};

/* mm/rmap.c — 역매핑을 통한 페이지 unmap (간략화) */
bool try_to_unmap(struct folio *folio, enum ttu_flags flags)
{
    struct rmap_walk_control rwc = {
        .rmap_one = try_to_unmap_one,  /* 각 PTE에 대해 호출 */
        .done = folio_not_mapped,       /* mapcount==0이면 조기 종료 */
    };

    if (folio_test_anon(folio))
        rmap_walk_anon(folio, &rwc, false);
    else
        rmap_walk_file(folio, &rwc, false);

    return !folio_mapcount(folio);  /* 모든 매핑 해제 성공? */
}

/* rmap_walk_anon: anon_vma의 인터벌 트리를 순회 */
static void rmap_walk_anon(struct folio *folio,
                           struct rmap_walk_control *rwc, ...)
{
    struct anon_vma *anon_vma = folio->mapping;
    struct anon_vma_chain *avc;
    pgoff_t pgoff_start, pgoff_end;

    anon_vma_lock_read(anon_vma);

    /* 인터벌 트리에서 이 페이지 오프셋과 겹치는 VMA 검색 */
    anon_vma_interval_tree_foreach(avc, &anon_vma->rb_root,
                                    pgoff_start, pgoff_end) {
        struct vm_area_struct *vma = avc->vma;

        /* 각 VMA에서 해당 페이지의 PTE를 찾아 unmap */
        rwc->rmap_one(folio, vma, address, rwc->arg);

        if (rwc->done && rwc->done(folio))
            break;  /* 모든 매핑 해제 완료 */
    }

    anon_vma_unlock_read(anon_vma);
}
코드 설명

역매핑(Reverse Mapping)의 핵심 자료구조와 동작:

  • anon_vma — 같은 물리 페이지를 공유하는 VMA들을 추적하는 루트 구조체입니다. fork() 체인에서 부모-자식 관계를 형성합니다. rb_rootanon_vma_chain들을 인터벌 트리로 관리하여 특정 페이지 오프셋을 참조하는 VMA를 빠르게 찾습니다.
  • anon_vma_chainanon_vmavm_area_struct를 연결하는 브리지입니다. 하나의 VMA가 여러 anon_vma에 연결될 수 있고(fork 체인), 하나의 anon_vma에 여러 VMA가 연결될 수 있습니다(N:M 관계).
  • try_to_unmap() — 페이지 회수(reclaim)의 핵심 함수입니다. LRU 스캐너(shrink_folio_list)가 페이지를 회수하려 할 때 호출됩니다. 모든 PTE 참조를 제거해야 페이지를 프리 리스트에 반환할 수 있습니다.
  • rmap_walk_anon()folio->mapping에서 anon_vma를 찾고, 인터벌 트리를 순회하여 해당 페이지 오프셋과 겹치는 모든 VMA를 찾습니다. 각 VMA에 대해 try_to_unmap_one()으로 PTE를 무효화합니다.
  • 성능 문제fork()가 깊이 중첩되면(fork bomb 등) anon_vma 체인이 길어져 역매핑 순회 비용이 증가합니다. 커널은 anon_vma_clone()에서 체인 길이를 제한하는 휴리스틱을 적용합니다.
💡

파일 매핑의 역매핑: 파일 매핑 페이지는 address_spacei_mmap 인터벌 트리를 사용합니다. rmap_walk_file()vma_interval_tree_foreach()로 파일 오프셋 범위와 겹치는 VMA를 순회합니다. 원리는 anon_vma와 동일하나, 파일의 address_space가 직접 인터벌 트리를 관리합니다.

glibc malloc ↔ mmap 연동

사용자 공간의 malloc()은 내부적으로 brk()mmap()을 조합하여 메모리를 할당합니다. 요청 크기에 따라 전략이 달라집니다.

glibc malloc: brk vs mmap 전략 분기 malloc(size) size ≥ mmap_threshold (128KB)? No (작은 할당) brk(sbrk) 경로 — Arena/Bin 할당 • brk()로 힙 영역 확장 (연속 가상 주소) • fastbin (≤160B): LIFO, lock-free • smallbin (≤1024B): FIFO, 정확한 크기 • largebin (~128KB): best-fit 정렬 • unsorted bin: 최근 해제 재사용 • top chunk: 분할 가능한 최상위 청크 장점: 빠름, 단점: 해제 후 OS 반환 어려움 Yes (큰 할당) mmap(MAP_PRIVATE|MAP_ANONYMOUS) • 독립적인 VMA 생성 • free() 시 munmap()으로 즉시 OS 반환 • 주소 공간 단편화 없음 • 한계: VMA 생성/삭제 오버헤드 • TLB flush 비용 • mallopt(M_MMAP_THRESHOLD) 조정 장점: 즉시 반환, 단점: 느림
/* glibc malloc 내부 경로 (ptmalloc2 기반, 간략화) */

/* 작은 할당 (size < mmap_threshold): brk 경로 */
void *malloc(size_t size)
{
    /* 1. fastbin/smallbin/unsortedbin에서 재사용 가능한 청크 검색 */
    chunk = _int_malloc(arena, size);

    /* 2. 없으면 top chunk에서 분할 */
    if (!chunk && top_chunk_small) {
        /* 3. top chunk 부족 → sbrk()/brk()로 힙 확장 */
        sysmalloc(size, arena);
        /* → sys_brk() → do_brk_flags() → VMA 확장 */
    }
    return chunk2mem(chunk);
}

/* 큰 할당 (size >= mmap_threshold): mmap 경로 */
/* sysmalloc() 내부에서: */
if (size >= mp_.mmap_threshold && n_mmaps < mp_.n_mmaps_max) {
    mm = (char *)MMAP(0, size,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS);
    /* → sys_mmap → do_mmap → 새 VMA 생성 */
}

/* free() 경로 */
void free(void *ptr)
{
    if (chunk_is_mmapped(p)) {
        /* mmap으로 할당된 청크: munmap()으로 즉시 OS에 반환 */
        munmap_chunk(p);
        /* → sys_munmap → do_munmap → VMA 삭제 + 페이지 해제 */
    } else {
        /* brk 청크: bin에 반환 (재사용 대기) */
        _int_free(arena, p, 0);
        /* top chunk와 합체 → 필요 시 brk()로 힙 축소 */
    }
}
파라미터기본값조정 방법설명
M_MMAP_THRESHOLD128KB (동적 조정)mallopt(M_MMAP_THRESHOLD, val)이 크기 이상이면 mmap 사용
M_MMAP_MAX65536mallopt(M_MMAP_MAX, val)최대 mmap 할당 수 (0=mmap 비활성화)
M_TRIM_THRESHOLD128KBmallopt(M_TRIM_THRESHOLD, val)brk 축소 임계값
M_TOP_PAD128KBmallopt(M_TOP_PAD, val)brk 확장 시 여유 크기
M_ARENA_MAX8 * CPU수mallopt(M_ARENA_MAX, val)최대 arena 수 (멀티스레드)
💡

동적 mmap_threshold 조정: glibc는 free()로 반환된 mmap 청크의 크기를 추적하여 mmap_threshold를 자동으로 올립니다 (최대 DEFAULT_MMAP_THRESHOLD_MAX = 32MB). 이를 통해 반복되는 큰 할당/해제에서 불필요한 mmap/munmap 호출을 줄입니다. M_MMAP_THRESHOLD를 수동 설정하면 동적 조정이 비활성화됩니다.

jemalloc/tcmalloc과의 차이:

jemalloc(Firefox, FreeBSD)과 tcmalloc(Google)은 glibc ptmalloc2보다 mmap 사용이 공격적입니다. 큰 영역을 mmap으로 확보한 뒤 내부적으로 페이지 단위로 관리하고, madvise(MADV_FREE)/MADV_DONTNEED로 사용하지 않는 페이지를 OS에 반환합니다. 이 접근은 VMA 수를 최소화하면서도 메모리 반환 효율을 높입니다.

process_madvise() — 외부 프로세스 메모리 관리 (5.10+)

기존 madvise()는 자기 자신의 메모리만 제어할 수 있었지만, process_madvise()다른 프로세스의 메모리에 대해 힌트를 제공할 수 있습니다. 컨테이너 환경의 메모리 관리자, 안드로이드의 LMKD(Low Memory Killer Daemon) 등에서 활용됩니다.

#include <sys/mman.h>
#include <sys/uio.h>

/* process_madvise(): 다른 프로세스의 메모리에 madvise 적용 (5.10+) */
ssize_t process_madvise(
    int pidfd,                    /* 대상 프로세스의 pidfd */
    const struct iovec *iovec,   /* 주소 범위 벡터 */
    size_t vlen,                  /* iovec 개수 */
    int advice,                   /* MADV_COLD, MADV_PAGEOUT 등 */
    unsigned int flags            /* 0 (예약) */
);

/* 사용 예: 컨테이너 메모리 관리 데몬 */
int pidfd = pidfd_open(target_pid, 0);

/* 대상 프로세스의 콜드 영역을 inactive로 강등 */
struct iovec iov[] = {
    { .iov_base = (void *)0x7f0000000000, .iov_len = 64 * 1024 * 1024 },
    { .iov_base = (void *)0x7f1000000000, .iov_len = 32 * 1024 * 1024 },
};

/* MADV_COLD: 페이지를 inactive 리스트로 이동 → 메모리 압박 시 우선 회수 */
process_madvise(pidfd, iov, 2, MADV_COLD, 0);

/* MADV_PAGEOUT: 페이지를 즉시 스왑으로 이동 (프로액티브 회수) */
process_madvise(pidfd, iov, 2, MADV_PAGEOUT, 0);

close(pidfd);
지원 advice동작권한 요구
MADV_COLD페이지를 LRU inactive 리스트로 강등CAP_SYS_PTRACE 또는 같은 UID
MADV_PAGEOUT페이지를 즉시 스왑으로 강제 이동CAP_SYS_PTRACE 또는 같은 UID
MADV_WILLNEED페이지 사전 로드 (prefault)같은 UID (5.18+)

Android LMKD 활용 패턴

안드로이드(Android)의 Low Memory Killer Daemon(LMKD)은 process_madvise()의 대표적인 실사용 사례입니다. LMKD는 포그라운드 앱의 성능을 보호하면서 백그라운드 앱의 메모리를 프로액티브하게 회수합니다.

단계LMKD 동작사용 API
1. 앱 상태 추적ActivityManager로부터 앱 전환 이벤트 수신epoll + socket
2. 우선순위 조정백그라운드 전환된 앱의 oom_score_adj 상향/proc/[pid]/oom_score_adj
3. 콜드 마킹백그라운드 앱 메모리를 inactive로 강등process_madvise(MADV_COLD)
4. 프로액티브 회수메모리 압박 시 백그라운드 앱 페이지를 스왑으로 이동process_madvise(MADV_PAGEOUT)
5. 앱 종료극심한 메모리 부족 시 oom_score_adj가 높은 앱부터 종료kill() 또는 pidfd_send_signal()

컨테이너 환경 메모리 관리

Kubernetes나 Docker 환경에서는 컨테이너 외부의 관리 데몬이 process_madvise()를 사용하여 개별 컨테이너 프로세스의 메모리를 세밀하게 제어할 수 있습니다. cgroup v2의 memory.pressure와 조합하면 메모리 압박 수준에 따라 단계적 대응이 가능합니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <sys/uio.h>
#include <sys/mman.h>

/* pidfd_open() 래퍼 (glibc 2.36 미만에서 직접 호출 필요) */
static int my_pidfd_open(pid_t pid, unsigned int flags)
{
    return syscall(SYS_pidfd_open, pid, flags);
}

/* process_madvise() 래퍼 */
static ssize_t my_process_madvise(int pidfd,
    const struct iovec *iovec, size_t vlen,
    int advice, unsigned int flags)
{
    return syscall(SYS_process_madvise, pidfd, iovec, vlen, advice, flags);
}

/* 컨테이너 메모리 관리 데몬 예시:
 * /proc/[pid]/maps를 파싱하여 anonymous 영역을 수집하고,
 * 메모리 압박 수준에 따라 단계적으로 process_madvise() 적용 */
int reclaim_container_memory(pid_t target_pid, int pressure_level)
{
    /* 1. pidfd 획득 — PID 재사용 경쟁 조건 방지 */
    int pidfd = my_pidfd_open(target_pid, 0);
    if (pidfd < 0) {
        perror("pidfd_open");
        return -1;
    }

    /* 2. /proc/[pid]/maps에서 anonymous rw-p 영역 수집 */
    char maps_path[64];
    snprintf(maps_path, sizeof(maps_path),
             "/proc/%d/maps", target_pid);

    FILE *fp = fopen(maps_path, "r");
    if (!fp) { close(pidfd); return -1; }

    struct iovec iovs[128];
    int iov_count = 0;
    char line[512];

    while (fgets(line, sizeof(line), fp) && iov_count < 128) {
        unsigned long start, end;
        char perms[5];
        if (sscanf(line, "%lx-%lx %4s", &start, &end, perms) != 3)
            continue;

        /* anonymous rw-p 영역만 대상 (힙, 스택 등) */
        if (perms[0] == 'r' && perms[1] == 'w' &&
            perms[3] == 'p' && !strstr(line, "/")) {
            iovs[iov_count].iov_base = (void *)start;
            iovs[iov_count].iov_len  = end - start;
            iov_count++;
        }
    }
    fclose(fp);

    /* 3. 압박 수준에 따라 적절한 advice 선택 */
    int advice;
    if (pressure_level == 1)
        advice = MADV_COLD;       /* 경미: inactive로 강등만 */
    else
        advice = MADV_PAGEOUT;    /* 심각: 즉시 스왑으로 이동 */

    ssize_t ret = my_process_madvise(pidfd, iovs, iov_count,
                                     advice, 0);
    if (ret < 0)
        perror("process_madvise");

    close(pidfd);
    return (ret < 0) ? -1 : 0;
}
설명

이 코드는 컨테이너 외부 메모리 관리 데몬의 핵심 로직을 보여줍니다:

  • pidfd_open() — PID 대신 파일 디스크립터를 사용하여 대상 프로세스를 식별합니다. PID는 프로세스 종료 후 재사용될 수 있지만, pidfd는 해당 프로세스의 생존 기간에만 유효하므로 경쟁 조건(Race Condition)을 방지합니다.
  • /proc/[pid]/maps 파싱 — anonymous rw-p 영역을 수집하여 iovec 배열을 구성합니다. 파일 매핑(라이브러리 코드 등)은 페이지 캐시에서 자동 관리되므로 제외합니다.
  • MADV_COLD vs MADV_PAGEOUTMADV_COLD는 LRU inactive 리스트로 강등만 수행하므로 부담이 적고, MADV_PAGEOUT은 즉시 스왑 아웃을 수행하여 물리 메모리를 확보합니다. 메모리 압박 수준에 따라 단계적으로 적용하는 것이 권장됩니다.
  • 권한 요구process_madvise()CAP_SYS_PTRACE 또는 대상 프로세스와 같은 UID를 요구합니다. 컨테이너 환경에서는 관리 데몬에 해당 capability를 부여해야 합니다.
pidfd와의 연동:

pidfd는 리눅스 5.2에서 도입되었으며, process_madvise()(5.10), pidfd_send_signal()(5.1), pidfd_getfd()(5.6) 등 프로세스 관리 API의 기반이 됩니다. pidfd_open()으로 획득한 fd는 poll()/epoll()로 대상 프로세스의 종료를 감지할 수 있으며, waitid(P_PIDFD, ...)로 종료 상태를 수집할 수도 있습니다. 이를 통해 PID 재사용 문제 없이 안전한 프로세스 수명 주기 관리가 가능합니다.

주의 사항:

process_madvise()는 대상 프로세스의 mmap_lock을 read lock으로 획득합니다. 대상 프로세스가 동시에 mmap()/munmap()을 수행 중이면 write lock 경합이 발생할 수 있습니다. 대량의 영역에 MADV_PAGEOUT을 적용할 때는 iovec을 적절히 분할하여 한 번에 처리하는 양을 제한하는 것이 좋습니다.

실용적 예제

proc/maps 읽기와 해석

# 프로세스 메모리 매핑 확인
cat /proc/self/maps
# 출력 형식:
# 주소범위           권한  오프셋   디바이스 inode  경로
# 55b5c8400000-55b5c8420000 r--p 00000000 08:01 1234   /usr/bin/bash
# 55b5c8420000-55b5c84e0000 r-xp 00020000 08:01 1234   /usr/bin/bash
# 55b5c84e0000-55b5c8510000 r--p 000e0000 08:01 1234   /usr/bin/bash
# 55b5c8510000-55b5c8514000 rw-p 00110000 08:01 1234   /usr/bin/bash
# 55b5c9a00000-55b5c9b20000 rw-p 00000000 00:00 0      [heap]
# 7f1234000000-7f1234200000 rw-p 00000000 00:00 0       (anonymous)
# 7ffd12300000-7ffd12321000 rw-p 00000000 00:00 0      [stack]
# 7ffd12321000-7ffd12325000 r--p 00000000 00:00 0      [vvar]
# 7ffd12325000-7ffd12327000 r-xp 00000000 00:00 0      [vdso]

# smaps_rollup: 프로세스 전체 메모리 요약
cat /proc/self/smaps_rollup
# Rss:              12340 kB  ← 실제 물리 메모리 사용량
# Pss:               8200 kB  ← 공유 페이지 비례 배분
# Shared_Clean:      4000 kB  ← 공유 라이브러리 코드
# Private_Dirty:     7500 kB  ← 프로세스 고유 수정 데이터
# Anonymous:         6000 kB  ← 파일 backing 없는 메모리

mmap으로 파일 매핑

/* 파일을 메모리에 매핑하여 읽기 (MAP_PRIVATE) */
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

int fd = open("/etc/passwd", O_RDONLY);
struct stat st;
fstat(fd, &st);

/* MAP_PRIVATE: COW로 원본 파일 보호 */
char *data = mmap(NULL, st.st_size,
                   PROT_READ, MAP_PRIVATE, fd, 0);
close(fd);  /* mmap 후 fd 닫아도 매핑 유지 */

/* 데이터 접근 — 첫 접근 시 페이지 폴트 → 페이지 캐시에서 매핑 */
printf("%.100s\n", data);

munmap(data, st.st_size);

/* MAP_SHARED: 파일에 직접 쓰기 (다른 프로세스와 공유) */
fd = open("/tmp/shared.dat", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 4096);
int *shared = mmap(NULL, 4096,
                     PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
shared[0] = 42;  /* 파일에 즉시 반영 (writeback) */
msync(shared, 4096, MS_SYNC);  /* 디스크 동기화 보장 */
munmap(shared, 4096);

공유 메모리와 대형 버퍼

/* 부모-자식 프로세스 간 공유 메모리 */
int *shared = mmap(NULL, 4096,
                     PROT_READ | PROT_WRITE,
                     MAP_SHARED | MAP_ANONYMOUS,  /* 파일 없이 공유 */
                     -1, 0);

if (fork() == 0) {
    /* 자식: 데이터 쓰기 */
    shared[0] = 12345;
    _exit(0);
}
wait(NULL);
printf("child wrote: %d\n", shared[0]);  /* 12345 */

/* 대형 버퍼 — Huge Pages로 TLB 미스 감소 */
void *huge = mmap(NULL, 2 * 1024 * 1024,  /* 2MB */
                    PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                    -1, 0);
if (huge == MAP_FAILED)
    perror("MAP_HUGETLB requires hugetlbfs or THP");
💡

MAP_POPULATE: mmap() 호출 시 MAP_POPULATE 플래그를 추가하면 매핑 즉시 페이지 테이블을 채워 이후 페이지 폴트를 방지합니다. 대용량 파일 읽기나 데이터베이스 초기화 시 유용합니다. 관련 내용은 Huge Pages 페이지페이지 캐시 페이지를 참고하세요.

참고자료

커널 문서

Man Pages

LWN 기사

커널 소스

VMA/mmap과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.