Huge Pages (2MB/1GB) & THP — 대형 페이지(Page)

리눅스 커널의 Huge Pages는 기본 4KB 페이지 대신 2MB 또는 1GB 크기의 대형 페이지를 사용하여 TLB(Translation Lookaside Buffer) 미스를 획기적으로 줄이고 메모리 접근 성능을 극대화하는 메커니즘입니다. 정적 예약 기반의 hugetlbfs부터 커널이 자동으로 대형 페이지를 생성하는 Transparent Huge Pages(THP), khugepaged 데몬, PMD 직접 매핑(Mapping), compound page 구조, NUMA 정책, 예약 시스템, 마이그레이션/compaction 연동, 그리고 데이터베이스/DPDK/VM 실전 적용까지 전 영역을 상세히 다룹니다.

전제 조건: MMU & TLB 문서와 페이지 할당자(Buddy Allocator) 문서를 먼저 읽으세요. 가상 주소 변환(Address Translation), 페이지 테이블(Page Table) 계층 구조, Buddy 시스템의 order 개념을 이해해야 Huge Pages의 동작 원리를 정확히 파악할 수 있습니다.
일상 비유: 일반 4KB 페이지는 편의점 봉투에 물건을 하나씩 담는 것과 같고, 2MB Huge Page는 대형 택배 상자에 512개의 물건을 한꺼번에 담는 것과 같습니다. 택배 상자를 사용하면 배송 추적(TLB 조회) 횟수가 512분의 1로 줄어들어 전체 배송 효율이 크게 향상됩니다. 1GB Huge Page는 아예 컨테이너(Container) 트럭으로 262,144개의 물건을 한 번에 운송하는 것에 해당합니다.

핵심 요약

  • Huge Page — 4KB보다 큰 페이지 크기(x86_64에서 2MB 또는 1GB)를 사용하여 TLB 효율을 극대화하는 메모리 관리(Memory Management) 기법
  • hugetlbfs — 사용자가 명시적으로 대형 페이지를 예약하고 사용하는 정적 방식의 파일시스템(Filesystem) 인터페이스
  • THP (Transparent Huge Pages) — 커널이 자동으로 4KB 페이지를 2MB 대형 페이지로 승격/분할하는 투명 메커니즘
  • khugepaged — 백그라운드에서 분산된 4KB 페이지를 스캔하여 2MB 대형 페이지로 병합하는 커널 데몬
  • PMD (Page Middle Directory) — x86_64에서 2MB Huge Page를 위한 페이지 테이블 레벨로, PTE 단계를 건너뛰어 직접 물리 프레임을 가리킴
  • compound page — 연속된 물리 페이지를 하나의 논리적 대형 페이지로 묶는 커널 자료구조
# Huge Pages 현황 빠른 확인
grep -i huge /proc/meminfo            # HugePages_Total/Free/Rsvd/Surp, Hugepagesize
cat /sys/kernel/mm/transparent_hugepage/enabled  # THP 정책: always/madvise/never
grep -E 'thp_fault|thp_collapse' /proc/vmstat    # THP 할당/병합 통계
cat /proc/<PID>/smaps_rollup | grep AnonHugePages  # 프로세스별 THP 사용량
ls /sys/kernel/mm/hugepages/          # 사용 가능한 Huge Page 크기 목록

단계별 이해

  1. TLB 미스 비용 이해
    CPU가 가상 주소(Virtual Address)를 물리 주소(Physical Address)로 변환할 때, TLB 캐시(Cache)에 없으면 4단계 페이지 테이블 워크가 발생합니다. 4KB 페이지로 1GB 메모리를 매핑하면 262,144개의 TLB 엔트리가 필요하지만, 2MB 페이지로는 512개, 1GB 페이지로는 단 1개면 충분합니다. 커널 함수: __handle_mm_fault()에서 PMD 레벨의 _PAGE_PSE 비트를 확인하여 2MB 직접 매핑을 판별합니다.
  2. 정적 Huge Pages 예약 (hugetlbfs)
    시스템 부팅 시 또는 런타임에 /proc/sys/vm/nr_hugepages를 통해 필요한 수의 Huge Pages를 미리 예약합니다. 예약된 페이지는 hugetlbfs를 통해 사용자 공간(User Space)에 매핑됩니다. 커널 함수: set_max_huge_pages()alloc_pool_huge_page()에서 Buddy Allocator로부터 order-9(2MB) 페이지를 확보합니다.
  3. 투명 대형 페이지 (THP) 활성화
    /sys/kernel/mm/transparent_hugepage/enabledalways 또는 madvise로 설정하면, 커널이 자동으로 연속된 4KB 페이지를 2MB 페이지로 승격합니다. 커널 함수: do_huge_pmd_anonymous_page()에서 THP 페이지 폴트를 처리하고, PMD 엔트리에 2MB compound page를 직접 매핑합니다.
  4. khugepaged 백그라운드 최적화
    THP가 활성화되면 khugepaged 데몬이 주기적으로 프로세스(Process)의 메모리를 스캔하여 병합 가능한 4KB 페이지 그룹을 2MB Huge Page로 통합합니다. 커널 함수: khugepaged_scan_mm_slot()collapse_huge_page()에서 512개의 연속 PTE를 하나의 PMD 매핑으로 치환합니다.
  5. 모니터링과 튜닝
    /proc/meminfo, /sys/kernel/mm/hugepages/, /sys/kernel/mm/transparent_hugepage/ 경로에서 Huge Pages 사용 현황과 THP 통계를 확인하고 세부 파라미터를 조정합니다. 커널 함수: hugetlb_report_meminfo()가 /proc/meminfo의 HugePages 관련 항목을 출력합니다.

개요 — 왜 Huge Pages가 필요한가

TLB 미스의 비용

현대 x86_64 프로세서에서 가상 주소를 물리 주소로 변환하는 과정은 4단계 페이지 테이블 워크 (PGD → PUD → PMD → PTE)를 거칩니다. TLB에 캐시된 변환이 있으면 1~2 사이클 내에 완료되지만, TLB 미스가 발생하면 최대 4번의 메모리 접근이 필요하여 수십~수백 사이클의 지연(Latency)이 발생합니다.

일반적인 x86_64 프로세서의 TLB 엔트리 수는 다음과 같습니다:

TLB 레벨4KB 엔트리 수2MB 엔트리 수1GB 엔트리 수
L1 DTLB (데이터) 64~72 32 4~8
L1 ITLB (명령어) 128 8
L2 STLB (공유) 1,536~2,048 1,536~2,048 (공유)
커버 가능 메모리 6~8 MB 3~4 GB 4~8 GB
핵심 포인트: 4KB 페이지로는 L2 STLB 2,048개 엔트리를 모두 사용해도 겨우 8MB의 메모리만 TLB로 커버할 수 있습니다. 반면 2MB Huge Pages는 동일한 TLB 엔트리로 약 4GB, 1GB Huge Pages는 8GB를 커버합니다. 메모리 집약적 워크로드에서 TLB 미스 감소 효과는 수십 퍼센트의 성능 향상으로 직결됩니다.

Huge Pages의 두 가지 방식

항목hugetlbfs (정적 Huge Pages)THP (투명 대형 페이지)
할당 방식사전 예약 (부팅 시 또는 런타임)커널 자동 (on-demand)
지원 크기2MB, 1GB2MB (기본)
사용자 인터페이스mmap(MAP_HUGETLB), hugetlbfs 마운트(Mount)투명 (madvise 힌트 가능)
OOM 위험낮음 (사전 예약)있음 (compaction 실패 시 fallback)
분할(split) 지원불가가능 (필요 시 4KB로 분할)
스왑(Swap) 지원불가가능 (swap-out 시 분할 후 스왑)
대표 사용처DPDK, 데이터베이스, VM일반 애플리케이션 자동 최적화

x86_64 페이지 크기 계층

x86_64 아키텍처는 3가지 페이지 크기를 하드웨어적으로 지원합니다. 각 크기는 페이지 테이블의 서로 다른 레벨에서 매핑됩니다.

/* x86_64 페이지 크기 계층 */
4KB   = 2^12  →  PTE 레벨 매핑 (기본)
2MB   = 2^21  →  PMD 레벨 매핑 (512 x 4KB)
1GB   = 2^30  →  PUD 레벨 매핑 (512 x 2MB = 262,144 x 4KB)

페이지 크기와 TLB 효과 비교

4KB vs 2MB vs 1GB TLB 커버리지

동일한 TLB 엔트리 수에서 페이지 크기에 따른 커버 가능한 메모리 양의 차이를 비교합니다. 아래 다이어그램은 64개의 DTLB 엔트리를 기준으로 각 페이지 크기별 커버리지를 보여줍니다.

64개 DTLB 엔트리 기준 — 페이지 크기별 메모리 커버리지 64 GB 48 GB 32 GB 16 GB 0 256 KB 4KB 페이지 64 x 4KB 128 MB 2MB 페이지 64 x 2MB 64 GB 1GB 페이지 64 x 1GB x512 x512 4KB (기본) 2MB (PMD) 1GB (PUD)

TLB 미스율 시뮬레이션

아래 표는 연속적으로 접근하는 메모리 영역 크기별 TLB 미스 횟수를 비교합니다. (L1 DTLB 64개, L2 STLB 2,048개 기준)

접근 메모리 크기4KB 페이지 (필요 엔트리)4KB TLB 미스2MB 페이지 (필요 엔트리)2MB TLB 미스
8 MB2,048L2 경계40 (L1 캐시)
64 MB16,384빈번320 (L1 캐시)
512 MB131,072매우 빈번2560 (L2 캐시)
4 GB1,048,576극심2,048L2 경계
32 GB8,388,608극심16,384빈번
# TLB 미스를 perf로 측정하여 Huge Pages 효과 비교
# 1) 일반 4KB 페이지 환경에서 측정
perf stat -e dTLB-load-misses,dTLB-store-misses,iTLB-load-misses \
    -p <PID> -- sleep 10

# 2) THP 활성화 후 동일 워크로드 측정
echo always > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses,dTLB-store-misses,iTLB-load-misses \
    -p <PID> -- sleep 10

# 3) 결과 비교: dTLB-load-misses가 90%+ 감소하면 Huge Pages 효과 확인
성능 효과: 실제 벤치마크에서 대용량 메모리를 순회하는 워크로드(데이터베이스 버퍼(Buffer) 풀, 과학 계산, VM 메모리 등)에서 THP 또는 hugetlbfs를 활성화하면 TLB 미스가 90% 이상 감소하고, 전체 성능이 5~30% 향상되는 사례가 보고됩니다.

hugetlbfs 아키텍처 — 예약 기반 정적 Huge Pages

hugetlbfs 개요

hugetlbfs는 커널 2.6부터 도입된 특수 파일시스템으로, 정적으로 예약된 Huge Pages를 사용자 공간에 제공합니다. Buddy 할당자에서 연속된 고차(order-9 또는 order-18) 물리 페이지를 미리 확보하여 전용 풀에 보관하며, 사용자가 hugetlbfs를 마운트하고 파일을 mmap하여 사용합니다.

hugetlbfs 예약 흐름

hugetlbfs 아키텍처: 예약 → 할당 → 매핑 흐름 사용자 공간 프로세스 A (mmap) 프로세스 B (shmget) libhugetlbfs sysctl / 부팅 파라미터 nr_hugepages 설정 커널 공간 hugetlbfs VFS 레이어 예약 관리 (resv_map) subpool 관리 HugeTLB 페이지 풀 free_huge_pages / nr_huge_pages NUMA Node 0 node_huge_pages[] NUMA Node 1 node_huge_pages[] Buddy 할당자 (order-9 / order-18) 물리 메모리 (연속 페이지 프레임) 초기 할당

hugetlbfs 페이지 풀 관리 구조체(Struct)

/* mm/hugetlb.c - 핵심 전역 변수 */
struct hstate hstates[HUGE_MAX_HSTATE];
unsigned int default_hstate_idx;

struct hstate {
    struct mutex resize_lock;
    int next_nid_to_alloc;
    int next_nid_to_free;
    unsigned int order;           /* 2MB: order=9, 1GB: order=18 */
    unsigned int demote_order;
    unsigned long mask;
    unsigned long max_huge_pages;
    unsigned long nr_huge_pages;    /* 현재 총 Huge Pages 수 */
    unsigned long free_huge_pages;  /* 미사용 Huge Pages 수 */
    unsigned long resv_huge_pages;  /* 예약된 Huge Pages 수 */
    unsigned long surplus_huge_pages;
    unsigned long nr_overcommit_huge_pages;
    struct list_head hugepage_activelist;
    struct list_head hugepage_freelists[MAX_NUMNODES];
    unsigned int nr_huge_pages_node[MAX_NUMNODES];
    unsigned int free_huge_pages_node[MAX_NUMNODES];
    unsigned int surplus_huge_pages_node[MAX_NUMNODES];
    char name[HSTATE_NAME_LEN];
};
코드 설명
  • 2행 hstates 배열은 시스템이 지원하는 각 Huge Page 크기별 상태를 관리합니다. x86_64에서는 일반적으로 2MB와 1GB 두 가지입니다.
  • 6행 order 필드는 Buddy 할당자에서의 주문 크기입니다. 2MB는 order-9(512개의 4KB 페이지), 1GB는 order-18(262,144개)입니다.
  • 11~13행 핵심 카운터: 전체 수(nr_huge_pages), 여유 수(free_huge_pages), 예약 수(resv_huge_pages)로 풀 상태를 추적합니다.
  • 16~17행 활성 목록과 여유 목록을 NUMA 노드별로 분리하여, NUMA 지역성을 고려한 할당이 가능합니다.

