VMA / mmap 심화
Linux 커널 가상 메모리 매핑: mmap 시스템 콜 인터페이스, VMA 구조와 관리, 페이지 폴트 처리, MAP_SHARED/PRIVATE 공유 메모리, mremap/mprotect/madvise, userfaultfd, 프로세스 주소 공간 레이아웃 종합 가이드.
핵심 요약
- mmap() — 파일, 익명 메모리, 디바이스 등을 프로세스의 가상 주소 공간에 매핑하는 시스템 콜입니다.
- VMA (vm_area_struct) — 프로세스 주소 공간의 연속된 가상 메모리 영역을 나타내는 커널 자료구조입니다.
- Demand Paging — mmap 시 가상 주소만 확보하고, 실제 물리 페이지는 첫 접근 시(page fault) 할당하는 지연 할당 기법입니다.
- MAP_SHARED / MAP_PRIVATE — 공유 매핑은 모든 프로세스가 동일한 물리 페이지를 공유하고, 사유 매핑은 쓰기 시 COW(Copy-On-Write)로 복사합니다.
- mprotect / mremap / madvise — 매핑된 영역의 보호 속성 변경, 크기 조정, 접근 힌트 제공 등 mmap 후 동적 제어 시스템 콜입니다.
단계별 이해
- mmap 호출 → VMA 생성 —
mmap()을 호출하면 커널은vm_area_struct를 생성하여 주소 범위, 권한, 파일 매핑 정보를 기록합니다.이 시점에는 페이지 테이블 엔트리가 없고, 가상 주소만 예약된 상태입니다.
- 첫 접근 → Page Fault — 프로세스가 매핑된 주소에 접근하면 page fault가 발생합니다. 커널은 VMA를 조회하여 유효한 접근인지 확인합니다.
유효하면 물리 페이지를 할당하고 페이지 테이블에 매핑을 생성합니다. 파일 매핑이면 파일에서 데이터를 읽어옵니다.
- COW (Copy-On-Write) — MAP_PRIVATE 매핑에서 쓰기를 시도하면 물리 페이지를 복사하여 프로세스 전용 페이지로 분리합니다.
fork() 후 부모-자식이 동일한 VMA를 공유하다가, 쓰기 시 비로소 복사되어 메모리를 절약합니다.
- 메모리 보호와 제어 — mprotect()로 실행 금지(NX), 읽기 전용 등 권한을 동적으로 변경할 수 있습니다.
JIT 컴파일러, 샌드박스, 디버거 등이 이를 활용하며, 보안 강화(ASLR, DEP)의 핵심 메커니즘입니다.
mmap 심화 — 가상 메모리 매핑
mmap()은 프로세스의 가상 주소 공간에 메모리 영역을 매핑하는 핵심 시스템 콜입니다.
파일 I/O, 공유 메모리, 익명 메모리 할당, 디바이스 메모리 접근 등 커널의 거의 모든 메모리 관련 기능이
mmap()을 통해 구현됩니다.
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에 고정 (스왑 방지) |
MAP_NORESERVE | 0x4000 | 스왑 영역 예약 없이 매핑 — 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) 구조체
커널은 프로세스의 가상 주소 공간을 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_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 없는 물리 주소 매핑 |
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개 엔트리 → 캐시 라인 활용 극대화 |
| 범위 연산 | 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는 트리 내 현재 위치(커서)를 추적하며, 연속된 연산에서 탐색 경로를
재활용하여 성능을 최적화합니다.
/* 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 순회를 대체합니다.
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 취약점입니다. expand_downwards()가 스택 VMA 하단 경계를
확장하는 과정에서, 다른 스레드가 경쟁적으로 해당 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) | 레지스터 주소 | 레지스터 값 | 6.4 |
| 파일 페이지 캐시 (계획) | 파일 오프셋 | struct folio * | 논의 중 |
vm_area_struct에서 vm_next,
vm_prev, vm_rb 필드가 모두 제거되어 구조체 크기가 줄었고,
6.4에서는 이를 기반으로 per-VMA lock이 도입되어
page fault 경로에서 mmap_lock 경합을 획기적으로 줄였습니다.
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 최대 개수를 제한합니다. 많은 공유 라이브러리를 로드하거나, JVM처럼 다수의 mmap을 사용하는
애플리케이션에서 이 한계에 도달할 수 있습니다.
sysctl -w vm.max_map_count=262144로 조정 가능합니다.
mmap 페이지 폴트 처리
mmap()은 lazy allocation을 사용합니다. 매핑 시점에 물리 메모리를 할당하지 않고,
실제 접근 시 page fault를 통해 물리 페이지를 할당합니다.
vm_operations_struct — 폴트 핸들러 콜백
/* 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 필드로 프로세스별 폴트 횟수를 확인할 수 있습니다.
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 버퍼풀 |
MADV_NOHUGEPAGE | THP 사용 금지 | latency 민감한 워크로드 |
MADV_MERGEABLE | KSM 대상으로 등록 | VM 중복 페이지 병합 |
MADV_COLD | 페이지를 inactive 리스트로 이동 | 우선순위 낮은 캐시 (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 버퍼를 사용자 공간에 직접 매핑할 수 있습니다.
/* 디바이스 드라이버 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
특수 매핑
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 |
| 실시간 시스템 | 스왑에 의한 지연 | MAP_LOCKED 또는 mlockall() |
| 메모리 할당/해제 반복 | VMA 단편화 | 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()은 커널↔사용자 공간 데이터 복사를 제거하므로 대용량 파일 랜덤 접근에서
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 권한 획득 */
패치: 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 잘못된 범위 검증
원인: 커널 모듈의 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 조합 시 파일 쓰기 권한 확인 | 읽기 전용 파일 변조 |
| 정수 오버플로 | offset + size 계산 시 오버플로 검사 | 범위 검증 우회 |
| 경쟁 조건 | 멀티스레드 접근 시 적절한 락 사용 | TOCTOU (Time-Of-Check-Time-Of-Use) |
| VMA 플래그 | VM_IO, VM_PFNMAP, VM_DONTEXPAND 설정 | 예상치 못한 VMA 확장/병합 |
| 캐시 일관성 | DMA 버퍼는 pgprot_noncached() 또는 dma_mmap_coherent() 사용 | 데이터 손상 |
mmap/페이지 폴트 디버깅 루틴
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
관련 문서
VMA/mmap과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.