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 실전 적용까지 전 영역을 상세히 다룹니다.
핵심 요약
- 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 크기 목록
단계별 이해
- 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 직접 매핑을 판별합니다. - 정적 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) 페이지를 확보합니다. - 투명 대형 페이지 (THP) 활성화
/sys/kernel/mm/transparent_hugepage/enabled를always또는madvise로 설정하면, 커널이 자동으로 연속된 4KB 페이지를 2MB 페이지로 승격합니다. 커널 함수:do_huge_pmd_anonymous_page()에서 THP 페이지 폴트를 처리하고, PMD 엔트리에 2MB compound page를 직접 매핑합니다. - khugepaged 백그라운드 최적화
THP가 활성화되면 khugepaged 데몬이 주기적으로 프로세스(Process)의 메모리를 스캔하여 병합 가능한 4KB 페이지 그룹을 2MB Huge Page로 통합합니다. 커널 함수:khugepaged_scan_mm_slot()→collapse_huge_page()에서 512개의 연속 PTE를 하나의 PMD 매핑으로 치환합니다. - 모니터링과 튜닝
/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 |
Huge Pages의 두 가지 방식
| 항목 | hugetlbfs (정적 Huge Pages) | THP (투명 대형 페이지) |
|---|---|---|
| 할당 방식 | 사전 예약 (부팅 시 또는 런타임) | 커널 자동 (on-demand) |
| 지원 크기 | 2MB, 1GB | 2MB (기본) |
| 사용자 인터페이스 | 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 엔트리를 기준으로 각 페이지 크기별 커버리지를 보여줍니다.
TLB 미스율 시뮬레이션
아래 표는 연속적으로 접근하는 메모리 영역 크기별 TLB 미스 횟수를 비교합니다. (L1 DTLB 64개, L2 STLB 2,048개 기준)
| 접근 메모리 크기 | 4KB 페이지 (필요 엔트리) | 4KB TLB 미스 | 2MB 페이지 (필요 엔트리) | 2MB TLB 미스 |
|---|---|---|---|---|
| 8 MB | 2,048 | L2 경계 | 4 | 0 (L1 캐시) |
| 64 MB | 16,384 | 빈번 | 32 | 0 (L1 캐시) |
| 512 MB | 131,072 | 매우 빈번 | 256 | 0 (L2 캐시) |
| 4 GB | 1,048,576 | 극심 | 2,048 | L2 경계 |
| 32 GB | 8,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 효과 확인
hugetlbfs 아키텍처 — 예약 기반 정적 Huge Pages
hugetlbfs 개요
hugetlbfs는 커널 2.6부터 도입된 특수 파일시스템으로, 정적으로 예약된 Huge Pages를 사용자 공간에 제공합니다. Buddy 할당자에서 연속된 고차(order-9 또는 order-18) 물리 페이지를 미리 확보하여 전용 풀에 보관하며, 사용자가 hugetlbfs를 마운트하고 파일을 mmap하여 사용합니다.
hugetlbfs 예약 흐름
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 설정 인터페이스
# 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 튜닝 파라미터
# 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 페이지가 됩니다.
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 두 레벨을 모두 건너뜁니다.
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 정책을 적용한 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로 설정합니다.
예약 흐름
mmap(MAP_HUGETLB)호출- 커널이
hugetlb_reserve_pages()를 호출하여 필요한 Huge Page 수 계산 resv_map에 예약 범위 추가- 전역 풀에서
resv_huge_pages카운터 증가 - subpool이 있으면 subpool 카운터도 증가
- 실제 페이지 폴트 시
alloc_huge_page()가 예약된 풀에서 할당 munmap()시 미사용 예약분 반환
Huge Page 마이그레이션과 compaction
THP 분할 (Split)
THP는 필요에 따라 512개의 4KB 페이지로 분할될 수 있습니다. 분할이 발생하는 주요 상황:
- 부분 unmap: Huge Page의 일부만
munmap()할 때 - 부분 mprotect: 2MB 영역의 일부만 권한을 변경할 때
- swap out: 메모리 압박 시 THP를 스왑 아웃할 때 (개별 4KB로 분할 후 스왑)
- NUMA balancing: 페이지를 다른 NUMA 노드로 마이그레이션할 때
- GUP (pin_user_pages): Direct I/O 등으로 페이지 핀이 걸릴 때
/* 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
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) 차이를 보여줍니다.
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>
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)를 크게 줄입니다.
# 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 한도 예시
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 분할/병합 상태 추적
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 활용률", "지연 스파이크"로 나타납니다. 아래 절차로 원인을 분류하면 대응이 빨라집니다.
# 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
madvise로 두고, 필요한 프로세스에만 MADV_HUGEPAGE를 적용하는 방식이 가장 예측 가능성이 높습니다.
khugepaged collapse 실패 분석
THP 성능을 기대했는데 실제로는 일반 페이지로 남는 경우, 핵심은 khugepaged의 collapse 실패 원인을 분리하는 것입니다.
연속 가상 주소가 있어도 물리 단편화(Fragmentation), 참조 상태, 페이지 핀 고정 상태에 따라 collapse가 반복 실패할 수 있습니다.
# 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 패턴을 기준으로 판단해야 합니다.
| 패턴 | 기대 효과 | 주의점 |
|---|---|---|
| 순차 읽기 중심 분석 | 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 할당자로부터 대형 페이지를 획득합니다.
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_flags에VM_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 |
|---|---|---|---|
order | unsigned int | Buddy 할당자 order. 2MB = 9 (2^9 * 4KB), 1GB = 18 (2^18 * 4KB) | - |
nr_huge_pages | unsigned long | 현재 시스템에 존재하는 해당 크기 Huge Page 총 수 | /proc/meminfo HugePages_Total |
free_huge_pages | unsigned long | freelist에 있는 미사용 Huge Page 수 | /proc/meminfo HugePages_Free |
resv_huge_pages | unsigned long | mmap 호출 시 예약되었으나 아직 폴트가 발생하지 않은 수 | /proc/meminfo HugePages_Rsvd |
surplus_huge_pages | unsigned long | overcommit 정책으로 부팅 예약 외에 추가 할당된 수 | /proc/meminfo HugePages_Surp |
nr_overcommit_huge_pages | unsigned long | surplus 허용 상한. 이 수까지 buddy에서 동적 할당 허용 | nr_overcommit_hugepages |
hugepage_freelists[] | list_head[MAX_NUMNODES] | NUMA 노드별 여유 Huge Page 리스트. dequeue_huge_page_vma()가 여기서 꺼냄 | - |
hugepage_activelist | list_head | 현재 매핑되어 사용 중인 Huge Page 리스트 | - |
resize_lock | mutex | 풀 크기 변경(nr_hugepages 쓰기) 시 직렬화 | - |
next_nid_to_alloc/free | int | 라운드 로빈 NUMA 노드 인덱스. 노드 간 균등 분배에 사용 | - |
demote_order | unsigned int | 1GB를 2MB로 분할(demote)할 때 목표 order. v5.18+에서 지원 | demote_size |
mask | unsigned long | 주소 정렬 마스크. 2MB: ~(2MB-1), 1GB: ~(1GB-1) | - |
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_policy는mbind()로 설정한 NUMA 정책을 구간별로 저장합니다. 프로세스가 아닌 파일 단위로 관리되므로 shared 매핑 시 모든 프로세스에 동일 정책이 적용됩니다. -
4행
vfs_inode는 컨테이너(Container) 패턴의 핵심입니다.container_of()매크로로 VFS inode 포인터에서hugetlbfs_inode_info를 역참조할 수 있습니다. -
5행
seals는memfd_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.policy | NUMA 정책 트리 (red-black tree) | alloc_inode() |
hugetlbfs_inode_info.seals | memfd 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와 전혀 다른 경로로 대형 페이지를 생성합니다. 두 가지 주요 진입점이 있습니다:
- 즉시 할당: 익명 페이지 폴트 시
do_huge_pmd_anonymous_page()가 직접 2MB를 할당 - 지연 병합:
khugepaged데몬이 기존 4KB 페이지 512개를 하나의 THP로 collapse
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_FALLBACKvmstat 카운터를 증가시키고 일반 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 레벨을 건너뜁니다.
- 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/vmstat의 thp_collapse_alloc_failed와 thp_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 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
| 크기 | order | PTE 수 | Buddy 할당 난이도 | 적합 워크로드 |
|---|---|---|---|---|
| 16KB | 2 | 4 | 매우 쉬움 | 일반 애플리케이션, 낮은 단편화 이점 |
| 64KB | 4 | 16 | 쉬움 | ARM64 기본 페이지와 동일, 균형적 |
| 256KB | 6 | 64 | 보통 | 중간 규모 버퍼, 파일 캐시 |
| 1MB | 8 | 256 | 어려움 | 대규모 힙, DB 버퍼 풀 |
| 2MB | 9 (PMD) | 512 / PMD 직접 | 매우 어려움 | 메모리 집약적 워크로드 |
HugeTLB Copy-on-Write (CoW) 상세
fork() 이후 부모·자식 프로세스가 Huge Page를 공유하다가 한쪽이 쓰기를 시도하면
hugetlb_wp()가 호출되어 Copy-on-Write를 수행합니다. 일반 4KB CoW와 달리
2MB 또는 1GB 전체를 복사해야 하므로 비용이 매우 높습니다.
/* 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를 설정합니다.
THP Deferred Split 메커니즘
THP가 부분적으로 unmap되거나 일부 PTE만 변경되면 즉시 분할하지 않고 deferred split 큐에 등록합니다. 실제 분할은 메모리 압박이 발생할 때 shrinker가 큐를 순회하며 처리합니다. 이 지연 전략은 불필요한 분할을 방지하여 THP의 성능 이점을 최대한 유지합니다.
/* 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 활성화 확인
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 사용법 (커널 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 힌트라는 고유한 메커니즘으로 다양한 크기의 대형 페이지를 하드웨어 수준에서 지원합니다.
ARM64 Contiguous PTE/PMD 상세
ARM64의 Contiguous 비트는 연속된 16개의 PTE 또는 PMD 엔트리가 물리적으로 연속된 메모리를 가리킬 때 설정됩니다. TLB는 이 16개 엔트리를 하나의 큰 TLB 엔트리로 병합하여 저장할 수 있어, 실질적으로 중간 크기의 Huge Page 효과를 얻습니다.
| ARM64 (4KB 기본) 페이지 크기 | 매핑 레벨 | Contiguous | TLB 엔트리 효과 |
|---|---|---|---|
| 4KB | PTE | 비트 없음 | 1 TLB 엔트리 = 4KB |
| 64KB | Contiguous PTE | 16 x PTE | 1 TLB 엔트리 = 64KB |
| 2MB | PMD 블록 | 비트 없음 | 1 TLB 엔트리 = 2MB |
| 32MB | Contiguous PMD | 16 x PMD | 1 TLB 엔트리 = 32MB |
| 1GB | PUD 블록 | 비트 없음 | 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 */
흔한 실수와 해결책
| 실수 | 증상 | 원인 | 해결책 |
|---|---|---|---|
| 부팅 후 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/vmstat의thp_fault_fallback,thp_split_page추이를 주기적으로 모니터링- 컨테이너 환경에서는 cgroup hugepage 제한 + Kubernetes 리소스 요청을 함께 설정
참고자료
- Linux Kernel Documentation — HugeTLB Pages
- Linux Kernel Documentation — Transparent Huge Pages
- LWN — Transparent huge pages (Jonathan Corbet, 2010)
- LWN — Transparent huge pages in 2.6.38
- LWN — Memory compaction
- Linux Kernel Documentation — Page Tables
- Linux Kernel Documentation — HugeTLB Reservations
- LWN — Multi-size THP (mTHP) — PTE 레벨 다중 크기 THP
- LWN — HugeTLB demote — 1GB → 2MB 크기 변환
- Linux Kernel Documentation — Multi-Gen LRU — THP/folio 회수 연동
- 소스 코드:
mm/hugetlb.c,mm/huge_memory.c,mm/khugepaged.c,include/linux/hugetlb.h,arch/x86/include/asm/pgtable_types.h,arch/arm64/include/asm/pgtable.h - mmap(2) — MAP_HUGETLB 플래그
- proc(5) — /proc/meminfo HugePages 통계
- LWN: THP shrinker (2023) — THP 회수 메커니즘을 다룹니다
- LWN: The state of large folios (2024) — 대형 폴리오의 현재 상태와 발전 방향을 다룹니다
- mm/hugetlb.c — HugeTLB 핵심 구현 소스입니다
- mm/huge_memory.c — THP(Transparent Huge Pages) 구현 소스입니다
- mm/khugepaged.c — khugepaged 데몬 구현 소스입니다
- MMU & TLB — 가상 주소 변환과 TLB 동작 원리
- 페이지 할당자 (Buddy Allocator) — 물리 페이지 할당 메커니즘
- NUMA — Non-Uniform Memory Access 아키텍처
- 메모리 관리 개요 — 리눅스 커널 메모리 관리 전체 그림
- VMA/mmap — 가상 메모리 영역과 mmap 매핑
- Folio — 폴리오 기반 메모리 관리와 mTHP 연동
- Memory Compaction — 메모리 압축과 Huge Page 할당 연동