hugetlbfs 마운트와 사용

# hugetlbfs 마운트
mount -t hugetlbfs -o pagesize=2M,size=4G,min_size=1G none /mnt/hugepages

# 2MB Huge Pages 예약 (1024개 = 2GB)
echo 1024 > /proc/sys/vm/nr_hugepages

# NUMA 노드별 예약
echo 512 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
echo 512 > /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages

# 1GB Huge Pages 예약 (부팅 파라미터로만 안정적 할당 가능)
# 커널 부팅 파라미터: hugepagesz=1G hugepages=16

# 현재 상태 확인
cat /proc/meminfo | grep -i huge
/* 사용자 공간에서 mmap으로 Huge Page 매핑 */
#include <sys/mman.h>

void *alloc_hugepage(size_t size)
{
    void *addr = mmap(NULL, size,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                       -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap MAP_HUGETLB");
        return NULL;
    }
    return addr;
}

/* 특정 크기 지정: MAP_HUGE_2MB 또는 MAP_HUGE_1GB */
void *alloc_1gb_hugepage(size_t size)
{
    void *addr = mmap(NULL, size,
                       PROT_READ | PROT_WRITE,
                       MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB |
                       MAP_HUGE_1GB,
                       -1, 0);
    return (addr == MAP_FAILED) ? NULL : addr;
}

Transparent Huge Pages (THP) 메커니즘

THP 개요

Transparent Huge Pages(THP)는 커널 2.6.38에 도입된 메커니즘으로, 사용자 공간 애플리케이션의 수정 없이 커널이 자동으로 2MB 대형 페이지를 할당하고 관리합니다. hugetlbfs와 달리 사전 예약이 필요 없고, 할당 실패 시 자동으로 4KB 페이지로 폴백합니다.

THP 동작 모드

모드설정값동작사용 사례
always always 모든 익명 메모리 매핑에 THP 시도 메모리 집약적 서버, 일반 데스크탑
madvise madvise madvise(MADV_HUGEPAGE) 힌트를 준 영역만 THP 적용 데이터베이스, 선택적 최적화
never never THP 완전 비활성화 지연 민감 실시간(Real-time) 시스템
THP 페이지 폴트 처리 흐름 Anonymous Page Fault THP 활성화? (enabled/madvise) 4KB 일반 할당 No Yes VMA >= 2MB? 정렬 확인 4KB 폴백 할당 No Yes compound page 할당 시도 할당 성공? PMD 직접 매핑 Yes 2MB Huge Page 성공 compaction 시도 후 4KB 폴백 할당 No khugepaged가 나중에 병합

THP 설정 인터페이스

# THP 모드 설정
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo never  > /sys/kernel/mm/transparent_hugepage/enabled

# defrag 정책 (할당 실패 시 compaction 수행 여부)
echo always  > /sys/kernel/mm/transparent_hugepage/defrag   # 항상 compaction
echo defer   > /sys/kernel/mm/transparent_hugepage/defrag   # kswapd에게 위임
echo madvise > /sys/kernel/mm/transparent_hugepage/defrag   # MADV_HUGEPAGE만
echo never   > /sys/kernel/mm/transparent_hugepage/defrag   # compaction 안 함

# THP 통계 확인
cat /proc/vmstat | grep thp
# thp_fault_alloc: THP 할당 성공 횟수
# thp_fault_fallback: THP 할당 실패 (4KB 폴백) 횟수
# thp_collapse_alloc: khugepaged 병합 성공 횟수
# thp_split_page: THP 분할 횟수

THP 핵심 코드 경로

/* mm/huge_memory.c - THP 페이지 폴트 핸들러 */
static vm_fault_t __do_huge_pmd_anonymous_page(
    struct vm_fault *vmf, struct page *page,
    gfp_t gfp)
{
    struct vm_area_struct *vma = vmf->vma;
    pgtable_t pgtable;
    unsigned long haddr = vmf->address & HPAGE_PMD_MASK;

    /* 2MB 정렬된 주소 확인 */
    VM_BUG_ON_PAGE(!PageCompound(page), page);
    VM_BUG_ON_PAGE(!PageHead(page), page);

    /* 페이지 초기화 (zeroing) */
    clear_huge_page(page, vmf->address, HPAGE_PMD_NR);

    /* PMD 엔트리를 직접 설정 (PTE 레벨 건너뜀) */
    __SetPageUptodate(page);

    spin_lock(vmf->ptl);
    if (unlikely(!pmd_none(*vmf->pmd))) {
        spin_unlock(vmf->ptl);
        goto out;
    }

    entry = mk_huge_pmd(page, vma->vm_page_prot);
    entry = maybe_pmd_mkwrite(pmd_mkdirty(entry), vma);
    page_add_new_anon_rmap(page, vma, haddr);
    lru_cache_add_inactive_or_unevictable(page, vma);
    pgtable_trans_huge_deposit(vma->vm_mm, vmf->pmd, pgtable);
    set_pmd_at(vma->vm_mm, haddr, vmf->pmd, entry);

    spin_unlock(vmf->ptl);
    return VM_FAULT_NOPAGE;

out:
    mem_cgroup_uncharge(page);
    put_page(page);
    return VM_FAULT_FALLBACK;
}
코드 설명
  • 8행 HPAGE_PMD_MASK로 주소를 2MB 경계에 정렬합니다. THP는 반드시 2MB 정렬된 가상 주소에 매핑됩니다.
  • 11~12행 할당된 페이지가 compound page(Head 페이지)인지 검증합니다. THP는 항상 compound page 형태입니다.
  • 15행 clear_huge_page()는 512개의 4KB 페이지를 한꺼번에 0으로 초기화합니다. HPAGE_PMD_NR은 512입니다.
  • 26행 mk_huge_pmd()가 PMD 엔트리를 생성합니다. PTE 레벨을 건너뛰고 PMD에서 직접 2MB 물리 프레임을 가리킵니다.
  • 30행 pgtable_trans_huge_deposit()는 나중에 THP가 분할될 때 사용할 PTE 페이지 테이블을 미리 보관합니다.
  • 31행 set_pmd_at()으로 PMD 엔트리를 원자적(Atomic)으로 설정하여 2MB 매핑을 완성합니다.

khugepaged 데몬 동작

khugepaged 개요

khugepaged는 THP 프레임워크의 백그라운드 병합 데몬입니다. 주기적으로 프로세스의 가상 메모리(Virtual Memory) 영역을 스캔하여, 연속된 512개의 4KB 페이지가 동일한 VMA에 속하고 병합 조건을 만족하면 하나의 2MB THP로 통합(collapse)합니다.

khugepaged 스캔 및 병합(collapse) 흐름 Sleep scan_sleep_ms mm_struct 선택 라운드 로빈 스캔 VMA 순회 hugepage_vma_check() 2MB 정렬 확인 HPAGE_PMD_MASK 512개 PTE 스캔 __collapse_huge_page_swapin() max_ptes_none, max_ptes_swap 검사 병합 가능? 건너뜀 (다음 VMA) Yes compound page 할당 alloc_charge_hpage(order=9) 512개 페이지 복사 copy_page(hpage + i, pages[i]) PMD 엔트리 교체 set_pmd_at() + TLB flush 병합 완료 thp_collapse_alloc++ 반복

khugepaged 튜닝 파라미터

# khugepaged 스캔 간격 (밀리초, 기본 10000 = 10초)
echo 5000 > /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs

# 한 번의 스캔에서 처리할 최대 페이지 수 (기본 4096)
echo 8192 > /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan

# 비어 있는 PTE 최대 허용 수 (기본 511, 최대 511)
echo 511 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_none

# 스왑된 PTE 최대 허용 수 (기본 64)
echo 64 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_swap

# 공유 PTE 최대 허용 수 (기본 256)
echo 256 > /sys/kernel/mm/transparent_hugepage/khugepaged/max_ptes_shared

Huge Page 할당 경로 — alloc_hugepage, compound page

compound page 구조

Huge Page는 내부적으로 compound page로 구현됩니다. 연속된 2^order개의 물리 페이지(struct page)를 하나의 논리적 단위로 묶어, 첫 번째 페이지가 Head 페이지, 나머지가 Tail 페이지가 됩니다.

compound page 구조 (order-9, 2MB Huge Page) Head Page PG_head 플래그 _refcount compound_order=9 Tail[0] PG_tail compound_head → Head Tail[1] PG_tail compound_head → Head ... Tail[510] PG_tail compound_head → Head Tail[511] PG_tail compound_head → Head 총 512개 Tail 페이지 (각 4KB) = 512 x 4KB = 2MB compound_dtor free_compound_page() 또는 free_transhuge_page() 소멸자 compound_mapcount 전체 compound page 매핑 횟수 compound_pincount GUP(pin_user_pages) 핀 횟수 각 Tail 페이지의 compound_head 포인터는 Head 페이지를 가리켜 임의의 Tail 페이지에서 O(1)으로 Head 페이지를 찾을 수 있습니다.

Huge Page 할당 코드 경로

/* mm/hugetlb.c - Huge Page 할당 핵심 함수 */
static struct page *dequeue_huge_page_vma(
    struct hstate *h,
    struct vm_area_struct *vma,
    unsigned long address, int avoid_reserve,
    long chg)
{
    struct page *page;
    struct zonelist *zonelist;
    struct zone *zone;
    struct zoneref *z;
    nodemask_t *nodemask;
    int nid;

    /* NUMA 정책에 따른 할당 우선순위 결정 */
    nid = huge_node(vma, address, huge_page_shift(h), &nodemask);
    zonelist = node_zonelist(nid, htlb_alloc_mask(h));

    /* 여유 풀에서 페이지 꺼내기 */
    for_each_zone_zonelist_nodemask(zone, z, zonelist,
                                     MAX_NR_ZONES - 1, nodemask) {
        nid = zone_to_nid(zone);
        if (!list_empty(&h->hugepage_freelists[nid])) {
            page = list_entry(
                h->hugepage_freelists[nid].next,
                struct page, lru);
            list_move(&page->lru, &h->hugepage_activelist);
            set_page_refcounted(page);
            h->free_huge_pages--;
            h->free_huge_pages_node[nid]--;
            return page;
        }
    }
    return NULL;
}

페이지 테이블 구조 — PMD 직접 매핑

4KB vs 2MB 페이지 테이블 비교

일반 4KB 페이지는 4단계(PGD → PUD → PMD → PTE)의 페이지 테이블 워크가 필요하지만, 2MB Huge Page는 PMD에서 직접 물리 프레임을 가리키므로 PTE 레벨이 생략됩니다. 1GB Huge Page는 PUD에서 직접 매핑하여 PMD와 PTE 두 레벨을 모두 건너뜁니다.

x86_64 페이지 테이블 계층: 4KB vs 2MB vs 1GB 매핑 비교 4KB 페이지 (4단계) PGD PUD (P4D) PMD PTE 4KB 물리 페이지 2MB Huge Page (3단계) PGD PUD (P4D) PMD (PSE 비트=1) PTE (생략) 2MB 물리 프레임 1GB Huge Page (2단계) PGD PUD (PSE 비트=1) PMD (생략) PTE (생략) 1GB 물리 프레임 TLB 미스 시 메모리 접근 횟수 (페이지 테이블 워크) 4KB: 4회 PGD → PUD → PMD → PTE 2MB: 3회 PGD → PUD → PMD(직접) 1GB: 2회 PGD → PUD(직접)

PMD 엔트리 구조 (x86_64)

/* arch/x86/include/asm/pgtable_types.h */
/*
 * PMD 엔트리 비트 필드 (2MB Huge Page 매핑 시)
 *
 * [63]    NX (No Execute)
 * [62:52] 소프트웨어 사용
 * [51:21] 물리 프레임 번호 (2MB 정렬)
 * [20:13] PAT, 소프트웨어 예약
 * [12]    PAT (Page Attribute Table)
 * [11:9]  소프트웨어 사용 (linux: _PAGE_SOFT_DIRTY 등)
 * [8]     Global
 * [7]     PS (Page Size) = 1 → 2MB Huge Page 표시
 * [6]     Dirty
 * [5]     Accessed
 * [4]     PCD (Cache Disable)
 * [3]     PWT (Write Through)
 * [2]     U/S (User/Supervisor)
 * [1]     R/W (Read/Write)
 * [0]     Present
 */

#define _PAGE_BIT_PSE     7    /* Page Size Extension: 1=대형 페이지 */
#define _PAGE_PSE         (1UL << _PAGE_BIT_PSE)

/* PMD가 Huge Page인지 확인 */
static inline int pmd_large(pmd_t pmd)
{
    return pmd_flags(pmd) & _PAGE_PSE;
}

/* THP를 위한 PMD 생성 */
static inline pmd_t mk_huge_pmd(struct page *page, pgprot_t pgprot)
{
    return pfn_pmd(page_to_pfn(page),
                   __pgprot(pgprot_val(pgprot) | _PAGE_PSE));
}

Huge Page와 NUMA

