메모리 관리 심화 (Advanced Memory Management)
CMA, HugeTLB, tmpfs/ramfs/shmem, memfd, devtmpfs, fallocate, tmpfs quota, memory cgroup, DAMON, KSM, zswap, page migration, compaction 등 Linux 커널 고급 메모리 관리 기법을 다룹니다.
핵심 요약
- mmap — 파일이나 디바이스를 프로세스의 가상 주소 공간에 직접 매핑하는 시스템 콜입니다.
- CMA — DMA 디바이스를 위해 대용량 연속 물리 메모리를 예약·할당하는 메커니즘입니다.
- HugeTLB — 2MB/1GB 대형 페이지로 TLB 미스를 줄여 성능을 개선합니다.
- KSM / zswap — 동일 내용의 페이지를 병합(KSM)하거나 압축(zswap)하여 메모리를 절약합니다.
- tmpfs / shmem — RAM 기반 파일시스템으로, 공유 메모리와 임시 파일에 사용됩니다.
단계별 이해
- mmap 이해 —
mmap()은 가상 주소만 확보하고, 실제 물리 페이지는 접근 시(page fault) 할당됩니다.이것이 "Demand Paging"이며, 메모리를 효율적으로 사용하는 핵심 원리입니다.
- 대형 페이지 활용 — 4KB 페이지 수천 개 대신 2MB HugePage 하나를 사용하면 TLB 엔트리를 절약할 수 있습니다.
데이터베이스, JVM 등 대용량 메모리 애플리케이션에서 큰 성능 향상을 줍니다.
- 메모리 절약 기법 — KSM은 동일 페이지를 탐지·병합하고, zswap은 swap-out 전에 페이지를 압축합니다.
가상 머신 환경에서 KSM은 게스트 간 동일 페이지를 공유하여 메모리를 크게 절약합니다.
- Memory cgroup과 DAMON — cgroup으로 프로세스 그룹의 메모리 사용량을 제한하고, DAMON으로 접근 패턴을 모니터링합니다.
컨테이너 환경에서 메모리 격리와 최적화의 핵심 도구입니다.
CMA (Contiguous Memory Allocator)
CMA는 DMA 디바이스를 위한 대용량 연속 메모리 할당을 지원합니다. 일반적인 메모리 단편화 상황에서도 연속된 물리 페이지를 확보할 수 있습니다.
/* CMA 영역에서 연속 페이지 할당 */
struct page *pages = cma_alloc(cma, count, align, gfp_mask);
/* CMA 메모리 해제 */
bool ok = cma_release(cma, pages, count);
/* DMA API를 통한 사용 (일반적인 방법) */
void *vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, vaddr, dma_handle);
# 커널 부트 파라미터로 CMA 영역 설정
cma=256M # 기본 CMA 영역 256MB
cma=256M@0-4G # 0~4GB 범위에 256MB CMA
# 디바이스 트리에서 CMA 설정
reserved-memory {
cma_region: linux,cma {
compatible = "shared-dma-pool";
reusable;
size = <0x10000000>; /* 256MB */
};
};
# CMA 상태 확인
cat /proc/meminfo | grep Cma
# CmaTotal: 262144 kB
# CmaFree: 245760 kB
HugeTLB Pages
Huge Pages는 기본 4KB 대신 2MB(x86) 또는 1GB 크기의 페이지를 사용하여 TLB 미스를 획기적으로 줄입니다. 일반적인 x86_64 CPU의 TLB는 4KB 엔트리를 수백~수천 개만 캐시할 수 있어, 대용량 메모리를 접근하면 빈번한 TLB 미스가 발생합니다. 2MB 페이지를 사용하면 동일한 TLB 엔트리 수로 512배 더 넓은 주소 공간을 커버할 수 있습니다.
페이지 테이블과 Huge Page 매핑
x86_64의 4단계 페이지 테이블에서, Huge Page는 중간 레벨에서 직접 물리 프레임을 가리키는 방식으로 동작합니다:
- PTE (Page Table Entry) — 4KB 일반 페이지. PGD→P4D→PUD→PMD→PTE→물리 페이지
- PMD (Page Middle Directory) — 2MB Huge Page. PMD 엔트리가
_PAGE_PSE(Page Size Extension) 비트를 설정하여 직접 2MB 물리 프레임 지시. PTE 단계 생략 - PUD (Page Upper Directory) — 1GB Huge Page. PUD 엔트리가 직접 1GB 물리 프레임 지시. PMD, PTE 단계 모두 생략
/* 4KB 일반 페이지: 4단계 페이지 워크 */
PGD → P4D → PUD → PMD → PTE → 4KB 물리 프레임
↑ 12비트 오프셋
/* 2MB Huge Page: PMD에서 직접 매핑 (PSE 비트) */
PGD → P4D → PUD → PMD ──────→ 2MB 물리 프레임
↑ PSE=1 ↑ 21비트 오프셋
/* 1GB Huge Page: PUD에서 직접 매핑 */
PGD → P4D → PUD ─────────────→ 1GB 물리 프레임
↑ PSE=1 ↑ 30비트 오프셋
Compound Page 내부 구조
커널 내부에서 Huge Page는 compound page로 관리됩니다. 연속된 물리 페이지들을 하나의 논리 단위로 묶어, 첫 번째 페이지(head page)가 전체를 대표합니다:
/* 커널 6.x: folio 기반 huge page 관리 (include/linux/mm_types.h) */
struct folio {
struct page page; /* head page — 참조 카운트, 매핑 정보 */
unsigned long _flags_1;
unsigned long _head_1;
unsigned char _folio_order; /* 2MB = order-9 (512 pages), 1GB = order-18 */
atomic_t _total_mapcount;
atomic_t _nr_pages_mapped;
/* ... */
};
/* compound page 구조:
* page[0] = head page (PG_head 플래그 설정)
* page[1] = first tail — compound_order, compound_dtor 저장
* page[2..N] = tail pages — compound_head 포인터로 head 참조
*
* 2MB huge page = 512개 연속 struct page (order-9)
* head page의 compound_order = 9
* 모든 tail page의 compound_head = head page 주소 | 1
*/
/* folio API로 huge page 조작 (mm/hugetlb.c) */
static struct folio *alloc_hugetlb_folio(
struct vm_area_struct *vma,
unsigned long addr, int avoid_reserve)
{
struct hugepage_subpool *spool = subpool_vma(vma);
struct hstate *h = hstate_vma(vma);
struct folio *folio;
/* hstate: hugepage 크기별(2MB, 1GB) 관리 구조체
* 각 hstate가 자체 free_hugepages 리스트를 관리 */
folio = dequeue_hugetlb_folio_vma(h, vma, addr, avoid_reserve);
/* ... */
}
HugeTLB Pages 설정
# Huge Pages 설정
echo 512 > /proc/sys/vm/nr_hugepages # 2MB 페이지 512개 (1GB)
echo 4 > /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages # 1GB 페이지 4개
# 확인
cat /proc/meminfo | grep -i huge
# HugePages_Total: 512
# HugePages_Free: 512
# Hugepagesize: 2048 kB
# 마운트하여 사용
mount -t hugetlbfs nodev /mnt/hugepages
# 커널 부팅 파라미터 (GRUB에 추가 — 부팅 시 즉시 예약)
# hugepagesz=2M hugepages=512 ← 2MB 512개
# hugepagesz=1G hugepages=4 ← 1GB 4개 (부팅 시만 가능)
# default_hugepagesz=2M ← 기본 hugepage 크기
# Surplus hugepage (overcommit) — 긴급 시 풀 외 할당
cat /proc/sys/vm/nr_overcommit_hugepages # 기본값 0
echo 64 > /proc/sys/vm/nr_overcommit_hugepages # 풀 소진 시 최대 64개 추가 할당
THP (Transparent Huge Pages)
THP는 애플리케이션 수정 없이 자동으로 Huge Page를 사용하게 합니다. Static HugeTLB와 달리 사전 예약이 불필요하며, 커널이 페이지 폴트 시점이나 백그라운드에서 자동으로 2MB 페이지를 할당합니다:
# THP 모드 설정
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# madvise 모드: MADV_HUGEPAGE를 호출한 영역만 THP 사용
# khugepaged 데몬: 4KB 페이지를 백그라운드에서 2MB로 합침
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan
# 4096 (기본값: 스캔당 4096 페이지 검사)
/* 애플리케이션에서 THP 요청 */
madvise(addr, length, MADV_HUGEPAGE); /* THP 사용 요청 */
madvise(addr, length, MADV_NOHUGEPAGE); /* THP 비사용 요청 */
THP 생명주기 — 할당, 승격, 분할
THP는 3가지 경로로 할당되고, 필요 시 다시 분할(split)됩니다:
/* THP 할당 경로 (mm/huge_memory.c) */
/* 경로 1: 페이지 폴트 시 즉시 할당 (synchronous) */
/*
* do_anonymous_page() 또는 do_huge_pmd_anonymous_page()
* → 2MB 연속 물리 메모리 할당 시도
* → 성공: PMD 엔트리에 직접 매핑 (PTE 단계 생략)
* → 실패: defrag 정책에 따라 compaction 또는 4KB fallback
*/
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
struct folio *folio;
gfp_t gfp = vma_thp_gfp_mask(vma);
folio = vma_alloc_folio(gfp, HPAGE_PMD_ORDER, vma, haddr);
if (!folio) {
/* 2MB 할당 실패 → 일반 4KB 페이지로 fallback */
count_vm_event(THP_FAULT_FALLBACK);
return VM_FAULT_FALLBACK;
}
count_vm_event(THP_FAULT_ALLOC);
/* PMD에 2MB 매핑 설치 */
set_pmd_at(mm, haddr, vmf->pmd, pmd_mkhuge(entry));
/* ... */
}
/* 경로 2: khugepaged 백그라운드 승격 (collapse) */
/*
* khugepaged 커널 스레드가 주기적으로 VMA를 스캔
* → 연속된 512개 4KB 페이지를 발견하면 2MB로 합체(collapse)
* → 새 compound page 할당 → 기존 데이터 복사 → PTE를 PMD로 교체
*
* scan_sleep_millisecs: 스캔 주기 (기본 10000ms)
* pages_to_scan: 1회 스캔할 페이지 수 (기본 4096)
* max_ptes_none: 빈 PTE 허용 수 (기본 511 — 512개 중 1개만 있어도 합체 시도)
*/
/* 경로 3: THP 분할 (split) */
/*
* 2MB THP를 다시 512개 4KB 페이지로 분할하는 경우:
* - 부분적 munmap() — 2MB 영역의 일부만 해제
* - 부분적 mprotect() — 영역 내 권한 변경
* - 메모리 회수(reclaim) — 2MB 전체를 회수할 수 없을 때
* - swap out — 2MB 단위 swap이 비효율적일 때
* - MADV_DONTNEED — 부분 영역 무효화
*/
int split_huge_page_to_list(struct page *page, struct list_head *list)
{
/* compound page 해제 → 개별 struct page로 전환
* head page에서 PG_head 플래그 제거
* 각 tail page를 독립 페이지로 초기화
* PMD 엔트리를 512개 PTE로 교체 */
}
THP defrag 모드 상세
THP 할당 실패 시 커널의 동작을 제어하는 defrag 파라미터입니다. 메모리 compaction(단편화 해소)의 동기/비동기 여부를 결정합니다:
| defrag 모드 | 페이지 폴트 시 동작 | khugepaged | 적합한 환경 |
|---|---|---|---|
always |
동기 compaction 실행 (프로세스 블록됨) | 활성 | HPC, 과학 계산 — THP 할당을 최대화 |
defer |
비동기 compaction 요청 후 즉시 fallback | 활성 | 일반 서버 — 지연 스파이크 방지 |
defer+madvise |
일반: defer 동작 / MADV_HUGEPAGE 영역: 동기 compaction | 활성 | 혼합 워크로드 — 명시 요청만 동기 대기 (권장) |
madvise |
MADV_HUGEPAGE 영역만 동기 compaction | 활성 | 지연 민감 서비스 + 선택적 THP |
never |
compaction 없음. 즉시 사용 가능한 2MB가 없으면 fallback | 활성 | 지연 최소화 — 이미 가용한 경우만 THP 사용 |
# defrag 현재 설정 확인
cat /sys/kernel/mm/transparent_hugepage/defrag
# always defer defer+madvise [madvise] never
# 프로덕션 권장: defer+madvise
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
# → 일반 페이지 폴트에서는 동기 compaction 없음 (지연 스파이크 방지)
# → MADV_HUGEPAGE 요청 영역에서만 동기 compaction (확실한 THP 할당)
Multi-size THP (mTHP) — 커널 6.8+
전통적인 THP는 2MB(PMD 크기) 단일 옵션만 제공했습니다. 커널 6.8부터 도입된 Multi-size THP(mTHP)는 2MB뿐 아니라 16KB, 32KB, 64KB, 128KB, 256KB, 512KB, 1MB 등 다양한 크기의 "large folio"를 지원합니다. PTE 레벨에서 연속된 엔트리를 사용하므로 PMD 매핑이 아닌 PTE-mapped large folio입니다:
CONT_PTE(64KB 연속 PTE)와도 자연스럽게 결합되어 모바일/임베디드에서 특히 효과적입니다.
# mTHP 지원 크기 확인 및 설정 (커널 6.8+)
ls /sys/kernel/mm/transparent_hugepage/hugepages-*
# hugepages-16kB/ hugepages-32kB/ hugepages-64kB/ hugepages-128kB/
# hugepages-256kB/ hugepages-512kB/ hugepages-1024kB/ hugepages-2048kB/
# 크기별 활성화 제어
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
# [always] inherit madvise never
# 특정 크기만 활성화 (예: 64KB만)
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
echo never > /sys/kernel/mm/transparent_hugepage/hugepages-2048kB/enabled
# inherit: 시스템 전역 THP 설정을 따름
# 각 크기별로 독립적으로 always/madvise/never 설정 가능
/* mTHP 할당 흐름 (mm/memory.c, 커널 6.8+) */
/*
* 페이지 폴트 발생 시:
* 1. PMD 크기(2MB) THP 할당 시도
* 2. 실패하면 점차 작은 크기로 fallback:
* 1MB → 512KB → 256KB → 128KB → 64KB → 32KB → 16KB → 4KB
* 3. 각 크기마다 해당 order의 compound page(folio) 할당 시도
* 4. PTE 연속 엔트리로 매핑 (contpte on ARM64)
*
* /proc/vmstat 카운터:
* thp_fault_alloc — PMD(2MB) 할당 성공
* thp_fault_fallback — PMD 할당 실패 → 더 작은 크기 시도
* thp_fault_fallback_charge — memcg 충전 실패로 fallback
*/
Hugepage 심화 — 주의사항과 고려사항
Hugepage 유형 비교
| 유형 | 페이지 크기 | 할당 방식 | 설정 방법 | 적합한 워크로드 |
|---|---|---|---|---|
| Static HugeTLB (2MB) | 2MB (x86 PMD) | 부팅 시 또는 런타임 예약 | nr_hugepages, hugetlbfs |
DB, DPDK, KVM 게스트 메모리 |
| Static HugeTLB (1GB) | 1GB (x86 PUD) | 부팅 시 커널 파라미터로만 예약 | hugepagesz=1G hugepages=4 |
대규모 인메모리 DB, ML/HPC |
| THP (Transparent) | 2MB (자동) | 페이지 폴트 시 자동 승격 | transparent_hugepage=always|madvise |
일반 애플리케이션 (자동 최적화) |
| HugeTLB + NUMA | 2MB/1GB | 노드별 예약 | /sys/devices/system/node/nodeN/hugepages/ |
NUMA 인지 애플리케이션 |
| mTHP (Multi-size THP) | 16KB~1MB (가변) | 페이지 폴트 시 자동 (PTE 레벨) | hugepages-NkB/enabled (커널 6.8+) |
내부 단편화 최소화, ARM64 CONT_PTE |
hugetlbfs 프로그래밍
#include <sys/mman.h>
/* 방법 1: mmap + MAP_HUGETLB */
void *addr = mmap(NULL, 2 * 1024 * 1024, /* 2MB */
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
-1, 0);
/* 1GB 페이지 명시적 요청 (커널 3.8+) */
void *addr = mmap(NULL, 1UL << 30,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB | MAP_HUGE_1GB,
-1, 0);
/* 방법 2: hugetlbfs 파일 통한 공유 메모리 */
int fd = open("/mnt/hugepages/shared_buf", O_CREAT | O_RDWR, 0600);
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
/* 방법 3: shmget + SHM_HUGETLB */
int shmid = shmget(key, size, IPC_CREAT | SHM_HUGETLB | 0600);
void *addr = shmat(shmid, NULL, 0);
/* 방법 4: memfd_create + MFD_HUGETLB (커널 4.14+) */
int fd = memfd_create("huge_shm", MFD_HUGETLB | MFD_HUGE_2MB);
ftruncate(fd, 2 * 1024 * 1024);
void *addr = mmap(NULL, 2 * 1024 * 1024,
PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* hugetlbfs 마운트 없이 fd 기반 huge page 공유 가능 */
HugeTLB 예약(Reservation) 메커니즘
HugeTLB는 mmap() 시점에 huge page를 예약(reserve)하고, 실제 접근 시 할당합니다. 이는 OOM 방지를 위한 핵심 메커니즘입니다:
/* HugeTLB 예약 흐름 (mm/hugetlb.c) */
/* 1. mmap() 호출 시: 예약 확보 (아직 물리 페이지 할당 아님) */
/*
* hugetlb_reserve_pages()
* → 풀(hstate->free_hugepages)에서 요청 크기만큼 예약
* → HugePages_Rsvd 카운터 증가
* → 풀이 부족하면 ENOMEM 반환 (mmap 실패)
*
* 이 단계에서 물리 페이지를 할당하지 않으므로:
* HugePages_Free는 변하지 않음
* HugePages_Rsvd만 증가
*/
/* 2. 첫 접근(페이지 폴트) 시: 예약된 페이지를 실제 할당 */
/*
* hugetlb_no_page() → alloc_hugetlb_folio()
* → 예약된 페이지에서 하나를 꺼내 물리 매핑
* → HugePages_Free 감소, HugePages_Rsvd 감소
*
* 예약이 있으므로 페이지 폴트 시 OOM이 발생하지 않음 보장
*/
/* /proc/meminfo 예시:
* HugePages_Total: 512 ← 전체 풀 크기
* HugePages_Free: 480 ← 미할당 (예약 포함)
* HugePages_Rsvd: 32 ← 예약됨 (mmap 했지만 미접근)
* HugePages_Surp: 0 ← overcommit으로 추가 할당된 수
*
* 실제 가용 = Free - Rsvd = 480 - 32 = 448
* 이미 매핑됨 = Total - Free = 512 - 480 = 32
*/
size= 또는 nr_inodes= 옵션으로 서브풀을 생성할 수 있습니다.
서브풀은 전체 hugepage 풀에서 일정량을 격리하여 특정 마운트포인트의 사용량을 제한합니다.
예: mount -t hugetlbfs -o size=1G nodev /mnt/app_hugepages — 최대 1GB(512개 2MB 페이지)로 제한.
HugeTLB Cgroup 제어
cgroup v2의 hugetlb 컨트롤러를 통해 프로세스 그룹별로 hugepage 사용량을 제한할 수 있습니다:
# cgroup v2에서 hugetlb 컨트롤러 활성화
echo "+hugetlb" > /sys/fs/cgroup/cgroup.subtree_control
# 프로세스 그룹별 hugepage 제한
mkdir /sys/fs/cgroup/myapp
echo "2048M" > /sys/fs/cgroup/myapp/hugetlb.2MB.max # 2MB hugepage 최대 2GB
echo "4G" > /sys/fs/cgroup/myapp/hugetlb.1GB.max # 1GB hugepage 최대 4GB
# 현재 사용량 확인
cat /sys/fs/cgroup/myapp/hugetlb.2MB.current # 현재 사용 중인 2MB hugepage 바이트
# 제한 초과 이벤트 카운터
cat /sys/fs/cgroup/myapp/hugetlb.2MB.events
# max 0 ← 제한에 도달하여 할당 실패한 횟수
# 예약(reservation) 제한도 별도로 설정 가능
echo "1024M" > /sys/fs/cgroup/myapp/hugetlb.2MB.rsvd.max # 예약 포함 최대 1GB
cat /sys/fs/cgroup/myapp/hugetlb.2MB.rsvd.current
THP (Transparent Huge Pages) 주의사항
- 메모리 팽창 — 4KB만 필요한 곳에 2MB 할당 시 내부 단편화로 메모리 낭비. 특히 희소(sparse) 접근 패턴에서 심각
- 페이지 폴트 지연 스파이크 — THP 할당 실패 시 compaction이 동기적으로 실행되어 수십~수백 ms 지연 발생 가능
- khugepaged CPU 오버헤드 — 백그라운드 THP 합체 데몬이 지속적으로 CPU 소비. 지연 민감 워크로드에서 jitter 유발
- CoW(Copy-on-Write) 비용 — fork() 후 2MB 전체를 복사해야 함 (4KB 대비 512배). Redis/PostgreSQL의 fork 기반 스냅샷에서 심각한 지연
- 메모리 회수 지연 — 2MB 페이지를 회수하려면 내부의 모든 4KB 서브페이지가 비어야 함. 메모리 압력 상황에서 OOM 더 빨리 도달
- NUMA 불균형 — THP 합체가 특정 NUMA 노드에서만 성공하면 메모리 편향 발생
THP 워크로드별 권장 설정
| 워크로드 | THP 설정 | 이유 |
|---|---|---|
| 일반 서버 | madvise |
애플리케이션이 명시적으로 요청한 영역만 THP 사용 |
| Redis, MongoDB | madvise 또는 never |
fork 기반 스냅샷의 CoW 오버헤드 방지 |
| Oracle, SAP HANA | Static HugeTLB | 예측 가능한 메모리 할당, THP 오버헤드 완전 제거 |
| DPDK | Static HugeTLB (1GB) | 대용량 연속 메모리 필요, DMA 매핑 효율 |
| KVM 게스트 | Static HugeTLB 또는 THP | EPT/NPT 페이지 테이블 크기 감소, TLB 미스 최소화 |
| 지연 민감 (HFT) | Static HugeTLB + mlock |
페이지 폴트 완전 제거, 결정적 지연 보장 |
Hugepage와 NUMA
# NUMA 노드별 hugepage 예약
echo 256 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 256 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
# 현재 NUMA 노드별 hugepage 상태
cat /sys/devices/system/node/node*/hugepages/hugepages-2048kB/free_hugepages
# numactl로 특정 NUMA 노드에 hugepage 할당
numactl --membind=0 ./my_app # node0의 hugepage만 사용
# hugepage 단편화 문제:
# 부팅 후 시간이 지나면 2MB 연속 블록 확보가 어려워짐
# 해결: 부팅 시 예약, 또는 CMA 영역 활용
# 커널 파라미터: hugepages=512 (부팅 시 즉시 예약 — 단편화 없음)
Hugepage 모니터링
# /proc/meminfo에서 hugepage 통계
grep -i huge /proc/meminfo
# AnonHugePages: 524288 kB ← THP 사용량
# ShmemHugePages: 0 kB ← tmpfs THP
# HugePages_Total: 512 ← 예약된 static hugepage
# HugePages_Free: 480 ← 미사용
# HugePages_Rsvd: 32 ← 예약 (mmap 했지만 아직 접근 안 함)
# HugePages_Surp: 0 ← surplus (overcommit)
# THP 이벤트 통계
grep thp /proc/vmstat
# thp_fault_alloc 12345 ← 페이지 폴트 시 THP 할당 성공
# thp_fault_fallback 6789 ← THP 할당 실패 → 4KB fallback
# thp_collapse_alloc 1234 ← khugepaged 합체 성공
# thp_split_page 567 ← THP → 4KB 분할 발생
# ↑ fallback/split이 높으면 단편화 문제 또는 THP가 부적합한 워크로드
# 프로세스별 hugepage 사용량
grep -i huge /proc/<pid>/smaps_rollup
# 프로세스의 smaps에서 THP 매핑 상세
grep -E "AnonHugePages|ShmemPmdMapped|FilePmdMapped" /proc/<pid>/smaps_rollup
# AnonHugePages: 524288 kB ← 익명 THP (heap, stack)
# ShmemPmdMapped: 0 kB ← PMD 매핑된 공유 메모리 THP
# FilePmdMapped: 0 kB ← PMD 매핑된 파일 backed THP
perf를 이용한 TLB 미스 분석
Huge Page 도입 전후의 TLB 성능 차이를 perf로 정량적으로 측정할 수 있습니다:
# 1. TLB 미스 카운터 측정 (PMU hardware event)
perf stat -e dTLB-loads,dTLB-load-misses,dTLB-stores,dTLB-store-misses \
-e iTLB-loads,iTLB-load-misses \
./my_application
# Performance counter stats:
# 1,234,567,890 dTLB-loads
# 456,789 dTLB-load-misses # 0.04% of dTLB-loads ← 낮을수록 좋음
# 345,678,901 dTLB-stores
# 12,345 dTLB-store-misses
# 234,567,890 iTLB-loads
# 1,234 iTLB-load-misses # 0.00%
# 2. 페이지 워크 사이클 측정 (TLB 미스의 실제 비용)
perf stat -e dTLB-load-misses,dtlb_load_misses.walk_completed,dtlb_load_misses.walk_active \
./my_application
# walk_completed: 페이지 테이블 워크 완료 횟수
# walk_active: 페이지 테이블 워크에 소비된 사이클
# 3. Huge Page 전후 비교 스크립트
# (a) 일반 4KB 페이지로 실행
echo never > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses -- ./benchmark 2>> result_4kb.txt
# (b) THP 활성화 후 실행
echo always > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses -- ./benchmark 2>> result_thp.txt
# (c) Static HugeTLB로 실행
echo 512 > /proc/sys/vm/nr_hugepages
LD_PRELOAD=libhugetlbfs.so HUGETLB_MORECORE=yes ./benchmark
perf stat -e dTLB-load-misses -- env LD_PRELOAD=libhugetlbfs.so \
HUGETLB_MORECORE=yes ./benchmark 2>> result_hugetlb.txt
# 4. THP 관련 tracepoint
perf stat -e 'huge_memory:mm_khugepaged_scan_pmd' \
-e 'huge_memory:mm_collapse_huge_page' \
-e 'huge_memory:mm_collapse_huge_page_swapin' \
-a sleep 10
# khugepaged의 스캔 및 합체 활동 모니터링 (시스템 전체, 10초간)
- < 0.1% — 양호. TLB 캐시가 워킹셋을 충분히 커버
- 0.1% ~ 1% — 경미한 오버헤드. Huge Page 도입으로 개선 가능
- > 1% — TLB 병목 심각. Huge Page가 큰 효과를 줄 수 있음
- 데이터 TLB(dTLB) 미스가 높으면 heap/data 영역에 Huge Page 적용
- 명령어 TLB(iTLB) 미스가 높으면 코드 영역(text)이 크거나 분산됨 — Huge Page 효과 제한적
Memory Compaction
메모리 단편화(fragmentation)를 해소하여 연속 페이지 할당을 가능하게 합니다:
# 수동 compaction 트리거
echo 1 > /proc/sys/vm/compact_memory
# 단편화 상태 확인
cat /proc/buddyinfo
# Node 0, zone Normal 1024 512 256 128 64 32 16 8 4 2 1
# 오른쪽으로 갈수록 큰 연속 블록 수
# 상세 페이지 타입 정보
cat /proc/pagetypeinfo
# proactive compaction (v5.9+)
echo 20 > /proc/sys/vm/compaction_proactiveness # 0~100, 0=비활성
Page Migration
NUMA 시스템에서 페이지를 노드 간 이동시켜 메모리 접근 지역성을 개선합니다:
/* 커널 내부 API */
int migrate_pages(struct list_head *l, new_page_t get_new,
free_page_t put_new, unsigned long private,
enum migrate_mode mode, int reason);
/* 사용자 공간에서 */
#include <numaif.h>
long move_pages(int pid, unsigned long count,
void **pages, const int *nodes,
int *status, int flags);
KSM (Kernel Samepage Merging)
KSM은 내용이 동일한 페이지를 자동으로 병합하여 메모리를 절약합니다. KVM 가상화에서 동일 게스트 OS 이미지를 여러 VM이 공유할 때 특히 유용합니다.
# KSM 활성화
echo 1 > /sys/kernel/mm/ksm/run
# KSM 상태 확인
cat /sys/kernel/mm/ksm/pages_shared # 공유된 페이지 수
cat /sys/kernel/mm/ksm/pages_sharing # 공유로 절약된 페이지 수
cat /sys/kernel/mm/ksm/pages_unshared # 고유한 페이지 수
# 튜닝 파라미터
echo 200 > /sys/kernel/mm/ksm/sleep_millisecs # 스캔 간격
echo 256 > /sys/kernel/mm/ksm/pages_to_scan # 스캔당 페이지 수
/* 애플리케이션에서 KSM 영역 지정 */
madvise(addr, length, MADV_MERGEABLE); /* KSM 스캔 대상 등록 */
madvise(addr, length, MADV_UNMERGEABLE); /* KSM 대상에서 제외 */
Swapping 서브시스템
Swapping은 물리 메모리(RAM) 부족 시 익명 페이지(anonymous page)를 디스크의 스왑 영역으로 내보내고, 다시 접근할 때 복원하는 메커니즘입니다. 파일 기반 페이지는 원본 파일에서 다시 읽을 수 있지만, 힙·스택·mmap(MAP_ANONYMOUS) 등 backing store가 없는 익명 페이지는 스왑 영역이 유일한 저장소입니다.
Swap vs Page Cache 회수: 커널의 메모리 회수(reclaim)는 두 가지 경로로 동작합니다. 파일 페이지(page cache)는 clean이면 즉시 버리고 dirty면 원본 파일에 writeback 후 회수합니다. 익명 페이지는 스왑 영역에 기록(swap out)해야만 회수할 수 있습니다. vm.swappiness로 이 두 경로의 비율을 조정합니다.
Swap 공간 설정
스왑 영역은 전용 파티션 또는 스왑 파일로 구성할 수 있습니다. 여러 스왑 영역을 동시에 사용할 수 있으며, 우선순위(priority)로 사용 순서를 제어합니다.
# === 스왑 파티션 설정 ===
mkswap /dev/sda2 # 파티션을 스왑으로 포맷
swapon /dev/sda2 # 스왑 활성화
swapon -p 10 /dev/sda2 # 우선순위 10으로 활성화
# === 스왑 파일 설정 ===
fallocate -l 4G /swapfile # 4GB 파일 생성
chmod 600 /swapfile # 권한 제한 (필수)
mkswap /swapfile # 스왑 포맷
swapon /swapfile # 활성화
# === /etc/fstab 영구 설정 ===
# /dev/sda2 none swap sw,pri=10 0 0
# /swapfile none swap sw,pri=5 0 0
# === 스왑 상태 확인 ===
swapon --show # 활성 스왑 영역 목록
cat /proc/swaps # 동일 정보 (proc 인터페이스)
free -h # 스왑 사용량 요약
# === 스왑 비활성화 ===
swapoff /dev/sda2 # 스왑 인 후 비활성화 (시간 소요)
swapoff -a # 모든 스왑 비활성화
스왑 파일 주의사항: Btrfs에서 스왑 파일을 사용하려면 chattr +C로 COW를 비활성화하고 별도 서브볼륨에 생성해야 합니다. ext4에서 fallocate 대신 dd를 사용해야 하는 오래된 커널(< 5.0)도 있으므로 주의하십시오. 스왑 파일은 반드시 chmod 600으로 권한을 제한해야 합니다.
우선순위(Priority) 동작 방식: 동일한 우선순위를 가진 스왑 영역들은 라운드 로빈으로 사용되어 I/O가 분산됩니다(RAID-0과 유사). 우선순위가 다르면 높은 우선순위의 영역을 먼저 사용하고, 가득 차면 낮은 우선순위로 넘어갑니다.
# 우선순위 기반 스왑 계층 구성 예시
swapon -p 100 /dev/zram0 # 1순위: zram (압축 메모리, 가장 빠름)
swapon -p 10 /dev/nvme0n1p2 # 2순위: NVMe SSD
swapon -p 1 /swapfile # 3순위: HDD 스왑 파일 (가장 느림)
# 결과 확인
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/zram0 partition 2G 0B 100
# /dev/nvme0n1p2 partition 8G 0B 10
# /swapfile file 4G 0B 1
Swap 핵심 자료구조
스왑 서브시스템은 세 가지 핵심 자료구조로 구성됩니다.
/* include/linux/swap.h — 스왑 영역 정보 */
struct swap_info_struct {
unsigned long flags; /* SWP_USED | SWP_WRITEOK 등 */
signed short prio; /* 스왑 우선순위 */
struct plist_node list; /* 우선순위 정렬 리스트 */
signed char type; /* 스왑 영역 인덱스 (0~MAX_SWAPFILES-1) */
unsigned int max; /* 최대 스왑 슬롯 수 */
unsigned char *swap_map; /* 슬롯별 참조 카운트 배열 */
struct swap_cluster_info *cluster_info; /* 클러스터별 정보 */
struct swap_cluster_list free_clusters; /* 빈 클러스터 리스트 */
unsigned int lowest_bit; /* 빈 슬롯 탐색 힌트 (시작) */
unsigned int highest_bit; /* 빈 슬롯 탐색 힌트 (끝) */
unsigned int pages; /* 사용 가능 총 페이지 수 */
unsigned int inuse_pages; /* 사용 중인 페이지 수 */
unsigned int cluster_next; /* 다음 할당 위치 힌트 */
unsigned int cluster_nr; /* 현재 클러스터 내 위치 */
struct percpu_cluster __percpu *percpu_cluster; /* Per-CPU 할당 */
struct block_device *bdev; /* 스왑 블록 디바이스 */
struct file *swap_file; /* 스왑 파일 (파일 기반 시) */
unsigned int old_block_size; /* 이전 블록 크기 */
};
/* include/linux/swapops.h — 스왑 엔트리 인코딩 */
/*
* swp_entry_t: PTE에 저장되는 스왑 위치 정보
* 페이지가 스왑 아웃되면, PTE의 present 비트가 0이 되고
* 나머지 비트에 스왑 영역 인덱스(type)와 오프셋(offset)이 인코딩됩니다.
*
* x86_64 레이아웃 (64비트 PTE):
* ┌──────────────────────────────────────────────────────┐
* │ bit 63..58 │ bit 57..5 │ bit 4..1 │ bit 0 │
* │ (unused) │ offset (53 bits) │ type (4b) │ P=0 │
* └──────────────────────────────────────────────────────┘
* P=0이므로 MMU는 page fault 발생 → do_swap_page() 호출
*/
typedef struct {
unsigned long val;
} swp_entry_t;
/* swp_entry_t 조작 매크로/함수 */
swp_type(entry) /* 스왑 영역 인덱스 추출 (0~MAX_SWAPFILES-1) */
swp_offset(entry) /* 스왑 영역 내 슬롯 오프셋 추출 */
swp_entry(type, off) /* type + offset → swp_entry_t 생성 */
/* PTE ↔ swp_entry_t 변환 */
pte_to_swp_entry(pte) /* non-present PTE → swp_entry_t */
swp_entry_to_pte(ent) /* swp_entry_t → non-present PTE */
/* mm/swap_state.c — swap_map: 슬롯별 참조 카운트 */
/*
* swap_map[offset] 값의 의미:
* 0 : 빈 슬롯 (할당 가능)
* 1~SWAP_MAP_MAX : 참조 카운트 (해당 슬롯을 참조하는 PTE 수)
* SWAP_MAP_BAD : 불량 슬롯 (사용 불가)
* SWAP_HAS_CACHE : 스왑 캐시에 존재 (비트 OR)
*
* 참조 카운트가 여러 개인 경우:
* fork() 시 CoW로 공유된 익명 페이지가 스왑 아웃되면
* 부모와 자식 프로세스의 PTE가 동일한 swap entry를 가리킴
*/
#define SWAP_HAS_CACHE 0x40 /* 스왑 캐시에 페이지 존재 */
#define SWAP_MAP_MAX 0x3e /* 최대 참조 카운트 (62) */
#define SWAP_MAP_BAD 0x3f /* 불량 슬롯 표시 */
#define SWAP_MAP_SHMEM 0x20 /* shmem/tmpfs 전용 참조 */
Swap Cache
Swap Cache는 스왑 영역과 메모리 사이의 중간 캐시 계층입니다. 페이지가 스왑 아웃/인될 때 일시적으로 스왑 캐시에 존재하며, 동일 페이지에 대한 중복 I/O를 방지하고 fork된 프로세스 간 일관성을 보장합니다.
/* mm/swap_state.c — Swap Cache 핵심 함수 */
/*
* swapper_spaces[]: 스왑 영역별 address_space 배열
* 각 address_space의 XArray에 swap offset → struct page 매핑 저장
* 일반 파일의 page cache와 동일한 인터페이스(find_get_page 등) 사용
*/
struct address_space *swapper_spaces[MAX_SWAPFILES];
/* 페이지를 Swap Cache에 추가 (swap out 시) */
int add_to_swap_cache(struct page *page, swp_entry_t entry,
gfp_t gfp, void **shadowp)
{
struct address_space *address_space = swap_address_space(entry);
pgoff_t idx = swp_offset(entry);
SetPageSwapCache(page); /* PG_swapcache 플래그 설정 */
set_page_private(page, entry.val); /* page->private에 swap entry 저장 */
/* XArray에 page 삽입 (page cache와 동일한 방식) */
xa_store(&address_space->i_pages, idx, page, gfp);
...
}
/* Swap Cache에서 페이지 검색 (swap in 시) */
struct page *lookup_swap_cache(swp_entry_t entry,
struct vm_area_struct *vma,
unsigned long addr)
{
struct page *page;
page = find_get_page(swap_address_space(entry),
swp_offset(entry));
if (page) {
/* Swap Cache 히트 — 디스크 I/O 없이 즉시 반환 */
mark_page_accessed(page);
}
return page;
}
Swap Cache vs Page Cache: Swap Cache는 사실상 Page Cache의 특수한 형태입니다. 일반 파일 페이지의 page->mapping이 파일의 address_space를 가리키듯, 스왑 캐시 페이지의 page->mapping은 swapper_spaces[]의 address_space를 가리킵니다. /proc/meminfo의 SwapCached 항목이 현재 스왑 캐시 크기를 나타냅니다.
페이지 Swap Out 경로
메모리 회수(reclaim) 과정에서 익명 페이지를 스왑 영역에 기록하는 전체 흐름입니다.
/*
* Swap Out 전체 경로 (간략화):
*
* kswapd / direct_reclaim
* → shrink_node()
* → shrink_lruvec()
* → shrink_list() ← inactive anon LRU 리스트 순회
* → shrink_folio_list()
* → add_to_swap() ← 1. 스왑 슬롯 할당 + 스왑 캐시 등록
* → pageout()
* → swap_writepage() ← 2. 디스크에 기록
* → try_to_unmap() ← 3. 모든 PTE에서 매핑 제거
* → rmap walk
* → try_to_unmap_one() ← PTE를 swap entry로 교체
* → free the page ← 4. 페이지 프레임 해제
*/
/* mm/vmscan.c — add_to_swap(): 스왑 슬롯 할당 핵심 */
bool add_to_swap(struct folio *folio)
{
swp_entry_t entry;
/* 1. 빈 스왑 슬롯 할당 (우선순위 기반) */
entry = folio_alloc_swap(folio);
if (!entry.val)
return false; /* 스왑 공간 부족 */
/* 2. Swap Cache에 등록 */
if (add_to_swap_cache(folio, entry, ...))
return true;
/* 실패 시 슬롯 반환 */
put_swap_folio(folio, entry);
return false;
}
/* mm/page_io.c — swap_writepage(): 실제 디스크 기록 */
int swap_writepage(struct page *page, struct writeback_control *wbc)
{
/* zswap이 활성화되어 있으면 압축 저장 시도 */
if (zswap_store(folio)) {
count_vm_event(ZSWPOUT);
return 0; /* zswap에 저장 성공 → 디스크 I/O 회피 */
}
/* 블록 디바이스에 비동기 기록 */
__swap_writepage(page, wbc);
...
}
/* mm/rmap.c — try_to_unmap_one(): PTE를 swap entry로 교체 */
static bool try_to_unmap_one(struct folio *folio,
struct vm_area_struct *vma, unsigned long address, ...)
{
pte_t pteval;
swp_entry_t entry;
/* 현재 PTE 값 읽기 및 unmap */
pteval = ptep_clear_flush(vma, address, pvmw.pte);
/* page->private에서 swap entry 추출 */
entry = make_readable_migration_entry(page_to_pfn(page));
if (PageSwapCache(page)) {
entry.val = page_private(page); /* swap entry */
}
/* PTE를 swap entry로 교체 (present=0) */
set_pte_at(mm, address, pvmw.pte,
swp_entry_to_pte(entry));
...
}
페이지 Swap In 경로
프로세스가 스왑 아웃된 페이지에 접근하면 page fault가 발생하고, do_swap_page()가 호출되어 페이지를 복원합니다.
/*
* Swap In 전체 경로:
*
* CPU가 PTE 접근 → present=0 → page fault
* → handle_pte_fault()
* → 비어있지 않은 non-present PTE → do_swap_page()
*
* do_swap_page() 내부:
* 1. PTE에서 swp_entry_t 추출
* 2. Swap Cache에서 페이지 검색 (hit이면 I/O 불필요)
* 3. Cache miss → swap_readpage()로 디스크에서 읽기
* 4. 읽은 페이지를 Swap Cache에 등록
* 5. PTE를 유효한 매핑으로 복원 (present=1)
* 6. swap_map 참조 카운트 감소
* 7. 참조 카운트가 0이면 → Swap Cache에서 제거 + 슬롯 해제
*/
/* mm/memory.c — do_swap_page() 핵심 로직 (간략화) */
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
swp_entry_t entry;
struct page *page;
pte_t pte;
/* 1. PTE에서 swap entry 추출 */
entry = pte_to_swp_entry(vmf->orig_pte);
/* 2. Swap Cache 검색 */
page = lookup_swap_cache(entry, vma, vmf->address);
if (!page) {
/* 3. Cache miss → 디스크에서 읽기 */
page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
vmf);
if (!page)
return VM_FAULT_OOM;
}
/* 4. 페이지 잠금 및 유효성 검증 */
lock_page(page);
/* 5. PTE를 유효한 매핑으로 복원 */
pte = mk_pte(page, vma->vm_page_prot);
if (vmf->flags & FAULT_FLAG_WRITE)
pte = maybe_mkwrite(pte_mkdirty(pte), vma);
set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);
/* 6. swap_map 참조 카운트 감소 */
swap_free(entry);
/* 참조 카운트가 0이면 Swap Cache에서도 제거됨 */
return 0;
}
/* mm/page_io.c — swap_readpage(): 디스크에서 읽기 */
int swap_readpage(struct page *page, bool synchronous,
struct swap_iocb **plug)
{
/* zswap에서 먼저 검색 (압축 저장된 경우) */
if (zswap_load(folio)) {
count_vm_event(ZSWPIN);
SetPageUptodate(page);
return 0; /* zswap에서 복원 성공 */
}
/* 블록 디바이스에서 비동기/동기 읽기 */
submit_bio(bio);
...
}
Swap Readahead
스왑 인 시 인접 페이지를 미리 읽어 성능을 개선합니다. 커널은 두 가지 readahead 전략을 사용합니다.
| 전략 | 방식 | 적합한 상황 | 설정 |
|---|---|---|---|
| 클러스터 readahead | 스왑 영역에서 물리적으로 인접한 슬롯들을 함께 읽기 | 순차 접근 패턴, HDD | /proc/sys/vm/page-cluster (2^n 페이지, 기본=3 → 8페이지) |
| VMA readahead | 가상 주소 공간에서 인접한 스왑 엔트리들을 함께 읽기 | 연속적 가상 메모리 접근, SSD | 자동 (swap in 패턴 분석) |
# Swap readahead 크기 설정
# page-cluster: 2^N 페이지를 한 번에 readahead
cat /proc/sys/vm/page-cluster # 기본값: 3 (2^3 = 8 페이지 = 32KB)
# SSD에서는 줄이는 것이 유리 (랜덤 읽기 비용이 낮음)
echo 0 > /proc/sys/vm/page-cluster # readahead 비활성화
echo 1 > /proc/sys/vm/page-cluster # 2페이지만 readahead
# HDD에서는 높은 값이 유리 (순차 읽기가 빠름)
echo 4 > /proc/sys/vm/page-cluster # 16페이지 readahead
Swappiness 튜닝
vm.swappiness는 커널의 메모리 회수 시 익명 페이지(swap out)와 파일 페이지(page cache 회수)의 상대적 비율을 조절합니다.
| 값 | 동작 | 적합한 워크로드 |
|---|---|---|
0 |
가능한 한 스왑 안 함 (파일 캐시를 우선 회수, 메모리 극히 부족할 때만 스왑) | 데이터베이스, 실시간 시스템 |
10 |
스왑을 최소화하되 필요 시 약간 허용 | 데스크톱, 일반 서버 |
60 |
기본값 — 파일 캐시와 익명 페이지를 균형 있게 회수 | 범용 서버 |
100 |
익명 페이지와 파일 페이지를 동일 비율로 회수 | 대용량 파일 캐시 유지가 중요한 경우 |
200 |
익명 페이지를 적극적으로 스왑 (cgroup v2 전용, 6.1+) | 메모리 오버커밋, 컨테이너 환경 |
# 전역 swappiness 설정
sysctl vm.swappiness=10 # 스왑 최소화
sysctl -w vm.swappiness=60 # 기본값으로 복원
# 영구 설정 (/etc/sysctl.conf)
# vm.swappiness = 10
# cgroup v2: 그룹별 독립 swappiness 설정
echo 0 > /sys/fs/cgroup/mydb/memory.swap.max # 해당 cgroup 스왑 금지
echo 10 > /sys/fs/cgroup/myapp/memory.swappiness # 그룹별 swappiness (v6.6+)
/* mm/vmscan.c — swappiness가 reclaim 비율에 미치는 영향 (간략화) */
static void get_scan_count(struct lruvec *lruvec,
struct scan_control *sc,
unsigned long *nr)
{
unsigned long ap, fp; /* anon pressure, file pressure */
unsigned long swappiness = mem_cgroup_swappiness(memcg);
/* swappiness가 0이면 → 메모리 극히 부족할 때만 anon 회수 */
if (!swappiness) {
/* 파일 캐시를 우선 회수, free가 극히 낮으면 anon도 회수 */
fraction[0] = 0; /* anon scan = 0 */
fraction[1] = 1; /* file scan = 전체 */
return;
}
/*
* anon과 file의 스캔 비율 결정:
* ap = swappiness * (최근 anon 참조 빈도의 역수)
* fp = (200 - swappiness) * (최근 file 참조 빈도의 역수)
* → swappiness가 높을수록 anon 스캔 비율 증가
*/
ap = swappiness * (total_cost + 1);
ap /= anon_cost + 1;
fp = (200 - swappiness) * (total_cost + 1);
fp /= file_cost + 1;
fraction[0] = ap; /* anon LRU 스캔 비율 */
fraction[1] = fp; /* file LRU 스캔 비율 */
}
swappiness=0은 스왑 완전 비활성이 아닙니다. vm.swappiness=0으로 설정해도, 시스템 전체 free 메모리가 zone의 high watermark + file cache보다 낮아지면 커널은 여전히 익명 페이지를 스왑 아웃합니다. 스왑을 완전히 금지하려면 swapoff -a로 스왑 영역을 비활성화하거나, cgroup v2에서 memory.swap.max=0으로 설정해야 합니다.
Multi-Gen LRU (MGLRU)
커널 6.1에 도입된 MGLRU는 기존의 active/inactive 2-리스트 LRU를 다중 세대(generation)로 확장하여, 페이지의 접근 빈도를 더 정밀하게 추적합니다. 이를 통해 스왑 아웃/페이지 회수 결정의 정확도가 크게 향상되어, 특히 메모리 부족 시 성능 저하가 줄어듭니다.
# MGLRU 활성화 상태 확인 (CONFIG_LRU_GEN 필요)
cat /sys/kernel/mm/lru_gen/enabled
# 0x0007 = 모든 기능 활성화 (Y+Y+Y)
# 비트 0: lru_gen 코어 활성화
# 비트 1: lru_gen에 의한 reclaim 활성화
# 비트 2: mm_walk(페이지 테이블 스캔)으로 세대 결정 활성화
# MGLRU 활성화/비활성화
echo 7 > /sys/kernel/mm/lru_gen/enabled # 전체 활성화
echo 0 > /sys/kernel/mm/lru_gen/enabled # 비활성화 (기존 LRU로 복귀)
# 세대별 페이지 분포 확인
cat /sys/kernel/mm/lru_gen/memcg_path
# memcg nid gen anon_pages file_pages birth_time
MGLRU 성능 효과: Google의 벤치마크에서 MGLRU는 기존 LRU 대비 메모리 부족 워크로드에서 최대 40%의 성능 향상을 보였습니다. 특히 대규모 서버, Android, ChromeOS 등에서 메모리 압박 시 OOM 발생률이 감소하고, swap thrashing으로 인한 성능 저하가 크게 줄어듭니다.
Swap 모니터링과 디버깅
# === /proc/meminfo — 스왑 관련 항목 ===
cat /proc/meminfo | grep -i swap
# SwapCached: 10240 kB ← Swap Cache 크기 (RAM에 캐시된 스왑 페이지)
# SwapTotal: 8388604 kB ← 전체 스왑 공간
# SwapFree: 7340032 kB ← 미사용 스왑 공간
# === /proc/vmstat — 스왑 I/O 통계 ===
cat /proc/vmstat | grep -E "pswp|swap"
# pswpin 123456 ← 스왑 인 된 총 페이지 수 (누적)
# pswpout 234567 ← 스왑 아웃 된 총 페이지 수 (누적)
# === vmstat 명령으로 실시간 스왑 활동 모니터링 ===
vmstat 1
# procs ---memory--- ---swap-- -----io---- ...
# r b swpd free si so bi bo ...
# 1 0 10240 65432 0 0 12 8 ...
# swpd: 사용 중인 스왑 (KB)
# si: 초당 스왑 인 (KB/s) — 높으면 스왑 thrashing 의심
# so: 초당 스왑 아웃 (KB/s)
# === 프로세스별 스왑 사용량 ===
cat /proc/<pid>/status | grep -i swap
# VmSwap: 1024 kB ← 해당 프로세스의 스왑 사용량
# 스왑 사용량이 큰 프로세스 상위 10개
for f in /proc/[0-9]*/status; do
awk '/^(Name|VmSwap)/{printf "%s ", $2}' "$f" 2>/dev/null
echo
done | sort -k2 -n -r | head -10
# === /proc/<pid>/smaps — 상세 VMA별 스왑 정보 ===
cat /proc/<pid>/smaps | grep -A 20 "heap" | grep Swap
# Swap: 1024 kB ← 해당 VMA의 스왑 사용량
# SwapPss: 512 kB ← PSS 비례 스왑 (공유 시 분할)
# === ftrace로 스왑 이벤트 추적 ===
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_lru_shrink_inactive/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_readpage/enable
echo 1 > /sys/kernel/debug/tracing/events/swap/swap_writepage/enable
cat /sys/kernel/debug/tracing/trace_pipe
Swap Thrashing 감지: vmstat의 si/so 값이 지속적으로 높으면(수 MB/s 이상) 스왑 thrashing 상태입니다. 이는 물리 메모리가 워크로드에 비해 크게 부족하다는 신호이며, 시스템 전체 성능이 급격히 저하됩니다. 해결 방법: 메모리 증설, 불필요한 프로세스 종료, vm.swappiness 조정, zswap/zram 도입, 또는 cgroup memory.high로 throttling 적용.
Swap 관련 커널 설정 요약
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_SWAP |
y | 스왑 서브시스템 활성화. 비활성 시 익명 페이지 회수 불가 |
CONFIG_SWAP_STATS |
y | 스왑 통계 수집 (/proc/vmstat의 pswpin/pswpout) |
CONFIG_ZSWAP |
y | zswap 압축 스왑 캐시 활성화 |
CONFIG_ZRAM |
m | zram 압축 블록 디바이스 (보통 모듈) |
CONFIG_LRU_GEN |
y (6.1+) | MGLRU 활성화 — 페이지 회수 정확도 향상 |
vm.swappiness |
60 | anon vs file 회수 비율 (0~200, cgroup v2) |
vm.page-cluster |
3 | swap readahead 크기: 2^N 페이지 |
vm.min_free_kbytes |
자동 | MIN watermark 기준 — 스왑/회수 트리거에 간접 영향 |
vm.watermark_boost_factor |
15000 | 단편화 방지 워터마크 부스트 팩터 |
MAX_SWAPFILES |
32 | 동시에 활성화 가능한 최대 스왑 영역 수 |
zswap과 zram
zswap과 zram은 압축을 활용하여 스왑 성능을 크게 개선하는 메커니즘입니다. 디스크 I/O를 줄이고 스왑 영역의 실질적 용량을 확장합니다.
zswap — 압축 스왑 캐시
zswap은 스왑 아웃될 페이지를 압축하여 RAM의 동적 풀에 캐시하는 커널 기능입니다. 실제 디스크/SSD 스왑 I/O가 발생하기 전에 RAM에서 압축 저장을 시도하므로, 디스크 I/O를 극적으로 줄입니다. zswap은 기존 스왑 영역 위에서 동작하는 write-back 캐시이며, 풀이 가득 차면 가장 오래된 페이지를 실제 스왑 영역으로 writeback합니다.
# === zswap 설정 ===
# 런타임 활성화
echo Y > /sys/module/zswap/parameters/enabled
# 압축 알고리즘 선택
echo lz4 > /sys/module/zswap/parameters/compressor
# 선택지: lzo (기본, 균형), lz4 (빠름), zstd (높은 압축률)
# 메모리 풀 할당자 선택
echo z3fold > /sys/module/zswap/parameters/zpool
# zbud: 2:1 압축 비율, 간단하고 예측 가능
# z3fold: 3:1 압축 비율, zbud보다 효율적 (권장)
# zsmalloc: 최고 압축 효율, 약간의 CPU 오버헤드
# 최대 풀 크기 (전체 RAM 대비 퍼센트)
echo 20 > /sys/module/zswap/parameters/max_pool_percent
# same-filled page 최적화 (0으로 채워진 페이지 특수 처리)
echo Y > /sys/module/zswap/parameters/same_filled_pages_enabled
# 커널 부트 파라미터 (권장 설정)
# zswap.enabled=1 zswap.compressor=lz4 zswap.zpool=z3fold zswap.max_pool_percent=25
# === zswap 상태 모니터링 ===
grep -r . /sys/kernel/debug/zswap/ 2>/dev/null
# pool_total_size: 압축 데이터가 차지하는 메모리 (바이트)
# stored_pages: 현재 저장된 페이지 수
# pool_limit_hit: 풀 크기 제한에 도달한 횟수
# reject_reclaim_fail: writeback 실패로 거절된 횟수
# reject_compress_poor: 압축 효율 낮아 거절된 횟수
# written_back_pages: 디스크로 writeback된 페이지 수
# same_filled_pages: same-filled로 최적화된 페이지 수
# 압축 비율 계산
# 원본 크기 = stored_pages × 4096
# 압축 크기 = pool_total_size
# 압축 비율 = 원본 / 압축
zswap 압축 알고리즘 선택 가이드: lz4는 압축/해제 속도가 가장 빨라 CPU 오버헤드가 적으며 대부분의 환경에서 권장됩니다. zstd는 압축률이 높아 메모리 절약이 최우선인 서버에 적합하지만 CPU 사용량이 증가합니다. lzo는 기본값으로 lz4와 유사한 성능을 보입니다. Android에서는 lz4가 표준입니다.
zram — 압축 RAM 블록 디바이스
zram은 RAM의 일부를 압축 블록 디바이스로 만들어 스왑 영역으로 사용하는 모듈입니다. zswap과 달리 독립적인 스왑 디바이스로 동작하며, 실제 디스크 스왑 영역이 없어도 사용할 수 있습니다. 디스크 없는 임베디드 시스템이나 SSD 수명을 보호하려는 환경에서 유용합니다.
# === zram 설정 ===
# 모듈 로드 (디바이스 수 지정)
modprobe zram num_devices=2
# 압축 알고리즘 설정 (디바이스 생성 전에 설정)
echo lz4 > /sys/block/zram0/comp_algorithm
# 지원 알고리즘 확인:
cat /sys/block/zram0/comp_algorithm
# lzo lzo-rle lz4 [lz4hc] zstd (대괄호=현재 선택)
# 디스크 크기 설정 (압축 전 논리적 크기)
echo 4G > /sys/block/zram0/disksize
# 메모리 사용량 제한 (선택사항)
echo 1G > /sys/block/zram0/mem_limit # 실제 RAM 사용 상한
# 스왑으로 활성화
mkswap /dev/zram0
swapon -p 100 /dev/zram0 # 높은 우선순위 (디스크보다 먼저 사용)
# === zram 상태 모니터링 ===
cat /sys/block/zram0/mm_stat
# orig_data_size compr_data_size mem_used_total mem_limit ...
# 4096000000 1024000000 1073741824 1073741824 ...
# ↑ 원본 크기 ↑ 압축 크기 ↑ 실제 메모리 ↑ 메모리 제한
zramctl # zram 디바이스 상태 요약
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4 4G 1.2G 320M 340M 4 [SWAP]
# === zram 비활성화 및 리셋 ===
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset # 디바이스 초기화
zswap vs zram 비교
| 특성 | zswap | zram |
|---|---|---|
| 동작 방식 | 기존 스왑 영역의 write-back 캐시 | 독립적인 스왑 블록 디바이스 |
| 디스크 스왑 필요 | 필수 (디스크 스왑 위에서 동작) | 불필요 (RAM만으로 동작 가능) |
| 풀 가득 참 시 | 디스크 스왑으로 writeback | 스왑 공간 부족 처리 (할당 실패) |
| 메모리 풀 | zbud/z3fold/zsmalloc (동적) | 자체 메모리 할당자 (zsmalloc) |
| 투명성 | 완전 투명 (기존 스왑 앞단에 삽입) | 별도 스왑 디바이스로 명시적 설정 |
| 주요 용도 | 디스크 스왑 I/O 감소, 서버 | 디스크 없는 시스템, SSD 보호, Android |
| 병용 가능 | 가능하지만 비권장 — 이중 압축 오버헤드 발생. 보통 하나만 선택 | |
권장 구성: SSD 기반 서버에서는 zswap + SSD 스왑 조합이 효과적입니다 (디스크 I/O 감소 + writeback 안전망). 디스크 없는 임베디드/IoT에서는 zram 단독이 유일한 선택입니다. 데스크톱/Android에서는 zram이 일반적입니다. 대규모 서버 환경에서 메모리 절약이 최우선이면 zswap + zstd 조합을 고려하십시오.
tmpfs (RAM 기반 파일시스템)
tmpfs는 페이지 캐시와 스왑을 저장소로 사용하는 메모리 기반 파일시스템입니다. 커널 내부적으로 mm/shmem.c에 구현되며, POSIX shared memory(/dev/shm), System V shared memory, mmap(MAP_ANONYMOUS)의 내부 backing store로도 사용됩니다.
tmpfs vs ramfs
| 특성 | tmpfs | ramfs |
|---|---|---|
| 크기 제한 | size= 옵션으로 제한 가능 (기본: 물리 메모리의 50%) |
제한 없음 — 메모리를 모두 소진할 수 있음 |
| 스왑 사용 | 메모리 부족 시 스왑으로 페이지 이동 | 스왑 불가 — 항상 RAM에 상주 |
| 페이지 회수 | kswapd/direct reclaim 대상 | 회수 불가능 (pinned) |
| memcg 계정 | memory cgroup에 정확히 계정됨 | 계정됨 (커널 버전에 따라 다름) |
| THP 지원 | 지원 (huge= 옵션) |
미지원 |
| 구현 | mm/shmem.c (복잡, 기능 풍부) |
fs/ramfs/ (단순, ~200줄) |
| 프로덕션 사용 | 권장 (/tmp, /run, /dev/shm) |
비권장 (OOM 위험) |
ramfs의 위험성: ramfs는 크기 제한 메커니즘이 없어, 실수로 대용량 데이터를 쓰면 시스템 전체 메모리를 소진하여 OOM Killer가 발동합니다. 프로덕션 환경에서는 반드시 tmpfs를 사용하십시오.
ramfs 내부 구현
ramfs는 Linux VFS의 가장 단순한 파일시스템 구현으로, fs/ramfs/inode.c 약 300줄로 이루어져 있습니다. VFS가 제공하는 simple_* 헬퍼 함수를 최대한 활용하여 최소한의 코드로 완전한 파일시스템을 구성합니다.
/* fs/ramfs/inode.c — ramfs 핵심 구현 */
/* ramfs의 inode 생성 */
struct inode *ramfs_get_inode(struct super_block *sb,
const struct inode *dir,
umode_t mode, dev_t dev)
{
struct inode *inode = new_inode(sb);
if (inode) {
inode->i_ino = get_next_ino();
inode_init_owner(&nop_mnt_idmap, inode, dir, mode);
inode->i_atime = inode->i_mtime = inode->i_ctime =
current_time(inode);
/* 매핑 초기화: 페이지 캐시에 직접 저장 */
mapping_set_gfp_mask(inode->i_mapping,
GFP_HIGHUSER | __GFP_ZERO);
mapping_set_unevictable(inode->i_mapping);
/* ↑ 핵심: AS_UNEVICTABLE 설정 → 페이지 회수 불가 */
switch (mode & S_IFMT) {
case S_IFREG: /* 일반 파일 */
inode->i_op = &ramfs_file_inode_operations;
inode->i_fop = &ramfs_file_operations;
break;
case S_IFDIR: /* 디렉토리 */
inode->i_op = &ramfs_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
inc_nlink(inode); /* . 엔트리 */
break;
case S_IFLNK: /* 심볼릭 링크 */
inode->i_op = &page_symlink_inode_operations;
inode_nohighmem(inode);
break;
default: /* 디바이스 노드 등 */
init_special_inode(inode, mode, dev);
break;
}
}
return inode;
}
/* ramfs 슈퍼블록 초기화 */
static int ramfs_fill_super(struct super_block *sb,
struct fs_context *fc)
{
sb->s_maxbytes = MAX_LFS_FILESIZE; /* 파일 크기 제한 없음 */
sb->s_blocksize = PAGE_SIZE;
sb->s_magic = RAMFS_MAGIC; /* 0x858458f6 */
sb->s_op = &ramfs_ops;
sb->s_time_gran = 1;
/* 루트 inode/dentry 생성 */
struct inode *inode = ramfs_get_inode(sb, NULL,
S_IFDIR | fsi->mount_opts.mode, 0);
sb->s_root = d_make_root(inode);
return sb->s_root ? 0 : -ENOMEM;
}
/* ramfs 파일 오퍼레이션 — 대부분 커널 generic 함수 재사용 */
const struct file_operations ramfs_file_operations = {
.read_iter = generic_file_read_iter,
.write_iter = generic_file_write_iter,
.mmap = generic_file_mmap,
.fsync = noop_fsync, /* 디스크 없음 → noop */
.splice_read = filemap_splice_read,
.llseek = generic_file_llseek,
};
/* ramfs 디렉토리 inode 오퍼레이션 */
const struct inode_operations ramfs_dir_inode_operations = {
.create = ramfs_create,
.lookup = simple_lookup,
.link = simple_link,
.unlink = simple_unlink,
.symlink = ramfs_symlink,
.mkdir = ramfs_mkdir,
.rmdir = simple_rmdir,
.mknod = ramfs_mknod,
.rename = simple_rename,
};
simple_* 함수(simple_lookup, simple_link, simple_unlink, simple_rmdir, simple_rename)와 generic_file_* 함수를 재사용합니다. 자체 구현은 inode 생성(ramfs_get_inode)과 슈퍼블록 초기화(ramfs_fill_super) 정도뿐입니다. 스왑, 크기 제한, quota 등 복잡한 기능이 없어 파일시스템 구현의 최소 사례(minimal reference)로 자주 인용됩니다.
mapping_set_unevictable() 호출이 ramfs의 핵심 특성을 결정합니다. 이 플래그(AS_UNEVICTABLE)가 설정되면 해당 매핑의 페이지들은 LRU의 unevictable 리스트에 배치되어 kswapd와 direct reclaim의 회수 대상에서 완전히 제외됩니다. 따라서 ramfs에 쓴 데이터는 삭제하기 전까지 RAM에서 절대 제거되지 않습니다.
rootfs와 initramfs
Linux 커널 부팅 과정에서 rootfs는 특수한 ramfs 인스턴스로, 커널이 가장 먼저 마운트하는 루트 파일시스템입니다. CONFIG_TMPFS가 활성화된 경우 rootfs는 tmpfs로 동작합니다.
/* init/do_mounts.c — rootfs 초기화 */
/*
* rootfs는 커널 부팅 초기에 마운트되는 특수 파일시스템입니다.
* init/main.c → vfs_caches_init() → mnt_init() → init_rootfs()
* → init_mount_tree() 순서로 초기화됩니다.
*/
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.init_fs_context = rootfs_init_fs_context,
.kill_sb = kill_litter_super,
};
int __init rootfs_init_fs_context(struct fs_context *fc)
{
#ifdef CONFIG_TMPFS
/* CONFIG_TMPFS 활성화 → rootfs를 tmpfs로 (스왑/크기 제한 지원) */
return shmem_init_fs_context(fc);
#else
/* CONFIG_TMPFS 비활성화 → 순수 ramfs */
return ramfs_init_fs_context(fc);
#endif
}
# initramfs 내용 확인
lsinitrd /boot/initramfs-$(uname -r).img # RHEL/Fedora
lsinitramfs /boot/initrd.img-$(uname -r) # Debian/Ubuntu
# initramfs 수동 해제
mkdir /tmp/initramfs && cd /tmp/initramfs
zcat /boot/initramfs-$(uname -r).img | cpio -idmv
# 커널 내장 initramfs 확인 (CONFIG_INITRAMFS_SOURCE)
cat /proc/cmdline | grep -o 'initrd=[^ ]*'
# rootfs가 tmpfs인지 ramfs인지 확인
grep CONFIG_TMPFS /boot/config-$(uname -r)
# CONFIG_TMPFS=y → rootfs는 tmpfs 기반
# CONFIG_TMPFS=n → rootfs는 ramfs 기반 (매우 드묾)
# switch_root 과정 (initramfs의 /init에서 실행)
# mount /dev/sda2 /mnt/root
# exec switch_root /mnt/root /sbin/init
# → rootfs의 모든 파일 삭제, 새 root로 pivot
switch_root vs pivot_root: switch_root는 initramfs(ramfs/tmpfs) 전용으로, 기존 rootfs의 모든 파일을 삭제한 후 새 루트로 전환합니다. ramfs/tmpfs는 마운트 해제가 불가능하므로(커널의 초기 마운트) 내용만 비웁니다. 반면 pivot_root는 블록 디바이스 기반 initrd에서 사용되며, 기존 루트를 다른 위치에 재마운트합니다. 현대 시스템은 대부분 initramfs + switch_root 조합을 사용합니다.
tmpfs 내부 구조 (shmem)
tmpfs는 커널의 shmem(shared memory) 서브시스템 위에 구축됩니다. 데이터는 페이지 캐시에 저장되고, 메모리 부족 시 스왑으로 내려갑니다.
/* mm/shmem.c — tmpfs의 핵심 자료구조 */
/* shmem inode 정보 */
struct shmem_inode_info {
spinlock_t lock;
unsigned int seals; /* F_SEAL_* 플래그 */
unsigned long flags;
unsigned long alloced; /* 할당된 페이지 수 */
unsigned long swapped; /* 스왑된 페이지 수 */
struct shared_policy policy; /* NUMA 정책 */
struct simple_xattrs xattrs; /* 확장 속성 */
struct inode vfs_inode; /* 내장 VFS inode */
};
/* shmem 슈퍼블록 정보 */
struct shmem_sb_info {
unsigned long max_blocks; /* 최대 페이지 수 (size= 옵션) */
unsigned long used_blocks; /* 현재 사용 중인 페이지 */
unsigned long max_inodes; /* 최대 inode 수 (nr_inodes=) */
unsigned long free_inodes; /* 남은 inode 수 */
int huge; /* THP 정책 */
kuid_t uid; /* 마운트 시 소유자 */
kgid_t gid;
umode_t mode; /* 퍼미션 */
struct mempolicy *mpol; /* NUMA 메모리 정책 */
spinlock_t stat_lock;
};
tmpfs 페이지 폴트 처리 경로
프로세스가 tmpfs 파일을 mmap()한 후 실제 메모리에 접근하면, 아직 물리 페이지가 할당되지 않았으므로 페이지 폴트가 발생합니다. 커널은 shmem_fault() → shmem_get_folio() 경로를 통해 페이지를 할당하거나 스왑에서 복원합니다.
/* mm/shmem.c — tmpfs 페이지 폴트 처리 */
/*
* VM 폴트 핸들러: VFS → vm_operations_struct.fault → shmem_fault()
* mmap된 tmpfs 파일의 페이지 접근 시 호출됩니다.
*/
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
gfp_t gfp = mapping_gfp_mask(inode->i_mapping);
struct folio *folio;
int err;
/* fallocate와의 경합 방지 */
if (unlikely(inode->i_private)) {
struct shmem_falloc *shmem_falloc;
spin_lock(&inode->i_lock);
shmem_falloc = inode->i_private;
if (shmem_falloc &&
shmem_falloc->waitq &&
vmf->pgoff >= shmem_falloc->start &&
vmf->pgoff < shmem_falloc->next)
wait_event_killable(*shmem_falloc->waitq, ...);
spin_unlock(&inode->i_lock);
}
/* 핵심: folio(페이지) 획득 */
err = shmem_get_folio_gfp(inode, vmf->pgoff,
&folio, SGP_CACHE, gfp, vmf, &vmf->ret);
if (err)
return vmf_error(err);
vmf->page = folio_file_page(folio, vmf->pgoff);
return VM_FAULT_LOCKED; /* 페이지가 잠긴 상태로 반환 */
}
/*
* shmem_get_folio_gfp() — tmpfs 페이지 획득의 핵심 함수
* 3가지 경로 중 하나로 페이지를 반환합니다:
*
* 1. Page Cache 히트: XArray에서 folio를 찾아 즉시 반환
* 2. Swap-in: XArray에 swap entry가 있으면 스왑에서 읽어 복원
* 3. 새 할당: folio를 할당하고 XArray에 삽입
*/
static int shmem_get_folio_gfp(struct inode *inode,
pgoff_t index, struct folio **foliop,
enum sgp_type sgp, gfp_t gfp,
struct vm_fault *vmf, vm_fault_t *fault_type)
{
struct address_space *mapping = inode->i_mapping;
struct shmem_inode_info *info = SHMEM_I(inode);
struct folio *folio;
int error;
repeat:
/* 1단계: XArray(Page Cache)에서 검색 */
folio = filemap_get_folio(mapping, index);
if (!IS_ERR(folio)) {
/* Page Cache 히트 — 빠른 경로 */
*foliop = folio;
return 0;
}
/* 2단계: swap entry인지 확인 */
folio = xa_load(&mapping->i_pages, index);
if (xa_is_value(folio)) {
/* Swap entry 발견 → 스왑에서 읽기 */
error = shmem_swapin_folio(inode, index,
foliop, sgp, gfp, vmf, fault_type);
if (!error)
info->swapped--;
return error;
}
/* 3단계: 새 folio 할당 */
folio = shmem_alloc_and_add_folio(gfp, inode,
index, fault_type, is_huge_enabled(info));
if (IS_ERR(folio))
return PTR_ERR(folio);
/* 새 페이지를 0으로 초기화 */
folio_zero_range(folio, 0, folio_size(folio));
/* 사용량 계정 업데이트 */
info->alloced += folio_nr_pages(folio);
shmem_recalc_inode(inode, ...);
*foliop = folio;
return 0;
}
shmem_get_folio_gfp()의 sgp 파라미터는 페이지 획득 정책을 결정합니다. SGP_READ: 존재하는 페이지만 반환 (없으면 zero page), SGP_CACHE: 없으면 새로 할당하여 캐시에 등록, SGP_WRITE: 쓰기를 위한 획득 (CoW 등 처리), SGP_FALLOC: fallocate용 (i_size 초과 영역도 할당).
tmpfs 마운트 옵션
# 기본 마운트 (기본 크기: 물리 메모리의 50%)
mount -t tmpfs tmpfs /mnt/tmp
# 크기 제한 지정
mount -t tmpfs -o size=2G tmpfs /mnt/tmp
mount -t tmpfs -o size=50% tmpfs /mnt/tmp # RAM의 50%
# 전체 옵션 예시
mount -t tmpfs -o size=1G,nr_inodes=100k,mode=1777,uid=0,gid=0 tmpfs /tmp
# 런타임 크기 변경 (리마운트)
mount -o remount,size=4G /tmp
# THP(Transparent Huge Pages) 활성화
mount -t tmpfs -o size=2G,huge=always tmpfs /mnt/huge_tmp
mount -t tmpfs -o size=2G,huge=within_size tmpfs /mnt/huge_tmp
# NUMA 메모리 정책 적용
mount -t tmpfs -o size=1G,mpol=bind:0 tmpfs /mnt/numa_tmp
mount -t tmpfs -o size=1G,mpol=interleave:0-1 tmpfs /mnt/numa_tmp
mount -t tmpfs -o size=1G,mpol=prefer:0 tmpfs /mnt/numa_tmp
| 옵션 | 기본값 | 설명 |
|---|---|---|
size= |
물리 메모리의 50% | 최대 사용 가능 크기 (bytes, k, m, g, 또는 %) |
nr_blocks= |
size 기반 계산 | 최대 블록 수 (페이지 단위). size=와 상호 배타적 |
nr_inodes= |
물리 메모리 페이지의 50% | 최대 inode(파일/디렉토리) 수. 0이면 무제한 |
mode= |
1777 |
루트 디렉토리 퍼미션 |
uid= / gid= |
0 / 0 | 루트 디렉토리 소유자 |
huge= |
never |
THP 정책: never, always, within_size, advise |
mpol= |
default |
NUMA 정책: default, prefer:N, bind:N, interleave:N-M |
noswap |
비활성 | 스왑 사용 금지 (v6.4+). 데이터가 항상 RAM에 상주 |
tmpfs와 스왑의 관계
tmpfs의 핵심 특성은 메모리 부족 시 데이터를 스왑 영역으로 내보낼 수 있다는 점입니다. ramfs와의 가장 큰 차이점이며, 이를 통해 물리 메모리보다 큰 tmpfs 사용이 이론적으로 가능합니다.
/* mm/shmem.c — 스왑 아웃 경로 (간략화) */
/*
* shmem_writepage()는 메모리 회수 시 호출됩니다.
* Page Cache의 페이지를 스왑 영역에 기록하고,
* XArray에는 swap entry를 저장합니다.
*/
static int shmem_writepage(struct page *page,
struct writeback_control *wbc)
{
struct shmem_inode_info *info;
swp_entry_t swap;
/* 스왑 슬롯 할당 */
swap = get_swap_page(page);
if (!swap.val)
return 0; /* 스왑 공간 부족 */
/* XArray에서 page를 swap entry로 교체 */
xa_store(&mapping->i_pages, index, swp_to_radix_entry(swap), ...);
info->swapped++;
/* 스왑 영역에 페이지 데이터 기록 */
swap_writepage(page, wbc);
return 0;
}
/*
* shmem_swapin_folio()는 스왑된 페이지 접근 시 호출됩니다.
* 스왑 영역에서 데이터를 읽어 Page Cache로 복원합니다.
*/
# tmpfs 스왑 관련 확인
df -h /tmp
# tmpfs 2.0G 156M 1.9G 8% /tmp
# tmpfs가 스왑에 얼마나 기록했는지 확인
cat /proc/meminfo | grep -i shmem
# Shmem: 512000 kB ← 모든 tmpfs/shmem의 총 사용량
# ShmemHugePages: 0 kB ← THP 사용 중인 tmpfs 페이지
# ShmemPmdMapped: 0 kB ← PMD 매핑된 tmpfs 페이지
# 개별 마운트 포인트의 스왑 사용량은 직접 확인 불가
# → inode별 swapped 카운트는 커널 내부 정보
# 스왑 공간이 없으면 tmpfs는 size= 제한과 물리 메모리 중 작은 값만 사용 가능
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/sda2 partition 8G 1.2G -2
mount -t tmpfs -o noswap tmpfs /mnt/secure로 마운트하면 해당 tmpfs의 데이터가 스왑 영역에 기록되지 않습니다. 암호화 키, 비밀번호 등 민감한 데이터가 디스크에 유출되는 것을 방지할 때 유용합니다. 단, 물리 메모리만 사용하므로 메모리 부족 시 쓰기가 실패(-ENOMEM)합니다.
시스템에서의 tmpfs 활용
현대 Linux 시스템에서 tmpfs는 다양한 핵심 위치에 마운트됩니다:
| 마운트 포인트 | 용도 | 일반적 크기 | 관리 주체 |
|---|---|---|---|
/tmp |
임시 파일 | RAM의 50% | systemd (tmp.mount unit) |
/run |
런타임 데이터 (PID 파일, 소켓) | RAM의 10~25% | initramfs / systemd |
/dev/shm |
POSIX shared memory (shm_open()) |
RAM의 50% | systemd / fstab |
/sys/fs/cgroup |
cgroup v1 마운트 포인트 | 소량 | systemd (cgroup v1 한정) |
/run/user/<uid> |
사용자별 런타임 디렉토리 | RAM의 10% | systemd-logind (RuntimeDirectory) |
컨테이너 /dev |
컨테이너 내부 디바이스 노드 | 64MB (Docker 기본) | 컨테이너 런타임 (runc) |
# 시스템의 모든 tmpfs 마운트 확인
mount -t tmpfs
# tmpfs on /run type tmpfs (rw,nosuid,nodev,noexec,size=3244852k,mode=755)
# tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
# tmpfs on /tmp type tmpfs (rw,nosuid,nodev,size=16224256k)
# tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,size=3244848k,...
# tmpfs 사용량 통합 확인
df -h -t tmpfs
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 3.1G 1.5M 3.1G 1% /run
# tmpfs 16G 0 16G 0% /dev/shm
# tmpfs 16G 128K 16G 1% /tmp
# systemd에서 /tmp를 tmpfs로 활성화/비활성화
systemctl enable tmp.mount # /tmp를 tmpfs로
systemctl disable tmp.mount # /tmp를 디스크에 유지
memfd와 File Sealing
memfd_create()는 tmpfs 기반의 익명 파일을 생성합니다. File sealing 메커니즘을 통해 파일 내용이나 크기를 봉인(seal)하여, IPC 시 수신 측이 데이터의 불변성을 보장받을 수 있습니다.
#include <sys/mman.h>
#include <linux/memfd.h>
#include <fcntl.h>
/* 1. memfd 생성 (sealing 가능) */
int fd = memfd_create("shared_buf", MFD_ALLOW_SEALING);
ftruncate(fd, 4096); /* 크기 설정 */
/* 2. 데이터 기록 */
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(p, data, data_len);
munmap(p, 4096);
/* 3. 봉인 적용 — 이후 쓰기/크기변경/축소 불가 */
fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_GROW);
/* 4. fd를 Unix 도메인 소켓으로 전달 (SCM_RIGHTS) */
/* 수신 측은 seal을 검증하여 데이터 불변성 확인 가능 */
int seals = fcntl(fd, F_GET_SEALS);
if (seals & F_SEAL_WRITE)
/* 안전: 송신 측이 더 이상 수정 불가 */
/* Seal 종류:
* F_SEAL_SEAL — 추가 seal 적용 금지
* F_SEAL_SHRINK — ftruncate 축소 금지
* F_SEAL_GROW — ftruncate 확대 금지
* F_SEAL_WRITE — write/mmap(PROT_WRITE) 금지
* F_SEAL_FUTURE_WRITE — 기존 writable mmap은 유지, 새 write 금지 (v5.1+)
*/
/* memfd + huge pages (THP) */
int fd = memfd_create("huge_buf", MFD_HUGETLB | MFD_HUGE_2MB);
/* HugeTLB 기반 memfd — DPDK, QEMU 등에서 활용 */
memfd 활용 사례: Wayland 컴포지터(Weston, Mutter)는 memfd_create() + F_SEAL_SHRINK로 클라이언트와 공유 버퍼를 교환합니다. QEMU/KVM은 MFD_HUGETLB로 게스트 메모리를 할당합니다. systemd의 MemoryDenyWriteExecute=와 함께 사용하면 JIT-free IPC 구현도 가능합니다.
tmpfs와 Memory Cgroup
tmpfs 사용량은 해당 파일을 생성한 프로세스의 memory cgroup에 계정됩니다. 컨테이너 환경에서 tmpfs 사용량이 메모리 제한에 포함된다는 점을 반드시 인지해야 합니다.
# 컨테이너 내 tmpfs 사용량이 memcg 제한에 포함됨
# Docker에서 --tmpfs 옵션 사용 시:
docker run --memory=512m --tmpfs /tmp:size=256m myapp
# /tmp에 200MB 쓰면 → memory.current가 200MB 증가
# 총 메모리 사용이 512MB 초과 시 OOM!
# cgroup v2에서 tmpfs/shmem 사용량 확인
cat /sys/fs/cgroup/myapp/memory.stat
# shmem 204800 ← tmpfs + shared memory 사용량 (bytes)
# file 1048576 ← 파일 캐시 (tmpfs 포함)
# Kubernetes Pod에서 emptyDir medium: Memory (tmpfs 기반)
# → Pod의 메모리 limit에 포함되므로 sizeLimit 설정 필수
# volumes:
# - name: cache
# emptyDir:
# medium: Memory
# sizeLimit: 128Mi
컨테이너 OOM 주의: Docker의 --tmpfs 또는 Kubernetes의 emptyDir.medium: Memory로 생성된 tmpfs 사용량은 컨테이너의 메모리 제한(cgroup memory.max)에 포함됩니다. tmpfs에 대량 데이터를 쓰면 예기치 않은 OOM Kill이 발생할 수 있습니다.
tmpfs와 Transparent Huge Pages
tmpfs는 THP를 사용하여 대용량 파일의 TLB 미스를 줄일 수 있습니다. 마운트 시 huge= 옵션으로 제어합니다.
huge= 값 | 동작 | 적합한 사용 사례 |
|---|---|---|
never |
THP 비사용 (기본값) | 소규모 파일이 많은 일반적인 /tmp |
always |
항상 THP 할당 시도 | 대용량 공유 메모리 (DB, QEMU) |
within_size |
파일 크기 범위 내에서만 THP | 크기가 다양한 파일 혼용 시 |
advise |
madvise(MADV_HUGEPAGE) 요청 시만 |
애플리케이션이 명시적으로 제어 |
# tmpfs THP 사용량 모니터링
grep -i shmem /proc/meminfo
# ShmemHugePages: 2097152 kB ← 2MB 단위 THP 사용량
# ShmemPmdMapped: 2097152 kB ← PMD(2MB) 매핑된 페이지
# 시스템 전역 tmpfs THP 정책 (per-mount 설정 우선)
cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
# [never] always within_size advise deny force
tmpfs 보안 고려사항
| 위협 | 설명 | 대응 |
|---|---|---|
| 스왑 유출 | tmpfs 데이터가 스왑을 통해 디스크에 기록될 수 있음 | noswap 옵션 (v6.4+), mlock(), 또는 암호화 스왑 사용 |
| 메모리 고갈 (DoS) | 사용자가 tmpfs에 대량 데이터를 써서 시스템 메모리 고갈 | size=로 크기 제한, memcg 쿼터 설정 |
| world-writable /tmp | 다른 사용자의 임시 파일 접근/심볼릭 링크 공격 | mode=1777 (sticky bit), O_NOFOLLOW, mkstemp() |
| 코어 덤프 유출 | tmpfs에 저장된 민감 데이터가 코어 덤프에 포함 | madvise(MADV_DONTDUMP), prctl(PR_SET_DUMPABLE, 0) |
| 재부팅 후 데이터 잔존 | tmpfs는 RAM 기반이므로 재부팅 시 데이터 소멸. 이를 장점으로 활용 가능 | 민감 데이터를 tmpfs에 저장하여 자동 삭제 보장 |
# 보안 강화된 /tmp tmpfs 마운트 (fstab 예시)
# tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev,size=2G,mode=1777 0 0
# noexec: /tmp에서 실행 파일 실행 금지 (악성코드 방지)
# nosuid: SUID 비트 무시
# nodev: 디바이스 파일 생성 금지
# 암호화 스왑으로 tmpfs 스왑 유출 방지
# /etc/crypttab:
# cswap /dev/sda2 /dev/urandom swap,cipher=aes-xts-plain64,size=256
# 프로세스별 tmpfs 격리 (systemd PrivateTmp)
# [Service]
# PrivateTmp=yes ← 서비스별 /tmp, /var/tmp를 별도 tmpfs로 격리
tmpfs 성능 특성
# tmpfs vs ext4 vs xfs 성능 비교 (일반적 경향)
#
# 작업 tmpfs ext4(SSD) ext4(HDD)
# ───────────────────────────────────────────────
# 순차 읽기 ~10GB/s ~3GB/s ~200MB/s
# 순차 쓰기 ~8GB/s ~2.5GB/s ~180MB/s
# 랜덤 4K IOPS ~2M ~500K ~200
# 파일 생성 ~500K/s ~100K/s ~5K/s
# 지연 시간 ~1μs ~10μs ~5ms
#
# ※ 실제 수치는 하드웨어, 워크로드, 메모리 상태에 따라 달라짐
# fio로 tmpfs 벤치마크
fio --name=tmpfs_test --directory=/tmp \
--rw=randrw --bs=4k --size=1G \
--numjobs=4 --time_based --runtime=30 \
--ioengine=libaio --direct=0
# tmpfs에서 direct=1(O_DIRECT)은 의미 없음
# tmpfs는 페이지 캐시 자체가 저장소이므로 항상 "cached" I/O
빌드 성능 최적화: 대규모 소프트웨어 빌드 시 /tmp이나 빌드 디렉토리를 tmpfs에 배치하면 I/O 병목을 제거할 수 있습니다. 특히 ccache, sccache와 함께 사용하면 빌드 시간이 크게 단축됩니다. 단, 충분한 RAM이 있어야 하며, 빌드 산출물이 tmpfs 크기를 초과하지 않도록 주의하십시오.
tmpfs fallocate & Hole Punch
tmpfs는 fallocate() 시스템 콜을 지원하여 공간 사전 할당과 hole punching(부분 해제)이 가능합니다. 특히 hole punch는 tmpfs에서 파일의 특정 범위만 메모리에서 해제하여 RAM을 즉시 회수하는 강력한 기법입니다.
/* mm/shmem.c — shmem_fallocate() 핵심 경로 */
static long shmem_fallocate(struct file *file,
int mode, loff_t offset, loff_t len)
{
struct inode *inode = file_inode(file);
struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
if (mode & FALLOC_FL_PUNCH_HOLE) {
/* Hole Punch: 지정 범위의 페이지를 Page Cache에서 제거
* → 물리 메모리 즉시 회수 (스왑된 페이지도 해제) */
shmem_truncate_range(inode,
offset, offset + len - 1);
return 0;
}
if (mode & FALLOC_FL_KEEP_SIZE) {
/* i_size 변경 없이 블록만 사전 할당
* → ENOSPC 방지용 공간 예약 */
}
/* 기본 fallocate: 페이지를 미리 할당하고 0으로 초기화
* sbinfo->used_blocks 업데이트 → 할당량(size=) 차감 */
for (index = start; index < end; index++) {
error = shmem_get_folio(inode, index,
&folio, SGP_FALLOC);
/* 이미 존재하는 페이지는 건너뜀 */
if (!error)
folio_unlock(folio);
}
/* 필요 시 i_size 확장 */
if (!(mode & FALLOC_FL_KEEP_SIZE))
i_size_write(inode, offset + len);
return error;
}
/* 사용자 공간에서 tmpfs fallocate 활용 예제 */
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
int fd = open("/tmp/data", O_RDWR | O_CREAT, 0644);
/* 1. 공간 사전 할당 (1GB) — ENOSPC 사전 감지 */
if (fallocate(fd, 0, 0, 1ULL << 30) < 0)
perror("tmpfs 공간 부족"); /* ENOSPC */
/* 2. Hole Punch — 오프셋 4KB~1MB 범위의 메모리 즉시 해제 */
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
4096, 1048576 - 4096);
/* 3. 파일 축소 없이 끝부분 블록만 예약 */
fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 2ULL << 30);
/* 4. Zero Range (v3.15+) — 범위를 0으로 초기화하되 할당 유지 */
fallocate(fd, FALLOC_FL_ZERO_RANGE | FALLOC_FL_KEEP_SIZE,
0, 4096);
Hole Punch 활용: 대용량 tmpfs 파일에서 더 이상 필요 없는 영역을 FALLOC_FL_PUNCH_HOLE로 해제하면 unlink() 없이도 메모리를 즉시 회수할 수 있습니다. 데이터베이스 임시 파일, 대규모 버퍼 풀, 또는 링 버퍼 구현 시 유용합니다. madvise(MADV_REMOVE)도 동일한 효과를 제공합니다.
tmpfs Quota (v6.1+)
커널 6.1부터 tmpfs는 사용자/그룹별 용량 제한(quota)을 지원합니다. 기존에는 size= 옵션으로 전체 크기만 제한할 수 있었지만, quota를 통해 멀티유저 환경에서 사용자별 tmpfs 사용량을 제어할 수 있습니다.
# tmpfs quota 활성화 마운트 (v6.1+)
mount -t tmpfs -o size=4G,usrquota tmpfs /tmp
mount -t tmpfs -o size=4G,usrquota,grpquota tmpfs /tmp
# quota 제한 설정 (xfs_quota, quota 도구 사용)
# setquota -u username 1G 2G 0 0 /tmp
# └─ 소프트 1G, 하드 2G (inode는 무제한)
# quota 사용량 확인
repquota -s /tmp
# User used soft hard grace files
# root -- 128M 0 0 15
# user1 +- 1.2G 1G 2G 6days 230
# 개별 사용자 quota 확인
quota -u user1 -f /tmp
/* mm/shmem.c — tmpfs quota 내부 구현 */
/* tmpfs quota는 VFS의 generic quota를 활용합니다.
* CONFIG_TMPFS_QUOTA=y 필요 (v6.1+)
*
* 기존 디스크 기반 quota와 달리:
* - quota 데이터가 메모리에만 존재 (재부팅 시 초기화)
* - quotacheck 불필요 (항상 정확)
* - 저널링 오버헤드 없음
*/
/* shmem 슈퍼블록에 quota 관련 옵션 추가 */
enum shmem_param {
Opt_quota,
Opt_usrquota,
Opt_grpquota,
Opt_usrquota_block_hardlimit,
Opt_usrquota_inode_hardlimit,
Opt_grpquota_block_hardlimit,
Opt_grpquota_inode_hardlimit,
/* ... */
};
/* 페이지 할당 시 quota 차감 흐름 */
/* shmem_get_folio() → dquot_alloc_block()
* → 사용자/그룹의 현재 사용량 + 요청량 vs 제한 검사
* → 초과 시 -EDQUOT 반환 → write()가 실패 */
| 마운트 옵션 | 설명 | 비고 |
|---|---|---|
usrquota |
사용자별 quota 활성화 | CONFIG_TMPFS_QUOTA=y 필요 |
grpquota |
그룹별 quota 활성화 | CONFIG_TMPFS_QUOTA=y 필요 |
usrquota_block_hardlimit= |
사용자 기본 블록 하드 제한 | v6.7+ (마운트 시 기본값 설정) |
usrquota_inode_hardlimit= |
사용자 기본 inode 하드 제한 | v6.7+ |
grpquota_block_hardlimit= |
그룹 기본 블록 하드 제한 | v6.7+ |
grpquota_inode_hardlimit= |
그룹 기본 inode 하드 제한 | v6.7+ |
tmpfs quota의 휘발성: tmpfs quota 정보는 RAM에만 존재하므로 재부팅 시 모든 quota 설정이 초기화됩니다. 영구적 quota 설정이 필요하면 /etc/fstab의 마운트 옵션에 하드 제한 기본값을 지정하고, 부팅 스크립트에서 setquota를 실행해야 합니다.
tmpfs ENOSPC 처리
tmpfs에서 size= 제한이나 물리 메모리 부족으로 공간이 부족하면 -ENOSPC(No space left on device) 에러가 발생합니다. 디스크 파일시스템과 달리 tmpfs의 공간 부족은 메모리 상황에 따라 동적으로 변할 수 있습니다.
/* mm/shmem.c — ENOSPC 발생 조건 */
/* 블록(페이지) 할당 시 검사 */
static int shmem_reserve_inode(struct super_block *sb, ...)
{
struct shmem_sb_info *sbinfo = SHMEM_SB(sb);
if (sbinfo->max_inodes) {
if (sbinfo->free_inodes < 1)
return -ENOSPC; /* nr_inodes= 제한 초과 */
sbinfo->free_inodes--;
}
return 0;
}
/* 페이지 쓰기 시 검사 (shmem_get_folio 내부) */
/*
* ENOSPC 발생 시나리오:
* 1. used_blocks >= max_blocks (size= 제한 도달)
* 2. free_inodes == 0 (nr_inodes= 제한 도달)
* 3. 메모리 할당 실패 + 스왑 공간 부족
* 4. quota 초과 시 -EDQUOT (v6.1+)
*/
# tmpfs 공간 상태 모니터링
df -h /tmp
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 2.0G 1.8G 200M 90% /tmp
# inode 사용률 확인 (파일 수 제한)
df -ih /tmp
# Filesystem Inodes IUsed IFree IUse% Mounted on
# tmpfs 100K 95K 5K 95% /tmp
# 런타임 크기 확장 (서비스 중단 없음)
mount -o remount,size=4G /tmp
# inode 제한도 런타임 변경 가능
mount -o remount,nr_inodes=200k /tmp
# 큰 파일부터 찾아서 정리
find /tmp -xdev -type f -size +100M -exec ls -lh {} \;
# 열린 파일이 삭제된 경우 (공간 미회수)
lsof +L1 /tmp # unlinked but open 파일 목록
# 해당 프로세스를 재시작해야 공간 회수
숨겨진 tmpfs 공간 소비: df로 확인되는 Used와 실제 메모리 사용량이 다를 수 있습니다. 삭제(unlink)되었지만 아직 열려 있는 파일, 스왑으로 내려간 페이지, 그리고 fallocate()로 예약만 하고 미사용 중인 블록이 있을 수 있습니다. lsof +L1로 삭제되었으나 열린 파일을 확인하고, /proc/meminfo의 Shmem 값으로 실제 메모리 사용량을 파악하세요.
devtmpfs (/dev 관리)
devtmpfs는 커널이 자동으로 디바이스 노드를 생성하는 특수한 tmpfs 변형입니다. 부팅 초기에 /dev에 마운트되어, 커널이 디바이스를 감지할 때마다 해당 디바이스 노드(/dev/sda, /dev/tty0 등)를 자동으로 생성합니다.
/* drivers/base/devtmpfs.c — devtmpfs 핵심 구현 */
/*
* devtmpfs는 kdevtmpfs 커널 스레드가 관리합니다.
* device_add() → devtmpfs_create_node()
* device_del() → devtmpfs_delete_node()
*/
static struct file_system_type dev_fs_type = {
.name = "devtmpfs",
.init_fs_context = shmem_init_fs_context, /* tmpfs 기반! */
/* CONFIG_TMPFS 비활성화 시 ramfs 기반으로 폴백 */
};
/* 디바이스 추가 시 노드 자동 생성 */
static int handle_create(const char *name,
umode_t mode, kuid_t uid,
kgid_t gid, struct device *dev)
{
struct dentry *dentry;
struct path path;
/* 필요한 중간 디렉토리 자동 생성 (/dev/bus/usb 등) */
dentry = kern_path_create(AT_FDCWD, name, &path, 0);
/* mknod로 디바이스 노드 생성 */
vfs_mknod(&nop_mnt_idmap, d_inode(path.dentry),
dentry, mode, dev->devt);
/* 소유권/퍼미션 설정 */
vfs_fchown(dentry, uid, gid);
vfs_fchmod(dentry, mode);
return 0;
}
/* kdevtmpfs 스레드: 요청 큐를 처리하는 루프 */
static int devtmpfsd(void *p)
{
while (1) {
wait_for_completion(&req_done);
/* 큐의 create/delete 요청 순차 처리 */
handle(requests->name, requests->mode, ...);
}
}
# devtmpfs 마운트 확인
mount | grep devtmpfs
# devtmpfs on /dev type devtmpfs (rw,nosuid,size=8168440k,nr_inodes=2042110,mode=755)
# devtmpfs가 자동 생성한 노드 vs udev가 보정한 노드
ls -la /dev/sda
# brw-rw---- 1 root disk 8, 0 ... /dev/sda ← udev가 퍼미션/그룹 설정
# udev 없이 devtmpfs만으로 부팅 가능 (임베디드/rescue)
# 커널 파라미터: devtmpfs.mount=1
# → 커널이 /dev를 직접 마운트 (initramfs 이전)
# devtmpfs 비활성화 (매우 드묾)
grep CONFIG_DEVTMPFS /boot/config-$(uname -r)
# CONFIG_DEVTMPFS=y
# CONFIG_DEVTMPFS_MOUNT=y ← 커널이 자동 마운트
# kdevtmpfs 커널 스레드 확인
ps aux | grep kdevtmpfs
# root 27 0.0 0.0 0 0 ? S ... [kdevtmpfs]
/dev/sda 등)를 즉시 생성합니다. udev는 이후 netlink uevent를 받아 심볼릭 링크(/dev/disk/by-id/..., /dev/disk/by-uuid/...), 퍼미션, 소유자 등을 rules에 따라 보정합니다. devtmpfs 덕분에 udev가 아직 시작되지 않은 부팅 초기에도 디바이스에 접근할 수 있으며, 임베디드 환경에서는 udev 없이 devtmpfs만으로 운영이 가능합니다.
Memory Cgroup (memcg)
cgroup을 통한 프로세스 그룹별 메모리 제한:
# cgroup v2 메모리 제한 설정
mkdir /sys/fs/cgroup/myapp
echo 512M > /sys/fs/cgroup/myapp/memory.max # 하드 제한
echo 400M > /sys/fs/cgroup/myapp/memory.high # 소프트 제한 (reclaim 유도)
echo 256M > /sys/fs/cgroup/myapp/memory.low # 보호 수준
echo 128M > /sys/fs/cgroup/myapp/memory.min # 절대 보호
# 프로세스를 cgroup에 할당
echo $PID > /sys/fs/cgroup/myapp/cgroup.procs
# 메모리 사용량 확인
cat /sys/fs/cgroup/myapp/memory.current # 현재 사용량
cat /sys/fs/cgroup/myapp/memory.stat # 상세 통계
cat /sys/fs/cgroup/myapp/memory.events # OOM 등 이벤트
DAMON (Data Access Monitor)
DAMON은 커널 5.15에서 도입된 메모리 접근 패턴 모니터링 프레임워크입니다. 실제 메모리 접근 패턴을 기반으로 reclaim, compaction 등을 최적화합니다.
# DAMON sysfs 인터페이스 (v5.18+)
ls /sys/kernel/mm/damon/
# admin/ (관리 인터페이스)
# DAMON 기반 reclaim (DAMON_RECLAIM)
echo Y > /sys/module/damon_reclaim/parameters/enabled
echo 200 > /sys/module/damon_reclaim/parameters/min_age # 최소 비접근 시간(ms)
# DAMON 기반 LRU 정렬 (DAMON_LRU_SORT, v6.0+)
echo Y > /sys/module/damon_lru_sort/parameters/enabled
Memory Hotplug
가상화 환경에서 런타임에 메모리를 추가/제거합니다:
# 메모리 블록 상태 확인
ls /sys/devices/system/memory/
# memory0/ memory1/ memory2/ ...
cat /sys/devices/system/memory/memory32/state
# online
# 메모리 블록 오프라인
echo offline > /sys/devices/system/memory/memory32/state
# 메모리 블록 온라인
echo online > /sys/devices/system/memory/memory32/state
KASAN / KFENCE
메모리 안전성 검증 도구:
# KASAN (Kernel Address Sanitizer) - 개발/테스트용
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y # 또는 CONFIG_KASAN_SW_TAGS (ARM64)
# 성능 오버헤드: ~2-3x, 메모리 오버헤드: ~2-3x
# KFENCE (Kernel Electric Fence) - 프로덕션 가능 (v5.12+)
CONFIG_KFENCE=y
# 확률적 샘플링 기반, 매우 낮은 오버헤드 (<1%)
echo 100 > /sys/module/kfence/parameters/sample_interval # ms
/proc 메모리 인터페이스
# /proc/meminfo 주요 항목
cat /proc/meminfo
# MemTotal: 32768000 kB ← 전체 물리 메모리
# MemFree: 10240000 kB ← 미사용 메모리
# MemAvailable: 20480000 kB ← 실제 사용 가능 추정치
# Buffers: 512000 kB ← 블록 디바이스 캐시
# Cached: 8192000 kB ← 페이지 캐시
# SwapCached: 10240 kB ← 스왑에서 다시 읽은 캐시
# Slab: 1024000 kB ← Slab 할당자 사용량
# SReclaimable: 768000 kB ← 회수 가능 Slab
# /proc/vmstat - VM 통계
cat /proc/vmstat | grep -E "pgfault|pgmajfault|pswpin|pswpout|compact"
# /proc/zoneinfo - 존별 상세 정보
cat /proc/zoneinfo | head -50
커널 버전별 주요 변경
| 버전 | 기능 | 설명 |
|---|---|---|
| 3.5 | CMA | Contiguous Memory Allocator 도입 |
| 4.14 | memcg v2 | cgroup v2 메모리 컨트롤러 안정화 |
| 5.9 | Proactive Compaction | 백그라운드 자동 compaction |
| 5.12 | KFENCE | 프로덕션용 경량 메모리 오류 감지 |
| 5.15 | DAMON | 데이터 접근 모니터링 프레임워크 |
| 5.18 | DAMON sysfs | DAMON sysfs 관리 인터페이스 |
| 6.0 | DAMON LRU Sort | DAMON 기반 LRU 리스트 최적화 |
| 6.1 | maple tree | VMA 관리에 maple tree 도입 (rbtree 대체) |
참고 자료: 커널 메모리 관리 문서, LWN의 memory management 시리즈, Documentation/mm/
메모리 할당자 심화
할당 컨텍스트별 제약사항
커널 메모리 할당은 호출 컨텍스트에 따라 사용 가능한 GFP 플래그가 엄격히 제한됩니다.
| 컨텍스트 | 허용 GFP | 슬립 가능 | 설명 |
|---|---|---|---|
| 프로세스 컨텍스트 | GFP_KERNEL | 예 | 가장 일반적. 직접 회수, I/O, 스왑 모두 가능 |
| 인터럽트 컨텍스트 (hardirq) | GFP_ATOMIC | 아니오 | 비상 예비 풀 사용. 실패 가능성 높음 |
| 소프트IRQ / 타이머 | GFP_ATOMIC | 아니오 | hardirq와 동일한 제약 |
| 스핀락 보유 중 | GFP_ATOMIC | 아니오 | 슬립 시 데드락 발생 |
| 프로세스 (I/O 불가) | GFP_NOIO | 예 | 블록 I/O 재귀 방지 (블록 드라이버 내부) |
| 프로세스 (FS 불가) | GFP_NOFS | 예 | 파일시스템 재귀 방지 (VFS 코드 내부) |
| 사용자 공간 대신 | GFP_USER | 예 | 사용자 프로세스 대신 할당, OOM killer 대상 |
| DMA 영역 필요 | GFP_DMA / GFP_DMA32 | 예 | ISA DMA(16MB 이하) 또는 32비트 DMA 장치 |
치명적 실수: 인터럽트 컨텍스트에서 GFP_KERNEL 사용 시 슬립이 발생하여 BUG: scheduling while atomic 패닉이 발생합니다. in_interrupt(), in_atomic() 등으로 컨텍스트를 확인하거나, 항상 가장 제한적인 GFP를 사용하십시오.
/* 올바른 컨텍스트별 할당 패턴 */
/* 인터럽트 핸들러에서 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_data *data;
data = kmalloc(sizeof(*data), GFP_ATOMIC);
if (!data)
return IRQ_HANDLED; /* GFP_ATOMIC 실패는 흔함 */
/* ... */
}
/* 블록 I/O 경로에서 */
static int my_block_submit(struct bio *bio)
{
/* GFP_NOIO: submit_bio → 할당 → submit_bio 무한 재귀 방지 */
buf = kmalloc(BUF_SIZE, GFP_NOIO);
/* ... */
}
/* 파일시스템 코드에서 */
static int my_fs_write(struct inode *inode)
{
/* GFP_NOFS: writeback → 할당 → writeback 무한 재귀 방지 */
page = alloc_page(GFP_NOFS);
/* ... */
}
kmalloc 내부 구현과 크기 클래스
kmalloc은 내부적으로 미리 생성된 SLUB 캐시(kmalloc-8, kmalloc-16, ..., kmalloc-8192)에서 오브젝트를 할당합니다. 요청 크기는 가장 가까운 2의 거듭제곱으로 반올림됩니다.
| 요청 크기 | 실제 할당 | 내부 단편화 | 사용 캐시 |
|---|---|---|---|
| 1~8 bytes | 8 bytes | 최대 87.5% | kmalloc-8 |
| 9~16 bytes | 16 bytes | 최대 43.75% | kmalloc-16 |
| 17~32 bytes | 32 bytes | 최대 46.875% | kmalloc-32 |
| 33~64 bytes | 64 bytes | 최대 48.44% | kmalloc-64 |
| 65~96 bytes | 96 bytes | 최대 32.29% | kmalloc-96 |
| 97~128 bytes | 128 bytes | 최대 24.22% | kmalloc-128 |
| 129~192 bytes | 192 bytes | 최대 32.81% | kmalloc-192 |
| 193~256 bytes | 256 bytes | 최대 24.61% | kmalloc-256 |
| ... | ... | ... | ... |
| 4097~8192 bytes | 8192 bytes | 최대 49.99% | kmalloc-8192 |
크기 최적화: 구조체 크기를 kmalloc 크기 클래스 경계에 맞추면 내부 단편화를 줄일 수 있습니다. cat /proc/slabinfo에서 kmalloc-* 캐시의 사용량을 모니터링하십시오. KMALLOC_MAX_SIZE는 아키텍처마다 다르며, x86-64에서 기본 SLUB_MAX_ORDER=3이므로 32KB(8 pages)입니다.
/* kmalloc 크기 확인 */
size_t actual = ksize(kmalloc(100, GFP_KERNEL));
/* ksize() 반환: 128 (실제 할당된 usable 크기) */
/* krealloc: 재할당 (가능하면 in-place) */
ptr = krealloc(ptr, new_size, GFP_KERNEL);
if (!ptr)
return -ENOMEM;
/* kcalloc: 배열 할당 (오버플로 검사 포함) */
arr = kcalloc(n_elements, sizeof(*arr), GFP_KERNEL);
/* n_elements * sizeof(*arr) 오버플로 시 NULL 반환 */
/* kmalloc_array: kcalloc과 동일하지만 zero-init 안 함 */
arr = kmalloc_array(n_elements, sizeof(*arr), GFP_KERNEL);
/* 큰 할당에 kvmalloc (kmalloc 시도 → vmalloc fallback) */
buf = kvmalloc(large_size, GFP_KERNEL);
kvfree(buf); /* kmalloc/vmalloc 자동 판별 해제 */
SLUB 디버깅과 튜닝
SLUB allocator는 다양한 디버깅 옵션과 런타임 튜닝 파라미터를 제공합니다.
# SLUB 디버깅 부트 파라미터
slub_debug=FZPU # 모든 캐시에 대해 디버깅 활성화
slub_debug=FZ,kmalloc-128 # 특정 캐시에만 적용
# 디버깅 플래그:
# F - Sanity checks (free 검증)
# Z - Red zone (오브젝트 경계 오버런 탐지)
# P - Poisoning (use-after-free 탐지)
# U - User tracking (할당/해제 호출자 기록)
# T - Trace (할당/해제 시 로그 출력)
# 런타임 슬랩 정보 확인
cat /proc/slabinfo
# name
# kmalloc-256 1024 1280 256 32 2
# slabinfo 도구로 상세 분석
slabinfo -T # Top slabs by size
slabinfo -S # Sort by slab size
slabinfo -A # Activity report
# slabtop: 실시간 모니터링
slabtop -s c # 캐시 크기 기준 정렬
# SLUB sysfs 인터페이스
ls /sys/kernel/slab/kmalloc-256/
# align, alloc_fastpath, cache_dma, cpu_slabs, hwcache_align,
# min_partial, object_size, objects, objs_per_slab, order,
# partial, red_zone, sanity_checks, shrink, slab_size, ...
# 특정 캐시의 통계
cat /sys/kernel/slab/kmalloc-256/alloc_fastpath
cat /sys/kernel/slab/kmalloc-256/alloc_slowpath
cat /sys/kernel/slab/kmalloc-256/free_fastpath
성능 영향: slub_debug는 할당/해제마다 검증을 수행하므로 성능이 크게 저하됩니다. 프로덕션 환경에서는 CONFIG_KFENCE=y를 사용하면 샘플링 기반으로 메모리 오류를 낮은 오버헤드로 탐지할 수 있습니다.
Per-CPU 할당자
Per-CPU 변수는 각 CPU마다 독립된 복사본을 유지하여 락 없이 데이터에 접근할 수 있는 메커니즘입니다. 멀티프로세서 환경에서 공유 데이터에 대한 캐시 라인 바운싱(cache line bouncing)을 방지하고, 동기화 오버헤드 없이 높은 성능을 달성합니다.
왜 Per-CPU인가?: 여러 CPU가 동일한 캐시 라인에 속한 변수를 수정하면, MESI 프로토콜에 의해 해당 캐시 라인이 끊임없이 무효화(invalidate)됩니다. 이를 false sharing 또는 cache line bouncing이라 합니다. Per-CPU 변수는 각 CPU에 독립된 메모리 영역을 할당하여 이 문제를 원천적으로 제거합니다. 통계 카운터, 참조 카운트, 임시 버퍼 등에서 spinlock 대비 수십 배의 성능 향상을 얻을 수 있습니다.
Per-CPU 메모리 레이아웃
커널의 Per-CPU 데이터는 정적 영역과 동적 영역으로 나뉩니다. 정적 Per-CPU 변수는 컴파일 시 .data..percpu ELF 섹션에 배치되고, 부팅 시 CPU 수만큼 복제됩니다.
/* include/asm-generic/percpu.h — 주소 변환 핵심 */
extern unsigned long __per_cpu_offset[NR_CPUS];
#define per_cpu_offset(cpu) (__per_cpu_offset[cpu])
/* Per-CPU 포인터 → 특정 CPU의 실제 주소 */
#define per_cpu_ptr(ptr, cpu) \
(typeof(ptr))(((unsigned long)(ptr)) + per_cpu_offset(cpu))
/* arch/x86/kernel/setup_percpu.c — 부팅 시 Per-CPU 영역 초기화 */
void __init setup_per_cpu_areas(void)
{
unsigned long delta;
int cpu;
/* pcpu_alloc_alloc 함수로 각 CPU의 Per-CPU 메모리 영역 할당 */
rc = pcpu_embed_first_chunk(PERCPU_MODULE_RESERVE,
PERCPU_DYNAMIC_RESERVE, PAGE_SIZE, NULL,
pcpu_fc_alloc, pcpu_fc_free);
/* 각 CPU에 대해 offset 설정 */
for_each_possible_cpu(cpu) {
per_cpu_offset(cpu) = delta + pcpu_unit_offsets[cpu];
/* 주요 Per-CPU 변수 초기화 */
per_cpu(this_cpu_off, cpu) = per_cpu_offset(cpu);
per_cpu(cpu_number, cpu) = cpu;
setup_percpu_segment(cpu); /* x86: GS/FS 세그먼트 설정 */
}
}
x86에서의 최적화: x86_64에서는 GS 세그먼트 레지스터가 현재 CPU의 Per-CPU 베이스를 가리킵니다. this_cpu_read(var)는 단일 mov %gs:offset, %reg 명령어로 컴파일되어, offset 배열 참조 없이 O(1)으로 접근합니다. ARM64에서는 TPIDR_EL1 레지스터가 동일한 역할을 합니다.
정적 Per-CPU 변수 선언
DEFINE_PER_CPU 매크로 패밀리로 정적 Per-CPU 변수를 선언합니다. 각 변형은 ELF 섹션 배치와 접근 특성이 다릅니다.
/* include/linux/percpu-defs.h — Per-CPU 선언 매크로 */
/* 기본 — .data..percpu 섹션, 캐시라인 정렬 없음 */
DEFINE_PER_CPU(unsigned long, my_counter);
/* 공유 정렬 — 캐시라인 경계에 정렬, false sharing 방지 */
DEFINE_PER_CPU_SHARED_ALIGNED(struct my_data, shared_data);
/* → SMP에서 ____cacheline_aligned_in_smp 적용 */
/* 첫 번째 청크 — 부트 시 사용, 성능 최적화 */
DEFINE_PER_CPU_FIRST(struct my_data, first_data);
/* → .data..percpu..first 섹션, 고정 오프셋 보장 */
/* 페이지 정렬 — 큰 구조체에 사용 */
DEFINE_PER_CPU_PAGE_ALIGNED(struct big_struct, page_data);
/* → .data..percpu..page_aligned 섹션, PAGE_SIZE 정렬 */
/* 읽기 전용 — 초기화 후 변경 없음 */
DEFINE_PER_CPU_READ_MOSTLY(int, cpu_constant);
/* → .data..percpu..read_mostly 섹션 */
/* → 쓰기 빈도가 낮아 다른 Per-CPU 변수와 캐시라인 분리 */
/* 선언만 (extern) — 다른 파일에서 정의된 변수 참조 */
DECLARE_PER_CPU(unsigned long, my_counter);
DECLARE_PER_CPU_SHARED_ALIGNED(struct my_data, shared_data);
DECLARE_PER_CPU_READ_MOSTLY(int, cpu_constant);
| 매크로 | ELF 섹션 | 정렬 | 용도 |
|---|---|---|---|
DEFINE_PER_CPU | .data..percpu | 기본 | 범용 Per-CPU 변수 |
DEFINE_PER_CPU_SHARED_ALIGNED | .data..percpu..shared_aligned | 캐시라인 | 자주 접근, false sharing 방지 |
DEFINE_PER_CPU_FIRST | .data..percpu..first | 페이지 | 고정 오프셋 필요 (irq stack 등) |
DEFINE_PER_CPU_PAGE_ALIGNED | .data..percpu..page_aligned | 페이지 | GDT, TSS 등 큰 구조체 |
DEFINE_PER_CPU_READ_MOSTLY | .data..percpu..read_mostly | 기본 | 초기화 후 거의 읽기만 |
Per-CPU 접근 API
Per-CPU 변수 접근에는 여러 API가 있으며, preemption 상태와 원자성 요구사항에 따라 선택합니다.
/* ============================================ */
/* 1. this_cpu_* 매크로 (현재 CPU, preemption disabled 필요) */
/* ============================================ */
preempt_disable();
/* 읽기/쓰기 */
val = this_cpu_read(my_counter);
this_cpu_write(my_counter, 42);
/* 산술 연산 */
this_cpu_inc(my_counter); /* ++ */
this_cpu_dec(my_counter); /* -- */
this_cpu_add(my_counter, 5); /* += */
this_cpu_sub(my_counter, 3); /* -= */
/* 비트 연산 */
this_cpu_and(my_flags, ~FLAG_MASK); /* &= */
this_cpu_or(my_flags, FLAG_MASK); /* |= */
/* 원자적 교환 (RMW) */
old = this_cpu_xchg(my_counter, new_val);
old = this_cpu_cmpxchg(my_counter, expected, new_val);
preempt_enable();
/* ============================================ */
/* 2. __this_cpu_* (preemption 검증 없는 고속 버전) */
/* ============================================ */
/* 이미 preemption disabled인 것이 보장된 컨텍스트 */
/* (인터럽트 핸들러, softirq, preempt_disable 구간 등) */
__this_cpu_inc(my_counter); /* preempt count 검증 생략 */
__this_cpu_add(my_counter, 5);
/* ============================================ */
/* 3. raw_cpu_* (인스트루먼테이션 없는 최저수준 접근) */
/* ============================================ */
/* NMI, 매우 이른 부팅 경로 등 특수 상황 */
raw_cpu_inc(my_counter);
/* ============================================ */
/* 4. get_cpu_var / put_cpu_var (자동 preemption 관리) */
/* ============================================ */
get_cpu_var(my_counter)++; /* preempt_disable + 현재 CPU 값 참조 */
put_cpu_var(my_counter); /* preempt_enable */
/* 포인터 형태 */
struct my_data *p = &get_cpu_var(my_data);
p->field = value;
put_cpu_var(my_data);
/* ============================================ */
/* 5. per_cpu / per_cpu_ptr (특정 CPU 접근) */
/* ============================================ */
/* 정적 Per-CPU 변수 — 특정 CPU의 값 */
val = per_cpu(my_counter, cpu_id);
/* 동적 Per-CPU 포인터 — 특정 CPU의 포인터 */
struct my_data *p = per_cpu_ptr(dyn_ptr, cpu_id);
/* 모든 CPU 순회 합산 (읽기 전용, preemption 불필요) */
unsigned long total = 0;
int cpu;
for_each_possible_cpu(cpu)
total += per_cpu(my_counter, cpu);
/* for_each_online_cpu: 현재 온라인 CPU만 (hotplug 안전) */
for_each_online_cpu(cpu)
total += *per_cpu_ptr(dyn_ptr, cpu);
| API 패밀리 | Preemption | 검증 | 대상 CPU | 사용 컨텍스트 |
|---|---|---|---|---|
this_cpu_* | 직접 관리 | O (debug) | 현재 CPU | 일반 커널 코드 |
__this_cpu_* | 이미 disabled | X | 현재 CPU | IRQ, softirq, preempt off 구간 |
raw_cpu_* | 없음 | X | 현재 CPU | NMI, early boot |
get/put_cpu_var | 자동 | O | 현재 CPU | 간단한 접근 |
per_cpu() | 불필요 | - | 지정 CPU | 다른 CPU 값 읽기 |
per_cpu_ptr() | 불필요 | - | 지정 CPU | 동적 Per-CPU 포인터 |
Preemption 주의: this_cpu_* 매크로는 preemption이 비활성화된 상태에서만 사용해야 합니다. preemption이 활성화된 상태에서 사용하면, 연산 도중 다른 CPU로 마이그레이션되어 잘못된 CPU의 데이터를 수정할 수 있습니다. get_cpu_var()/put_cpu_var() 쌍이 자동으로 관리하지만, 성능이 중요한 경로에서는 명시적 preempt_disable()/preempt_enable()을 사용하십시오.
동적 Per-CPU 할당
모듈이나 런타임에 Per-CPU 데이터가 필요한 경우 동적 할당을 사용합니다. 내부적으로 Chunk 기반 할당자가 Per-CPU 영역의 빈 공간을 관리합니다.
/* include/linux/percpu.h — 동적 Per-CPU 할당 API */
/* 기본 할당 — 자연 정렬 */
void __percpu *ptr = alloc_percpu(struct my_data);
/* → __alloc_percpu(sizeof(struct my_data), __alignof__(struct my_data)) */
/* 명시적 크기/정렬 지정 */
void __percpu *ptr = __alloc_percpu(size, align);
/* GFP 플래그 지정 (커널 5.15+) */
void __percpu *ptr = __alloc_percpu_gfp(size, align, GFP_KERNEL);
/* 해제 */
free_percpu(ptr);
/* ---- 사용 예제 ---- */
struct net_stats {
u64 rx_packets;
u64 tx_packets;
u64 rx_bytes;
u64 tx_bytes;
};
struct my_device {
struct net_stats __percpu *stats; /* __percpu 어노테이션 */
/* ... */
};
static int my_dev_init(struct my_device *dev)
{
dev->stats = alloc_percpu(struct net_stats);
if (!dev->stats)
return -ENOMEM;
return 0;
}
/* 패킷 수신 경로 (softirq 컨텍스트, preemption 이미 disabled) */
static void my_dev_rx(struct my_device *dev, struct sk_buff *skb)
{
struct net_stats *stats = this_cpu_ptr(dev->stats);
stats->rx_packets++;
stats->rx_bytes += skb->len;
}
/* 통계 읽기 (user context, 모든 CPU 합산) */
static void my_dev_get_stats(struct my_device *dev,
struct net_stats *total)
{
int cpu;
memset(total, 0, sizeof(*total));
for_each_possible_cpu(cpu) {
struct net_stats *s = per_cpu_ptr(dev->stats, cpu);
total->rx_packets += s->rx_packets;
total->tx_packets += s->tx_packets;
total->rx_bytes += s->rx_bytes;
total->tx_bytes += s->tx_bytes;
}
}
static void my_dev_cleanup(struct my_device *dev)
{
free_percpu(dev->stats);
}
Per-CPU 할당자 내부 구현
동적 Per-CPU 할당자는 mm/percpu.c에 구현되어 있으며, Chunk 단위로 메모리를 관리합니다. 각 Chunk는 모든 CPU에 대해 동일 크기의 Unit을 포함하며, Unit 내에서 first-fit 방식으로 할당합니다.
/* mm/percpu-internal.h — Chunk 핵심 구조체 */
struct pcpu_chunk {
struct list_head list; /* pcpu_slot[] 리스트 */
int free_bytes; /* 총 여유 바이트 */
int contig_bits; /* 최대 연속 여유 블록 (비트) */
int first_bit; /* 첫 여유 비트 위치 */
int nr_pages; /* 매핑된 페이지 수 */
unsigned long *alloc_map; /* 사용 중 비트맵 */
unsigned long *bound_map; /* 할당 경계 비트맵 */
struct pcpu_block_md *md_blocks; /* 블록별 메타데이터 */
void *base_addr; /* Unit 0 시작 주소 */
struct page **pages; /* 물리 페이지 배열 */
};
/* mm/percpu.c — 할당 핵심 경로 (단순화) */
static void __percpu *pcpu_alloc(size_t size, size_t align,
bool reserved, gfp_t gfp)
{
struct pcpu_chunk *chunk;
int off;
/* 1단계: 기존 chunk에서 여유 공간 탐색 (pcpu_slot[] 순회) */
list_for_each_entry(chunk, &pcpu_slot[slot], list) {
off = pcpu_find_block_fit(chunk, bits, bit_align);
if (off >= 0)
goto area_found;
}
/* 2단계: 기존 chunk에 공간 없음 → 새 chunk 생성 */
chunk = pcpu_create_chunk(pcpu_gfp);
area_found:
/* 3단계: 비트맵 갱신, 물리 페이지 매핑 (필요시) */
pcpu_alloc_area(chunk, bits, bit_align, off);
pcpu_populate_chunk(chunk, rs, re, gfp); /* 페이지 할당/매핑 */
/* 반환: base_addr + offset (모든 CPU에 동일 offset 적용) */
return __addr_to_pcpu_ptr(chunk->base_addr + off);
}
percpu_counter — 정밀 Per-CPU 카운터
단순 Per-CPU 변수 합산은 모든 CPU를 순회해야 하므로 비용이 높습니다. percpu_counter는 중앙 카운트(count)와 CPU별 로컬 카운트(counters)를 결합하여, 읽기 시 근사값을 O(1)로 제공하고, 정확한 값이 필요할 때만 전체 합산합니다.
/* include/linux/percpu_counter.h */
struct percpu_counter {
raw_spinlock_t lock;
s64 count; /* 중앙 카운트 */
s32 __percpu *counters; /* CPU별 로컬 카운트 */
};
/* 초기화/해제 */
percpu_counter_init(&pcnt, initial_value, GFP_KERNEL);
percpu_counter_destroy(&pcnt);
/* 더하기 — 로컬 카운트에 누적 */
percpu_counter_add(&pcnt, 1);
/* → 로컬 |count| > batch이면 중앙으로 flush */
/* → batch 기본값 = max(32, nr_cpus * 2) */
/* 빼기 */
percpu_counter_sub(&pcnt, 5);
/* batch 크기 직접 지정 */
percpu_counter_add_batch(&pcnt, delta, my_batch);
/* 근사값 읽기 — O(1), 오차 ±(nr_cpus × batch) */
s64 approx = percpu_counter_read_positive(&pcnt);
/* 정확한 합산 읽기 — 모든 CPU 순회, 비용 높음 */
s64 exact = percpu_counter_sum(&pcnt);
/* 0 이상인지 근사 판정 */
bool positive = percpu_counter_positive(&pcnt);
/* 임계값 비교 (정확도와 성능 균형) */
bool over = percpu_counter_compare(&pcnt, threshold) > 0;
/* → |count - threshold| > batch*nr_cpus이면 근사값만으로 판정 */
/* → 아니면 sum()으로 정확히 비교 */
커널 내 사용 사례: ext4의 free block/inode 카운트(ext4_sb_info.s_freeclusters_counter), 네트워크의 소켓 메모리 카운팅, VM의 vm_committed_as(가상 메모리 커밋 추적) 등이 percpu_counter를 사용합니다. 파일시스템에서 여유 블록 수를 매 할당마다 정확히 계산하면 병목이 되므로, 근사값으로 판단하고 임계치에 가까울 때만 정확히 합산합니다.
percpu_ref — Per-CPU 참조 카운트
percpu_ref는 참조 카운팅의 빠른 경로(Per-CPU)와 정확한 경로(atomic)를 모드 전환으로 결합합니다. 일반적으로 Per-CPU 모드로 동작하여 락프리 성능을 제공하고, 리소스 해제 시 atomic 모드로 전환하여 참조가 0이 되는 순간을 정확히 감지합니다.
/* include/linux/percpu-refcount.h */
struct percpu_ref {
atomic_long_t count; /* atomic 모드 카운트 */
unsigned long __percpu *percpu_count_ptr; /* Per-CPU 카운트 */
percpu_ref_func_t *release; /* count=0 시 호출 */
percpu_ref_func_t *confirm_switch; /* 모드 전환 완료 콜백 */
unsigned long flags;
};
/* 생명주기: Per-CPU 모드 → kill → atomic 모드 → count=0 → release */
/* 초기화 — Per-CPU 모드로 시작, release 콜백 등록 */
percpu_ref_init(&ref, my_release_fn, 0, GFP_KERNEL);
/* 참조 획득/해제 — Per-CPU 모드: 매우 빠름 (preempt_disable + 로컬 inc/dec) */
percpu_ref_get(&ref);
percpu_ref_put(&ref);
/* tryget — 이미 kill된 상태이면 실패 */
if (!percpu_ref_tryget_live(&ref))
return -ENODEV;
/* kill — atomic 모드로 전환, 초기 참조 1 제거 */
/* → 이후 모든 get/put은 atomic_long_inc/dec */
/* → Per-CPU 카운트를 atomic으로 합산 (call_rcu 기반) */
percpu_ref_kill(&ref);
/* 모든 참조가 해제되면 release 콜백 자동 호출 */
static void my_release_fn(struct percpu_ref *ref)
{
struct my_obj *obj = container_of(ref, struct my_obj, ref);
complete(&obj->done); /* 또는 kfree, workqueue 등 */
}
사용 사례: block I/O의 request_queue 참조 카운팅, cgroup 서브시스템의 참조 관리, MD/RAID의 I/O 활성 추적 등에서 percpu_ref를 사용합니다. 일반 운영 시 수백만 회/초의 get/put이 락 없이 수행되고, 장치 제거나 cgroup 삭제 시에만 atomic 모드로 전환하여 정확한 참조 추적을 수행합니다.
Per-CPU 사용 시 주의사항
| 실수 유형 | 증상 | 올바른 해결책 |
|---|---|---|
| Preemption 미비활성화 | 다른 CPU 데이터 수정 (데이터 손상) | preempt_disable() 또는 get_cpu_var() 사용 |
this_cpu_*를 슬립 가능 구간에서 사용 | BUG: scheduling while atomic | 변수를 로컬에 복사 후 작업 |
per_cpu_ptr 포인터를 preempt 구간 밖에서 역참조 | 잘못된 CPU 데이터 접근 | 포인터 역참조를 preempt off 내에서 수행 |
| CPU 오프라인 미고려 | 오프라인 CPU의 데이터 누락/유실 | CPU hotplug 노티파이어에서 데이터 마이그레이션 |
for_each_possible_cpu 대신 for_each_online_cpu 사용 | 나중에 온라인된 CPU 데이터 누락 | 초기화 순회: possible, 합산 순회: 목적에 따라 선택 |
| Per-CPU 변수에 포인터 저장 후 다른 CPU에서 역참조 | cache coherency 문제 | 포인터 대상 데이터도 Per-CPU이거나 적절히 동기화 |
alloc_percpu 반환값 직접 역참조 | 컴파일 경고 또는 잘못된 값 | 반드시 per_cpu_ptr() 또는 this_cpu_ptr()로 접근 |
모듈 언로드 시 free_percpu 누락 | Per-CPU 메모리 누수 | cleanup 경로에서 반드시 해제 |
/* ❌ 잘못된 패턴: preemption 없이 this_cpu 사용 */
this_cpu_inc(my_counter); /* BUG: 마이그레이션 가능! */
/* ✅ 올바른 패턴 1: 명시적 preemption 관리 */
preempt_disable();
this_cpu_inc(my_counter);
preempt_enable();
/* ✅ 올바른 패턴 2: get/put 사용 */
get_cpu_var(my_counter)++;
put_cpu_var(my_counter);
/* ❌ 잘못된 패턴: preempt 구간 밖에서 포인터 사용 */
struct my_data *p = this_cpu_ptr(percpu_ptr);
preempt_enable();
p->field = val; /* BUG: 이미 다른 CPU로 이동했을 수 있음 */
/* ✅ 올바른 패턴: 포인터 역참조를 preempt off 내에서 수행 */
preempt_disable();
struct my_data *p = this_cpu_ptr(percpu_ptr);
p->field = val;
preempt_enable();
/* CPU hotplug 안전한 합산 패턴 */
static int percpu_hotplug_dead(unsigned int dead_cpu)
{
struct my_data *dead_stats = per_cpu_ptr(global_stats, dead_cpu);
struct my_data *my_stats = this_cpu_ptr(global_stats);
/* 오프라인된 CPU의 데이터를 현재 CPU로 병합 */
my_stats->count += dead_stats->count;
dead_stats->count = 0;
return 0;
}
/* cpuhp_setup_state로 hotplug 콜백 등록 */
cpuhp_setup_state_nocalls(CPUHP_AP_ONLINE_DYN,
"my_module:online", NULL, percpu_hotplug_dead);
Per-CPU 디버깅과 모니터링
# Per-CPU 할당자 상태 확인
cat /proc/percpu_stats
# Percpu Memory Statistics
# Chunk Info:
# nr_alloc nr_max_alloc free_bytes contig_bytes sum_frag ...
# 152 208 28672 16384 4096
# 전체 Per-CPU 메모리 사용량
grep -i percpu /proc/meminfo
# Percpu: 4352 kB ← 전체 Per-CPU 할당 크기
# CPU별 메모리 사용 상세 (vmstat)
cat /proc/vmstat | grep percpu
# nr_percpu_alloc 152 ← 동적 Per-CPU 할당 횟수
# nr_percpu_free 23 ← 동적 Per-CPU 해제 횟수
# 부팅 로그에서 Per-CPU 초기화 확인
dmesg | grep -i percpu
# percpu: Embedded 45 pages/cpu s143360 r8192 d28672 u524288
# s=정적영역 r=예약영역 d=동적영역 u=단위크기
# percpu: max_distance=0x3fe00000000 too large for vmalloc space 0x0
# percpu: 15132 allocators registered
# Per-CPU 할당 추적 (ftrace)
echo 1 > /sys/kernel/debug/tracing/events/percpu/percpu_alloc_percpu/enable
echo 1 > /sys/kernel/debug/tracing/events/percpu/percpu_free_percpu/enable
cat /sys/kernel/debug/tracing/trace
# my_module-1234 [002] percpu_alloc_percpu: size=64 align=8 ptr=0x...
성능 고려사항: Per-CPU 메모리는 nr_cpu_ids × 크기만큼 소비됩니다. 256 CPU 시스템에서 64바이트 Per-CPU 변수 하나는 16KB를 사용합니다. 대량의 Per-CPU 할당이 필요한 경우, percpu_counter처럼 작은 로컬 카운트 + 중앙 집계 방식을 검토하거나, CPU가 많은 NUMA 시스템에서는 노드 단위 집계를 고려하십시오. 또한 alloc_percpu는 GFP_KERNEL으로 슬립할 수 있으므로 atomic 컨텍스트에서 호출할 수 없습니다.
mempool (메모리 풀)
mempool은 메모리 부족 상황에서도 최소한의 할당을 보장하는 예비 풀입니다. 블록 I/O, 스토리지 드라이버 등 메모리 할당 실패가 허용되지 않는 경로에서 사용합니다.
/* mempool 생성 */
struct kmem_cache *my_cache;
mempool_t *my_pool;
my_cache = kmem_cache_create("my_obj", sizeof(struct my_obj),
0, SLAB_HWCACHE_ALIGN, NULL);
/* 최소 16개 오브젝트를 예비로 확보 */
my_pool = mempool_create_slab_pool(16, my_cache);
if (!my_pool) {
kmem_cache_destroy(my_cache);
return -ENOMEM;
}
/* mempool에서 할당 (GFP_NOIO 경로에서도 안전) */
struct my_obj *obj = mempool_alloc(my_pool, GFP_NOIO);
/* mempool_alloc은 예비 풀이 있으므로 NULL 반환하지 않음 */
/* 해제 → 예비 풀이 부족하면 풀로 반환 */
mempool_free(obj, my_pool);
/* 풀 크기 조정 */
mempool_resize(my_pool, 32); /* 예비 풀을 32개로 확장 */
/* 정리 */
mempool_destroy(my_pool);
kmem_cache_destroy(my_cache);
/* 페이지 기반 mempool */
mempool_t *page_pool = mempool_create_page_pool(8, 0);
/* 최소 8개 order-0 페이지 예비 확보 */
메모리 회수 (Reclaim) 메커니즘
커널이 메모리 부족 시 페이지를 회수하는 과정은 kswapd(백그라운드)와 direct reclaim(동기적)으로 나뉩니다.
# 워터마크 확인
cat /proc/zoneinfo | grep -A 5 "Normal"
# pages free 65432
# min 4096
# low 5120
# high 6144
# 워터마크 조정 (낮출수록 OOM 위험 증가)
sysctl vm.min_free_kbytes=65536 # MIN 워터마크 기준값
sysctl vm.watermark_boost_factor=15000 # 단편화 방지 부스트
sysctl vm.watermark_scale_factor=10 # LOW-HIGH 간격 (0.1% 단위)
# 페이지 회수 통계
cat /proc/vmstat | grep -E "pgscan|pgsteal|pswp|pgfault"
# pgscan_kswapd: kswapd가 스캔한 페이지 수
# pgscan_direct: direct reclaim 스캔 수
# pgsteal_kswapd: kswapd가 회수한 페이지 수
# vmscan 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_begin/enable
echo 1 > /sys/kernel/debug/tracing/events/vmscan/mm_vmscan_direct_reclaim_end/enable
cat /sys/kernel/debug/tracing/trace
메모리 할당 주의사항과 흔한 실수
| 실수 유형 | 증상 | 올바른 해결책 |
|---|---|---|
인터럽트에서 GFP_KERNEL 사용 | BUG: scheduling while atomic | GFP_ATOMIC 사용 또는 workqueue로 지연 처리 |
| NULL 반환값 미검사 | NULL pointer dereference Oops | 모든 할당 후 반드시 NULL 체크 |
| kmalloc으로 큰 메모리 할당 | order-too-high 할당 실패 | kvmalloc() 또는 vmalloc() 사용 |
| double free | SLUB 검증 실패, 커널 패닉 | 해제 후 포인터를 NULL로 설정 |
| use-after-free | 데이터 손상, 간헐적 크래시 | KASAN/KFENCE로 탐지, 참조 카운팅 사용 |
| 메모리 누수 | 시간이 지나며 메모리 부족 | kmemleak으로 탐지 (CONFIG_DEBUG_KMEMLEAK) |
__GFP_NOFAIL 남용 | OOM 상황에서 무한 루프 | 에러 경로를 올바르게 구현 |
| slab cache 미해제 | 모듈 언로드 시 메모리 누수 | kmem_cache_destroy() 호출 확인 |
| GFP 플래그 혼합 | 예측 불가능한 동작 | 하나의 기본 플래그 + 수정자 조합 |
| order > MAX_ORDER 할당 | 즉시 실패 | vmalloc 또는 CMA 사용 |
/* 안전한 할당 패턴들 */
/* 1. 구조체 할당 시 sizeof(*ptr) 패턴 */
struct my_data *p = kzalloc(sizeof(*p), GFP_KERNEL);
/* sizeof(struct my_data) 대신 sizeof(*p) → 타입 변경 시 자동 반영 */
/* 2. 배열 할당 시 오버플로 방지 */
/* BAD: kmalloc(n * sizeof(elem)) → 오버플로 가능 */
/* GOOD: */
arr = kcalloc(n, sizeof(*arr), GFP_KERNEL);
/* 또는 flexible array member: */
p = kzalloc(struct_size(p, items, n), GFP_KERNEL);
/* 3. 오류 경로에서 메모리 해제 (goto cleanup 패턴) */
static int my_init(void)
{
a = kmalloc(SIZE_A, GFP_KERNEL);
if (!a) return -ENOMEM;
b = kmalloc(SIZE_B, GFP_KERNEL);
if (!b) goto err_free_a;
c = kmalloc(SIZE_C, GFP_KERNEL);
if (!c) goto err_free_b;
return 0;
err_free_b:
kfree(b);
err_free_a:
kfree(a);
return -ENOMEM;
}
할당자 선택 전략
할당자 선택 결정 트리:
- 크기 < 페이지 크기? →
kmalloc()/kzalloc() - 동일 타입 오브젝트 반복 할당? →
kmem_cache_create() - 크기 > 페이지 크기 + 물리 연속 필요? →
alloc_pages()+ CMA - 크기 > 페이지 크기 + 물리 연속 불필요? →
vmalloc() - 크기 불확정? →
kvmalloc()(kmalloc 시도 후 vmalloc fallback) - DMA 장치 버퍼? →
dma_alloc_coherent() - 할당 실패 불허? →
mempool - Per-CPU 통계/카운터? →
alloc_percpu() - 수명이 짧은 임시 버퍼? → 스택 변수 또는 Per-CPU 버퍼
| 할당자 | 최적 용도 | 오버헤드 | 주의점 |
|---|---|---|---|
kmalloc | 작은 범용 할당 (<8KB) | 최소 | 내부 단편화, order 높으면 실패 |
kmem_cache | 동일 크기 반복 할당 | 최소 | 캐시 해제 누락 주의 |
vmalloc | 큰 비연속 버퍼 | 페이지 테이블 | DMA 불가, TLB 미스 증가 |
kvmalloc | 크기 가변 할당 | 가변 | 해제 시 kvfree 사용 |
alloc_pages | low-level 페이지 제어 | 없음 | 수동 가상 주소 매핑 필요 |
mempool | 할당 보장 필요 | 예비 메모리 | 예비 풀이 메모리 낭비 |
per_cpu | Per-CPU 데이터 | CPU 수 × 크기 | preemption disable 필요 |
CMA | 큰 연속 블록 | 예약 영역 | migratable 페이지만 공존 |
DMA 메모리 매핑 심화
DMA 메모리 존(Zone)과 주소 제한
| Zone | x86_64 범위 | 용도 | 비고 |
|---|---|---|---|
ZONE_DMA |
0 ~ 16MB | ISA DMA (24비트 주소) | 레거시 ISA 디바이스 전용, 현대 시스템에서는 거의 불필요 |
ZONE_DMA32 |
0 ~ 4GB | 32비트 DMA 디바이스 | PCI 디바이스의 기본 DMA 가능 영역 |
ZONE_NORMAL |
4GB ~ 끝 | 일반 커널 메모리 | 64비트 DMA 가능 디바이스만 접근 가능 |
/* DMA 주소 마스크에 따른 zone 선택 흐름 */
/*
* dma_set_mask(dev, DMA_BIT_MASK(32))
* → ZONE_DMA32에서 할당 시도
* → IOMMU가 있으면 ZONE_NORMAL에서도 할당 가능 (리매핑)
*
* dma_set_mask(dev, DMA_BIT_MASK(64))
* → 모든 zone에서 할당 가능
* → SWIOTLB bounce buffer 불필요
*/
/* SWIOTLB bounce buffer가 사용되는 조건 */
/* 1. IOMMU가 없는 시스템에서 */
/* 2. 디바이스의 DMA mask가 물리 메모리 주소를 커버하지 못할 때 */
/* 3. 커널이 swiotlb=force 부트 파라미터로 시작했을 때 */
/* Bounce buffer 성능 영향 확인 */
/* # cat /sys/kernel/debug/swiotlb/io_tlb_used */
/* 값이 계속 증가하면 bounce buffer 사용 중 → 64비트 DMA 마스크 설정 필요 */
DMA 캐시 일관성 아키텍처별 차이
| 아키텍처 | DMA coherent 구현 | Streaming DMA sync 비용 | 비고 |
|---|---|---|---|
| x86/x86_64 | HW cache snoop (DMA coherent) | 거의 없음 (noop) | x86은 기본적으로 DMA coherent — sync가 실질적 비용 없음 |
| ARM (non-coherent) | Uncached 매핑 | 캐시 flush/invalidate (비용 큼) | 대부분의 ARM SoC. dma_alloc_coherent는 uncached → CPU 접근 느림 |
| ARM (CCI/CCN) | HW coherency unit | 낮음 | Cache Coherent Interconnect 지원 SoC (고가 서버급) |
| RISC-V | 구현 의존 | 구현 의존 | Sv39/48 + 벤더별 coherency 구현 다양 |
dma_alloc_noncoherent() + 명시적 sync를 사용하여 cached 매핑의 이점을 얻을 수 있습니다.
단, sync 시점을 정확히 관리해야 하므로 코드 복잡도가 증가합니다.
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 지정 저장 | 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에서 Maple Tree 노드 교체 시 RCU grace period를 올바르게 기다리지 않는
경쟁 조건이 발견되었습니다. mas_store() 과정에서 기존 노드가 RCU 콜백으로 해제되기 전에
읽기 측이 해당 노드에 접근하여 Use-After-Free가 발생했습니다.
Linux 6.4.1에서 수정되었으며, 이후 Maple Tree의 RCU 안전성 검증이 대폭 강화되었습니다.
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() 시스템 콜이 커널 내부에서 처리되는 과정을 단계별로 살펴봅니다.
사용자: mmap(addr, len, prot, flags, fd, offset)
│
├─→ ksys_mmap_pgoff()
│ └─→ vm_mmap_pgoff()
│ ├─→ security_mmap_file() ← LSM 보안 검사 (SELinux 등)
│ └─→ do_mmap()
│ ├─→ get_unmapped_area() ← 적절한 가상 주소 탐색
│ │ ├─ file→f_op→get_unmapped_area() (파일 매핑)
│ │ └─ arch_get_unmapped_area_topdown() (익명 매핑)
│ ├─→ mmap_region() ← 실제 VMA 생성
│ │ ├─ VMA 병합 시도 (인접 VMA와 속성 동일 시)
│ │ ├─ vm_area_alloc() → 새 VMA 할당
│ │ ├─ file→f_op→mmap() 호출 (파일 매핑)
│ │ └─ vma_merge() / vma_link() → maple tree 삽입
│ └─→ populate 처리 (MAP_POPULATE 시)
│
└─→ 반환: 매핑된 가상 주소 (또는 에러 코드)
/* mm/mmap.c — do_mmap 핵심 로직 (간략화) */
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, vm_flags_t vm_flags,
unsigned long pgoff, unsigned long *populate,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
/* 1. 길이 정렬 및 오버플로 검사 */
len = PAGE_ALIGN(len);
if (!len || len > TASK_SIZE)
return -ENOMEM;
/* 2. vm_flags 계산 (prot + flags → VM_READ|VM_WRITE|...) */
vm_flags |= calc_vm_prot_bits(prot, 0) |
calc_vm_flag_bits(flags);
/* 3. MAP 개수 제한 확인 (/proc/sys/vm/max_map_count) */
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
/* 4. 적절한 가상 주소 탐색 */
addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (IS_ERR_VALUE(addr))
return addr;
/* 5. 실제 매핑 생성 */
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
/* 6. MAP_POPULATE 처리 — 사전 페이지 폴트 */
if (populate)
*populate = (flags & MAP_POPULATE) ? len : 0;
return addr;
}
/proc/sys/vm/max_map_count (기본값 65530)은 프로세스당
VMA 최대 개수를 제한합니다. 많은 공유 라이브러리를 로드하거나, JVM처럼 다수의 mmap을 사용하는
애플리케이션에서 이 한계에 도달할 수 있습니다.
sysctl -w vm.max_map_count=262144로 조정 가능합니다.
mmap 페이지 폴트 처리
mmap()은 lazy allocation을 사용합니다. 매핑 시점에 물리 메모리를 할당하지 않고,
실제 접근 시 page fault를 통해 물리 페이지를 할당합니다.
사용자 공간: *ptr = 42; (매핑된 주소에 쓰기)
│
├─→ MMU: 페이지 테이블 항목 없음 → #PF (Page Fault)
│
├─→ do_page_fault()
│ └─→ handle_mm_fault()
│ └─→ __handle_mm_fault()
│ └─→ handle_pte_fault()
│ │
│ ├─ PTE 없음 (첫 접근):
│ │ ├─ 파일 매핑 → do_fault()
│ │ │ ├─ 읽기 → do_read_fault() → filemap_fault()
│ │ │ ├─ 쓰기 → do_cow_fault() → COW 사본 생성
│ │ │ └─ 공유쓰기 → do_shared_fault()
│ │ └─ 익명 매핑 → do_anonymous_page()
│ │ └─ 제로 페이지 할당 (쓰기 시)
│ │
│ ├─ PTE 존재 + !present (스왑됨):
│ │ └─ do_swap_page() → 스왑에서 복구
│ │
│ └─ PTE 존재 + 쓰기 폴트 (COW):
│ └─ do_wp_page() → COW break
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 영역의 위치와 역할을 확인합니다.
x86-64 프로세스 가상 주소 공간 (128TB 유저 영역)
0xFFFF_FFFF_FFFF_FFFF ┌──────────────────────┐
│ 커널 공간 │
0xFFFF_8000_0000_0000 ├──────────────────────┤
│ (비표준 영역 — 홀) │
0x0000_7FFF_FFFF_FFFF ├──────────────────────┤
│ Stack (↓ 성장) │ ← RLIMIT_STACK
│ ··· │
│ ┌─ stack_guard_gap ─┐│
├──────────────────────┤
│ mmap 영역 (↓ 성장) │ ← 공유 라이브러리, MAP_ANONYMOUS
│ ld-linux.so │ MAP_SHARED, 파일 매핑
│ libc.so │
│ libpthread.so │
│ 익명 매핑들 │
│ ··· │
├──────────────────────┤
│ Heap (↑ 성장) │ ← brk() / sbrk()
│ ··· │ malloc 소규모 할당
├──────────────────────┤
│ BSS (.bss) │ ← 초기화 안 된 전역 변수
│ Data (.data) │ ← 초기화된 전역 변수
│ Text (.text) │ ← 실행 코드 (r-x)
0x0000_0000_0040_0000 ├──────────────────────┤ ← PIE: ASLR 랜덤 기준
│ (NULL 페이지 보호) │
0x0000_0000_0000_0000 └──────────────────────┘
# 프로세스의 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 개수 */
mmap()은 커널↔사용자 공간 데이터 복사를 제거하므로 대용량 파일 랜덤 접근에서
read()/write()보다 유리합니다.
반면, 소규모 순차 읽기에서는 read()의 VFS 최적화(read-ahead)가 더 효율적일 수 있습니다.
매핑 생성/해제의 오버헤드(VMA 할당, 페이지 테이블 구성, TLB flush)도 고려해야 합니다.
역사적 메모리 관련 주요 버그 사례
리눅스 커널의 메모리 관리 서브시스템에서 발견된 주요 버그 사례들을 분석합니다. 이 사례들은 복잡한 메모리 관리 코드에서 발생할 수 있는 미묘한 결함과 그로 인한 보안 영향을 이해하는 데 핵심적인 교훈을 제공합니다.
1. Dirty COW (CVE-2016-5195) — Copy-on-Write 경쟁 조건
madvise(MADV_DONTNEED)와 write() 사이의 race condition을 악용하여
읽기 전용 파일(예: /etc/passwd)에 쓰기가 가능합니다.
근본 원인: get_user_pages()에서 COW(Copy-on-Write) 처리 도중,
write 권한 없이도 dirty 플래그가 설정될 수 있는 경쟁 조건이 존재했습니다.
두 스레드가 동시에 동작할 때 발생하는 TOCTOU(Time-of-Check to Time-of-Use) 문제입니다.
/* Dirty COW 공격 흐름 (개념적 설명) */
/* Thread 1: /proc/self/mem을 통해 읽기 전용 매핑에 쓰기 시도 */
lseek(fd_mem, map_addr, SEEK_SET);
write(fd_mem, payload, payload_len);
/* 내부적으로 get_user_pages(FOLL_WRITE) 호출 */
/* → COW break 발생 → 사본 페이지 할당 */
/* → 사본에 쓰기 수행 */
/* Thread 2: madvise()로 페이지 무효화 */
madvise(map_addr, page_size, MADV_DONTNEED);
/* → COW 사본 폐기 → 원본 페이지로 복귀 */
/* → Thread 1이 원본 페이지에 직접 쓰기하게 됨! */
/* 경쟁 조건 타이밍:
* Thread 1: get_user_pages() → [COW break] → follow_page_mask()
* Thread 2: madvise(MADV_DONTNEED) ← 이 사이에 실행
* Thread 1: → 원본 페이지에 dirty 플래그 설정 → 쓰기 완료
*/
커널 수정 패치 핵심:
/* mm/gup.c - 수정된 COW 처리 */
static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
{
/* FOLL_COW 플래그 도입: COW break 완료 여부를 명시적으로 확인 */
if (flags & FOLL_COW) {
/* COW break가 실제로 완료되었고, 페이지가 dirty인 경우만 허용 */
return pte_dirty(pte);
}
return pte_write(pte);
}
/* faultin_page()에서 COW 처리 후 FOLL_COW 플래그 설정 */
if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
*flags |= FOLL_COW; /* 새로 도입된 플래그 */
get_user_pages()의 FOLL_WRITE → fault → retry 과정에서 페이지 테이블 상태가 변경될 수 있다는 점을
항상 고려해야 합니다. 이 버그는 TOCTOU 패턴의 전형적 사례로, 검증(check)과 사용(use) 사이에
상태가 변경되면 보안 경계가 무너질 수 있음을 보여줍니다.
2. KASAN이 발견한 Use-After-Free 패턴 (CVE-2016-8655)
packet_set_ring()과 packet_setsockopt() 사이의 경쟁 조건으로
해제된 타이머 구조체에 접근하여 로컬 권한 상승이 가능합니다.
Use-After-Free의 일반적 패턴: SLAB allocator에서 object가 해제된 후에도 dangling pointer를 통해 접근하는 경우 발생합니다.
/* Use-After-Free 일반 패턴 */
struct my_object *obj;
/* 1. 할당 */
obj = kmem_cache_alloc(my_cache, GFP_KERNEL);
/* 2. 사용 */
obj->data = 42;
/* 3. 해제 */
kfree(obj);
/* 4. 다른 할당이 같은 메모리를 재사용 */
another = kmem_cache_alloc(my_cache, GFP_KERNEL);
/* 5. Dangling pointer 접근 → Use-After-Free! */
printk("data = %d\n", obj->data); /* 이미 해제된 메모리 */
CVE-2016-8655 구체적 흐름:
/* net/packet/af_packet.c - 경쟁 조건 */
/* Thread A: packet_set_ring() */
packet_set_ring(sk, &req) {
/* ring buffer 설정 중 타이머 초기화 */
init_prb_bdqc(po, ...); /* retire_blk_timer 초기화 */
}
/* Thread B: 소켓 버전 변경 */
packet_setsockopt(sk, PACKET_VERSION, ...) {
/* ring buffer 해제 */
packet_set_ring(sk, &req_u.req); /* ring 해제 */
/* → retire_blk_timer가 여전히 pending 상태! */
}
/* Timer fires: 해제된 구조체 접근 → Use-After-Free */
prb_retire_rx_blk_timer_expired(...) {
/* 해제된 ring buffer의 메모리에 접근 */
}
KASAN 탐지 메커니즘:
/* KASAN(Kernel Address SANitizer) 동작 원리 */
/* 1. Shadow memory: 8바이트 실제 메모리 당 1바이트 shadow */
/* shadow 값: 0 = 전체 접근 가능, N(1-7) = 처음 N바이트만 접근 가능 */
/* 음수 = 접근 불가 (red zone, freed 등) */
/* 2. kfree() 시 shadow memory를 KASAN_FREE_SHADOW(0xFB)로 마킹 */
static void kasan_poison_slab_free(struct kmem_cache *cache, void *object)
{
kasan_poison_shadow(object, round_up(cache->object_size,
KASAN_SHADOW_SCALE_SIZE), KASAN_KMALLOC_FREE);
}
/* 3. 이후 접근 시 shadow 값 확인 → BUG 리포트 출력 */
/* BUG: KASAN: use-after-free in prb_retire_rx_blk_timer_expired */
/* Read of size 8 at addr ffff8800XXXXXXXX by task swapper/0 */
/* 4. Quarantine: 해제된 object를 즉시 재사용하지 않고 격리 */
/* 일정 크기가 쌓이면 실제로 slab에 반환 */
/* → UAF 탐지 윈도우 확대 */
CONFIG_KASAN=y)하면
이러한 버그를 조기에 탐지할 수 있습니다.
해제 전 타이머(del_timer_sync())와 워크큐(cancel_work_sync())를
반드시 정리하고, RCU 기반 수명 관리로 안전한 해제 시점을 보장하는 것이 핵심입니다.
3. Stack Guard Page 우회 — Stack Clash (CVE-2017-1000364)
alloca()나 VLA(Variable Length Array)로
guard page를 한 번에 넘어 인접 메모리를 덮어쓸 수 있습니다.
Guard Page 우회 원리:
/* 정상 상태: 스택과 힙 사이에 guard page 존재 */
/*
* 가상 주소 공간 (높은 주소 → 낮은 주소):
*
* ┌──────────────────┐ 높은 주소
* │ Stack (grows ↓) │
* │ [사용중 스택] │
* │ ··· │
* ├──────────────────┤
* │ Guard Page │ ← 4KB (단일 페이지)
* │ (접근 시 SIGSEGV) │
* ├──────────────────┤
* │ Heap / mmap │
* │ (grows ↑) │
* └──────────────────┘ 낮은 주소
*/
/* 공격: 큰 alloca()로 guard page를 건너뛰기 */
void vulnerable_function(size_t user_size) {
/* user_size가 guard page(4KB)보다 크면 */
/* guard page를 건너뛰어 heap 영역에 직접 착지 */
char buf[user_size]; /* VLA 또는 alloca(user_size) */
/* buf가 heap 영역과 겹침 → 임의 메모리 쓰기 가능 */
memset(buf, 0, user_size);
}
커널 수정 사항:
/* 수정 1: Guard page 크기를 1MB로 확대 */
/* include/linux/mm.h */
#define STACK_GUARD_GAP 256 /* 256 pages = 1MB (4KB pages 기준) */
/* 수정 2: stack_guard_gap 커널 파라미터 도입 */
/* 부트 파라미터로 guard gap 크기 조정 가능 */
/* stack_guard_gap=N (페이지 단위) */
/* 수정 3: expand_stack()에서 guard gap 검증 강화 */
int expand_stack(struct vm_area_struct *vma, unsigned long address)
{
struct vm_area_struct *prev;
/* 인접 VMA와의 거리가 stack_guard_gap 이상인지 확인 */
prev = find_vma_prev(vma->vm_mm, address, &prev);
if (prev && prev->vm_end + stack_guard_gap > address)
return -ENOMEM; /* guard gap 침범 → 확장 거부 */
/* ... */
}
| 항목 | 수정 전 (취약) | 수정 후 (안전) |
|---|---|---|
| Guard Gap 크기 | 4KB (1 page) | 1MB (256 pages, 조정 가능) |
| VMA 확장 검증 | 인접 VMA 거리 미확인 | stack_guard_gap 이상 거리 강제 |
| 커널 파라미터 | 없음 | stack_guard_gap=N |
| rlimit 연동 | RLIMIT_STACK만 확인 | guard gap + RLIMIT_STACK 동시 확인 |
alloca()는 커널 코드에서 사용을 지양하며,
Linux 커널은 -Wvla 컴파일 경고를 활성화하여 VLA 사용을 금지하고 있습니다.
4. Transparent Huge Pages (THP) OOM 문제
khugepaged 커널 스레드가 과도한 CPU 시간을 소모하여
프로덕션 서버 성능이 심각하게 저하되는 문제입니다. 보안 취약점은 아니지만
실제 서비스 장애를 유발한 대표적인 메모리 관리 이슈입니다.
문제 발생 메커니즘:
/* THP compaction 문제 흐름 */
/*
* 1. 프로세스가 페이지 폴트 발생
* → THP가 enabled(always)면 2MB huge page 할당 시도
*
* 2. 연속 2MB 물리 메모리가 없으면 compaction 시작
* → 페이지 이동(migration)으로 연속 공간 확보 시도
*
* 3. 메모리 단편화가 심한 경우:
* → compaction이 반복적으로 실패
* → khugepaged가 CPU 100% 점유
* → 실제 워크로드에 CPU 자원 부족
* → 응답 지연 → OOM killer 발동 가능
*/
/* khugepaged 동작 (mm/khugepaged.c) */
static int khugepaged(void *none)
{
while (!kthread_should_stop()) {
/* 모든 프로세스의 VMA를 스캔하며 huge page 병합 시도 */
khugepaged_do_scan();
/* → collapse_huge_page() → 연속 512개 4KB 페이지를 2MB로 병합 */
/* → 실패 시 재시도 → 단편화 심하면 무한 루프에 가까워짐 */
wait_event_freezable_timeout(khugepaged_wait,
..., msecs_to_jiffies(khugepaged_scan_sleep_millisecs));
}
return 0;
}
영향을 받는 주요 애플리케이션과 해결 방법:
| 애플리케이션 | THP 영향 | 권장 설정 |
|---|---|---|
| Redis | fork() 기반 RDB/AOF 저장 시 COW로 인한 메모리 2배 사용, latency spike | THP 비활성화 필수 |
| MongoDB | WiredTiger 엔진의 메모리 매핑과 충돌, 성능 불안정 | THP 비활성화 권장 |
| Oracle DB | HugePages(명시적)와 THP 혼용 시 예측 불가능한 메모리 사용 | THP 비활성화, 명시적 HugePages 사용 |
| Java (JVM) | GC pause 시간 증가, 메모리 사용량 비예측적 증가 | madvise 모드 또는 비활성화 |
/* THP 제어 방법 */
/* 1. 시스템 전역 설정 */
# 현재 설정 확인
$ cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
# madvise 모드로 변경 (명시적 요청만 THP 사용)
$ echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 완전 비활성화
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
/* 2. defrag 설정 (compaction 동작 제어) */
$ echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
/* 3. khugepaged 스캔 간격 조정 */
# 기본값: 10000ms → 더 긴 간격으로 CPU 부하 완화
$ echo 60000 > /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs
/* 4. 프로세스별 제어 (madvise 모드일 때) */
madvise(addr, length, MADV_HUGEPAGE); /* 이 영역에 THP 사용 */
madvise(addr, length, MADV_NOHUGEPAGE); /* 이 영역에 THP 미사용 */
madvise 모드를 사용하여 애플리케이션이 명시적으로
huge page 사용 여부를 결정하도록 하는 것이 안전한 기본 전략입니다.
버그 사례 비교 요약
| 버그 | 유형 | 잠복 기간 | 영향 | 핵심 교훈 |
|---|---|---|---|---|
| Dirty COW | Race Condition (TOCTOU) | 9년 (2007~2016) | 로컬 권한 상승 | COW 경로의 원자성 보장 필수 |
| AF_PACKET UAF | Use-After-Free | 수개월 | 로컬 권한 상승 | KASAN 활용, 해제 전 타이머/워크큐 정리 |
| Stack Clash | Guard Page 우회 | 수년 | 로컬 권한 상승 | 방어 메커니즘의 크기 가정 재검토 |
| THP OOM | 성능 결함 | 지속적 | 서비스 장애 | 워크로드별 최적화 전략 필요 |
Android 메모리 관리 특화
Android는 메모리 제약이 큰 모바일 환경에서 다수의 앱을 동시에 관리해야 하므로, 커널 메모리 관리에 여러 특화 기법을 적용한다.
ashmem → memfd_create 전환: 초기 Android의 ashmem(Anonymous Shared Memory)은 커널 5.18에서 제거되었으며, 메인라인의 memfd_create()로 대체되었다. memfd_create()는 파일 디스크립터 기반 공유 메모리를 제공하고, F_SEAL_* 플래그로 크기 변경 방지가 가능하다.
lowmemorykiller → lmkd: 커널 내 lowmemorykiller 드라이버는 OOM killer와 충돌 문제로 제거되었으며(4.12+), 유저스페이스 lmkd 데몬이 /proc/pressure/memory(PSI)를 모니터링하여 메모리 부족 시 적절한 프로세스를 종료한다. PSI 기반이므로 CONFIG_PSI=y가 필수다.
/* 기존 ashmem (deprecated, 5.18에서 제거) */
int fd = open("/dev/ashmem", O_RDWR);
/* 메인라인 대체: memfd_create (3.17+) */
int fd = memfd_create("shm_region", MFD_CLOEXEC | MFD_ALLOW_SEALING);
ftruncate(fd, 4096);
fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW);
MGLRU와 Android Go: MGLRU(Multi-Gen LRU, 커널 6.1+)는 Android Go(저사양) 디바이스에서 앱 kill 빈도를 약 18% 감소시키고, 페이지 에이징 정확도를 크게 개선했다. Android 15부터 GKI에서 기본 활성화된다. 관련 내용은 Android 커널 — 메모리 관리를 참고하라.
메모리 관리 주요 버그 사례
메모리 관리 서브시스템은 커널의 가장 복잡한 부분 중 하나로, Copy-on-Write, 페이지 폴트 처리, mmap 등에서 심각한 보안 취약점이 발견되어 왔습니다.
Dirty COW (CVE-2016-5195)
get_user_pages()의 Copy-on-Write 처리에서 madvise(MADV_DONTNEED)와의 경쟁 조건으로, 읽기 전용 파일의 페이지 캐시에 직접 쓰기가 가능합니다. Linux 2.6.22~4.8.3(약 9년간)에 존재했으며, /etc/passwd 수정, SUID 바이너리 덮어쓰기 등으로 권한 상승이 가능합니다. Android 루팅, 서버 침해에 광범위하게 악용되었습니다.
/* Dirty COW 핵심 경쟁 조건 */
/*
* Thread A: write(/proc/self/mem, target_file_offset)
* → get_user_pages(FOLL_WRITE)
* → faultin_page() — CoW 복사 수행 (새 페이지 할당)
*
* Thread B: madvise(MADV_DONTNEED, target_file_range)
* → CoW 복사본 페이지를 무효화
* → PTE를 원본 읽기 전용 페이지로 되돌림
*
* Thread A: follow_page_mask() 재시도
* → CoW 복사가 실패한 것으로 판단
* → FOLL_WRITE 제거 후 원본 페이지에 쓰기 접근 반환!
* → 읽기 전용 파일의 페이지 캐시에 직접 쓰기 발생
*
* 수정 (commit 19be0eaffa3a):
* FOLL_COW 플래그 도입 — CoW 복사 완료 여부를 정확히 추적
* retry 시 CoW 상태를 재검증하여 원본 페이지 접근 차단
*/
Huge Page CoW 성능 함정
2MB Huge Page를 사용하는 프로세스가 fork()를 호출하면, 이후 쓰기 발생 시 일반 4KB 페이지 대신 2MB 전체를 복사해야 합니다. Redis, PostgreSQL 등 대용량 메모리 + fork 기반 스냅샷을 사용하는 서비스에서 수백 밀리초의 지연(latency spike)과 메모리 사용량 폭증을 유발합니다.
/* Huge Page CoW 문제와 완화 */
/* 문제: Redis fork + THP → 전체 데이터셋 2MB 단위 복사 */
/* 증상: fork 후 쓰기 시 RSS가 2배로 증가, 수백ms latency spike */
# THP를 madvise 모드로 변경 (Redis 공식 권장)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# Redis 설정에서 THP 경고 비활성화
# redis.conf: activedefrag yes
/* 커널 6.1+ 개선: THP split-on-fork
* fork() 시 huge page를 4KB 페이지로 분할하여
* CoW 복사 단위를 줄임 → latency spike 완화
* MADV_NOHUGEPAGE로 특정 영역의 THP 비활성화 가능 */
madvise(addr, len, MADV_NOHUGEPAGE); /* 해당 영역 THP 비활성화 */
mmap/VMA 관련 취약점
Linux 6.1에서 도입된 Maple Tree 기반 VMA 관리에서, RCU 보호와 mmap_lock 상호작용에 경쟁 조건이 존재합니다. Maple Tree 노드 교체 과정에서 RCU 콜백이 아직 읽기 중인 노드를 해제하여 Use-After-Free가 발생합니다. Linux 6.1~6.4에 영향을 미칩니다.
tty_struct가 해제된 후에도 mmap을 통해 매핑된 메모리 영역이 유효한 것으로 취급되어, 해제된 tty 구조체에 접근하는 Use-After-Free가 발생합니다. 이를 통해 커널 메모리 임의 읽기/쓰기가 가능합니다.
/* VMA(Virtual Memory Area) 관련 일반적 버그 패턴 */
/*
* 1. mmap_lock 누락: VMA 순회 중 lock 미보유 → VMA 변경 시 UAF
* → mmap_read_lock() / mmap_write_lock() 필수
*
* 2. vm_ops->fault 재진입: 페이지 폴트 핸들러에서 다른 lock 획득 시
* → 데드락 가능 (lock ordering 위반)
*
* 3. userfaultfd 악용: userfaultfd를 통해 페이지 폴트를 임의 시간
* 지연시켜 TOCTOU 윈도우를 확대
* → vm.unprivileged_userfaultfd=0 설정으로 방어
*
* 4. remap_pfn_range 권한 검사: 드라이버가 커널 물리 메모리를
* 사용자 공간에 매핑할 때 접근 권한 미검증
* → PFN 범위와 vm_flags 검증 필수
*/
# 메모리 관련 보안 sysctl 설정
sysctl -w vm.unprivileged_userfaultfd=0 # userfaultfd 비특권 사용 차단
sysctl -w vm.mmap_min_addr=65536 # NULL deref 공격 완화
sysctl -w kernel.randomize_va_space=2 # 전체 ASLR 활성화