Huge Pages (2MB/1GB) & THP — 대형 페이지 심화
리눅스 커널의 Huge Pages는 기본 4KB 페이지 대신 2MB 또는 1GB 크기의 대형 페이지를 사용하여 TLB(Translation Lookaside Buffer) 미스를 획기적으로 줄이고 메모리 접근 성능을 극대화하는 메커니즘입니다. 정적 예약 기반의 hugetlbfs부터 커널이 자동으로 대형 페이지를 생성하는 Transparent Huge Pages(THP), khugepaged 데몬, PMD 직접 매핑, compound page 구조, NUMA 정책, 예약 시스템, 마이그레이션/compaction 연동, 그리고 데이터베이스/DPDK/VM 실전 적용까지 전 영역을 상세히 다룹니다.
핵심 요약
- Huge Page — 4KB보다 큰 페이지 크기(x86_64에서 2MB 또는 1GB)를 사용하여 TLB 효율을 극대화하는 메모리 관리 기법
- hugetlbfs — 사용자가 명시적으로 대형 페이지를 예약하고 사용하는 정적 방식의 파일시스템 인터페이스
- THP (Transparent Huge Pages) — 커널이 자동으로 4KB 페이지를 2MB 대형 페이지로 승격/분할하는 투명 메커니즘
- khugepaged — 백그라운드에서 분산된 4KB 페이지를 스캔하여 2MB 대형 페이지로 병합하는 커널 데몬
- PMD (Page Middle Directory) — x86_64에서 2MB Huge Page를 위한 페이지 테이블 레벨로, PTE 단계를 건너뛰어 직접 물리 프레임을 가리킴
- compound page — 연속된 물리 페이지를 하나의 논리적 대형 페이지로 묶는 커널 자료구조
단계별 이해
- TLB 미스 비용 이해
CPU가 가상 주소를 물리 주소로 변환할 때, TLB 캐시에 없으면 4단계 페이지 테이블 워크가 발생합니다. 4KB 페이지로 1GB 메모리를 매핑하면 262,144개의 TLB 엔트리가 필요하지만, 2MB 페이지로는 512개, 1GB 페이지로는 단 1개면 충분합니다. - 정적 Huge Pages 예약 (hugetlbfs)
시스템 부팅 시 또는 런타임에/proc/sys/vm/nr_hugepages를 통해 필요한 수의 Huge Pages를 미리 예약합니다. 예약된 페이지는 hugetlbfs를 통해 사용자 공간에 매핑됩니다. - 투명 대형 페이지 (THP) 활성화
/sys/kernel/mm/transparent_hugepage/enabled를always또는madvise로 설정하면, 커널이 자동으로 연속된 4KB 페이지를 2MB 페이지로 승격합니다. - khugepaged 백그라운드 최적화
THP가 활성화되면 khugepaged 데몬이 주기적으로 프로세스의 메모리를 스캔하여 병합 가능한 4KB 페이지 그룹을 2MB Huge Page로 통합합니다. - 모니터링과 튜닝
/proc/meminfo,/sys/kernel/mm/hugepages/,/sys/kernel/mm/transparent_hugepage/경로에서 Huge Pages 사용 현황과 THP 통계를 확인하고 세부 파라미터를 조정합니다.
개요 — 왜 Huge Pages가 필요한가
TLB 미스의 비용
현대 x86_64 프로세서에서 가상 주소를 물리 주소로 변환하는 과정은 4단계 페이지 테이블 워크 (PGD → PUD → PMD → PTE)를 거칩니다. TLB에 캐시된 변환이 있으면 1~2 사이클 내에 완료되지만, TLB 미스가 발생하면 최대 4번의 메모리 접근이 필요하여 수십~수백 사이클의 지연이 발생합니다.
일반적인 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 마운트 | 투명 (madvise 힌트 가능) |
| OOM 위험 | 낮음 (사전 예약) | 있음 (compaction 실패 시 fallback) |
| 분할(split) 지원 | 불가 | 가능 (필요 시 4KB로 분할) |
| 스왑 지원 | 불가 | 가능 (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 | 빈번 |
hugetlbfs 아키텍처 — 예약 기반 정적 Huge Pages
hugetlbfs 개요
hugetlbfs는 커널 2.6부터 도입된 특수 파일시스템으로, 정적으로 예약된 Huge Pages를 사용자 공간에 제공합니다. Buddy 할당자에서 연속된 고차(order-9 또는 order-18) 물리 페이지를 미리 확보하여 전용 풀에 보관하며, 사용자가 hugetlbfs를 마운트하고 파일을 mmap하여 사용합니다.
hugetlbfs 예약 흐름
hugetlbfs 페이지 풀 관리 구조체
/* 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 완전 비활성화 | 지연 민감 실시간 시스템 |
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 엔트리를 원자적으로 설정하여 2MB 매핑을 완성합니다.
khugepaged 데몬 동작
khugepaged 개요
khugepaged는 THP 프레임워크의 백그라운드 병합 데몬입니다. 주기적으로 프로세스의
가상 메모리 영역을 스캔하여, 연속된 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 토폴로지와 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()을 호출하면 즉시 물리 페이지를 할당하지 않고,
예약만 수행합니다. 이는 실제 페이지 폴트가 발생할 때까지 물리 할당을 지연하되,
할당 실패가 발생하지 않도록 여유 페이지 수를 미리 확보하는 방식입니다.
/* 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 미스율과 처리량 차이를 보여줍니다.
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 패킷 처리 | hugetlbfs 1GB 전용 예약 | 최대 TLB 효율, 고정 할당 보장 |
| KVM/QEMU VM | THP=always 또는 hugetlbfs 백엔드 |
VM 메모리의 대부분이 대형 페이지 활용 가능 |
| 과학 계산 / HPC | hugetlbfs 2MB/1GB 사전 예약 | 대규모 배열 순회 시 TLB 미스 최소화 |
| 일반 데스크탑 | THP=always, defrag=defer+madvise |
자동 최적화, 사용자 개입 불필요 |
모니터링과 디버깅
/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을
공유/재활용하여 메타데이터 메모리 오버헤드를 크게 줄입니다.
# 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 재배치·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 실패 원인을 분리하는 것입니다.
연속 가상 주소가 있어도 물리 단편화, 참조 상태, 페이지 핀 고정 상태에 따라 collapse가 반복 실패할 수 있습니다.
HugeTLB 풀 운영 정책 (예약/버스트/격리)
HugeTLB는 사전 예약 방식이라 서비스별 용량 계획이 중요합니다. 버스트 트래픽이 있는 서비스는 최소 예약과 버스트 여유를 분리하고, 컨테이너 환경에서는 cgroup 제한과 함께 운영해야 안정적입니다.
| 운영 시나리오 | 권장 정책 | 실패 시 신호 |
|---|---|---|
| 고정 워크로드 DB | 정적 hugepages 예약 + 재부팅 시 일관 적용 | MAP_HUGETLB 실패 없음, 지연 안정 |
| 버스트 분석 작업 | 기본 예약 + 버스트 한도 별도 | HugePages_Free 급락 후 복구 지연 |
| 컨테이너 다중 테넌트 | cgroup huge limit로 테넌트 격리 | 일부 테넌트 과점유/기아 |
# 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 오버헤드 가능 |
| 혼합 워크로드 | 프로세스/영역별 선택 적용 필요 | 정책 일괄 적용 시 회귀 위험 |
참고자료
- 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
- 소스 코드:
mm/hugetlb.c,mm/huge_memory.c,mm/khugepaged.c,include/linux/hugetlb.h,arch/x86/include/asm/pgtable_types.h
- MMU & TLB — 가상 주소 변환과 TLB 동작 원리
- 페이지 할당자 (Buddy Allocator) — 물리 페이지 할당 메커니즘
- NUMA — Non-Uniform Memory Access 아키텍처
- 메모리 관리 개요 — 리눅스 커널 메모리 관리 전체 그림
- VMA/mmap 심화 — 가상 메모리 영역과 mmap 매핑