NUMA 토폴로지(Topology)와 Huge Page 할당

NUMA(Non-Uniform Memory Access) 시스템에서 Huge Page의 할당 위치는 성능에 큰 영향을 미칩니다. 원격 NUMA 노드에서 할당된 Huge Page는 로컬 노드 대비 30~50% 더 높은 접근 지연시간을 보입니다. 커널은 NUMA 정책(mbind, set_mempolicy)과 연계하여 Huge Page 할당 노드를 결정합니다.

NUMA 시스템의 Huge Page 풀 구조 NUMA Node 0 CPU 0~15 (소켓 0) 메모리 컨트롤러 0 2MB HugePage 풀 (Node 0) hugepage_freelists[0]: 512개 예약 1GB HugePage 풀 (Node 0) hugepage_freelists[0]: 8개 예약 일반 4KB 페이지 + Buddy 시스템 NUMA Node 1 CPU 16~31 (소켓 1) 메모리 컨트롤러 1 2MB HugePage 풀 (Node 1) hugepage_freelists[1]: 512개 예약 1GB HugePage 풀 (Node 1) hugepage_freelists[1]: 8개 예약 일반 4KB 페이지 + Buddy 시스템 QPI/UPI 인터커넥트 (원격 접근 시 지연 증가) 로컬 접근: ~70ns 원격 접근: ~130ns

NUMA 정책과 Huge Page

/* NUMA 정책을 적용한 Huge Page 할당 예제 */
#include <numaif.h>
#include <sys/mman.h>

void *alloc_numa_hugepage(size_t size, int node)
{
    void *addr;
    unsigned long nodemask = 1UL << node;

    /* NUMA 바인드 정책 설정 */
    set_mempolicy(MPOL_BIND, &nodemask, sizeof(nodemask) * 8);

    /* Huge Page 할당 */
    addr = mmap(NULL, size,
                PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB,
                -1, 0);

    /* 기본 정책 복원 */
    set_mempolicy(MPOL_DEFAULT, NULL, 0);

    return (addr == MAP_FAILED) ? NULL : addr;
}
# NUMA 노드별 Huge Page 예약 상태 확인
cat /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages
cat /sys/devices/system/node/node0/hugepages/hugepages-2048kB/free_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-2048kB/nr_hugepages
cat /sys/devices/system/node/node1/hugepages/hugepages-2048kB/free_hugepages

# numactl을 사용한 Huge Page 바인딩
numactl --membind=0 --hugepage ./my_application

HugeTLB 예약 시스템 — resv_map, subpool

예약(Reservation) 메커니즘

hugetlbfs에서 mmap()을 호출하면 즉시 물리 페이지를 할당하지 않고, 예약만 수행합니다. 이는 실제 페이지 폴트(Page Fault)가 발생할 때까지 물리 할당을 지연하되, 할당 실패가 발생하지 않도록 여유 페이지 수를 미리 확보하는 방식입니다.

/* include/linux/hugetlb.h - 예약 맵 구조체 */
struct resv_map {
    struct kref refs;
    spinlock_t lock;
    struct list_head regions;        /* 예약된 영역 리스트 */
    long adds_in_progress;            /* 진행 중인 추가 수 */
    struct list_head region_cache;    /* 영역 캐시 */
    long region_cache_count;
};

/* 예약 영역 단위 */
struct file_region {
    struct list_head link;
    long from;                        /* 시작 인덱스 (Huge Page 단위) */
    long to;                          /* 끝 인덱스 */
};

/* subpool: 마운트별 Huge Page 제한 */
struct hugepage_subpool {
    spinlock_t lock;
    long count;                       /* 현재 사용 중인 페이지 수 */
    long max_hpages;                  /* 최대 허용 페이지 수 (size= 옵션) */
    long used_hpages;
    struct hstate *hstate;
    long min_hpages;                  /* 최소 보장 페이지 수 (min_size= 옵션) */
    long rsv_hpages;                  /* 예약 중인 페이지 수 */
};
코드 설명
  • 3~8행 resv_map은 파일(inode)당 하나 생성됩니다. regions 리스트로 예약된 인덱스 범위를 추적합니다.
  • 12~15행 file_region은 연속된 예약 범위를 [from, to) 형태로 표현합니다. 범위가 겹치면 병합됩니다.
  • 19~26행 hugepage_subpool은 hugetlbfs 마운트 포인트별로 Huge Page 사용량을 제한합니다. mount -o size=4G로 설정합니다.

예약 흐름

  1. mmap(MAP_HUGETLB) 호출
  2. 커널이 hugetlb_reserve_pages()를 호출하여 필요한 Huge Page 수 계산
  3. resv_map에 예약 범위 추가
  4. 전역 풀에서 resv_huge_pages 카운터 증가
  5. subpool이 있으면 subpool 카운터도 증가
  6. 실제 페이지 폴트 시 alloc_huge_page()가 예약된 풀에서 할당
  7. munmap() 시 미사용 예약분 반환

Huge Page 마이그레이션과 compaction

THP 분할 (Split)

THP는 필요에 따라 512개의 4KB 페이지로 분할될 수 있습니다. 분할이 발생하는 주요 상황:

/* mm/huge_memory.c - THP 분할 핵심 */
int split_huge_page_to_list(struct page *page,
                            struct list_head *list)
{
    struct page *head = compound_head(page);
    struct deferred_split *ds_queue;
    int ret;

    /* Head 페이지의 참조 카운트 확인 */
    if (!PageCompound(page))
        return 0;

    /* anon_vma 잠금 (역방향 매핑 보호) */
    anon_vma_lock_write(head->mapping);

    /* PMD 엔트리를 512개 PTE로 교체 */
    ret = __split_huge_page(page, list, end);

    /* 통계 업데이트 */
    if (!ret)
        count_vm_event(THP_SPLIT_PAGE);

    return ret;
}

Memory Compaction과 Huge Page

THP 할당이 실패하면 커널은 memory compaction을 시도합니다. Compaction은 사용 중인 4KB 페이지를 한쪽으로 이동시켜 연속된 빈 영역을 만들고, 이 영역에서 2MB compound page를 할당합니다.

# compaction 관련 통계
cat /proc/vmstat | grep compact
# compact_stall: compaction 대기 횟수
# compact_success: compaction 성공 횟수
# compact_fail: compaction 실패 횟수

# 수동 compaction 트리거
echo 1 > /proc/sys/vm/compact_memory

# proactive compaction 설정 (커널 5.9+)
echo 20 > /proc/sys/vm/compaction_proactiveness
compaction 비용: Memory compaction은 페이지 이동(Page Migration)을 수반하므로 CPU 시간과 I/O를 소비합니다. 실시간 시스템이나 지연 민감 워크로드에서는 defrag=never로 설정하여 compaction을 비활성화하고, 대신 hugetlbfs 사전 예약을 사용하는 것이 바람직합니다.

사용자 공간 인터페이스 — mmap, shmget, madvise

mmap을 통한 Huge Page 할당

/* MAP_HUGETLB를 사용한 Huge Page 할당 */
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>

#define HUGEPAGE_SIZE  (2 * 1024 * 1024)  /* 2MB */
#define NUM_PAGES      64

int main(void)
{
    size_t total_size = HUGEPAGE_SIZE * NUM_PAGES;  /* 128MB */

    /* 익명 Huge Page 할당 */
    void *addr = mmap(NULL, total_size,
                      PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB |
                      MAP_HUGE_2MB,    /* 2MB 명시 */
                      -1, 0);
    if (addr == MAP_FAILED) {
        perror("mmap");
        return 1;
    }

    /* 메모리 사용 */
    memset(addr, 0xAB, total_size);
    printf("Huge Pages 할당 성공: %p, 크기: %zu MB\n",
           addr, total_size / (1024 * 1024));

    /* 해제 */
    munmap(addr, total_size);
    return 0;
}

shmget을 통한 공유 Huge Page

/* System V 공유 메모리로 Huge Page 사용 */
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE  (256 * 1024 * 1024)  /* 256MB */

int shmid = shmget(IPC_PRIVATE, SHM_SIZE,
                   IPC_CREAT | SHM_HUGETLB | 0666);
if (shmid < 0) {
    perror("shmget SHM_HUGETLB");
    return 1;
}

void *addr = shmat(shmid, NULL, 0);
if (addr == (void *)-1) {
    perror("shmat");
    return 1;
}

/* 사용 후 해제 */
shmdt(addr);
shmctl(shmid, IPC_RMID, NULL);

madvise를 통한 THP 힌트

/* THP madvise 모드에서 선택적 활성화 */
#include <sys/mman.h>

void *addr = mmap(NULL, size,
                  PROT_READ | PROT_WRITE,
                  MAP_PRIVATE | MAP_ANONYMOUS,
                  -1, 0);

/* 이 영역에 THP를 적용해달라고 커널에 힌트 */
madvise(addr, size, MADV_HUGEPAGE);

/* THP를 비활성화하려면 */
madvise(addr, size, MADV_NOHUGEPAGE);

/* THP 분할을 자발적으로 요청 (커널 5.4+) */
madvise(addr, size, MADV_PAGEOUT);   /* 스왑 아웃 힌트 (분할 수반) */
madvise(addr, size, MADV_COLD);      /* 비활성 페이지로 표시 */

커널 설정과 부팅 파라미터

Kconfig 옵션

# Huge Pages 기본 지원
CONFIG_HUGETLBFS=y           # hugetlbfs 파일시스템 활성화
CONFIG_HUGETLB_PAGE=y        # Huge Page 인프라 (자동 선택됨)

# Transparent Huge Pages
CONFIG_TRANSPARENT_HUGEPAGE=y          # THP 지원 활성화
CONFIG_TRANSPARENT_HUGEPAGE_ALWAYS=y   # 기본 모드: always
CONFIG_TRANSPARENT_HUGEPAGE_MADVISE=y  # 기본 모드: madvise

# Huge Page 관련 고급 옵션
CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP=y # HVO: vmemmap 최적화 (커널 6.1+)
CONFIG_ARCH_HAS_HUGEPD=y               # 아키텍처별 HugePD 지원
CONFIG_HUGETLB_PAGE_SIZE_VARIABLE=y    # 가변 Huge Page 크기 지원

# compaction 관련
CONFIG_COMPACTION=y          # 메모리 compaction (THP에 필수)
CONFIG_MIGRATION=y           # 페이지 마이그레이션 지원

부팅 파라미터

파라미터설명예제
hugepagesz= Huge Page 크기 지정 hugepagesz=2M, hugepagesz=1G
hugepages= 지정된 크기의 예약 페이지 수 hugepages=1024
default_hugepagesz= 기본 Huge Page 크기 default_hugepagesz=2M
transparent_hugepage= THP 초기 모드 transparent_hugepage=madvise
hugepages=0:512,1:512 NUMA 노드별 예약 (커널 6.1+) Node0에 512개, Node1에 512개
# GRUB 부팅 파라미터 예제 (/etc/default/grub)
GRUB_CMDLINE_LINUX="default_hugepagesz=2M hugepagesz=2M hugepages=2048 hugepagesz=1G hugepages=16 transparent_hugepage=madvise"

# 효과: 2MB x 2048 = 4GB (2MB 풀) + 1GB x 16 = 16GB (1GB 풀)

성능 벤치마크와 튜닝

TLB 미스율 비교 벤치마크

아래 차트는 대규모 메모리 접근 워크로드에서 4KB 페이지와 2MB THP, 1GB hugetlbfs의 상대적 TLB 미스율과 처리량(Throughput) 차이를 보여줍니다.

16GB 메모리 랜덤 접근 벤치마크: TLB 미스율 및 처리량 비교 TLB 미스율 (상대값 %) 100% 75% 50% 25% 0% 100% 4KB 기준값 8% 2MB THP 6% 2MB hugetlbfs 0.4% 1GB hugetlbfs 상대 처리량 1.0x 4KB 1.15x THP 1.20x 2MB 1.30x 1GB 4KB 일반 2MB THP 2MB hugetlbfs 1GB hugetlbfs

perf를 활용한 TLB 미스 측정

# TLB 미스 이벤트 측정
perf stat -e dTLB-load-misses,dTLB-loads,iTLB-load-misses,iTLB-loads \
    -e dTLB-store-misses,dTLB-stores \
    ./my_application

# 출력 예시:
#  12,345,678  dTLB-load-misses     # 0.45% of all dTLB loads
#  2,741,234,567  dTLB-loads
#  1,234,567  iTLB-load-misses

# THP 활성화 후 동일 측정으로 미스율 비교
echo always > /sys/kernel/mm/transparent_hugepage/enabled
perf stat -e dTLB-load-misses,dTLB-loads ./my_application

# 페이지 워크 사이클 측정 (Intel 프로세서)
perf stat -e cpu/event=0x08,umask=0x01,name=dtlb_load_misses_walk_completed/ \
    ./my_application

주요 튜닝 가이드라인

