VMA / mmap
Linux 커널 가상 메모리(Virtual Memory) 매핑(Mapping): mmap 시스템 콜(System Call) 인터페이스, VMA 구조와 관리, 페이지 폴트(Page Fault) 처리, MAP_SHARED/PRIVATE 공유 메모리, mremap/mprotect/madvise, userfaultfd, 프로세스(Process) 주소 공간(Address Space) 레이아웃 종합 가이드.
핵심 요약
- mmap() — 파일, 익명 메모리, 디바이스 등을 프로세스의 가상 주소 공간에 매핑하는 시스템 콜입니다.
- VMA (vm_area_struct) — 프로세스 주소 공간의 연속된 가상 메모리 영역을 나타내는 커널 자료구조입니다.
- Demand Paging — mmap 시 가상 주소(Virtual Address)만 확보하고, 실제 물리 페이지는 첫 접근 시(page fault) 할당하는 지연 할당 기법입니다.
- MAP_SHARED / MAP_PRIVATE — 공유 매핑은 모든 프로세스가 동일한 물리 페이지를 공유하고, 사유 매핑은 쓰기 시 COW(Copy-On-Write)로 복사합니다.
- mprotect / mremap / madvise — 매핑된 영역의 보호 속성 변경, 크기 조정, 접근 힌트 제공 등 mmap 후 동적 제어 시스템 콜입니다.
단계별 이해
- mmap 호출 → VMA 생성 —
mmap()을 호출하면 커널은vm_area_struct를 생성하여 주소 범위, 권한, 파일 매핑 정보를 기록합니다.이 시점에는 페이지 테이블(Page Table) 엔트리가 없고, 가상 주소만 예약된 상태입니다.
- 첫 접근 → Page Fault — 프로세스가 매핑된 주소에 접근하면 page fault가 발생합니다. 커널은 VMA를 조회하여 유효한 접근인지 확인합니다.
유효하면 물리 페이지를 할당하고 페이지 테이블에 매핑을 생성합니다. 파일 매핑이면 파일에서 데이터를 읽어옵니다.
- COW (Copy-On-Write) — MAP_PRIVATE 매핑에서 쓰기를 시도하면 물리 페이지를 복사하여 프로세스 전용 페이지로 분리합니다.
fork() 후 부모-자식이 동일한 VMA를 공유하다가, 쓰기 시 비로소 복사되어 메모리를 절약합니다.
- 메모리 보호와 제어 — 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_create | IPC, 공유 캐시, 멀티미디어 버퍼 |
| 공유 라이브러리 로딩 | 파일 매핑 (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_NONE | 0x0 | 접근 불가 — guard page, 주소 공간 예약에 사용 |
PROT_READ | 0x1 | 읽기 허용 |
PROT_WRITE | 0x2 | 쓰기 허용 (x86에서 PROT_READ 자동 포함) |
PROT_EXEC | 0x4 | 실행 허용 — NX bit 지원 시 W^X 정책과 상호작용 |
execmem 정책은
PROT_WRITE | PROT_EXEC 동시 사용을 금지합니다. JIT 컴파일러는 먼저
PROT_WRITE로 코드를 쓴 후 mprotect()로 PROT_EXEC로 전환하는
2단계 패턴을 사용합니다.
매핑 유형 플래그 (flags) — 필수
| 플래그 | 설명 | COW | 디스크 반영 |
|---|---|---|---|
MAP_SHARED | 공유 매핑 — 여러 프로세스가 동일 물리 페이지 공유 | 없음 | msync/munmap 시 반영 |
MAP_SHARED_VALIDATE | MAP_SHARED + 알 수 없는 플래그 시 EOPNOTSUPP 반환 | 없음 | 반영 |
MAP_PRIVATE | 사적 매핑 — 쓰기 시 COW로 사본 생성 | 있음 | 반영 안 됨 |
매핑 동작 플래그 (flags) — 선택
| 플래그 | 값 | 설명 |
|---|---|---|
MAP_ANONYMOUS | 0x20 | 파일 없는 익명 매핑 — 힙 확장, 큰 메모리 할당에 사용 |
MAP_FIXED | 0x10 | addr에 정확히 배치 — 기존 매핑 덮어씀 (위험) |
MAP_FIXED_NOREPLACE | 0x100000 | addr에 배치하되, 충돌 시 EEXIST 반환 (안전한 대안) |
MAP_POPULATE | 0x8000 | 매핑 시 모든 페이지를 사전 폴트 — read-ahead 효과 |
MAP_LOCKED | 0x2000 | mlock과 동일 — 페이지를 RAM에 고정 (스왑(Swap) 방지) |
MAP_NORESERVE | 0x4000 | 스왑 영역(Swap Area) 예약 없이 매핑 — overcommit 의존 |
MAP_GROWSDOWN | 0x100 | 스택처럼 아래로 성장하는 매핑 |
MAP_HUGETLB | 0x40000 | Huge Page 사용 — MAP_HUGE_2MB, MAP_HUGE_1GB 조합 |
MAP_SYNC | 0x80000 | DAX/PMEM — 영속 메모리에 대한 동기화 보장 |
MAP_32BIT | 0x40 | x86-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_ops—vm_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_policy—set_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/EXEC | mprotect 허용 범위 | mprotect()로 추가 가능한 최대 권한 |
VM_GROWSDOWN | 아래로 확장 | 스택 VMA에 설정 |
VM_DONTEXPAND | 확장 금지 | mremap 확장 방지 |
VM_DONTCOPY | fork 시 복사 금지 | VM_WIPEONFORK: fork 시 제로화 |
VM_IO | I/O 메모리 | 디바이스 메모리 매핑 — core dump 제외 |
VM_PFNMAP | PFN 직접 매핑 | struct page 없는 물리 주소(Physical Address) 매핑 |
VM_LOCKED | mlock됨 | 페이지 스왑/회수 방지 |
VM_HUGETLB | Huge page | hugetlb 매핑 |
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 동작 원리
Maple Tree는 범위 인덱싱을 기본으로 합니다. 각 엔트리는 (index, last) 범위를 가지며,
VMA의 경우 vm_start가 index, vm_end - 1이 last가 됩니다.
pivot 배열은 정렬된 경계값을 저장하여, slot[i]는 pivot[i-1]+1 ~ pivot[i] 범위의 데이터를 가리킵니다.
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_vma는 mas_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 접근 가능
* → 멀티스레드 워크로드에서 극적인 확장성 향상
*/
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 * | 논의 중 |
vm_area_struct에서 vm_next,
vm_prev, vm_rb 필드가 모두 제거되어 구조체 크기가 줄었고,
6.4에서는 이를 기반으로 per-VMA lock이 도입되어
page fault 경로에서 mmap_lock 경합(Contention)을 획기적으로 줄였습니다.
do_mmap 내부 처리 흐름
mmap() 시스템 콜이 커널 내부에서 처리되는 과정을 단계별로 살펴봅니다.
/* 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;
}
/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_count가sysctl_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_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_cachepslab 캐시에서 새 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를 통해 물리 페이지를 할당합니다.
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;
}
/proc/[pid]/stat의 minflt, majflt 필드로 프로세스별 폴트 횟수를 확인할 수 있습니다.
COW (Copy-On-Write) 상세 메커니즘
COW는 MAP_PRIVATE 매핑과 fork()에서 핵심적으로 사용되는 지연 복사 기법입니다.
쓰기가 발생하기 전까지 물리 페이지를 공유하고, 실제 쓰기 시에만 사본을 생성하여 메모리를 절약합니다.
/* 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_LOCKEDVMA이면 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 비율입니다.
mmap 관련 시스템 콜
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_RANDOM | read-ahead 비활성화 | DB 인덱스 랜덤 접근 |
MADV_WILLNEED | 비동기 prefetch (readahead) | 곧 접근할 데이터 사전 로드 |
MADV_DONTNEED | 페이지 즉시 해제 (재접근 시 재폴트) | 메모리 해제, GC |
MADV_FREE | lazy 해제 — 메모리 압박 시에만 회수 | malloc free pool (4.5+) |
MADV_HUGEPAGE | THP 사용 권장 | 대용량 힙, DB 버퍼(Buffer)풀 |
MADV_NOHUGEPAGE | THP 사용 금지 | latency 민감한 워크로드 |
MADV_MERGEABLE | KSM 대상으로 등록 | 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 영역의 위치와 역할을 확인합니다.
# 프로세스의 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)의 시작 주소를 실행마다 무작위화하여 공격자가 메모리 주소를 예측하기 어렵게 만드는 보안 메커니즘입니다.
/* 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]
정보 누출(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, ®);
/* 폴트 이벤트 수신 (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, ©);
/* → 커널이 페이지를 매핑하고 폴트를 해제 */
mmap 성능 최적화
| 시나리오 | 문제 | 최적화 |
|---|---|---|
| 대용량 파일 순차 읽기 | minor fault 누적 | MAP_POPULATE + MADV_SEQUENTIAL |
| DB 랜덤 I/O | 불필요한 read-ahead | MADV_RANDOM |
| 대용량 익명 매핑 | TLB 미스 | MAP_HUGETLB 또는 MADV_HUGEPAGE |
| 실시간(Real-time) 시스템 | 스왑에 의한 지연 | MAP_LOCKED 또는 mlockall() |
| 메모리 할당/해제 반복 | VMA 단편화(Fragmentation) | MADV_FREE (해제 대신 lazy reclaim) |
| VM 라이브 마이그레이션 | 다운타임 | userfaultfd로 점진적 전송 |
| NUMA 노드 미스 | 원격 메모리 접근 | mbind() / set_mempolicy() |
성능 모니터링:
- 프로세스별 폴트 통계:
/proc/[pid]/stat→ field 10(minflt), 12(majflt) - 시스템 전체:
perf stat -e page-faults,minor-faults,major-faults ./app - ftrace로 폴트 추적:
echo 1 > /sys/kernel/debug/tracing/events/exceptions/page_fault_user/enable - 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()은 커널↔사용자 공간 데이터 복사를 제거하므로 대용량 파일 랜덤 접근에서
read()/write()보다 유리합니다.
반면, 소규모 순차 읽기에서는 read()의 VFS 최적화(read-ahead)가 더 효율적일 수 있습니다.
매핑 생성/해제의 오버헤드(VMA 할당, 페이지 테이블 구성, TLB flush)도 고려해야 합니다.
보안: VMA/mmap 관련 취약점 사례
VMA와 mmap 구현의 버그는 권한 상승(privilege escalation)이나 임의 메모리 접근으로 이어질 수 있어, 커널 보안의 핵심 영역입니다. 대표적인 CVE 사례를 통해 안전한 드라이버 및 애플리케이션 개발 방법을 학습할 수 있습니다.
CVE-2016-5195: Dirty COW (Copy-On-Write 경쟁 조건)
원인: 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 잘못된 범위 검증
원인: 커널 모듈(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 크기 검증 우회
원인: 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 보안 체크리스트
| 영역 | 검증 항목 | 위험 |
|---|---|---|
| 드라이버 mmap | vm_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() 사용 | 데이터 손상 |
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 생성
*/
}
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개로 분할되는 과정을 보여줍니다.
/* 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;
}
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 lock —
rcu_read_lock()으로 VMA가 탐색 중에 해제되는 것을 방지합니다. Maple Tree 노드와 VMA 구조체는 RCU에 의해 보호되므로,mmap_lock없이도 안전하게 읽을 수 있습니다. - vma_start_read() —
vma->vm_lock_seq와mm->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_struct에 vm_lock(rw_semaphore)과 vm_lock_seq(시퀀스 카운터)가 추가되었습니다. mmap_write_lock() 시 mm->mm_lock_seq를 증가시키면 모든 per-VMA lock이 무효화(Invalidation)되어 일관성을 보장합니다. 자세한 동기화 메커니즘은 동기화 기법 페이지를 참고하세요.
fork()와 VMA 복제 (dup_mmap)
fork() 시스템 콜은 부모 프로세스의 전체 주소 공간을 자식에게 복제합니다.
이 과정에서 모든 VMA가 복사되고, 사적 매핑의 페이지는 COW로 설정됩니다.
/* 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_cachepslab에서 새 VMA를 할당하고 부모 VMA의 모든 필드를 복사합니다.vm_mm만 자식의mm_struct로 변경됩니다.VM_DONTCOPY—madvise(MADV_DONTFORK)으로 설정. GPU 메모리, DMA 버퍼 등 자식에게 복사하면 위험한 매핑에 사용됩니다.VM_WIPEONFORK—madvise(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)을 구현하는
핵심 자료구조입니다.
/* 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_root는anon_vma_chain들을 인터벌 트리로 관리하여 특정 페이지 오프셋을 참조하는 VMA를 빠르게 찾습니다.anon_vma_chain—anon_vma와vm_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_space의 i_mmap
인터벌 트리를 사용합니다. rmap_walk_file()이 vma_interval_tree_foreach()로
파일 오프셋 범위와 겹치는 VMA를 순회합니다. 원리는 anon_vma와 동일하나,
파일의 address_space가 직접 인터벌 트리를 관리합니다.
glibc malloc ↔ mmap 연동
사용자 공간의 malloc()은 내부적으로 brk()와 mmap()을 조합하여
메모리를 할당합니다. 요청 크기에 따라 전략이 달라집니다.
/* 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_THRESHOLD | 128KB (동적 조정) | mallopt(M_MMAP_THRESHOLD, val) | 이 크기 이상이면 mmap 사용 |
M_MMAP_MAX | 65536 | mallopt(M_MMAP_MAX, val) | 최대 mmap 할당 수 (0=mmap 비활성화) |
M_TRIM_THRESHOLD | 128KB | mallopt(M_TRIM_THRESHOLD, val) | brk 축소 임계값 |
M_TOP_PAD | 128KB | mallopt(M_TOP_PAD, val) | brk 확장 시 여유 크기 |
M_ARENA_MAX | 8 * 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(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_PAGEOUT —
MADV_COLD는 LRU inactive 리스트로 강등만 수행하므로 부담이 적고,MADV_PAGEOUT은 즉시 스왑 아웃을 수행하여 물리 메모리를 확보합니다. 메모리 압박 수준에 따라 단계적으로 적용하는 것이 권장됩니다. - 권한 요구 —
process_madvise()는CAP_SYS_PTRACE또는 대상 프로세스와 같은 UID를 요구합니다. 컨테이너 환경에서는 관리 데몬에 해당 capability를 부여해야 합니다.
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
- mmap(2) — Linux man page
- munmap(2) — Linux man page
- mremap(2) — Linux man page
- madvise(2) — Linux man page
LWN 기사
- LWN: The Maple Tree (2022) — VMA 관리에 사용되는 새로운 자료구조입니다
- LWN: The future of struct page (2018) — struct page 구조 변화와 VMA 영향을 다룹니다
- LWN: Per-VMA locking (2020) — mmap_lock 경합을 줄이기 위한 VMA별 잠금 기법입니다
- LWN: VMA lock improvements (2023) — Per-VMA 잠금 성능 개선 사항을 다룹니다
커널 소스
- mm/mmap.c — mmap 핵심 구현 소스입니다
- include/linux/mm_types.h — struct vm_area_struct 정의입니다
- mm/memory.c — 페이지 폴트 처리 구현입니다
관련 문서
VMA/mmap과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.