워크로드추천 설정이유
데이터베이스 (PostgreSQL, MySQL) THP=madvise, defrag=madvise DB가 공유 버퍼에만 선택적으로 THP 적용
Redis / 인메모리 캐시 THP=never (hugetlbfs 사용 권장) THP 분할/병합의 지연 스파이크 방지
DPDK 패킷(Packet) 처리 hugetlbfs 1GB 전용 예약 최대 TLB 효율, 고정 할당 보장
KVM/QEMU VM THP=always 또는 hugetlbfs 백엔드 VM 메모리의 대부분이 대형 페이지 활용 가능
과학 계산 / HPC hugetlbfs 2MB/1GB 사전 예약 대규모 배열 순회 시 TLB 미스 최소화
일반 데스크탑 THP=always, defrag=defer+madvise 자동 최적화, 사용자 개입 불필요

모니터링과 디버깅(Debugging)

/proc/meminfo Huge Page 항목

# Huge Page 관련 /proc/meminfo 항목
cat /proc/meminfo | grep -i huge

# 출력 예시:
# AnonHugePages:    524288 kB    ← THP로 매핑된 익명 메모리
# ShmemHugePages:        0 kB    ← THP로 매핑된 공유 메모리
# FileHugePages:         0 kB    ← THP로 매핑된 파일 캐시
# HugePages_Total:    1024       ← 예약된 총 Huge Pages 수
# HugePages_Free:      512       ← 미사용 Huge Pages 수
# HugePages_Rsvd:      256       ← 예약되었지만 아직 할당 안 된 수
# HugePages_Surp:        0       ← surplus (초과 할당) 수
# Hugepagesize:       2048 kB    ← 기본 Huge Page 크기
# Hugetlb:          2097152 kB   ← HugeTLB에 사용 중인 총 메모리

/sys 파일시스템 인터페이스

# hugetlbfs 풀 상태
ls /sys/kernel/mm/hugepages/
# hugepages-1048576kB/  hugepages-2048kB/

# 2MB Huge Page 상세 정보
cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
cat /sys/kernel/mm/hugepages/hugepages-2048kB/free_hugepages
cat /sys/kernel/mm/hugepages/hugepages-2048kB/resv_hugepages
cat /sys/kernel/mm/hugepages/hugepages-2048kB/surplus_hugepages
cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_overcommit_hugepages

# THP 상태 및 설정
cat /sys/kernel/mm/transparent_hugepage/enabled
# [always] madvise never

cat /sys/kernel/mm/transparent_hugepage/defrag
cat /sys/kernel/mm/transparent_hugepage/use_zero_page
cat /sys/kernel/mm/transparent_hugepage/hpage_pmd_size
# 2097152 (= 2MB)

# khugepaged 통계
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_collapsed
cat /sys/kernel/mm/transparent_hugepage/khugepaged/full_scans

프로세스별 Huge Page 사용 현황

# 특정 프로세스의 smaps에서 Huge Page 매핑 확인
cat /proc/<pid>/smaps | grep -E "(AnonHugePages|ShmemPmd|FilePmd)"

# 간략한 요약
cat /proc/<pid>/smaps_rollup | grep -i huge
# AnonHugePages:    262144 kB

# 프로세스의 THP 사용 비율 계산
# AnonHugePages / (Anonymous 총 메모리) x 100 = THP 활용률

# 모든 프로세스의 Huge Page 사용량 정렬
for pid in /proc/[0-9]*; do
    hp=$(grep AnonHugePages "$pid/smaps_rollup" 2>/dev/null | awk '{print $2}')
    [ -n "$hp" ] && [ "$hp" -gt 0 ] && \
        echo "$hp kB  $(cat $pid/comm 2>/dev/null)  (PID: $(basename $pid))"
done | sort -rn | head -20

/proc/vmstat THP 통계

# THP 관련 vmstat 카운터
grep thp /proc/vmstat

# 주요 카운터 의미:
# thp_fault_alloc         - 페이지 폴트 시 THP 할당 성공
# thp_fault_fallback      - 페이지 폴트 시 THP 할당 실패 (4KB 폴백)
# thp_collapse_alloc      - khugepaged 병합 성공
# thp_collapse_alloc_failed - khugepaged 병합 실패
# thp_split_page          - THP 분할 (page 단위)
# thp_split_pmd           - PMD 분할 (매핑 단위)
# thp_zero_page_alloc     - 제로 THP 할당
# thp_deferred_split_page - 분할 지연 대기 중인 THP
# thp_swpout             - 스왑 아웃된 THP 수
# thp_swpout_fallback    - THP 스왑 아웃 실패 (분할 후 스왑)

실전 사용 사례

데이터베이스 (PostgreSQL)

PostgreSQL의 공유 버퍼(shared_buffers)는 대규모 메모리를 사용하므로 Huge Pages 적용 시 상당한 성능 향상을 얻을 수 있습니다.

# PostgreSQL Huge Pages 설정

# 1. 필요한 Huge Pages 수 계산
# shared_buffers = 8GB일 때: 8GB / 2MB = 4096개
# 여유분 포함: 4096 + 100 = 4196개
echo 4196 > /proc/sys/vm/nr_hugepages

# 2. PostgreSQL 설정 (postgresql.conf)
# huge_pages = try   # 또는 on (실패 시 시작 불가)
# shared_buffers = 8GB

# 3. postgres 사용자에게 huge page 권한 부여
# /etc/sysctl.conf:
# vm.hugetlb_shm_group = <postgres GID>

# 4. 확인
grep -i huge /proc/meminfo

DPDK 고성능 패킷 처리

DPDK(Data Plane Development Kit)는 커널을 우회하는 사용자 공간 패킷 처리 프레임워크로, 1GB Huge Pages를 사용하여 TLB 미스를 최소화하고 패킷 버퍼 접근 속도를 극대화합니다.

# DPDK용 1GB Huge Pages 설정

# 1. 부팅 파라미터 설정
# hugepagesz=1G hugepages=16 default_hugepagesz=1G

# 2. 또는 런타임에 2MB Huge Pages 예약
echo 8192 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages

# 3. hugetlbfs 마운트
mkdir -p /dev/hugepages-1G
mount -t hugetlbfs -o pagesize=1G none /dev/hugepages-1G

mkdir -p /dev/hugepages-2M
mount -t hugetlbfs -o pagesize=2M none /dev/hugepages-2M

# 4. DPDK EAL 파라미터
./dpdk-app --socket-mem 4096,4096 --huge-dir /dev/hugepages-1G

# 5. DPDK 메모리 정보 확인
dpdk-proc-info -- --stats

KVM/QEMU 가상 머신

KVM 가상 머신의 메모리를 Huge Pages로 백엔드하면 VM 내부의 메모리 접근 성능이 크게 향상됩니다. 특히 중첩 페이지 테이블(EPT/NPT)을 사용하는 환경에서 Huge Pages는 2단계 변환의 TLB 미스를 함께 줄여줍니다.

# QEMU에서 hugetlbfs 백엔드 사용
qemu-system-x86_64 \
    -m 16G \
    -mem-path /dev/hugepages-2M \
    -mem-prealloc \
    -smp 8 \
    -enable-kvm \
    ...

# libvirt XML 설정 (hugepages 사용)
# <memoryBacking>
#   <hugepages>
#     <page size="2048" unit="KiB"/>
#   </hugepages>
# </memoryBacking>

# NUMA별 Huge Page 크기 지정 (libvirt)
# <memoryBacking>
#   <hugepages>
#     <page size="1048576" unit="KiB" nodeset="0"/>
#     <page size="2048" unit="KiB" nodeset="1"/>
#   </hugepages>
# </memoryBacking>
워크로드별 Huge Page 추천 구성 Huge Pages 2MB / 1GB 대형 페이지 데이터베이스 hugetlbfs + THP=madvise DPDK / SmartNIC hugetlbfs 1GB 전용 KVM / QEMU hugetlbfs + THP=always HPC / 과학 계산 hugetlbfs 2MB/1GB Redis / 캐시 THP=never, hugetlbfs 일반 서버/데스크탑 THP=always, defrag=defer THP의 분할/병합 지연이 문제되는 지연 민감 워크로드는 hugetlbfs 정적 예약을 우선 고려
Redis와 THP 경고: Redis는 공식적으로 THP 비활성화를 권장합니다. THP의 분할/병합 과정에서 발생하는 지연 스파이크가 Redis의 밀리초 단위 응답 시간에 악영향을 미칩니다. Redis 서버에서는 반드시 echo never > /sys/kernel/mm/transparent_hugepage/enabled를 실행하세요. Redis 시작 시 이 설정을 경고하는 로그도 출력됩니다.

HVO — HugeTLB Vmemmap Optimization

HugeTLB 페이지는 실제 데이터 페이지 외에도 메타데이터(struct page) 배열이 필요합니다. 1GB Huge Page 하나는 4KB 페이지 262,144개로 구성되므로, 기본 방식에서는 struct page도 같은 수만큼 필요합니다. 커널 6.x의 HVO(HugeTLB Vmemmap Optimization)는 Tail 페이지의 vmemmap을 공유/재활용(Recycling)하여 메타데이터 메모리 오버헤드(Overhead)를 크게 줄입니다.

HugeTLB vmemmap 오버헤드와 HVO 최적화 기본 방식 (최적화 없음) 1GB Huge Page = 262,144개의 struct page Head + 모든 Tail 페이지 각각 vmemmap 엔트리 유지 메타데이터 메모리 소비: 262,144 x sizeof(struct page) (대략 수 MB) Huge Page를 많이 쓰는 시스템에서 vmemmap 자체가 의미 있는 메모리 압박 요인 HVO 활성화 Tail 페이지 vmemmap 중복분 제거/재활용 필수 엔트리만 남기고 나머지는 재매핑/해제 효과: HugeTLB 메타데이터 오버헤드 감소 대규모 1GB Huge Page 풀에서 체감 큼 조건: CONFIG_HUGETLB_PAGE_OPTIMIZE_VMEMMAP=y
# HVO 지원 여부 점검
grep HUGETLB_PAGE_OPTIMIZE_VMEMMAP /boot/config-$(uname -r)

# 커널 로그에서 HugeTLB/VMEMMAP 관련 메시지 확인
dmesg | grep -Ei 'hugetlb|vmemmap|hvo'

cgroup v2와 컨테이너 Huge Pages

컨테이너 환경에서는 Huge Pages를 일반 메모리와 별도로 제한해야 합니다. cgroup v2는 페이지 크기별 hugepage 한도를 제공하며, overcommit 정책과 결합해 특정 워크로드가 HugeTLB 풀을 독점하지 않도록 제어합니다.

cgroup v2 HugeTLB 제어 계층 시스템 HugeTLB 풀 2MB / 1GB 전역 페이지 cgroup A (DB) hugetlb.2MB.max=8G hugetlb.1GB.max=4G cgroup B (DPDK) hugetlb.2MB.max=2G hugetlb.1GB.max=8G 핵심 포인트 메모리 cgroup(limit_in_bytes)와 HugeTLB cgroup은 별도 회계 한도 초과 시 MAP_HUGETLB 할당이 즉시 실패 (OOM killer와 경로 다름)
# cgroup v2 HugeTLB 한도 예시
CG=/sys/fs/cgroup/mydb
mkdir -p $CG
echo $((8*1024*1024*1024))  > $CG/hugetlb.2MB.max
echo $((4*1024*1024*1024))  > $CG/hugetlb.1GB.max

# 사용량/실패 통계
cat $CG/hugetlb.2MB.current
cat $CG/hugetlb.2MB.events

THP 분할과 NUMA demotion 경로

THP는 항상 유지되는 것이 아니라, 메모리 압박·NUMA 재배치(Relocation)·mprotect/munmap 이벤트로 분할될 수 있습니다. 특히 자동 NUMA balancing이 켜진 시스템에서는 원격 노드 접근 패턴에 따라 페이지 이동이 발생하고, 이 과정에서 PMD 단위 매핑이 PTE 단위로 강등될 수 있습니다.

THP 생애주기: 할당 → 유지 → 분할/재병합 PMD THP 생성 thp_fault_alloc++ 정상 실행 TLB 효율 최대 분할 이벤트 split_huge_page PTE 512개 4KB 모드 khugepaged 조건 충족 시 재병합 분할 유발 주요 원인 mprotect/munmap 부분 범위, swap, GUP pin, NUMA migration, 메모리 회수 분할이 잦으면 thp_split_* 카운터가 빠르게 증가
# THP 분할/병합 상태 추적
grep -E 'thp_split|thp_collapse|thp_fault' /proc/vmstat

# numa balancing과 함께 확인
grep -E 'numa_hint_faults|numa_pages_migrated' /proc/vmstat

Huge Pages 장애 대응 플레이북

Huge Pages 문제는 대부분 "할당 실패", "예상보다 낮은 THP 활용률", "지연 스파이크"로 나타납니다. 아래 절차로 원인을 분류하면 대응이 빨라집니다.

Huge Pages 문제 분석 절차 1) 증상 확인 mmap 실패/지연 스파이크 2) 풀 상태 점검 HugePages_Free/Rsvd 3) THP 통계 확인 thp_fault_fallback 4) 정책 조정 예약/defrag/모드 판정 기준 예시 MAP_HUGETLB 실패 + HugePages_Free=0: 예약 부족 또는 경쟁 사용 thp_fault_fallback 급증: compaction 실패/분산 메모리 단편화 thp_split_page 급증: workload 패턴상 분할 유발 (권한 변경/부분 unmap) 가능성
# 1) HugeTLB/THP 상태 요약
grep -i huge /proc/meminfo
grep -E 'thp_fault|thp_split|thp_collapse' /proc/vmstat

# 2) THP 모드/defrag 확인
cat /sys/kernel/mm/transparent_hugepage/enabled
cat /sys/kernel/mm/transparent_hugepage/defrag

# 3) compaction 압력 확인
grep compact /proc/vmstat

# 4) 프로세스별 HugePages 사용 확인
cat /proc/<pid>/smaps_rollup | grep -i huge
운영 권장: 지연 민감 서비스는 THP를 전역 madvise로 두고, 필요한 프로세스에만 MADV_HUGEPAGE를 적용하는 방식이 가장 예측 가능성이 높습니다.

khugepaged collapse 실패 분석

THP 성능을 기대했는데 실제로는 일반 페이지로 남는 경우, 핵심은 khugepaged의 collapse 실패 원인을 분리하는 것입니다. 연속 가상 주소가 있어도 물리 단편화(Fragmentation), 참조 상태, 페이지 핀 고정 상태에 따라 collapse가 반복 실패할 수 있습니다.

khugepaged collapse 실패 분기 후보 VMA 스캔 khugepaged PMD 범위 512개 PTE 점검 mapcount/refcount/dirty 상태 collapse 성공/실패 다음 스캔 주기 주요 실패 원인 1) compaction 실패로 연속 물리 메모리 확보 불가 2) 장기 pin(FOLL_PIN/RDMA) 페이지 존재 3) 자주 split되는 접근 패턴 (부분 mprotect/munmap) 4) 정책 미스매치: madvise/always/never 설정 부적합
# khugepaged collapse 실패 원인 진단
# 1) collapse 성공/실패 통계 확인
grep -E 'thp_collapse_alloc|thp_collapse_alloc_failed' /proc/vmstat
# thp_collapse_alloc_failed가 높으면 물리 단편화 문제

# 2) compaction 상태 확인 — collapse는 연속 512페이지가 필요
grep -E 'compact_success|compact_fail|compact_stall' /proc/vmstat

# 3) 특정 프로세스의 THP 사용 현황
grep AnonHugePages /proc/<PID>/smaps_rollup  # 실제 THP 매핑 크기
grep -c 'AnonHugePages:        0' /proc/<PID>/smaps  # THP 미적용 VMA 수

# 4) khugepaged 스캔 속도 튜닝
cat /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan   # 스캔당 페이지 수
cat /sys/kernel/mm/transparent_hugepage/khugepaged/scan_sleep_millisecs  # 스캔 간격
echo 4096 > /sys/kernel/mm/transparent_hugepage/khugepaged/pages_to_scan  # 더 공격적 스캔
/* khugepaged collapse 핵심 경로 (mm/khugepaged.c) */
static int collapse_huge_page(struct mm_struct *mm,
                              unsigned long address, int referenced)
{
    /* 1단계: order-9 compound page 할당 시도 */
    new_page = khugepaged_alloc_page(hpage, gfp, node);
    if (!new_page)
        return SCAN_ALLOC_HUGE_PAGE_FAIL;  /* compaction 실패 */

    /* 2단계: 512개 PTE를 순회하며 복사 가능 여부 점검 */
    for (_address = address; _address < address + HPAGE_PMD_SIZE;
         _address += PAGE_SIZE) {
        /* 핀 고정, swap, 특수 페이지 등 점검 */
        if (page_count(page) != 1 + page_mapcount(page))
            return SCAN_PAGE_COUNT;  /* 참조 카운트 불일치 */
    }

    /* 3단계: 4KB 페이지 → 2MB compound page 복사 후 PMD 교체 */
    copy_user_highpage(new_page + i, page, _address, vma);
    set_pmd_at(mm, address, pmd, _pmd);  /* PMD 엔트리 갱신 */
}
코드 설명

collapse_huge_page()는 khugepaged가 512개의 연속 4KB 페이지를 하나의 2MB THP로 병합하는 핵심 함수입니다. 먼저 order-9 compound page를 할당하고, 대상 영역의 모든 PTE를 순회하며 복사 가능 여부(참조 카운트, 핀 상태, swap 여부)를 점검합니다. 모든 조건을 충족하면 데이터를 복사하고 PMD 엔트리를 갱신하여 직접 매핑으로 전환합니다.

HugeTLB 풀 운영 정책 (예약/버스(Bus)트/격리(Isolation))

HugeTLB는 사전 예약 방식이라 서비스별 용량 계획이 중요합니다. 버스트 트래픽이 있는 서비스는 최소 예약과 버스트 여유를 분리하고, 컨테이너 환경에서는 cgroup 제한과 함께 운영해야 안정적입니다.

운영 시나리오권장 정책실패 시 신호
고정 워크로드 DB정적 hugepages 예약 + 재부팅 시 일관 적용MAP_HUGETLB 실패 없음, 지연 안정
버스트 분석 작업기본 예약 + 버스트 한도 별도HugePages_Free 급락 후 복구 지연
컨테이너 다중 테넌트cgroup huge limit로 테넌트 격리일부 테넌트 과점유/기아(Starvation)
# HugeTLB 풀 상태 확인
grep -i huge /proc/meminfo

# THP collapse/split 동향
grep -E 'thp_fault|thp_collapse|thp_split' /proc/vmstat

# 서비스별 smaps_rollup에서 huge 사용량 확인
cat /proc/<pid>/smaps_rollup | grep -Ei 'AnonHuge|FileHuge|Hugetlb'

File THP와 워크로드 적합성 판단

최근 커널에서는 익명 메모리뿐 아니라 파일 기반 매핑에서도 THP 효과를 노릴 수 있는 경로가 확대되고 있습니다. 하지만 모든 파일 I/O 패턴에 이득이 있는 것은 아니며, 재사용 지역성과 fault 패턴을 기준으로 판단해야 합니다.

File THP 적용 판단 흐름 파일 매핑 워크로드 read-mostly / random fault/reclaim 패턴 계측 file fault locality 평가 THP 정책 결정 적용/제외 분리 적합/비적합 예시 적합: 대형 순차 스캔 + 높은 재사용 지역성 + fault 집중 비적합: 랜덤 접근 + 잦은 split 유발 + reclaim 압력 높은 환경 결론: 전역 강제보다 워크로드별 선택 적용이 안정적 성능/메모리 효율/지연을 함께 측정해 의사결정
패턴기대 효과주의점
순차 읽기 중심 분석TLB miss 완화, fault 수 감소reclaim 시 큰 단위 회수 비용
랜덤 조회 캐시효과 제한적split/compaction 오버헤드 가능
혼합 워크로드프로세스/영역별 선택 적용 필요정책 일괄 적용 시 회귀 위험
# File THP 적합성 판단을 위한 계측
# 1) 파일 매핑의 THP 사용 현황 (커널 6.8+ mTHP 포함)
grep -E 'FilePmdMapped|FileHugePages' /proc/meminfo

# 2) fault 패턴 분석 — 순차 vs 랜덤 판단
perf stat -e page-faults,dTLB-load-misses -p <PID> -- sleep 10

# 3) 프로세스별 madvise로 선택적 THP 적용
# 코드에서: madvise(addr, len, MADV_HUGEPAGE)  — 해당 영역만 THP 활성
# 코드에서: madvise(addr, len, MADV_NOHUGEPAGE) — 해당 영역 THP 비활성

# 4) 전역 THP를 madvise 모드로 전환 (워크로드별 선택 적용)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
/* File THP 페이지 폴트 경로 (mm/filemap.c, 커널 6.8+) */
static vm_fault_t filemap_map_folio_range(struct vm_fault *vmf,
                                          struct folio *folio,
                                          pgoff_t start, pgoff_t end)
{
    /* folio order에 따라 PTE 매핑 크기 결정
     * order-0: 4KB, order-4: 64KB (mTHP), order-9: 2MB
     */
    unsigned int nr_pages = folio_nr_pages(folio);
    /* 연속된 PTE에 folio 내 페이지를 매핑 */
    set_pte_range(vmf, folio, page, nr_pages, vmf->address);
}
코드 설명

filemap_map_folio_range()는 파일 매핑의 페이지 폴트 시 folio 단위로 PTE 매핑을 수행합니다. 커널 6.8+의 mTHP에서는 folio의 order에 따라 64KB, 2MB 등 다양한 크기의 연속 매핑이 가능합니다. 순차 접근 패턴의 워크로드에서는 큰 folio로 fault 수가 감소하여 성능이 향상되지만, 랜덤 접근에서는 불필요한 메모리 소비가 발생할 수 있습니다.

hugetlb_fault() 호출 체인

사용자 공간에서 MAP_HUGETLB로 매핑한 영역에 처음 접근하면 페이지 폴트(Page Fault)가 발생합니다. 일반 페이지 폴트와 달리 Huge Page 폴트는 전용 처리 경로를 타고, 최종적으로 alloc_huge_page()에서 풀(Pool) 또는 Buddy 할당자로부터 대형 페이지를 획득합니다.

hugetlb_fault() 호출 체인 handle_mm_fault() mm/memory.c — 폴트 진입점 is_vm_hugetlb? No → __handle_mm_fault() Yes hugetlb_fault() mm/hugetlb.c — PTE 확인, 락 획득 huge_pte_none? No → hugetlb_wp() (CoW) Yes hugetlb_no_page() 신규 Huge Page 할당 + 매핑 alloc_huge_page() 예약 풀 우선 → surplus → buddy fallback dequeue_huge_page_vma() alloc_buddy_huge_page() freelist에서 꺼냄 (빠름) buddy에서 order-9/18 할당 (느림)

handle_mm_fault()는 VMA 플래그에서 VM_HUGETLB를 확인하면 일반 __handle_mm_fault() 대신 hugetlb_fault()를 호출합니다. 이 함수는 huge PTE를 조회한 뒤, 항목이 비어 있으면 hugetlb_no_page()로 분기하여 새 Huge Page를 할당하고 PTE에 기록합니다. 이미 PTE가 존재하면서 쓰기 폴트인 경우 hugetlb_wp()가 Copy-on-Write를 처리합니다.

/* mm/memory.c — 페이지 폴트 진입점 (간략화) */
vm_fault_t handle_mm_fault(struct vm_area_struct *vma,
                           unsigned long address,
                           unsigned int flags,
                           struct pt_regs *regs)
{
    /* ... */
    if (is_vm_hugetlb_page(vma))
        return hugetlb_fault(vma->vm_mm, vma, address, flags);

    return __handle_mm_fault(vma, address, flags);
}

/* mm/hugetlb.c — Huge Page 폴트 핸들러 (간략화) */
vm_fault_t hugetlb_fault(struct mm_struct *mm,
                         struct vm_area_struct *vma,
                         unsigned long address,
                         unsigned int flags)
{
    struct hstate *h = hstate_vma(vma);
    pte_t *ptep, entry;

    ptep = huge_pte_alloc(mm, vma, address, huge_page_size(h));
    if (!ptep)
        return VM_FAULT_OOM;

    entry = huge_ptep_get(ptep);

    if (huge_pte_none(entry))
        return hugetlb_no_page(mm, vma, address, ptep, flags);

    if (flags & FAULT_FLAG_WRITE && !huge_pte_write(entry))
        return hugetlb_wp(mm, vma, address, ptep, flags, ...);

    return 0;
}
코드 설명
  • 8행 is_vm_hugetlb_page()는 VMA의 vm_flagsVM_HUGETLB가 설정되어 있는지 확인합니다. hugetlbfs 위에 생성된 매핑이면 항상 참입니다.
  • 9행 Huge Page 전용 폴트 핸들러로 분기합니다. 일반 __handle_mm_fault()와 완전히 다른 경로입니다.
  • 21행 hstate_vma()는 VMA에 연결된 hstate를 찾아 해당 Huge Page 크기(2MB/1GB)의 상태 정보를 반환합니다.
  • 23행 huge_pte_alloc()는 페이지 테이블에서 해당 가상 주소에 대응하는 huge PTE 슬롯을 확보합니다. 2MB의 경우 PMD 엔트리, 1GB의 경우 PUD 엔트리입니다.
  • 28행 PTE가 비어 있으면 최초 접근이므로 hugetlb_no_page()에서 새 Huge Page를 할당하고 매핑합니다.
  • 30~31행 쓰기 폴트인데 PTE가 읽기 전용이면 Copy-on-Write(CoW)가 필요합니다. hugetlb_wp()가 새 Huge Page를 할당하고 내용을 복사합니다.

struct hstate 필드별 해설

struct hstate는 Huge Page 크기별 풀(Pool) 전체 상태를 관리하는 핵심 구조체입니다. include/linux/hugetlb.h에 정의되며, 시스템에서 지원하는 각 Huge Page 크기마다 하나의 hstate 인스턴스가 전역 배열 hstates[]에 존재합니다.

/* include/linux/hugetlb.h — struct hstate 전체 필드 (v6.x 기준) */
struct hstate {
    struct mutex resize_lock;              /* 풀 크기 변경 보호 */
    int next_nid_to_alloc;                  /* 다음 할당 대상 NUMA 노드 */
    int next_nid_to_free;                   /* 다음 해제 대상 NUMA 노드 */
    unsigned int order;                     /* buddy order (2MB=9, 1GB=18) */
    unsigned int demote_order;               /* 디모트 시 목표 order */
    unsigned long mask;                     /* 주소 정렬 마스크 */
    unsigned long max_huge_pages;            /* 허용 최대 페이지 수 */
    unsigned long nr_huge_pages;             /* 현재 총 Huge Pages 수 */
    unsigned long free_huge_pages;           /* 미사용 (freelist) 수 */
    unsigned long resv_huge_pages;           /* mmap 예약분 (guarantee) */
    unsigned long surplus_huge_pages;        /* 초과 할당분 (overcommit) */
    unsigned long nr_overcommit_huge_pages;  /* surplus 허용 한도 */
    struct list_head hugepage_activelist;    /* 사용 중인 페이지 목록 */
    struct list_head hugepage_freelists[MAX_NUMNODES]; /* 노드별 여유 목록 */
    unsigned int nr_huge_pages_node[MAX_NUMNODES];    /* 노드별 총 수 */
    unsigned int free_huge_pages_node[MAX_NUMNODES];  /* 노드별 여유 수 */
    unsigned int surplus_huge_pages_node[MAX_NUMNODES]; /* 노드별 surplus */
    char name[HSTATE_NAME_LEN];              /* "hugepages-2048kB" 등 */
};
필드타입역할관련 sysfs / procfs
orderunsigned intBuddy 할당자 order. 2MB = 9 (2^9 * 4KB), 1GB = 18 (2^18 * 4KB)-
nr_huge_pagesunsigned long현재 시스템에 존재하는 해당 크기 Huge Page 총 수/proc/meminfo HugePages_Total
free_huge_pagesunsigned longfreelist에 있는 미사용 Huge Page 수/proc/meminfo HugePages_Free
resv_huge_pagesunsigned longmmap 호출 시 예약되었으나 아직 폴트가 발생하지 않은 수/proc/meminfo HugePages_Rsvd
surplus_huge_pagesunsigned longovercommit 정책으로 부팅 예약 외에 추가 할당된 수/proc/meminfo HugePages_Surp
nr_overcommit_huge_pagesunsigned longsurplus 허용 상한. 이 수까지 buddy에서 동적 할당 허용nr_overcommit_hugepages
hugepage_freelists[]list_head[MAX_NUMNODES]NUMA 노드별 여유 Huge Page 리스트. dequeue_huge_page_vma()가 여기서 꺼냄-
hugepage_activelistlist_head현재 매핑되어 사용 중인 Huge Page 리스트-
resize_lockmutex풀 크기 변경(nr_hugepages 쓰기) 시 직렬화-
next_nid_to_alloc/freeint라운드 로빈 NUMA 노드 인덱스. 노드 간 균등 분배에 사용-
demote_orderunsigned int1GB를 2MB로 분할(demote)할 때 목표 order. v5.18+에서 지원demote_size
maskunsigned long주소 정렬 마스크. 2MB: ~(2MB-1), 1GB: ~(1GB-1)-
핵심 불변식(Invariant): nr_huge_pages = free_huge_pages + 사용중(active) + surplus_huge_pages에서의 사용분
free_huge_pages ≥ resv_huge_pages — 예약분은 항상 여유 풀에서 보장됩니다. 이 불변식이 깨지면 alloc_huge_page()-ENOSPC를 반환합니다.

struct hugetlbfs_inode_info 주요 필드

hugetlbfs 파일시스템(Filesystem)에서 각 파일(inode)은 struct hugetlbfs_inode_info로 확장됩니다. 이 구조체는 Huge Page 예약 맵(Reservation Map), 정책 플래그, shared 매핑의 공유 상태 등을 inode 단위로 관리합니다. fs/hugetlbfs/inode.c에 정의되어 있습니다.

/* fs/hugetlbfs/inode.c — hugetlbfs inode 확장 정보 (간략화) */
struct hugetlbfs_inode_info {
    struct shared_policy policy;           /* NUMA 메모리 정책 */
    struct inode vfs_inode;                /* VFS inode (컨테이너 패턴) */
    unsigned int seals;                    /* F_SEAL_* 플래그 (memfd) */
};

/* include/linux/hugetlb.h — 파일별 예약 맵 */
struct resv_map {
    struct kref refs;                     /* 참조 카운트 */
    spinlock_t lock;                       /* 동시 접근 보호 */
    struct list_head regions;              /* file_region 리스트 */
    long adds_in_progress;                 /* 진행 중인 예약 추가 수 */
    struct list_head region_cache;         /* 미리 할당된 region 캐시 */
    long region_cache_count;               /* 캐시 내 region 수 */
    struct page_counter *reservation_counter; /* cgroup 예약 카운터 */
    unsigned long pages_per_hpage;         /* Huge Page당 base page 수 */
};
코드 설명
  • 3행 shared_policymbind()로 설정한 NUMA 정책을 구간별로 저장합니다. 프로세스가 아닌 파일 단위로 관리되므로 shared 매핑 시 모든 프로세스에 동일 정책이 적용됩니다.
  • 4행 vfs_inode는 컨테이너(Container) 패턴의 핵심입니다. container_of() 매크로로 VFS inode 포인터에서 hugetlbfs_inode_info를 역참조할 수 있습니다.
  • 5행 sealsmemfd_create()로 생성한 hugetlbfs 파일에 F_SEAL_SHRINK, F_SEAL_GROW 등의 변경 제한을 설정합니다.
  • 9~17행 resv_map은 inode의 i_mapping->private_data에 저장됩니다. regions 리스트가 예약된 파일 오프셋 범위를 file_region 구조체로 관리하며, mmap 시 예약을 추가하고 munmap 시 해제합니다.
  • 14행 region_cache는 예약 추가 시 메모리 할당 실패를 방지하기 위해 file_region을 미리 할당해 캐싱합니다. 원자적(Atomic) 컨텍스트에서도 안전하게 예약을 추가할 수 있습니다.
구조체.필드역할생성 시점
hugetlbfs_inode_info.policyNUMA 정책 트리 (red-black tree)alloc_inode()
hugetlbfs_inode_info.sealsmemfd seal 비트마스크memfd_create(MFD_HUGETLB)
resv_map.regions예약된 [from, to) 범위 리스트resv_map_alloc()
resv_map.adds_in_progress동시 예약 추가 트래킹 (race 방지)region_add() 진입 시
resv_map.region_cache미리 할당된 file_region 풀region_chg() 호출 시

hugetlb_no_page() 함수 구현 분석

hugetlb_no_page()는 Huge Page 폴트에서 가장 핵심적인 함수로, PTE가 비어 있는 상태(최초 접근)에서 새 Huge Page를 할당하고 페이지 테이블에 매핑하는 전체 과정을 수행합니다. mm/hugetlb.c에 위치하며, 약 200줄의 복잡한 함수이지만 핵심 흐름은 아래와 같이 요약할 수 있습니다.

/* mm/hugetlb.c — hugetlb_no_page() 핵심 흐름 (간략화) */
static vm_fault_t hugetlb_no_page(
    struct mm_struct *mm, struct vm_area_struct *vma,
    unsigned long address, pte_t *ptep,
    unsigned int flags)
{
    struct hstate *h = hstate_vma(vma);
    struct page *page;
    pte_t new_pte;
    bool new_page = false;

    /* 1단계: 페이지 캐시에서 기존 페이지 검색 (shared 매핑) */
    page = find_lock_page(vma->vm_file->f_mapping,
                           vma_hugecache_offset(h, vma, address));

    if (!page) {
        /* 2단계: 새 Huge Page 할당 */
        page = alloc_huge_page(vma, address, 0);
        if (IS_ERR(page))
            return VM_FAULT_SIGBUS;  /* 할당 실패 → SIGBUS */

        /* 3단계: 페이지 초기화 (zero fill) */
        clear_huge_page(page, address, pages_per_huge_page(h));
        __SetPageUptodate(page);
        new_page = true;

        /* shared 매핑이면 페이지 캐시에 삽입 */
        if (vma->vm_flags & VM_SHARED)
            hugetlb_add_to_page_cache(page, vma->vm_file->f_mapping, ...);
    }

    /* 4단계: PTE 구성 및 설정 */
    new_pte = make_huge_pte(vma, page, vma->vm_flags & VM_WRITE);
    set_huge_pte_at(mm, address, ptep, new_pte);

    /* 5단계: rmap 등록 (역매핑 정보) */
    if (new_page)
        page_add_new_anon_rmap(page, vma, address);
    else
        page_add_file_rmap(page, vma, true);

    return 0;
}
코드 설명
  • 7행 hstate_vma()로 이 VMA가 사용하는 Huge Page 크기(2MB/1GB)의 hstate를 가져옵니다.
  • 13~14행 shared 매핑의 경우 다른 프로세스가 이미 같은 파일 오프셋에 Huge Page를 할당했을 수 있으므로, 페이지 캐시(Page Cache)를 먼저 검색합니다.
  • 18행 alloc_huge_page()는 예약 풀 → surplus → buddy 순서로 Huge Page를 할당합니다. 실패 시 ERR_PTR(-ENOSPC)를 반환합니다.
  • 20행 할당 실패 시 VM_FAULT_SIGBUS를 반환하여 프로세스에 SIGBUS 시그널을 전달합니다. 일반 페이지 폴트의 OOM killer 호출과는 다른 처리입니다.
  • 23행 clear_huge_page()는 보안상 필수입니다. 이전 사용자의 데이터가 남아있을 수 있으므로 전체 2MB/1GB를 0으로 채웁니다.
  • 28~29행 shared 매핑이면 페이지 캐시에 삽입하여 다른 프로세스가 같은 파일 오프셋에 접근할 때 이 페이지를 재사용할 수 있게 합니다.
  • 32행 make_huge_pte()는 물리 페이지 프레임 번호(PFN)와 보호 비트를 결합하여 huge PTE 값을 생성합니다.
  • 33행 set_huge_pte_at()는 아키텍처별 함수로, PMD(2MB) 또는 PUD(1GB) 엔트리에 huge PTE를 원자적으로 기록합니다.
  • 36~39행 rmap(역매핑)을 등록하여 나중에 페이지 회수나 마이그레이션 시 이 페이지를 매핑한 모든 VMA를 찾을 수 있게 합니다.

THP do_huge_pmd_anonymous_page() 및 khugepaged collapse 경로

THP(Transparent Huge Pages)는 hugetlbfs와 전혀 다른 경로로 대형 페이지를 생성합니다. 두 가지 주요 진입점이 있습니다:

  1. 즉시 할당: 익명 페이지 폴트 시 do_huge_pmd_anonymous_page()가 직접 2MB를 할당
  2. 지연 병합: khugepaged 데몬이 기존 4KB 페이지 512개를 하나의 THP로 collapse
THP 생성 경로: 즉시 할당 vs khugepaged collapse 경로 1: 즉시 할당 (페이지 폴트) handle_pte_fault() do_huge_pmd_anonymous_page() alloc_pages_vma(order=9, GFP_TRANSHUGE) set_pmd_at() → 2MB PMD 매핑 완성 실패 시: VM_FAULT_FALLBACK → 일반 4KB do_anonymous_page() 경로 2: 지연 병합 (khugepaged) khugepaged_scan_mm_slot() hpage_collapse_scan_pmd() collapse_huge_page() 512개 PTE → 1개 PMD 교체 기존 4KB 페이지 내용 복사 후 해제 실패 조건: 혼합 NUMA, mlock, 핀, shared → max_ptes_none/shared 임계값 초과 시 skip 결과: PMD 엔트리가 2MB 물리 프레임을 직접 가리킴

do_huge_pmd_anonymous_page() 핵심 흐름

/* mm/huge_memory.c — THP 익명 페이지 폴트 (간략화) */
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
    struct vm_area_struct *vma = vmf->vma;
    gfp_t gfp;
    struct page *page;
    unsigned long haddr = vmf->address & HPAGE_PMD_MASK;

    /* VMA가 2MB 정렬 범위를 충분히 커버하는지 검증 */
    if (haddr < vma->vm_start || haddr + HPAGE_PMD_SIZE > vma->vm_end)
        return VM_FAULT_FALLBACK;

    /* THP 정책 확인: always / madvise / never */
    if (transparent_hugepage_flags == TRANSPARENT_HUGEPAGE_NEVER)
        return VM_FAULT_FALLBACK;

    /* zero page 최적화: 읽기 전용 폴트면 huge zero page 매핑 */
    if (!(vmf->flags & FAULT_FLAG_WRITE) &&
        !mm_forbids_zeropage(vma->vm_mm)) {
        pgtable_t pgtable = pte_alloc_one(vma->vm_mm);
        page = mm_get_huge_zero_page(vma->vm_mm);
        if (page) {
            set_huge_zero_page(pgtable, vma->vm_mm, vma, haddr, vmf->pmd, page);
            return VM_FAULT_NOPAGE;
        }
    }

    /* 2MB compound page 할당 시도 */
    gfp = vma_thp_gfp_mask(vma);
    page = vma_alloc_folio(gfp, HPAGE_PMD_ORDER, vma, haddr, true);
    if (unlikely(!page)) {
        count_vm_event(THP_FAULT_FALLBACK);
        return VM_FAULT_FALLBACK;    /* 할당 실패 → 4KB fallback */
    }

    prep_transhuge_page(page);
    /* ... PMD 설정, rmap 등록, LRU 추가 ... */
    count_vm_event(THP_FAULT_ALLOC);
    return VM_FAULT_NOPAGE;
}
코드 설명
  • 7행 HPAGE_PMD_MASK로 폴트 주소를 2MB 경계로 내림 정렬합니다. THP는 반드시 2MB 정렬된 가상 주소에 매핑되어야 합니다.
  • 10~11행 VMA 범위가 2MB 영역을 완전히 포함하지 않으면 THP를 생성할 수 없으므로 VM_FAULT_FALLBACK으로 4KB 경로로 전환합니다.
  • 17~25행 읽기 전용 폴트이면 커널 전역의 huge zero page(물리 메모리 2MB를 하나만 공유)를 매핑합니다. 프로세스 시작 시 BSS 영역 등에서 메모리를 크게 절약합니다.
  • 28~29행 GFP_TRANSHUGE__GFP_NORETRY | __GFP_COMP 등을 포함하여, 할당 실패 시 과도한 재시도 없이 빠르게 폴백합니다.
  • 30~33행 order-9 compound page 할당이 실패하면 THP_FAULT_FALLBACK vmstat 카운터를 증가시키고 일반 4KB 경로로 전환합니다. 이것이 THP의 "투명성"의 핵심입니다.
  • 35행 prep_transhuge_page()는 compound page에 THP 전용 소멸자(free_transhuge_page)를 설정하고 deferred split 큐에 등록할 준비를 합니다.

khugepaged collapse_huge_page() 핵심 흐름

/* mm/khugepaged.c — collapse 핵심 (간략화) */
static int collapse_huge_page(
    struct mm_struct *mm, unsigned long address,
    struct collapse_control *cc)
{
    struct page *new_page;
    pmd_t *pmd, _pmd;

    /* 1단계: 새 2MB 페이지 할당 */
    new_page = khugepaged_alloc_page(cc, HPAGE_PMD_ORDER);
    if (!new_page)
        return SCAN_ALLOC_HUGE_PAGE_FAIL;

    /* 2단계: mmap_write_lock으로 mm 보호 */
    mmap_write_lock(mm);

    /* 3단계: 512개 PTE 스캔 — 각 4KB 페이지를 new_page에 복사 */
    __collapse_huge_page_copy(pte, new_page, vma, address, ptl);

    /* 4단계: PTE 테이블을 PMD 엔트리로 교체 */
    _pmd = mk_huge_pmd(new_page, vma->vm_page_prot);
    _pmd = maybe_pmd_mkwrite(pmd_mkdirty(_pmd), vma);
    pmdp_collapse_flush(vma, address, pmd);
    set_pmd_at(mm, address, pmd, _pmd);

    /* 5단계: 기존 4KB 페이지들 해제 */
    __collapse_huge_page_release(...);

    mmap_write_unlock(mm);
    return SCAN_SUCCEED;
}
코드 설명
  • 10행 khugepaged_alloc_page()는 order-9 compound page를 할당합니다. 여기서 실패하면 SCAN_ALLOC_HUGE_PAGE_FAIL로 이번 스캔을 건너뜁니다.
  • 15행 mmap_write_lock()으로 mm 전체를 잠급니다. collapse 중에 다른 스레드가 같은 영역을 수정하면 안 되기 때문입니다. 이것이 khugepaged의 주요 지연 원인입니다.
  • 18행 __collapse_huge_page_copy()가 512개의 4KB 페이지 내용을 새 2MB 페이지에 복사합니다. 비어 있는 PTE(아직 폴트가 발생하지 않은 슬롯)는 0으로 채웁니다.
  • 23행 pmdp_collapse_flush()는 기존 PMD(PTE 테이블을 가리키던)를 무효화하고 TLB 플러시를 수행합니다. 이 순간 다른 CPU에서 해당 영역 접근 시 일시적으로 폴트가 발생합니다.
  • 24행 set_pmd_at()으로 새 PMD 엔트리를 설정하면 2MB 직접 매핑이 완성됩니다. 이후 TLB 미스 시 PMD에서 바로 물리 주소를 얻어 PTE 레벨을 건너뜁니다.
collapse 실패 조건:
  • 512개 PTE 중 max_ptes_none(기본 511)개를 초과하는 빈 슬롯이 있으면 skip
  • 페이지가 mlock(), GUP pin, 또는 다른 NUMA 노드에 분산되어 있으면 skip
  • max_ptes_shared(기본 256)개를 초과하는 파일 페이지가 있으면 skip
  • compaction으로도 order-9 연속 프레임을 확보하지 못하면 SCAN_ALLOC_HUGE_PAGE_FAIL
/proc/vmstatthp_collapse_alloc_failedthp_scan_exceed_* 카운터로 실패 원인을 진단할 수 있습니다.

mTHP — Multi-size THP (커널 6.8+)

기존 THP는 x86_64에서 오직 2MB(PMD 레벨) 크기만 지원했습니다. 커널 6.8부터 도입된 mTHP(Multi-size THP)는 2MB보다 작은 다양한 크기 (16KB, 32KB, 64KB, 128KB, 256KB, 512KB, 1MB 등)의 대형 폴리오(Folio)를 지원합니다. 이로써 2MB 전체를 확보하기 어려운 단편화 환경에서도 중간 크기의 THP 이점을 얻을 수 있습니다.

mTHP 동작 원리

mTHP는 PTE 레벨에서 여러 개의 연속 PTE를 하나의 폴리오(Folio)에 매핑하는 방식으로 동작합니다. 2MB PMD 매핑과 달리, PTE 테이블은 그대로 유지하면서 물리적으로 연속된 페이지를 논리적 단위로 묶어 관리합니다. ARM64의 Contiguous PTE 힌트와 유사한 개념이지만 아키텍처 독립적으로 구현되어 있습니다.

mTHP 크기 계층: PTE 레벨 다중 크기 폴리오 기존 THP (커널 2.6.38~): PMD 레벨 2MB 단일 크기만 지원 할당 실패 시 바로 4KB로 폴백 → 중간 크기 부재로 단편화 환경에서 비효율 mTHP (커널 6.8+): PTE 레벨 다중 크기 지원 4KB order-0 1 PTE 16KB order-2 4 PTE 32KB order-3 8 PTE 64KB order-4 16 PTE 128KB order-5 32 PTE 256KB order-6 64 PTE 512KB order-7 128 PTE 1MB order-8 256 PTE 2MB order-9 PMD mTHP 대상: order-2 ~ order-8 (PTE 레벨 batched folio) 할당 폴백 체인 2MB (PMD) 시도 실패 1MB 시도 실패 64KB 시도 실패 ... 16KB 시도 실패 4KB (최종) mTHP 이점 단편화 환경에서도 중간 크기 폴리오로 TLB 미스 부분 완화 + 페이지 폴트 횟수 감소 2MB 전체 할당 대비 compaction 부담 감소, swap/split 시 세분화된 단위로 처리 가능

mTHP sysfs 설정

# mTHP 크기별 활성화 상태 확인 (커널 6.8+)
ls /sys/kernel/mm/transparent_hugepage/hugepages-*

# 특정 크기 활성화/비활성화
# hugepages-64kB 예시
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
# [inherit] always madvise never

echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/enabled
echo never  > /sys/kernel/mm/transparent_hugepage/hugepages-16kB/enabled

# mTHP 통계 (크기별)
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/stats/anon_fault_alloc
cat /sys/kernel/mm/transparent_hugepage/hugepages-64kB/stats/anon_fault_fallback

# 전체 mTHP 통계 확인
for d in /sys/kernel/mm/transparent_hugepage/hugepages-*; do
    size=$(basename $d)
    alloc=$(cat $d/stats/anon_fault_alloc 2>/dev/null)
    fallback=$(cat $d/stats/anon_fault_fallback 2>/dev/null)
    echo "$size: alloc=$alloc fallback=$fallback"
done
크기orderPTE 수Buddy 할당 난이도적합 워크로드
16KB24매우 쉬움일반 애플리케이션, 낮은 단편화 이점
64KB416쉬움ARM64 기본 페이지와 동일, 균형적
256KB664보통중간 규모 버퍼, 파일 캐시
1MB8256어려움대규모 힙, DB 버퍼 풀
2MB9 (PMD)512 / PMD 직접매우 어려움메모리 집약적 워크로드
mTHP 핵심 이점: 기존 THP에서 2MB 할당 실패 시 바로 4KB로 폴백하던 것이, mTHP에서는 64KB → 16KB 등 점진적으로 작은 크기를 시도합니다. 이를 통해 단편화된 메모리에서도 페이지 폴트 횟수 감소(64KB 폴리오 하나 = 4KB 폴트 16회 절약)와 부분적 TLB 효율 개선을 동시에 얻습니다.

HugeTLB Copy-on-Write (CoW) 상세

fork() 이후 부모·자식 프로세스가 Huge Page를 공유하다가 한쪽이 쓰기를 시도하면 hugetlb_wp()가 호출되어 Copy-on-Write를 수행합니다. 일반 4KB CoW와 달리 2MB 또는 1GB 전체를 복사해야 하므로 비용이 매우 높습니다.

HugeTLB Copy-on-Write 흐름 단계 1: fork() 직후 부모 PTE (R/O) 자식 PTE (R/O) 2MB Huge Page mapcount=2, refcount=2 자식 쓰기 단계 2: hugetlb_wp() CoW 부모 PTE → 원본 자식 PTE → 복사본 원본 2MB mapcount=1 복사본 2MB mapcount=1, R/W 복사! CoW 비용 분석 2MB Huge Page CoW 512개 x 4KB 복사 ~0.2~0.5ms 지연 1GB Huge Page CoW 262,144개 x 4KB 복사 ~50~200ms 지연 4KB 일반 CoW 1개 x 4KB 복사 ~1μs 이하 fork() 후 exec()까지 사이에 쓰기가 발생하면 불필요한 대형 복사 비용 발생 Redis fork 기반 BGSAVE에서 THP가 문제를 일으키는 핵심 원인 핵심 함수: hugetlb_wp() → alloc_huge_page() → copy_huge_page() → set_huge_pte_at()
/* mm/hugetlb.c — Huge Page Copy-on-Write (간략화) */
static vm_fault_t hugetlb_wp(
    struct mm_struct *mm, struct vm_area_struct *vma,
    unsigned long address, pte_t *ptep,
    unsigned int flags, struct page *pagecache_page)
{
    struct hstate *h = hstate_vma(vma);
    struct page *old_page, *new_page;
    int outside_reserve = 0;

    old_page = pte_page(huge_ptep_get(ptep));

    /* mapcount == 1이면 자신만 매핑 중 → CoW 불필요, 쓰기 허용 */
    if (page_mapcount(old_page) == 1) {
        huge_ptep_set_wrprotect(mm, address, ptep);  /* R/W로 전환 */
        return 0;
    }

    /* 새 Huge Page 할당 */
    new_page = alloc_huge_page(vma, address, outside_reserve);
    if (IS_ERR(new_page))
        return VM_FAULT_SIGBUS;

    /* 2MB/1GB 전체 복사 — 가장 비용이 큰 단계 */
    copy_huge_page(new_page, old_page);

    /* PTE를 새 페이지로 교체 + 쓰기 가능 설정 */
    huge_ptep_clear_flush(vma, address, ptep);
    set_huge_pte_at(mm, address, ptep,
                    make_huge_pte(vma, new_page, 1));  /* writable=1 */
    page_remove_rmap(old_page, vma, true);
    put_page(old_page);

    return 0;
}
코드 설명
  • 13~16행 page_mapcount()가 1이면 다른 프로세스가 이 페이지를 공유하지 않으므로 복사 없이 쓰기 허용으로 전환합니다. fork 후 exec를 바로 호출하면 이 최적화가 적용됩니다.
  • 24행 copy_huge_page()는 2MB(512 x 4KB)를 memcpy하는 것과 동일합니다. 1GB 페이지라면 262,144 x 4KB를 복사하므로 수십~수백 밀리초가 소요됩니다.
  • 27~29행 기존 PTE를 TLB flush와 함께 제거하고 새 페이지를 가리키는 쓰기 가능한 PTE를 설정합니다.
fork + Huge Page 주의: fork 기반 스냅샷(Redis BGSAVE, PostgreSQL checkpoint 등)에서 Huge Page는 CoW 비용이 극도로 높습니다. 2MB 페이지 하나의 쓰기도 전체 2MB 복사를 유발하므로, 쓰기가 빈번한 Huge Page 영역이 많으면 fork 직후 메모리 사용량이 급증하고 지연 스파이크가 발생합니다. Redis에서 THP를 비활성화해야 하는 핵심 이유입니다.

THP Deferred Split 메커니즘

THP가 부분적으로 unmap되거나 일부 PTE만 변경되면 즉시 분할하지 않고 deferred split 큐에 등록합니다. 실제 분할은 메모리 압박이 발생할 때 shrinker가 큐를 순회하며 처리합니다. 이 지연 전략은 불필요한 분할을 방지하여 THP의 성능 이점을 최대한 유지합니다.

THP Deferred Split 메커니즘 분할 트리거 이벤트 • 부분 munmap • 부분 mprotect • NUMA balancing 이동 즉시 분할 필요? 대부분 No Deferred Split 큐 등록 deferred_split_huge_page() Yes (드문 경우) split_huge_page() 즉시 실행 대기 큐에서 대기 중 thp_deferred_split_page 카운터 추적 메모리 압박 Shrinker 실행 deferred_split_scan() → split_huge_page() 2MB → 512개 4KB 분할 완료, thp_split_page++ pgdat별 큐 구조 pgdat->deferred_split_queue split_queue_lock (spinlock) split_queue_len (대기 수)
/* mm/huge_memory.c — deferred split 등록 */
void deferred_split_huge_page(struct page *page)
{
    struct pglist_data *pgdata = page_pgdat(page);
    unsigned long flags;

    /* 이미 큐에 있으면 중복 등록 방지 */
    if (!list_empty(page_deferred_list(page)))
        return;

    spin_lock_irqsave(&pgdata->deferred_split_queue.split_queue_lock, flags);
    list_add_tail(page_deferred_list(page),
                  &pgdata->deferred_split_queue.split_queue);
    pgdata->deferred_split_queue.split_queue_len++;
    spin_unlock_irqrestore(&pgdata->deferred_split_queue.split_queue_lock, flags);
}

/* mm/huge_memory.c — shrinker에 의한 실제 분할 */
static unsigned long deferred_split_scan(
    struct shrinker *shrink,
    struct shrink_control *sc)
{
    struct pglist_data *pgdata;
    struct page *page;
    unsigned long freed = 0;

    /* 큐에서 페이지를 꺼내 분할 시도 */
    list_for_each_entry_safe(page, ..., &list, ...) {
        if (!trylock_page(page))
            continue;
        if (!split_huge_page(page))
            freed++;
        unlock_page(page);
    }
    return freed;
}
# deferred split 대기 중인 페이지 수 확인
grep thp_deferred_split_page /proc/vmstat

# 이 값이 지속적으로 증가하면:
# → 부분 unmap/mprotect가 빈번한 워크로드
# → 메모리 압박 시 대량 분할로 지연 스파이크 가능

Huge Zero Page 최적화

THP 익명 페이지 폴트에서 읽기 전용 접근인 경우, 커널은 2MB의 물리 메모리를 새로 할당하지 않고 시스템 전체에서 하나만 존재하는 huge zero page를 매핑합니다. 프로세스가 BSS 영역이나 calloc()으로 할당한 대규모 메모리에 처음 읽기 접근할 때 적용되며, 쓰기가 발생하면 그때 실제 2MB 페이지를 할당합니다 (CoW 방식).

Huge Zero Page 최적화 읽기 폴트 프로세스 A 프로세스 B Huge Zero Page 시스템 전역 1개, 물리 2MB 프로세스 A 쓰기 발생! 쓰기 폴트 → CoW 프로세스 A 새 2MB 할당 (실제 물리 메모리) zero 초기화 + 쓰기 데이터 기록 PMD R/W 설정 프로세스 B 여전히 Huge Zero Page 공유 물리 메모리 추가 소비 없음 PMD R/O 유지 메모리 절약: 읽기만 하는 2MB 영역마다 물리 2MB 절약 BSS, calloc, mmap 초기 접근 시 THP 폴트 비용도 최소화 관련 통계 및 설정 thp_zero_page_alloc (vmstat) | /sys/kernel/mm/transparent_hugepage/use_zero_page (활성화 여부, 기본 1)
# huge zero page 활성화 확인
cat /sys/kernel/mm/transparent_hugepage/use_zero_page
# 1 (활성화, 기본값)

# huge zero page 할당 통계
grep thp_zero_page /proc/vmstat
# thp_zero_page_alloc         1   ← 시스템 전체에서 1회만 할당됨
# thp_zero_page_alloc_failed  0

# 비활성화 (드물지만 특수한 보안 요구 시)
echo 0 > /sys/kernel/mm/transparent_hugepage/use_zero_page

HugeTLB Demote — 1GB를 2MB로 분할 (커널 5.18+)

커널 5.18에서 도입된 HugeTLB demote 기능은 1GB Huge Page를 512개의 2MB Huge Page로 분할합니다. 이는 일반 4KB로의 분할이 아닌 Huge Page 간 크기 변환입니다. 1GB 풀의 유연성을 높이면서도 TLB 효율을 유지하는 데 유용합니다.

HugeTLB Demote: 1GB → 512 x 2MB 분할 1GB Huge Page order-18, PUD 레벨 매핑 compound page 262,144개 x 4KB 물리 페이지 = 512개 x 2MB 영역 hstate: order=18 demote sysfs 트리거 512개 x 2MB Huge Pages order-9, PMD 레벨 매핑 2MB 2MB 2MB ... 2MB 2MB 2MB 512개의 독립적인 2MB compound page 각각 hugepage_freelists[]에 추가됨 hstate: order=9, 2MB 풀 카운터 증가 1GB 풀 nr_huge_pages 감소 사용 사례 1GB 예약이 과도한 경우 일부를 2MB로 분할하여 2MB 풀 확보 (미사용 1GB 페이지만 가능) 역방향(2MB → 1GB promote)은 불가 — 물리 연속성 보장 불가
# HugeTLB demote 사용법 (커널 5.18+)

# 현재 demote 대상 크기 확인
cat /sys/kernel/mm/hugepages/hugepages-1048576kB/demote_size
# 2048kB (기본: 1GB → 2MB)

# 1GB Huge Page 1개를 2MB로 분할
echo 1 > /sys/kernel/mm/hugepages/hugepages-1048576kB/demote

# 결과 확인
cat /sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages    # 1 감소
cat /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages       # 512 증가

# NUMA 노드별 demote
echo 1 > /sys/devices/system/node/node0/hugepages/hugepages-1048576kB/demote

# 주의: 사용 중인(매핑된) 1GB 페이지는 demote 불가
# free_hugepages > 0인 경우에만 동작

ARM64 및 기타 아키텍처 Huge Page 지원

x86_64 외에도 ARM64, RISC-V, POWER 등 다양한 아키텍처가 각자의 방식으로 Huge Page를 지원합니다. 특히 ARM64는 x86_64와 달리 Contiguous PTE/PMD 힌트라는 고유한 메커니즘으로 다양한 크기의 대형 페이지를 하드웨어 수준에서 지원합니다.

아키텍처별 Huge Page 크기와 매핑 메커니즘 x86_64 기본 페이지: 4KB 2MB — PMD 레벨 (PSE 비트) 1GB — PUD 레벨 (PSE 비트) 특징 • 2가지 크기만 하드웨어 지원 • mTHP로 PTE 레벨 다중 크기 • TLB에 4KB/2MB/1GB 전용 엔트리 ARM64 (AArch64) 기본 페이지: 4KB / 16KB / 64KB 선택 4KB 기본 페이지 모드: 64KB — Contiguous PTE (16 x 4KB) 2MB — PMD 블록 매핑 32MB — Contiguous PMD (16 x 2MB) 1GB — PUD 블록 매핑 특징 • Contiguous 비트로 TLB 효율 극대화 • 다양한 중간 크기 하드웨어 지원 • mTHP와 자연스럽게 연동 RISC-V / POWER RISC-V (Sv48): 2MB — PMD 매핑 (megapage) 1GB — PUD 매핑 (gigapage) POWER (Radix MMU): 2MB — PMD 레벨 1GB — PUD 레벨 특징 • x86_64와 유사한 2단계 크기 • 커널 공통 코드 공유

ARM64 Contiguous PTE/PMD 상세

ARM64의 Contiguous 비트는 연속된 16개의 PTE 또는 PMD 엔트리가 물리적으로 연속된 메모리를 가리킬 때 설정됩니다. TLB는 이 16개 엔트리를 하나의 큰 TLB 엔트리로 병합하여 저장할 수 있어, 실질적으로 중간 크기의 Huge Page 효과를 얻습니다.

ARM64 (4KB 기본) 페이지 크기매핑 레벨ContiguousTLB 엔트리 효과
4KBPTE비트 없음1 TLB 엔트리 = 4KB
64KBContiguous PTE16 x PTE1 TLB 엔트리 = 64KB
2MBPMD 블록비트 없음1 TLB 엔트리 = 2MB
32MBContiguous PMD16 x PMD1 TLB 엔트리 = 32MB
1GBPUD 블록비트 없음1 TLB 엔트리 = 1GB
/* arch/arm64/include/asm/pgtable.h — Contiguous 관련 정의 */
#define CONT_PTE_SHIFT    (PAGE_SHIFT + 4)       /* 4KB x 16 = 64KB */
#define CONT_PTE_SIZE     (1UL << CONT_PTE_SHIFT)  /* 64KB */
#define CONT_PTES         (1 << (CONT_PTE_SHIFT - PAGE_SHIFT))  /* 16 */

#define CONT_PMD_SHIFT    (PMD_SHIFT + 4)        /* 2MB x 16 = 32MB */
#define CONT_PMD_SIZE     (1UL << CONT_PMD_SHIFT)  /* 32MB */
#define CONT_PMDS         (1 << (CONT_PMD_SHIFT - PMD_SHIFT))  /* 16 */

/* Contiguous 비트 확인 */
#define PTE_CONT          (1UL << 52)  /* ARMv8.0 Contiguous bit */
ARM64와 mTHP: ARM64의 Contiguous PTE 메커니즘은 mTHP와 자연스럽게 결합됩니다. mTHP가 64KB(order-4) 폴리오를 할당하면, ARM64에서는 16개의 연속 PTE에 Contiguous 비트를 설정하여 TLB가 이를 하나의 64KB 엔트리로 캐시합니다. 이는 x86_64에서의 mTHP(소프트웨어만 최적화)보다 TLB 효율이 더 높습니다.

흔한 실수와 해결책

실수증상원인해결책
부팅 후 1GB Huge Page 예약 실패 nr_hugepages에 쓰기 해도 값 변경 안 됨 부팅 후에는 1GB 연속 메모리 확보가 거의 불가능 부팅 파라미터 hugepagesz=1G hugepages=N으로 설정
mmap(MAP_HUGETLB) 실패 (ENOMEM) mmap: Cannot allocate memory nr_hugepages 미설정 또는 풀 고갈 사전 예약 확인: grep Huge /proc/meminfo
THP 활성화했는데 효과 없음 AnonHugePages 값이 0에 가까움 enabled=madvise인데 앱이 madvise() 호출 안 함 always로 변경하거나 앱에서 MADV_HUGEPAGE 호출
Redis 지연 스파이크 p99 지연 수십 ms 급증 THP always + fork 기반 BGSAVE → CoW 비용 echo never > /sys/.../transparent_hugepage/enabled
THP 할당 실패율 높음 thp_fault_fallback 급증 메모리 단편화로 order-9 연속 프레임 확보 불가 defrag=defer+madvise, compaction_proactiveness 조정
hugetlbfs 마운트 후 권한 오류 mmap: Permission denied 비특권 사용자가 Huge Page 사용 불가 vm.hugetlb_shm_group sysctl 설정 또는 CAP_IPC_LOCK 부여
Huge Page 예약이 일반 메모리 압박 시스템 메모리 부족, OOM 발생 과도한 nr_hugepages 설정으로 일반 메모리 영역 부족 예약량 재계산, 실제 사용량 대비 10~20% 여유만 유지
컨테이너에서 Huge Page 사용 불가 Pod 내 MAP_HUGETLB 실패 Kubernetes hugepages 리소스 미설정 Pod spec에 hugepages-2Mi 리소스 요청/제한 추가
khugepaged가 CPU를 과도하게 사용 khugepaged 프로세스 CPU 5~10% pages_to_scan 과다 또는 scan_sleep_millisecs 과소 pages_to_scan 감소, scan_sleep_millisecs 증가
NUMA 불균형 Huge Page 할당 원격 NUMA 접근으로 성능 저하 전역 nr_hugepages만 설정하여 한쪽 노드에 집중 할당 노드별 개별 예약: /sys/devices/system/node/nodeN/hugepages/...

Kubernetes에서 Huge Page 사용

# Pod spec에서 Huge Page 요청 예시
apiVersion: v1
kind: Pod
metadata:
  name: hugepage-app
spec:
  containers:
  - name: app
    image: myapp:latest
    resources:
      requests:
        memory: "1Gi"
        hugepages-2Mi: "512Mi"     # 2MB Huge Pages 512MB 요청
      limits:
        memory: "1Gi"
        hugepages-2Mi: "512Mi"     # 제한도 동일하게 설정
    volumeMounts:
    - mountPath: /hugepages
      name: hugepage
  volumes:
  - name: hugepage
    emptyDir:
      medium: HugePages-2Mi       # hugetlbfs 자동 마운트
# 노드에서 Huge Page 가용량 확인
kubectl describe node <node-name> | grep hugepages
# hugepages-2Mi: 4Gi
# hugepages-1Gi: 16Gi

# 노드에서 Huge Page 예약 (DaemonSet 또는 initContainer로 자동화)
echo 2048 > /proc/sys/vm/nr_hugepages
운영 체크리스트:
  • Huge Page 예약량 = 실제 사용량 + 10~20% 여유. 과다 예약은 일반 메모리 부족 유발
  • THP defrag 설정이 always면 지연 민감 서비스에서 스파이크 발생 가능
  • NUMA 시스템에서는 반드시 노드별 균등 예약
  • /proc/vmstatthp_fault_fallback, thp_split_page 추이를 주기적으로 모니터링
  • 컨테이너 환경에서는 cgroup hugepage 제한 + Kubernetes 리소스 요청을 함께 설정

참고자료

다음 학습: